# Copyright 2008 by Wade Brainerd.
# This file is part of Finance.
#
# Finance is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Finance is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Finance. If not, see .
# Import standard Python modules.
import datetime
import locale
from gettext import gettext as _
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
# Set up localization.
locale.setlocale(locale.LC_ALL, '')
# Import activity module
import finance
BUDGET_HELP = _(
'The Budget view allows you to set a monthly budget for each expense '
'category, and to keep track of your\nbudgets. To set a budget, type '
'the amount in the box to the right of the category.')
class BudgetScreen(Gtk.VBox):
def __init__(self, activity):
GObject.GObject.__init__(self)
self.activity = activity
self.category_total = {}
self.sorted_categories = []
self.budgetbox = Gtk.VBox()
scroll = Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add_with_viewport(self.budgetbox)
self.pack_start(scroll, True, True, 0)
def build(self):
# Build the category totals.
self.category_total = {}
for t in self.activity.visible_transactions:
cat = t['category']
amount = t['amount']
if t['type'] == 'debit':
if not cat in self.category_total:
self.category_total[cat] = amount
else:
self.category_total[cat] += amount
# Generate a list of names sorted by total.
self.sorted_categories = self.category_total.keys()
self.sorted_categories.sort()
# Clear all widgets.
for w in self.budgetbox.get_children():
self.budgetbox.remove(w)
# Build header.
catlabel = Gtk.Label()
catlabel.set_markup('' + _('Category') + '')
spentlabel = Gtk.Label()
spentlabel.set_markup('' + _('Spent') + '')
budgetlabel = Gtk.Label()
budgetlabel.set_markup('' + _('Budget') + '')
headerbox = Gtk.HBox()
headerbox.pack_start(catlabel, False, True, 20)
headerbox.pack_start(spentlabel, True, True, 10)
headerbox.pack_start(budgetlabel, False, True, 20)
self.budgetbox.pack_start(headerbox, False, False, 10)
catgroup = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
catgroup.add_widget(catlabel)
spentgroup = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
spentgroup.add_widget(spentlabel)
budgetgroup = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
budgetgroup.add_widget(budgetlabel)
# Build categories.
for c in self.sorted_categories:
description = c
# If there is no category, display as Unknown
if c is '':
description = _('Unknown')
catbox = Gtk.Label(label=description)
catbox.set_padding(10, 0)
color = finance.get_category_color_str(c)
ebox = Gtk.EventBox()
parse, color = Gdk.Color.parse(color)
ebox.modify_bg(Gtk.StateType.NORMAL, color)
ebox.add(catbox)
catgroup.add_widget(ebox)
bar = Gtk.DrawingArea()
bar.connect('draw', self.bar_draw_cb, c)
spentgroup.add_widget(bar)
budgetentry = Gtk.Entry()
budgetentry.connect('changed', self.budget_changed_cb, c)
budgetentry.set_width_chars(10)
if c in self.activity.data['budgets']:
b = self.activity.data['budgets'][c]
budgetentry.set_text(locale.currency(b['amount'], False))
budgetgroup.add_widget(budgetentry)
#freqcombo = Gtk.ComboBoxText()
#freqcombo.append_text(_('Daily'))
#freqcombo.append_text(_('Weekly'))
#freqcombo.append_text(_('Monthly'))
#freqcombo.append_text(_('Annually'))
#freqcombo.set_active(2)
hbox = Gtk.HBox()
hbox.pack_start(ebox, False, False, 20)
hbox.pack_start(bar, True, True, 10)
hbox.pack_start(budgetentry, False, False, 20)
#hbox.pack_start(freqcombo, True, True, 0)
self.budgetbox.pack_start(hbox, False, False, 5)
self.show_all()
def bar_draw_cb(self, widget, cr, category):
bounds = widget.get_allocation()
# Draw amount of time spent in period if sensible.
period_ratio = None
if self.activity.period != _('Day') and \
self.activity.period != _('Forever'):
period_length = (
self.activity.get_next_period(self.activity.period_start) - \
self.activity.period_start).days
period_ratio = float(
(datetime.date.today() - self.activity.period_start).days) / \
period_length
if period_ratio > 0:
cr.set_source_rgb(0.9, 0.9, 0.9)
cr.rectangle(0, 0, bounds.width * period_ratio, bounds.height)
cr.fill()
# Draw outline.
cr.set_source_rgb(0, 0, 0)
cr.rectangle(0, 0, bounds.width, bounds.height)
cr.stroke()
# Draw arrow and cost.
total = self.category_total[category]
if category in self.activity.data['budgets']:
budget = self.activity.data['budgets'][category]['amount']
# Convert from monthly budget.
if self.activity.period == _('Day'):
budget = budget / 30.0
elif self.activity.period == _('Week'):
budget = budget / 4.0
elif self.activity.period == _('Year'):
budget = budget * 12.0
ratio = total / budget
cr.move_to(5, 5)
cr.line_to(ratio * (bounds.width - 30), 5)
cr.line_to(ratio * (bounds.width - 5), bounds.height / 2)
cr.line_to(ratio * (bounds.width - 30), bounds.height - 5)
cr.line_to(5, bounds.height - 5)
cr.close_path()
if ratio > 1.0:
cr.set_source_rgb(1.0, 0.6, 0.6)
elif period_ratio != None and ratio > period_ratio:
cr.set_source_rgb(0.9, 0.9, 0.6)
else:
cr.set_source_rgb(0.6, 1.0, 0.6)
cr.fill_preserve()
cr.set_source_rgb(0.5, 0.5, 0.5)
cr.stroke()
text = locale.currency(total)
cr.set_source_rgb(0, 0, 0)
cr.set_font_size(20)
x_bearing, y_bearing, width, height = cr.text_extents(text)[:4]
cr.move_to(20, (bounds.height - height) / 2 - y_bearing)
cr.show_text(text)
def budget_changed_cb(self, widget, category):
text = widget.get_text()
if text != '':
self.activity.data['budgets'][category] = {'amount': float(text)}
else:
del self.activity.data['budgets'][category]
self.queue_draw()