Ejemplo n.º 1
0
 def change_entry(self, entry, date=NOEDIT, reconciliation_date=NOEDIT, description=NOEDIT, 
         payee=NOEDIT, checkno=NOEDIT, transfer=NOEDIT, amount=NOEDIT):
     assert entry is not None
     if date is not NOEDIT and amount is not NOEDIT and amount != 0:
         Currency.get_rates_db().ensure_rates(date, [amount.currency.code, entry.account.currency.code])
     if reconciliation_date is NOEDIT:
         global_scope = self._query_for_scope_if_needed([entry.transaction])
     else:
         global_scope = False # It doesn't make sense to set a reconciliation date globally
     action = self._get_action_from_changed_transactions([entry.transaction], global_scope)
     self._undoer.record(action)
     
     candidate_dates = [entry.date, date, reconciliation_date, entry.reconciliation_date]
     min_date = min(d for d in candidate_dates if d is not NOEDIT and d is not None)
     if reconciliation_date is not NOEDIT:
         if isinstance(entry.split.transaction, Spawn):
             # At this point we have to hijack the entry so we modify the materialized transaction
             # It's a little hackish, but well... it takes what it takes
             entry.split = self._reconcile_spawn_split(entry.split, reconciliation_date)
             action.added_transactions.add(entry.split.transaction)
         else:
             entry.split.reconciliation_date = reconciliation_date
     if (amount is not NOEDIT) and (len(entry.splits) == 1):
         entry.change_amount(amount)
     if (transfer is not NOEDIT) and (len(entry.splits) == 1) and (transfer != entry.transfer):
         auto_create_type = AccountType.Expense if entry.split.amount < 0 else AccountType.Income
         transfer_account = self.accounts.find(transfer, auto_create_type) if transfer else None
         entry.splits[0].account = transfer_account
     self._change_transaction(entry.transaction, date=date, description=description, 
         payee=payee, checkno=checkno, global_scope=global_scope)
     self._cook(from_date=min_date)
     self._clean_empty_categories()
     if not self._adjust_date_range(entry.date):
         self.notify('transaction_changed')
Ejemplo n.º 2
0
    def change_transactions(self, transactions, date=NOEDIT, description=NOEDIT, payee=NOEDIT, 
            checkno=NOEDIT, from_=NOEDIT, to=NOEDIT, amount=NOEDIT, currency=NOEDIT):
        if from_ is not NOEDIT:
            from_ = self.accounts.find(from_, AccountType.Income) if from_ else None
        if to is not NOEDIT:
            to = self.accounts.find(to, AccountType.Expense) if to else None
        if date is not NOEDIT and amount is not NOEDIT and amount != 0:
            currencies_to_ensure = [amount.currency.code, self.default_currency.code]
            Currency.get_rates_db().ensure_rates(date, currencies_to_ensure)
        if len(transactions) == 1:
            global_scope = self._query_for_scope_if_needed(transactions)
        else:
            global_scope = False
        action = self._get_action_from_changed_transactions(transactions, global_scope)
        self._undoer.record(action)

        min_date = date if date is not NOEDIT else datetime.date.max
        for transaction in transactions:
            min_date = min(min_date, transaction.date)
            self._change_transaction(transaction, date=date, description=description, 
                payee=payee, checkno=checkno, from_=from_, to=to, amount=amount, currency=currency, 
                global_scope=global_scope)
        self._cook(from_date=min_date)
        self._clean_empty_categories()
        if not self._adjust_date_range(transaction.date):
            self.notify('transaction_changed')
        if action.changed_schedules:
            self.notify('schedule_changed')
Ejemplo n.º 3
0
def test_parse_currencies_with_large_exponents():
    # Dinars have 3 decimal places, making them awkward to parse because for "normal" currencies, we
    # specifically look for 2 digits after the separator to avoid confusion with thousand sep. For
    # dinars, however, we look for 3 digits adter the decimal sep. So yes, we are vulnerable to
    # confusion with the thousand sep, but well, there isn't much we can do about that.
    eq_(parse_amount('1,000 BHD'), Amount(1, BHD))
    # Moreover, with custom currencies, we might have currencies with even bigger exponent.
    ABC = Currency.register('ABC', 'My foo currency', exponent=5)
    eq_(parse_amount('1.23456 abc'), Amount(1.23456, ABC))
