Esempio n. 1
0
def command(args):
    import os
    from subprocess import check_call

    from sr.tools import spending
    from sr.tools.budget import BudgetTree

    root = spending.find_root()
    budget = spending.load_budget_with_spending(root)
    line = budget.path(args.budgetline)

    if isinstance(line, BudgetTree):
        print("Summary for tree", args.budgetline)
        budgeted = sum([x.cost for x in line.walk()])
        spent = sum([x.spent for x in line.walk()])
    else:
        print("Summary for line", args.budgetline)
        budgeted = line.cost
        spent = line.spent

    print("Budgeted:\t", budgeted)
    print("Spent:\t\t", spent)

    if spent > budgeted:
        print("OVER BUDGET")

    print("Transactions:")

    account = spending.budget_line_to_account(args.budgetline)
    check_call(["ledger",
                "--file", os.path.join(root, "spending.dat"),
                "reg", account])
Esempio n. 2
0
def command(args):
    import os
    from subprocess import check_call

    from sr.tools import spending
    from sr.tools.budget import BudgetTree

    root = spending.find_root()
    budget = spending.load_budget_with_spending(root)
    line = budget.path(args.budgetline)

    if isinstance(line, BudgetTree):
        print("Summary for tree", args.budgetline)
        budgeted = sum([x.cost for x in line.walk()])
        spent = sum([x.spent for x in line.walk()])
    else:
        print("Summary for line", args.budgetline)
        budgeted = line.cost
        spent = line.spent

    print("Budgeted:\t", budgeted)
    print("Spent:\t\t", spent)

    if spent > budgeted:
        print("OVER BUDGET")

    print("Transactions:")

    account = spending.budget_line_to_account(args.budgetline)
    check_call([
        "ledger", "--file",
        os.path.join(root, "spending.dat"), "reg", account
    ])
Esempio n. 3
0
def command(args):
    from decimal import Decimal as D
    import sys

    from sr.tools import spending
    from sr.tools.budget import BudgetTree

    try:
        root = spending.find_root()
    except spending.NotSpendingRepo:
        print("Please run in spending.git", file=sys.stderr)
        sys.exit(1)

    bud = spending.load_budget_with_spending(root)

    if args.path != "/":
        try:
            budline = bud.path(args.path)
        except KeyError:
            print("Budget line '" + args.path + "' not found", file=sys.stderr)
            sys.exit(1)
    else:
        budline = bud

    total = D(0)
    spent = D(0)
    total_unspent = D(0)

    if isinstance(budline, BudgetTree):
        for line in budline.walk():
            spent += line.spent
            total += line.cost
            unspent = line.cost - line.spent

            if unspent != D(0):
                print(line.name, unspent)
            total_unspent += unspent

        print("-" * 10)

    else:
        total_unspent = budline.cost - budline.spent

    print("Total budget:\t", total)
    print("Total unspent:\t", total_unspent)
Esempio n. 4
0
def command(args):
    from decimal import Decimal as D
    import os
    from subprocess import check_output
    import sys

    import yaml

    from sr.tools import budget, spending
    from sr.tools.config import Config

    config = Config()

    try:
        root = budget.find_root()
    except budget.NotBudgetRepo:
        print("Error: Please run in budget.git", file=sys.stderr)
        exit(1)

    os.chdir(root)
    b = budget.load_budget("./")

    try:
        f = open("funds-in.yaml", "r")
    except IOError:
        if os.path.exists("check"):
            os.execv("./check", ["./check"] + sys.argv[1:])
        exit(1)

    funds_in = yaml.load(f)
    total_in = D(0)

    for i in funds_in["incoming"]:
        # Convert to str to avoid some rounding issues!
        total_in += D(str(i["amount"]))

    # All the lines in funds-in.yaml are divided by 1.1 at the moment
    MAX = total_in * budget.FUDGE_FACTOR

    # Calculate how much has been budgeted in total
    budgeted = D(0)
    for line in b.walk():
        budgeted += (D(1) + line.uncertainty) * line.cost

    if budgeted > MAX:
        print("Fail: Budget is £%s too high" % (budgeted - MAX),
              file=sys.stderr)
        exit(1)

    print("OK: ", end='')
    if budgeted == MAX:
        print("Budget is at maximum.")
    else:
        print("Budget is £%s below maximum." % (MAX - budgeted))

    def list_referenced_lines(root):
        """Return a list of budget lines referenced in ``spending.git``."""

        cmd = [
            "ledger", "--file",
            os.path.join(root, "spending.dat"), "reg", "^Expenses:",
            "--format", "%A\n"
        ]
        output = check_output(cmd, universal_newlines=True).strip()
        lines = output.splitlines()
        return [spending.account_to_budget_line(line) for line in lines]

    spending_root = args.spending
    if spending_root is None:
        spending_root_config = config.get("spending")
        if spending_root_config is not None:
            spending_root = os.path.expanduser(spending_root_config)

    if spending_root is not None:
        # Check stuff with spending.git
        inv_budget_lines = set()

        # Check that all transactions reference valid budget lines
        for line in list_referenced_lines(spending_root):
            try:
                b.path(line)

            except budget.InvalidPath:
                inv_budget_lines.add(line)

        if len(inv_budget_lines):
            print("{} non-existent budget lines referenced from spending.git:".
                  format(len(inv_budget_lines)))

            for line in inv_budget_lines:
                print(" -", line)
        else:
            print("OK: All spending.git budget line references are valid.")

        # Lines that have been closed but don't match their spends
        close_fail = {}

        # Lines that are over budget
        over_budget = {}

        # Load the budget again with spending information
        b = spending.load_budget_with_spending(spending_root)

        # Check that the spends line up with the budget moneywise
        for bline in b.walk():
            if bline.closed:
                # Closed lines must match their spent amount
                if bline.cost != bline.spent:
                    close_fail[bline] = bline.spent

                continue

            line_max = bline.cost * (D(1) + bline.uncertainty)
            if bline.spent > line_max:
                over_budget[bline] = bline.spent

        if len(close_fail):
            print("{0} closed lines do not match the amount spent against "
                  "them:".format(len(close_fail)))
            for line, spent in close_fail.items():
                print(" - {0}: £{1} spent, £{2} allocated".format(
                    line.name, spent, line.cost))
        else:
            print("OK: All closed lines have the correct value.")

        if len(over_budget):
            print("{0} lines are over budget:".format(len(over_budget)))
            for line, spent in over_budget.items():

                print(" - {0}: £{1} spent, £{2} allocated (including fudge)".
                      format(line.name, spent,
                             line.cost * budget.FUDGE_FACTOR))
        else:
            print("OK: No open budget lines are overspent.")
Esempio n. 5
0
def command(args):
    import re
    import sys

    import sr.tools.spending as srspending
    import sr.tools.budget as srbudget

    try:
        spending_root = srspending.find_root()
    except srspending.NotSpendingRepo:
        print("Please run in spending.git repository", file=sys.stderr)
        exit(1)

    budget = srspending.load_budget_with_spending(spending_root)

    path = budget.path(args.LINE)

    if isinstance(path, srbudget.BudgetItem):
        items = [path]
    else:
        items = path.walk()

    for item in items:
        if item.closed:
            # Line is already closed; nothing to do
            continue

        if item.spent > (item.cost * srbudget.FUDGE_FACTOR):
            # More has been spent than the budget line's value
            print(
                "Cannot close {0}: More has been spent than budgeted.".format(
                    item.name),
                file=sys.stderr)
            print("\tSpent: £{0}\n\tBudgeted: £{1}".format(
                item.spent, item.cost * srbudget.FUDGE_FACTOR),
                  file=sys.stderr)
            # Skip this item
            continue

        orig_contents = open(item.fname, 'r').read()
        contents = re.sub(r'^(\s*cost\s*:\s*).+$',
                          r'\g<1>{0}'.format(item.spent), orig_contents, 0,
                          re.MULTILINE)

        # Check that the cost has actually been changed
        # within the string if it actually needed to be
        if item.spent != item.cost and contents == orig_contents:
            print("Warning: Failed to set the cost of {0} -- skipping it.".
                  format(item.name),
                  file=sys.stderr)
            continue

        # Mark as closed
        m = re.search(r"^\s*closed\s*:.*$", contents, flags=re.MULTILINE)
        if m is not None:
            # There's a closed line in there already
            contents = re.sub(r"^(\s*closed\s*:\s*).*$",
                              r"\1true",
                              contents,
                              flags=re.MULTILINE)
        else:
            contents += "\nclosed: true\n"

        with open(item.fname, 'w') as file:
            file.write(contents)
