cashflow 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []