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')
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')
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))
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])
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)