Esempio n. 6
0
def command(args):
    import re
    import sys

    import sr.tools.spending as srspending
    import sr.tools.budget as srbudget

    try:
        spending_root = srspending.find_root()
    except srspending.NotSpendingRepo:
        print("Please run in spending.git repository", file=sys.stderr)
        exit(1)

    budget = srspending.load_budget_with_spending(spending_root)

    path = budget.path(args.LINE)

    if isinstance(path, srbudget.BudgetItem):
        items = [path]
    else:
        items = path.walk()

    for item in items:
        if item.closed:
            # Line is already closed; nothing to do
            continue

        if item.spent > (item.cost * srbudget.FUDGE_FACTOR):
            # More has been spent than the budget line's value
            print("Cannot close {0}: More has been spent than budgeted."
                  .format(item.name), file=sys.stderr)
            print("\tSpent: £{0}\n\tBudgeted: £{1}"
                  .format(item.spent, item.cost * srbudget.FUDGE_FACTOR),
                  file=sys.stderr)
            # Skip this item
            continue

        orig_contents = open(item.fname, 'r').read()
        contents = re.sub(r'^(\s*cost\s*:\s*).+$',
                          r'\g<1>{0}'.format(item.spent),
                          orig_contents, 0, re.MULTILINE)

        # Check that the cost has actually been changed
        # within the string if it actually needed to be
        if item.spent != item.cost and contents == orig_contents:
            print("Warning: Failed to set the cost of {0} -- skipping it."
                  .format(item.name), file=sys.stderr)
            continue

        # Mark as closed
        m = re.search(r"^\s*closed\s*:.*$", contents, flags=re.MULTILINE)
        if m is not None:
            # There's a closed line in there already
            contents = re.sub(r"^(\s*closed\s*:\s*).*$",
                              r"\1true",
                              contents,
                              flags=re.MULTILINE)
        else:
            contents += "\nclosed: true\n"

        with open(item.fname, 'w') as file:
            file.write(contents)
