cashflow 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 576c6d93c4d566d3524dd76bf75aff69239aaa3f
4
+ data.tar.gz: ffc085bdaf1a9d72824f2bc87528311723dc5449
5
+ SHA512:
6
+ metadata.gz: 84adc6c6a09b7f9825e46443013e1658ea66f174e2147277445e6dcb80a62236ba790dccba2dc709e7725c641f9ebcf52b50bc56290ea2c81629c1c76a0d6b7b
7
+ data.tar.gz: cd3096ba3cab1d2bb8ddfe1be8f21c5e5f44ab51ba926c63e09e04a12feab8cfb4a95c7855bd33794ea5326e4768ac81b8ff2267270f1b3015ad0a5e6799f3c3
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 2.0.0@cashflow --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cashflow.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Mike Fulcher
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ Cashflow
2
+ --------
3
+
4
+ **Compare your latest income & expenses to your average.**
5
+
6
+ Cashflow is a simple command-line application written in Ruby using [Thor](https://github.com/wycats/thor). It's purpose is simple: to analyse your bank transactions (collecting only the _date_ and _amount_) in order to build an average if your income and expenses over the last few months and compare these with the income and expenses for the current month.
7
+
8
+ Only transactions up to the current day of the month are included, so the app gives you good insight into your financial position at this exact point in the month - extremely useful when you have regular monthly transactions such as rent and salary.
9
+
10
+ By default, only transactions from 4 months prior are factored into the calculations (or as far back as the stored transactions go), but this is configurable.
11
+
12
+ Usage
13
+ -----
14
+
15
+ Basic useage is simple: you load a set of transactions (in OFX format), then print a report.
16
+
17
+ ![Useage screenshot](https://raw.github.com/6twenty/cashflow/master/screenshot.png)
18
+
19
+ Commands
20
+ --------
21
+
22
+ ```
23
+ cashflow help # Show this help message
24
+ cashflow help [COMMAND] # Describe available commands or one specific command
25
+ cashflow load PATH # Loads the OFX file at the specified PATH
26
+ cashflow report # Analyses the stored transactions and prints a comparison report
27
+ cashflow flush # Flushes the database, removing all stored transactions
28
+ cashflow version # Prints the current version number
29
+ ```
30
+
31
+ **`cashflow report`** takes a single optional parameter. You can specify the date range for the transactions to compare against by passing "-s" or "--span" followed by a number repensenting the number of months to use.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/cashflow ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Add load path and require the module.
4
+ lib = File.expand_path('../../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'cashflow'
7
+
8
+ # Quick check for shortcuts to the "help"
9
+ # and "version" commands.
10
+ if %w(-v --version).include?(ARGV[0])
11
+ Cashflow::Cli.new.version
12
+ elsif %w(-h --help).include?(ARGV[0])
13
+ Cashflow::Cli.new.help
14
+ else
15
+ Cashflow::Cli.start
16
+ end
data/cashflow.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cashflow/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cashflow"
8
+ spec.version = Cashflow::VERSION
9
+ spec.authors = ["Mike Fulcher"]
10
+ spec.email = ["mike@drawingablank.me"]
11
+ spec.description = %q{Compares your latest income & expense to your average, giving you a quick at-a-glance view of your financial position.}
12
+ spec.summary = %q{Compares your latest income & expense to your average.}
13
+ spec.homepage = "http://github.com/6twenty/cashflow"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_dependency "iconv"
25
+ spec.add_dependency "sqlite3"
26
+ spec.add_dependency "thor"
27
+ spec.add_dependency "ofx"
28
+ end
data/db/cashflow.db ADDED
Binary file
@@ -0,0 +1,93 @@
1
+ module Cashflow
2
+ class Cli < ::Thor
3
+ include Thor::Actions
4
+
5
+ desc "version", "Prints the current version number"
6
+ def version
7
+ say("Cashflow v#{Cashflow::VERSION}", :green)
8
+ end
9
+
10
+ desc "load PATH", "Loads the OFX file at the specified PATH"
11
+ def load(ofx_file_path)
12
+ map_transactions(OFX(ofx_file_path).account.transactions).each(&:save)
13
+ say("Successfully loaded the transactions from '#{ofx_file_path}'", :green)
14
+ end
15
+
16
+ # Option to factor only up to the day of the month.
17
+ desc "report", "Analyses the stored transactions and prints a comparison report"
18
+ option :span, :type => :numeric, :aliases => "-s", :default => 4
19
+ def report
20
+ # Build report.
21
+ report = Report.new(options)
22
+ report.build_report
23
+
24
+ # Define columns widths.
25
+ cols = "%-11s %-11s %-11s %-11s\n"
26
+
27
+ # Intro.
28
+ say("\n")
29
+ say("Using transactions up to and including the #{Date.today.day.ordinalize} of the month")
30
+ say("Transactions begin #{report.start.strftime('%B')} #{report.start.day.ordinalize}, #{report.start.strftime('%Y')}")
31
+ say("\n")
32
+
33
+ # Table header.
34
+ printf(cols, "", "Average", "This month", "Difference")
35
+
36
+ # Print the debits & credits comparison.
37
+ Transaction::TYPES.each do |type|
38
+ # Calculate comparison.
39
+ avg = report.send("#{type}_average")
40
+ current = report.send("#{type}_current_sum")
41
+ diff = report.send("#{type}_diff")
42
+ in_or_out = type == :debits ? 'out' : 'in'
43
+ is_good = diff <= 0
44
+ colour = is_good ? :green : :red
45
+
46
+ # Table row.
47
+ printf(cols, "Money #{in_or_out}:", currencify(avg), currencify(current), set_color(currencify(diff), colour))
48
+ end
49
+
50
+ # Calculate net position.
51
+ net_position = report.debits_diff + report.credits_diff
52
+ is_good = net_position <= 0
53
+ colour = is_good ? :green : :red
54
+ position = is_good ? 'improvement' : 'decline'
55
+
56
+ # Print the net position.
57
+ say("\n")
58
+ say("Comparison: #{set_color(currencify(net_position) + ' ' + position + ' on average', colour)}")
59
+ say("\n")
60
+ end
61
+
62
+ desc "flush", "Flushes the database, removing all stored transactions"
63
+ def flush
64
+ Cashflow.flush
65
+ say("Successfully flushed database", :green)
66
+ end
67
+
68
+ private
69
+
70
+ def currencify(number)
71
+ is_neg = number < 0
72
+ number = number / 100.0
73
+ int, frac = ("%.2f" % number).split('.')
74
+ int.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
75
+ int = int.slice(1..-1) if is_neg
76
+ "$" + int + "." + frac
77
+ end
78
+
79
+ # Takes a collection of OFX transactions and maps them to
80
+ # Transaction instances, which are saved to the database.
81
+ def map_transactions(transactions)
82
+ transactions.map do |record|
83
+ Transaction.new({
84
+ :id => record.fit_id.to_s,
85
+ :type => record.type,
86
+ :amount => record.amount_in_pennies,
87
+ :date => record.posted_at.to_date
88
+ })
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ class String
2
+ def to_date
3
+ year, month, day = self.split("-").map(&:to_i)
4
+ Date.strptime("{ #{year}, #{month}, #{day} }", "{ %Y, %m, %d }")
5
+ end
6
+ end
7
+
8
+ class Fixnum
9
+ def ordinalize
10
+ if (11..13).include?(self % 100)
11
+ "#{self}th"
12
+ else
13
+ case self % 10
14
+ when 1; "#{self}st"
15
+ when 2; "#{self}nd"
16
+ when 3; "#{self}rd"
17
+ else "#{self}th"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,112 @@
1
+ module Cashflow
2
+ class Report
3
+ TYPES = Cashflow::Transaction::TYPES
4
+ attr_accessor *TYPES
5
+ attr_accessor *TYPES.map { |type| :"#{type}_sums" }
6
+ attr_accessor *TYPES.map { |type| :"#{type}_current" }
7
+ attr_accessor *TYPES.map { |type| :"#{type}_current_sum" }
8
+ attr_accessor *TYPES.map { |type| :"#{type}_average" }
9
+ attr_accessor *TYPES.map { |type| :"#{type}_diff" }
10
+ attr_accessor :start
11
+
12
+ def initialize(options)
13
+ date = Date.today << options[:span]
14
+ @start = Date.new(date.year, date.month, 1)
15
+ end
16
+
17
+ def debits
18
+ @debits ||= Transaction.merged(Transaction.debits(start))
19
+ end
20
+
21
+ def credits
22
+ @credits ||= Transaction.merged(Transaction.credits(start))
23
+ end
24
+
25
+ def build_report
26
+ filter_up_to_day_of_the_month!
27
+ separate_current_month!
28
+ group_priors_by_month!
29
+ sum_all_by_month!
30
+ calculate_averages!
31
+ calculate_differences!
32
+ end
33
+
34
+ private
35
+
36
+ # Filters the credits and debits to only include the transactions
37
+ # that occurred up to the current day of the month.
38
+ def filter_up_to_day_of_the_month!
39
+ TYPES.each do |type|
40
+ filtered = send(type).select do |transaction|
41
+ transaction.date.day <= Date.today.day
42
+ end
43
+ send("#{type}=", filtered)
44
+ end
45
+ end
46
+
47
+ # Divide the credits & debits into those for the current month
48
+ # and all others prior.
49
+ def separate_current_month!
50
+ TYPES.each do |type|
51
+ this_month = send(type).select do |transaction|
52
+ transaction.date.year == Date.today.year &&
53
+ transaction.date.month == Date.today.month
54
+ end
55
+ send("#{type}_current=", this_month)
56
+ send("#{type}=", send(type) - this_month)
57
+ end
58
+ end
59
+
60
+ # Group all prior transactions into months (as a hash).
61
+ def group_priors_by_month!
62
+ TYPES.each do |type|
63
+ by_month = send(type).inject({}) do |hash, transaction|
64
+ key = "#{transaction.date.year}-#{transaction.date.month}"
65
+ hash[key] ||= []
66
+ hash[key] << transaction
67
+ hash
68
+ end
69
+ send("#{type}=", by_month)
70
+ end
71
+ end
72
+
73
+ # Sum all transactions by month.
74
+ def sum_all_by_month!
75
+ TYPES.each do |type|
76
+ # Current month:
77
+ current_sum = send("#{type}_current").inject(0) do |sum, transaction|
78
+ sum += transaction.amount
79
+ sum
80
+ end
81
+ send("#{type}_current_sum=", current_sum)
82
+
83
+ # Prior months:
84
+ prior_sums = {}
85
+ send("#{type}").each do |key, transactions|
86
+ prior_sums[key] = transactions.inject(0) do |sum, transaction|
87
+ sum += transaction.amount
88
+ sum
89
+ end
90
+ end
91
+ send("#{type}_sums=", prior_sums)
92
+ end
93
+ end
94
+
95
+ # Calculate the average debits & credits based on the prior transactions.
96
+ def calculate_averages!
97
+ TYPES.each do |type|
98
+ average = send("#{type}_sums").values.inject(0, :+) / send("#{type}_sums").values.length
99
+ send("#{type}_average=", average)
100
+ end
101
+ end
102
+
103
+ # Calculate the difference between the current month totals and the prior averages.
104
+ def calculate_differences!
105
+ TYPES.each do |type|
106
+ diff = send("#{type}_average") - send("#{type}_current_sum")
107
+ send("#{type}_diff=", diff)
108
+ end
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,60 @@
1
+ module Cashflow
2
+ class Transaction
3
+ attr_accessor :id, :type, :amount, :date
4
+
5
+ TYPES = %i(debits credits)
6
+
7
+ class << self
8
+
9
+ def select_from_sql(sql)
10
+ Cashflow.db.execute(sql).map do |(id, type, amount, date)|
11
+ Transaction.new({
12
+ :id => id,
13
+ :type => type.to_sym,
14
+ :amount => amount,
15
+ :date => date.to_date
16
+ })
17
+ end
18
+ end
19
+
20
+ def all(since)
21
+ select_from_sql("SELECT * FROM #{Cashflow.table_name} WHERE date >= '#{since.to_s}' ORDER BY date ASC")
22
+ end
23
+
24
+ def debits(since)
25
+ select_from_sql("SELECT * FROM #{Cashflow.table_name} WHERE date >= '#{since.to_s}' AND type = 'debit' ORDER BY date ASC")
26
+ end
27
+
28
+ def credits(since)
29
+ select_from_sql("SELECT * FROM #{Cashflow.table_name} WHERE date >= '#{since.to_s}' AND type = 'credit' ORDER BY date ASC")
30
+ end
31
+
32
+ def merged(transactions)
33
+ by_date = {}
34
+ transactions.each do |transaction|
35
+ key = "#{transaction.date.to_s}-#{transaction.type.to_s}"
36
+ # If a transaction already exists for this date and type, combine them.
37
+ transaction.amount += by_date[key].amount if by_date[key]
38
+ by_date[key] = transaction
39
+ end
40
+ # Return the combined transactions, sorted by date.
41
+ by_date.values.sort_by { |transaction| transaction.date }
42
+ end
43
+ end
44
+
45
+ def initialize(attrs)
46
+ attrs.each do |(attribute, value)|
47
+ send("#{attribute}=", value)
48
+ end
49
+ end
50
+
51
+ def save
52
+ begin
53
+ Cashflow.db.execute("INSERT INTO activity (id, type, amount, date) VALUES (?, ?, ?, ?)", [ id, type.to_s, amount, date.to_s ])
54
+ true
55
+ rescue Exception => e
56
+ false
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module Cashflow
2
+ VERSION = "0.0.1"
3
+ end
data/lib/cashflow.rb ADDED
@@ -0,0 +1,49 @@
1
+ # Ruby libs.
2
+ require 'kconv' # For OFX gem fix.
3
+ require 'logger'
4
+ require 'sqlite3'
5
+ require 'thor'
6
+ require 'ofx'
7
+
8
+ # App modules.
9
+ require 'cashflow/version'
10
+ require 'cashflow/patches'
11
+ require 'cashflow/transaction'
12
+ require 'cashflow/report'
13
+ require 'cashflow/cli'
14
+
15
+ module Cashflow
16
+
17
+ # Define the database table name.
18
+ def self.table_name
19
+ "activity"
20
+ end
21
+
22
+ # Define the root directory.
23
+ def self.root
24
+ @root ||= File.expand_path(File.join(__FILE__, '../../'))
25
+ end
26
+
27
+ # Set up & retrieve the logger.
28
+ def self.logger
29
+ @logger ||= Logger.new(File.join(root, 'log/cashflow.log'))
30
+ end
31
+
32
+ # Initialize & retrieve the database.
33
+ def self.db
34
+ @db ||= begin
35
+ # Initialize the database. Create the table if necessary.
36
+ db = SQLite3::Database.new(File.join(root, 'db/cashflow.db'))
37
+ db.execute("CREATE TABLE IF NOT EXISTS #{table_name} (id string primary key, type varchar(6), amount integer, date date);")
38
+ # Return the database connection.
39
+ db
40
+ end
41
+ end
42
+
43
+ # Erase the database.
44
+ def self.flush
45
+ db.execute("DROP TABLE IF EXISTS #{table_name};")
46
+ @db = nil
47
+ end
48
+
49
+ end
data/log/cashflow.log ADDED
File without changes
data/screenshot.png ADDED
Binary file
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cashflow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mike Fulcher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: iconv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ofx
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Compares your latest income & expense to your average, giving you a quick
98
+ at-a-glance view of your financial position.
99
+ email:
100
+ - mike@drawingablank.me
101
+ executables:
102
+ - cashflow
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - .gitignore
107
+ - .rvmrc
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/cashflow
113
+ - cashflow.gemspec
114
+ - db/cashflow.db
115
+ - lib/cashflow.rb
116
+ - lib/cashflow/cli.rb
117
+ - lib/cashflow/patches.rb
118
+ - lib/cashflow/report.rb
119
+ - lib/cashflow/transaction.rb
120
+ - lib/cashflow/version.rb
121
+ - log/cashflow.log
122
+ - screenshot.png
123
+ homepage: http://github.com/6twenty/cashflow
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - '>='
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.0.0
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Compares your latest income & expense to your average.
147
+ test_files: []