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 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