Esempio n. 7
0
def command(args):
    import datetime

    import sr.tools.spending as srspending
    import sr.tools.budget as srbudget
    from sr.tools.trac import TracProxy, WrongServer

    try:
        spending_root = srspending.find_root()
    except srspending.NotSpendingRepo:
        print("Please run in spending.git top level directory",
              file=sys.stderr)
        sys.exit(1)

    try:
        budget = srspending.load_budget_with_spending(spending_root)
    except srbudget.NoBudgetConfig as nbc:
        print("Error:", nbc.message, file=sys.stderr)
        print("Have you initialised the budget submodule?", file=sys.stderr)
        print("If not, run 'git submodule update --init'.", file=sys.stderr)
        sys.exit(1)

    if args.spend_file is not None:
        spend_request = SpendRequest(args.spend_file)
    else:
        spend_request = SpendRequest.from_editor()

    ticket_text = "Payee: {} \\\\\n".format(spend_request.username)

    if spend_request.supplier_url is None:
        ticket_text += "Supplier: {} \\\\\n".format(spend_request.supplier)
    else:
        ticket_text += "Supplier: [{url} {supplier}] \\\\\n" \
                       .format(url=spend_request.supplier_url,
                               supplier=spend_request.supplier)

    budget_line_totals = {}

    for purchase in spend_request.purchases:
        try:
            budget_line = budget.path(purchase.budget_line)
        except KeyError:
            # TODO: Move this check up into the parser so it's caught and the
            # user can fix it
            print('Budget line "{0}" not found', file=sys.stderr)
            sys.exit(1)

        bl_request_total = D(0)

        ticket_text += """
    === Items from [budget:{budget_line}] ===
    {summary} \\\\
    ||= '''Item''' =||= '''Cost''' =||
    """.format(budget_line=purchase.budget_line,
               summary=purchase.summary)

        for item in purchase.items:
            ticket_text += "|| {desc} || £{cost} ||\n".format(desc=item.desc,
                                                              cost=item.cost)

            bl_request_total += item.cost

        # How much has already been spent against this budget line
        spent = budget_line.spent

        req_total = spent + bl_request_total

        # It is over the limit?
        if req_total > budget_line.cost:
            print("Warning: This purchase exceeds the budget line '{0}'"
                  .format(purchase.budget_line))
            print("\tBudget line's value: £{0}".format(budget_line.cost))
            print("\tRequested Expenditure: £{0} ({1}%)"
                  .format(req_total, 100 * (req_total) / budget_line.cost))
            if not input("Continue anyway? [y/N] ").lower() == 'y':
                sys.exit()

        if purchase.budget_line not in budget_line_totals:
            budget_line_totals[purchase.budget_line] = D(0)
        budget_line_totals[purchase.budget_line] += bl_request_total

    ticket_text += """
    === Budget Line Totals ===
    ||= '''Budget Line''' =||= '''Total''' =||
    """
    for line, total in budget_line_totals.items():
        ticket_text += "|| [budget:{line}] || £{total} ||\n" \
                       .format(line=line, total=total)

    print(ticket_text)

    if args.dry_run:
        print("Stopping before actually creating ticket.")
        sys.exit(0)

    try:
        server = TracProxy(server=args.server, port=args.port)
    except WrongServer:
        print("Error: The specified server is not a Trac instance",
              file=sys.stderr)
        sys.exit(1)

    ticketNum = server.ticket.create(spend_request.summary,
                                     ticket_text,
                                     {'component': "Purchasing",
                                      'owner': "treasurer",
                                      'type': "task"})

    if not ticketNum > 0:
        print("Unable to create a valid ticket")
        sys.exit()

    if args.port == 443:
        hostname = args.server
    else:
        hostname = "{0}:{1}".format(args.server, args.port)

    print("Purchasing ticket created: https://{0}/trac/ticket/{1}".format(
        hostname, ticketNum))

    print("Spending Entries:")

    for purchase in spend_request.purchases:
        print()
        today = datetime.date.today()

        i = "{date} ! {summary}\n"
        i += "    {account}\t£{amount}\n"
        i += "    Liabilities:{payee}\n"
        i += "    ; trac: #{trac}"

        print(i.format(date=today.isoformat(),
                       summary=purchase.summary,
                       account=srspending.budget_line_to_account(
                           purchase.budget_line),
                       amount=sum([x.cost for x in purchase.items]),
                       payee=spend_request.username,
                       trac=ticketNum))
