def __init__(self, transactions, date_range=None, chart=None, entry_pred=None, acc_map=None, use_edate=False): self.date_range = date_range self.first_date = None self.last_date = None self._raw_balances = defaultdict(lambda: struct(cdate=defaultdict(lambda: 0), total=0)) if chart: for acc in chart.substantial_accounts(): self._raw_balances[acc].total = 0 for t in transactions: date = t.edate if use_edate else t.date if self.date_range is None or date in self.date_range: if self.first_date is None or date < self.first_date: self.first_date = date if self.last_date is None or date > self.last_date: self.last_date = date for e in t.entries: if entry_pred is None or entry_pred(e): acc = e.account if chart: acc = chart[acc] assert acc is not None assert acc.is_substantial(), 'acc=%r' % (acc,) if acc_map is not None: mapped = acc_map(acc) if mapped is not None: acc = mapped cdate = None if e.cdate is None or self.date_range is None or e.cdate in self.date_range else e.cdate rb = self._raw_balances[acc] rb.cdate[cdate] += e.amount rb.total += e.amount self.pred = lambda a, m: True self._balances = None
def compute_dues(due_accounts, when): due_all = [] for account, entries in due_accounts.items(): entries.sort(key=lambda e: e.cdate or e.transaction.date) due = defaultdict(lambda: struct(account=account, entries=[])) for e in entries: date = e.cdate or when #e.transaction.date amount = e.amount while amount and due: earliest = sorted(due)[0] if sign(due[earliest].entries[0].amount) == sign(amount): break while abs(amount) >= abs(due[earliest].entries[0].amount): amount += due[earliest].entries.pop(0).amount if not due[earliest].entries: del due[earliest] break if amount and earliest in due: e1 = due[earliest].entries[0] assert abs(amount) < abs(e1.amount) due[earliest].entries[0] = e1.replace(amount= e1.amount + amount)._attach(e1.transaction) amount = 0 if amount: due[date].entries.append(e.replace(amount= amount)._attach(e.transaction)) due_all += list(due.items()) due_all.sort(key=lambda a: (a[0], sum(e.amount for e in a[1].entries))) return due_all
def cmd_acc(config, opts): chart = get_chart(config, opts) try: accounts = filter_accounts(chart, opts['<PRED>'].lstrip()) except ValueError as e: raise InvalidArg('<PRED>', e) logging.debug('accounts = %r' % list(map(str, accounts))) common_root_account = abo.account.common_root(accounts) logging.debug('common_root_account = %r' % str(common_root_account)) all_transactions = get_transactions(chart, config, opts) range, bf, transactions = filter_period(chart, all_transactions, opts) if opts['--control']: entries = [e for e in chain(*(t.entries for t in all_transactions)) if chart[e.account] in accounts and (e.cdate or e.transaction.date) in range] entries.sort(key=lambda e: e.cdate or e.transaction.date) else: entries = [e for e in chain(*(t.entries for t in transactions)) if chart[e.account] in accounts] if opts['--omit-empty'] and not entries: return dw = 11 mw = config.money_column_width() bw = config.balance_column_width() if config.output_width(): width = max(50, config.output_width()) pw = max(1, width - (dw + 2 + 2 * (mw + 1) + 1 + bw)) else: pw = 35 width = dw + 2 + pw + 2 * (mw + 1) + 1 + bw fmt = '%-{dw}.{dw}s %-{pw}.{pw}s %{mw}s %{mw}s %{bw}s'.format(**locals()) if not opts['--bare']: if config.heading: yield config.heading.center(width) yield 'STATEMENT OF ACCOUNT'.center(width) yield range_line(range).center(width) if opts['--title']: yield opts['--title'].center(width) elif common_root_account is not None: yield common_root_account.full_name(prefix='', separator=': ').center(width) yield '' yield fmt % ('Date', 'Particulars', 'Debit', 'Credit', 'Balance') yield fmt % ('-' * dw, '-' * pw, '-' * mw, '-' * mw, '-' * bw) tally = struct() tally.balance = 0 tally.totdb = 0 tally.totcr = 0 if bf: def bflines(): for account in accounts: if account.is_substantial() and account.atype is not abo.account.AccountType.ProfitLoss or opts['--bring-forward']: if opts['--control']: amount = bf.cbalance(account) if amount != 0: tally.balance += amount yield fmt % ('', '; '.join(filter(bool, ['Brought forward', account.relative_name(common_root_account)])), config.format_money(-amount) if amount < 0 else '', config.format_money(amount) if amount > 0 else '', config.format_money(tally.balance)) else: for e in sorted(bf.entries(), key=lambda e: (e.cdate or datetime.date.min, e.amount, e.account)): if chart[e.account] is account and e.amount != 0: tally.balance += e.amount yield fmt % ('', '; '.join(filter(bool, ['Brought forward', 'due ' + e.cdate.strftime(r'%-d-%b-%Y') if e.cdate else '', account.relative_name(common_root_account)])), config.format_money(-e.amount) if e.amount < 0 else '', config.format_money(e.amount) if e.amount > 0 else '', config.format_money(tally.balance)) lines = list(bflines()) if not opts['--bare'] or tally.balance != 0: for line in lines: yield line for entry in entries: date = entry.cdate if opts['--control'] and entry.cdate else entry.transaction.edate if opts['--effective'] else entry.transaction.date tally.balance += entry.amount if entry.amount < 0: tally.totdb += entry.amount elif entry.amount > 0: tally.totcr += entry.amount desc = entry.description(with_due=not opts['--control'], config=config) acc = chart[entry.account] if not opts['--short'] and acc is not common_root_account: rel = [] for par in chain(reversed(list(acc.parents_not_in_common_with(common_root_account))), (acc,)): b = par.bare_name() for w in b.split(): if w not in desc: rel.append(b) break if rel: desc = '; '.join(s for s in [':'.join(rel), desc] if s) if not desc and len(entry.transaction.entries) == 2: oe = [e for e in entry.transaction.entries if e is not entry] assert len(oe) == 1 oe = oe[0] desc = chart[oe.account].bare_name() if opts['--control'] or (opts['--effective'] and entry.transaction.edate != entry.transaction.date): desc = config.format_date_short(entry.transaction.date, relative_to=date) + ' ' + desc desc = textwrap.wrap(desc, width=pw) yield fmt % (date.strftime(r'%_d-%b-%Y'), desc.pop(0) if desc else '', config.format_money(-entry.amount) if entry.amount < 0 else '', config.format_money(entry.amount) if entry.amount > 0 else '', config.format_money(tally.balance)) if opts['--wrap']: while desc: yield fmt % ('', desc.pop(0), '', '', '') yield fmt % ('-' * dw, '-' * pw, '-' * mw, '-' * mw, '-' * bw) yield fmt % ('', 'Totals for period', config.format_money(-tally.totdb), config.format_money(tally.totcr), '') yield fmt % ('', 'Balance', '', '', config.format_money(tally.balance))
def remove_account(chart, pred, transactions): from itertools import chain from collections import defaultdict logging.debug("remove") queues = defaultdict(list) todo = list(transactions) done = [] while todo: t = todo.pop(0) remove = defaultdict(lambda: struct(amount=0, entries=[])) keep = [] keep_total = 0 for e in t.entries: if pred(chart[e.account]): s = sign(e.amount) remove[s].amount += e.amount remove[s].entries.append(e) else: keep.append(e) keep_total += e.amount # If the transaction involves exclusively removed accounts, then remove # the entire transaction. if not keep: continue # Cancel removed entries against each other, leaving removable entries # with only one sign. if remove[1].entries and remove[-1].entries: remove_amount = remove[-1].amount + remove[1].amount assert remove_amount == -keep_total if remove_amount == 0: assert keep assert keep_total == 0 done.append(t.replace(entries=keep)) logging.debug(" done %r" % (done[-1],)) continue else: s = sign(remove_amount) e1, e2 = abo.transaction._divide_entries(remove[s].entries, -remove[-s].amount) assert e1 assert e2 assert sum(e.amount for e in e1) == -remove[-s].amount assert sum(e.amount for e in e2) == remove_amount remove = e2 t = t.replace(entries= chain(e2 + keep)) elif remove[1].entries: remove_amount = remove[1].amount remove = remove[1].entries elif remove[-1].entries: remove_amount = remove[-1].amount remove = remove[-1].entries else: done.append(t) logging.debug(" done %r" % (done[-1],)) continue logging.debug("remove %u entries from t = %s %s" % (len(remove), t.amount(), t.date)) while remove: account = remove[0].account entries = [e for e in remove if e.account == account] assert entries remove = [e for e in remove if e.account != account] assert t is not None assert entries == [e for e in t.entries if e.account == account] amount = sum(e.amount for e in entries) assert sign(amount) == sign(remove_amount) assert abs(amount) <= abs(remove_amount) # If this account is not the only one to be removed from this # transaction, then split this transaction into one containing only # this account and a remainder containg all the other removable # accounts. if remove: k1, k2 = abo.transaction._divide_entries(keep, -amount) assert sum(e.amount for e in k1) == -amount assert k2 tr = t.replace(entries= chain(entries + k1)) t = t.replace(entries= chain(remove + k2)) else: tr, t = t, None assert tr is not None queue = queues[account] if not queue or sign(queue[0].amount) == sign(amount): logging.debug(" enqueue %s" % (account,)) queue.append(struct(amount=amount, transaction=tr)) # TODO sort by due date else: while queue and abs(queue[0].amount) <= abs(amount): logging.debug(" amount=%s queue[0].amount=%s" % (amount, queue[0].amount)) assert sign(queue[0].amount) != sign(amount) if abs(queue[0].amount) == abs(amount): k1, keep = keep, None else: k1, k2 = abo.transaction._divide_entries(keep, queue[0].amount) assert k1 assert k2 assert len(k1) <= len(keep) keep = k2 keep_total = sum(e.amount for e in keep) assert sum(e.amount for e in k1) == queue[0].amount done.append(tr.replace(entries= [e for e in chain(k1, queue[0].transaction.entries) if e.account != account])) logging.debug(" done %r" % (done[-1],)) amount += queue[0].amount e1, e2 = abo.transaction._divide_entries(entries, -queue[0].amount) assert sum(e.amount for e in e1) == -queue[0].amount assert sum(e.amount for e in e2) == amount entries = e2 queue.pop(0) if amount and queue: assert entries assert keep logging.debug(" amount=%s queue[0].amount=%s" % (amount, queue[0].amount)) assert abs(amount) < abs(queue[0].amount) assert sign(amount) != sign(queue[0].amount) assert abs(amount) <= abs(keep_total) if keep_total == -amount: k1, keep = keep, None else: k1, k2 = abo.transaction._divide_entries(keep, -amount) assert k1 assert k2 assert len(k1) <= len(keep) keep = k2 assert sum(e.amount for e in k1) == -amount qa = [e for e in queue[0].transaction.entries if e.account == account] qo = [e for e in queue[0].transaction.entries if e.account != account] assert sum(e.amount for e in qa) == queue[0].amount assert sum(e.amount for e in qo) == -queue[0].amount qa1, qa2 = abo.transaction._divide_entries(qa, -amount) qo1, qo2 = abo.transaction._divide_entries(qo, amount) assert sum(e.amount for e in qa1) == -amount assert sum(e.amount for e in qo1) == amount done.append(tr.replace(entries= list(chain(k1, qo1)))) logging.debug(" done %r" % (done[-1],)) queue[0].amount += amount queue[0].transaction = tr.replace(entries= list(chain(qa2, qo2))) return done