Ejemplo n.º 4
0
def test_ensures_rates(tmpdir, fake_server, monkeypatch):
    # Upon calling save and load, rates are asked for both EUR and PLN.
    app = app_entry_with_foreign_currency()
    rates_db = Currency.get_rates_db()
    monkeypatch.setattr(rates_db, 'ensure_rates', log_calls(rates_db.ensure_rates))
    filename = str(tmpdir.join('foo.xml'))
    app.doc.save_to_xml(filename)
    app.doc.load_from_xml(filename)
    calls = rates_db.ensure_rates.calls
    eq_(len(calls), 1)
    eq_(set(calls[0]['currencies']), set(['PLN', 'EUR', 'CAD']))
    eq_(calls[0]['start_date'], date(2007, 10, 1))
Ejemplo n.º 5
0
def initialize_db(path):
    """Initialize the app wide currency db if not already initialized."""
    ratesdb = RatesDB(str(path))
    ratesdb.register_rate_provider(default_currency_rate_provider)
    Currency.set_rates_db(ratesdb)
Ejemplo n.º 6
0
 def load(self):
     """Loads the parsed info into self.accounts and self.transactions.
     
     You must have called parse() before calling this.
     """
     def load_transaction_info(info):
         description = info.description
         payee = info.payee
         checkno = info.checkno
         date = info.date
         transaction = Transaction(date, description, payee, checkno)
         transaction.notes = nonone(info.notes, '')
         for split_info in info.splits:
             account = split_info.account
             amount = split_info.amount
             if split_info.amount_reversed:
                 amount = -amount
             memo = nonone(split_info.memo, '')
             split = Split(transaction, account, amount)
             split.memo = memo
             if split_info.reconciliation_date is not None:
                 split.reconciliation_date = split_info.reconciliation_date                    
             elif split_info.reconciled: # legacy
                 split.reconciliation_date = transaction.date
             split.reference = split_info.reference
             transaction.splits.append(split)
         while len(transaction.splits) < 2:
             transaction.splits.append(Split(transaction, None, 0))
         transaction.balance()
         transaction.mtime = info.mtime
         if info.reference is not None:
             for split in transaction.splits:
                 if split.reference is None:
                     split.reference = info.reference
         return transaction
     
     self._load()
     self.flush_account() # Implicit
     # Now, we take the info we have and transform it into model instances
     currencies = set()
     start_date = datetime.date.max
     for info in self.group_infos:
         group = Group(info.name, info.type)
         self.groups.append(group)
     for info in self.account_infos:
         account_type = info.type
         if account_type not in AccountType.All:
             account_type = AccountType.Asset
         account_currency = self.default_currency
         try:
             if info.currency:
                 account_currency = Currency(info.currency)
         except ValueError:
             pass # keep account_currency as self.default_currency
         account = Account(info.name, account_currency, account_type)
         if info.group:
             account.group = self.groups.find(info.group, account_type)
         if info.budget:
             self.budget_infos.append(BudgetInfo(info.name, info.budget_target, info.budget))
         account.reference = info.reference
         account.account_number = info.account_number
         account.notes = info.notes
         currencies.add(account.currency)
         self.accounts.add(account)
     
     # Pre-parse transaction info. We bring all relevant info recorded at the txn level into the split level
     all_txn = self.transaction_infos + [r.transaction_info for r in self.recurrence_infos] +\
               flatten([stripfalse(r.date2exception.values()) for r in self.recurrence_infos]) +\
               flatten([r.date2globalchange.values() for r in self.recurrence_infos])
     for info in all_txn:
         split_accounts = [s.account for s in info.splits]
         if info.account and info.account not in split_accounts:
             info.splits.insert(0, SplitInfo(info.account, info.amount, info.currency, False))
         if info.transfer and info.transfer not in split_accounts:
             info.splits.append(SplitInfo(info.transfer, info.amount, info.currency, True))
         for split_info in info.splits:
             # this amount is just to determine the auto_create_type
             str_amount = split_info.amount
             if split_info.currency:
                 str_amount += split_info.currency
             amount = self.parse_amount(str_amount, self.default_currency)
             auto_create_type = AccountType.Income if amount >= 0 else AccountType.Expense
             split_info.account = self.accounts.find(split_info.account, auto_create_type) if split_info.account else None
             currency = split_info.account.currency if split_info.account is not None else self.default_currency
             split_info.amount = self.parse_amount(str_amount, currency)
             if split_info.amount:
                 currencies.add(split_info.amount.currency)
     
     self.transaction_infos.sort(key=attrgetter('date'))
     for date, transaction_infos in groupby(self.transaction_infos, attrgetter('date')):
         start_date = min(start_date, date)
         for position, info in enumerate(transaction_infos, start=1):
             transaction = load_transaction_info(info)
             self.transactions.add(transaction, position=position)
     
     # Scheduled
     for info in self.recurrence_infos:
         ref = load_transaction_info(info.transaction_info)
         recurrence = Recurrence(ref, info.repeat_type, info.repeat_every)
         recurrence.stop_date = info.stop_date
         for date, transaction_info in info.date2exception.items():
             if transaction_info is not None:
                 exception = load_transaction_info(transaction_info)
                 spawn = Spawn(recurrence, exception, date, exception.date)
                 recurrence.date2exception[date] = spawn
             else:
                 recurrence.delete_at(date)
         for date, transaction_info in info.date2globalchange.items():
             change = load_transaction_info(transaction_info)
             spawn = Spawn(recurrence, change, date, change.date)
             recurrence.date2globalchange[date] = spawn
         self.schedules.append(recurrence)
     # Budgets
     TODAY = datetime.date.today()
     fallback_start_date = datetime.date(TODAY.year, TODAY.month, 1)
     for info in self.budget_infos:
         account = self.accounts.find(info.account)
         if account is None:
             continue
         target = self.accounts.find(info.target) if info.target else None
         amount = self.parse_amount(info.amount, account.currency)
         start_date = nonone(info.start_date, fallback_start_date)
         budget = Budget(account, target, amount, start_date, repeat_type=info.repeat_type)
         budget.notes = nonone(info.notes, '')
         budget.stop_date = info.stop_date
         if info.repeat_every:
             budget.repeat_every = info.repeat_every
         self.budgets.append(budget)
     self._post_load()
     self.oven.cook(datetime.date.min, until_date=None)
     Currency.get_rates_db().ensure_rates(start_date, [x.code for x in currencies])