Esempio n. 8
0
def command(args):
    from decimal import Decimal as D
    import os
    from subprocess import check_output
    import sys

    import yaml

    from sr.tools import budget, spending
    from sr.tools.config import Config

    config = Config()

    try:
        root = budget.find_root()
    except budget.NotBudgetRepo:
        print("Error: Please run in budget.git", file=sys.stderr)
        exit(1)

    os.chdir(root)
    b = budget.load_budget("./")

    try:
        f = open("funds-in.yaml", "r")
    except IOError:
        if os.path.exists("check"):
            os.execv("./check", ["./check"] + sys.argv[1:])
        exit(1)

    funds_in = yaml.load(f)
    total_in = D(0)

    for i in funds_in["incoming"]:
        # Convert to str to avoid some rounding issues!
        total_in += D(str(i["amount"]))

    # All the lines in funds-in.yaml are divided by 1.1 at the moment
    MAX = total_in * budget.FUDGE_FACTOR

    # Calculate how much has been budgeted in total
    budgeted = D(0)
    for line in b.walk():
        budgeted += (D(1) + line.uncertainty) * line.cost

    if budgeted > MAX:
        print("Fail: Budget is £%s too high" %
              (budgeted - MAX), file=sys.stderr)
        exit(1)

    print("OK: ", end='')
    if budgeted == MAX:
        print("Budget is at maximum.")
    else:
        print("Budget is £%s below maximum." % (MAX - budgeted))

    def list_referenced_lines(root):
        """Return a list of budget lines referenced in ``spending.git``."""

        cmd = ["ledger",
               "--file", os.path.join(root, "spending.dat"),
               "reg", "^Expenses:",
               "--format", "%A\n"]
        output = check_output(cmd, universal_newlines=True).strip()
        lines = output.splitlines()
        return [spending.account_to_budget_line(line) for line in lines]

    spending_root = args.spending
    if spending_root is None:
        spending_root_config = config.get("spending")
        if spending_root_config is not None:
            spending_root = os.path.expanduser(spending_root_config)

    if spending_root is not None:
        # Check stuff with spending.git
        inv_budget_lines = set()

        # Check that all transactions reference valid budget lines
        for line in list_referenced_lines(spending_root):
            try:
                b.path(line)

            except budget.InvalidPath:
                inv_budget_lines.add(line)

        if len(inv_budget_lines):
            print("{} non-existent budget lines referenced from spending.git:"
                  .format(len(inv_budget_lines)))

            for line in inv_budget_lines:
                print(" -", line)
        else:
            print("OK: All spending.git budget line references are valid.")

        # Lines that have been closed but don't match their spends
        close_fail = {}

        # Lines that are over budget
        over_budget = {}

        # Load the budget again with spending information
        b = spending.load_budget_with_spending(spending_root)

        # Check that the spends line up with the budget moneywise
        for bline in b.walk():
            if bline.closed:
                # Closed lines must match their spent amount
                if bline.cost != bline.spent:
                    close_fail[bline] = bline.spent

                continue

            line_max = bline.cost * (D(1) + bline.uncertainty)
            if bline.spent > line_max:
                over_budget[bline] = bline.spent

        if len(close_fail):
            print("{0} closed lines do not match the amount spent against "
                  "them:".format(len(close_fail)))
            for line, spent in close_fail.items():
                print(" - {0}: £{1} spent, £{2} allocated".format(
                    line.name,
                    spent,
                    line.cost))
        else:
            print("OK: All closed lines have the correct value.")

        if len(over_budget):
            print("{0} lines are over budget:".format(len(over_budget)))
            for line, spent in over_budget.items():

                print(" - {0}: £{1} spent, £{2} allocated (including fudge)"
                      .format(line.name, spent,
                              line.cost * budget.FUDGE_FACTOR))
        else:
            print("OK: No open budget lines are overspent.")
Esempio n. 9
0
def command(args):
    import datetime

    import sr.tools.spending as srspending
    import sr.tools.budget as srbudget
    from sr.tools.trac import TracProxy, WrongServer

    try:
        spending_root = srspending.find_root()
    except srspending.NotSpendingRepo:
        print("Please run in spending.git top level directory",
              file=sys.stderr)
        sys.exit(1)

    try:
        budget = srspending.load_budget_with_spending(spending_root)
    except srbudget.NoBudgetConfig as nbc:
        print("Error:", nbc.message, file=sys.stderr)
        print("Have you initialised the budget submodule?", file=sys.stderr)
        print("If not, run 'git submodule update --init'.", file=sys.stderr)
        sys.exit(1)

    if args.spend_file is not None:
        spend_request = SpendRequest(args.spend_file)
    else:
        spend_request = SpendRequest.from_editor()

    ticket_text = "Payee: {} \\\\\n".format(spend_request.username)

    if spend_request.supplier_url is None:
        ticket_text += "Supplier: {} \\\\\n".format(spend_request.supplier)
    else:
        ticket_text += "Supplier: [{url} {supplier}] \\\\\n" \
                       .format(url=spend_request.supplier_url,
                               supplier=spend_request.supplier)

    budget_line_totals = {}

    for purchase in spend_request.purchases:
        try:
            budget_line = budget.path(purchase.budget_line)
        except KeyError:
            # TODO: Move this check up into the parser so it's caught and the
            # user can fix it
            print('Budget line "{0}" not found', file=sys.stderr)
            sys.exit(1)

        bl_request_total = D(0)

        ticket_text += """
    === Items from [budget:{budget_line}] ===
    {summary} \\\\
    ||= '''Item''' =||= '''Cost''' =||
    """.format(budget_line=purchase.budget_line, summary=purchase.summary)

        for item in purchase.items:
            ticket_text += "|| {desc} || £{cost} ||\n".format(desc=item.desc,
                                                              cost=item.cost)

            bl_request_total += item.cost

        # How much has already been spent against this budget line
        spent = budget_line.spent

        req_total = spent + bl_request_total

        # It is over the limit?
        if req_total > budget_line.cost:
            print(
                "Warning: This purchase exceeds the budget line '{0}'".format(
                    purchase.budget_line))
            print("\tBudget line's value: £{0}".format(budget_line.cost))
            print("\tRequested Expenditure: £{0} ({1}%)".format(
                req_total, 100 * (req_total) / budget_line.cost))
            if not input("Continue anyway? [y/N] ").lower() == 'y':
                sys.exit()

        if purchase.budget_line not in budget_line_totals:
            budget_line_totals[purchase.budget_line] = D(0)
        budget_line_totals[purchase.budget_line] += bl_request_total

    ticket_text += """
    === Budget Line Totals ===
    ||= '''Budget Line''' =||= '''Total''' =||
    """
    for line, total in budget_line_totals.items():
        ticket_text += "|| [budget:{line}] || £{total} ||\n" \
                       .format(line=line, total=total)

    print(ticket_text)

    if args.dry_run:
        print("Stopping before actually creating ticket.")
        sys.exit(0)

    try:
        server = TracProxy(server=args.server, port=args.port)
    except WrongServer:
        print("Error: The specified server is not a Trac instance",
              file=sys.stderr)
        sys.exit(1)

    ticketNum = server.ticket.create(spend_request.summary, ticket_text, {
        'component': "Purchasing",
        'owner': "treasurer",
        'type': "task"
    })

    if not ticketNum > 0:
        print("Unable to create a valid ticket")
        sys.exit()

    if args.port == 443:
        hostname = args.server
    else:
        hostname = "{0}:{1}".format(args.server, args.port)

    print("Purchasing ticket created: https://{0}/trac/ticket/{1}".format(
        hostname, ticketNum))

    print("Spending Entries:")

    for purchase in spend_request.purchases:
        print()
        today = datetime.date.today()

        i = "{date} ! {summary}\n"
        i += "    {account}\t£{amount}\n"
        i += "    Liabilities:{payee}\n"
        i += "    ; trac: #{trac}"

        print(
            i.format(date=today.isoformat(),
                     summary=purchase.summary,
                     account=srspending.budget_line_to_account(
                         purchase.budget_line),
                     amount=sum([x.cost for x in purchase.items]),
                     payee=spend_request.username,
                     trac=ticketNum))