Ejemplo n.º 7
0
 def _hook_currency_plugins(self):
     currency_plugins = [p for p in self.plugins if issubclass(p, CurrencyProviderPlugin)]
     for p in currency_plugins:
         Currency.get_rates_db().register_rate_provider(p().wrapped_get_currency_rates)
Ejemplo n.º 8
0
def set_ratedb_for_tests(async=False, slow_down_provider=False, provider=None):
    log = []
    
    # Returns a RatesDB that isn't async and that uses a fake provider
    def fake_provider(currency, start_date, end_date):
        log.append((start_date, end_date, currency))
        number_of_days = (end_date - start_date).days + 1
        return [(start_date + timedelta(i), 1.42 + (.01 * i)) for i in range(number_of_days)]
    
    db = RatesDB(':memory:', async=async)
    if provider is None:
        provider = fake_provider
    if slow_down_provider:
        provider = slow_down(provider)
    db.register_rate_provider(provider)
    Currency.set_rates_db(db)
    return db, log

def test_unknown_currency():
    # Only known currencies are accepted.
    with raises(ValueError):
        Currency('FOO')

def test_async_and_repeat():
    # If you make an ensure_rates() call and then the same call right after (before the first one
    # is finished, the server will not be hit twice.
    db, log = set_ratedb_for_tests(async=True, slow_down_provider=True)
    db.ensure_rates(date(2008, 5, 20), ['USD'])
    db.ensure_rates(date(2008, 5, 20), ['USD'])
    jointhreads()
    eq_(len(log), 1)
Ejemplo n.º 9
0
 def __init__(self):
     Plugin.__init__(self)
     self.supported_currency_codes = set()
     for code, name, exponent, fallback_rate in self.register_currencies():
         Currency.register(code, name, exponent, latest_rate=fallback_rate)
         self.supported_currency_codes.add(code)