cashflow 0.0.6 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d2d210d93eb390fa23ea7f01df90370c205bc33d
4
- data.tar.gz: ad8df9cff209c08540e4c9ac630ae5f35890e602
3
+ metadata.gz: 21cd2b9ecd6447a4baca231557a05e16520b3be9
4
+ data.tar.gz: cfacacd1b4925cf5a5af951b01b1d8d2782f5061
5
5
  SHA512:
6
- metadata.gz: 54e603219f3177f595928136a92bd665d09a90044cd51059c4341459dddfee358f959a055d32cea4d56ffd9ab96f11b4166cc177d53c5533a88491641ef25e14
7
- data.tar.gz: b1137ac647b1b4c02e5432146ecc4617a8a68c4df01ed7bb92a8322fad1302e99abcbda79cf592a3b9bbb665b344da596baeb1539dac4ceea68c092463c00076
6
+ metadata.gz: d48aa2900d9fa8213a83e0697f9d8e534050a3ef0e5cc04049ccedd0f633cf8311ccce172370a4b03708c5f3ff9767bb3e17c1dc1897beefafe978b98c38a607
7
+ data.tar.gz: 4271b448b3a7765e8d45e12876805526f8e283a11da3a78430cf5cdd929bd0b3864d97cd70d981069b0e4b5c8663d202556b8ffee7e67c266d483c51f10ee9d8
data/.gitignore CHANGED
@@ -15,4 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
- TODO
18
+ TODO
19
+ db/*.sqlite3
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ cashflow
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0
data/Gemfile CHANGED
@@ -1,4 +1,2 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in cashflow.gemspec
4
2
  gemspec
data/README.md CHANGED
@@ -5,7 +5,7 @@ Cashflow
5
5
 
6
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
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.
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
9
 
10
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
11
 
@@ -26,8 +26,9 @@ cashflow help # Show this help message
26
26
  cashflow help COMMAND # Describe available commands or one specific command
27
27
  cashflow load PATH # Loads the OFX file at the specified PATH
28
28
  cashflow report # Analyses the stored transactions and prints a comparison report
29
- cashflow flush # Flushes the database, removing all stored transactions
30
29
  cashflow version # Prints the current version number
31
30
  ```
32
31
 
33
- `cashflow report` takes optional parameters. 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. You can also specify the target day of the month to filter transactions against by passing `-d` or `--day` followed by a number representing the desired day of the month.
32
+ `cashflow load` may be used mutiple times -- duplicate transactions will not be imported. You may also load `ofx` exports for multiple accounts.
33
+
34
+ `cashflow report` takes optional parameters. 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. To print a report for a single account only, you may pass `-a` or `--account` followed by the account name or number (this is displayed when the file is imported).
data/Rakefile CHANGED
@@ -1 +1 @@
1
- require 'bundler/gem_tasks'
1
+ require 'bundler/gem_tasks'
data/cashflow.gemspec CHANGED
@@ -1,4 +1,4 @@
1
- # coding: utf-8
1
+ # encoding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'cashflow/version'
@@ -21,8 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake"
23
23
 
24
- spec.add_dependency "iconv", "~> 1.0.2"
25
- spec.add_dependency "sqlite3", "~> 1.3.7"
24
+ spec.add_dependency "iconv", "~> 1.0.3"
25
+ spec.add_dependency "sqlite3", "~> 1.3.8"
26
+ spec.add_dependency "active_record_migrations", "~> 4.0.0"
26
27
  spec.add_dependency "thor", "~> 0.18.1"
27
- spec.add_dependency "ofx", "~> 0.3.1"
28
- end
28
+ spec.add_dependency "ofx", "~> 0.3.2"
29
+ end
@@ -0,0 +1,11 @@
1
+ class CreateTransaction < ActiveRecord::Migration
2
+ def change
3
+ create_table :transactions do |t|
4
+ t.string :transaction_id, null: false
5
+ t.string :transaction_type, null: false
6
+ t.string :account_number, null: false
7
+ t.integer :amount, null: false
8
+ t.date :date, null: false
9
+ end
10
+ end
11
+ end
data/db/schema.rb ADDED
@@ -0,0 +1,24 @@
1
+ # encoding: UTF-8
2
+ # This file is auto-generated from the current state of the database. Instead
3
+ # of editing this file, please use the migrations feature of Active Record to
4
+ # incrementally modify your database, and then regenerate this schema definition.
5
+ #
6
+ # Note that this schema.rb definition is the authoritative source for your
7
+ # database schema. If you need to create the application database on another
8
+ # system, you should be using db:schema:load, not running all the migrations
9
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
11
+ #
12
+ # It's strongly recommended that you check this file into your version control system.
13
+
14
+ ActiveRecord::Schema.define(version: 20131110065134) do
15
+
16
+ create_table "transactions", force: true do |t|
17
+ t.string "transaction_id", null: false
18
+ t.string "transaction_type", null: false
19
+ t.string "account_number", null: false
20
+ t.integer "amount", null: false
21
+ t.date "date", null: false
22
+ end
23
+
24
+ end
data/lib/cashflow.rb CHANGED
@@ -1,52 +1,48 @@
1
1
  # Ruby libs.
2
- require 'kconv' # For OFX gem fix.
3
- require 'logger'
2
+ require 'rake'
4
3
  require 'sqlite3'
4
+ require 'active_record'
5
+ require 'active_record_migrations'
5
6
  require 'thor'
6
7
  require 'ofx'
7
8
 
8
9
  # App modules.
9
10
  require 'cashflow/version'
10
11
  require 'cashflow/patches'
12
+ require 'cashflow/config'
11
13
  require 'cashflow/transaction'
12
14
  require 'cashflow/report'
13
15
  require 'cashflow/cli'
14
16
 
15
17
  module Cashflow
18
+ class << self
16
19
 
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
20
+ def create_directory
21
+ FileUtils.mkdir_p(File.expand_path('~/.cashflow'))
22
+ end
26
23
 
27
- # Set up & retrieve the logger.
28
- def self.logger
29
- @logger ||= Logger.new(File.join(root, 'log/cashflow.log'))
30
- end
24
+ def create_database
25
+ unless File.exists?(File.expand_path(Config.db_config[Config.env]['database']))
26
+ Rake::Task['db:create'].invoke
27
+ Rake::Task['db:schema:load'].invoke
28
+ end
29
+ end
31
30
 
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.expand_path('~/.cashflow/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
31
+ def establish_connection
32
+ ::ActiveRecord::Base.establish_connection(Config.db_config[Config.env])
40
33
  end
41
- end
42
34
 
43
- # Erase the database.
44
- def self.flush
45
- db.execute("DROP TABLE IF EXISTS #{table_name};")
46
- @db = nil
47
35
  end
36
+ end
48
37
 
38
+ ActiveRecordMigrations.configure do |c|
39
+ c.environment = ENV['CASHFLOW_ENV'] || 'production'
40
+ c.database_configuration = Cashflow::Config.db_config
41
+ c.db_dir = File.join(Cashflow::Config.root_path, "db").to_s
49
42
  end
50
43
 
51
- # Ensure cashflow is in the home directory.
52
- FileUtils.mkdir_p(File.expand_path('~/.cashflow'))
44
+ ActiveRecordMigrations.load_tasks
45
+
46
+ Cashflow.create_directory
47
+ Cashflow.create_database
48
+ Cashflow.establish_connection
data/lib/cashflow/cli.rb CHANGED
@@ -2,6 +2,9 @@ module Cashflow
2
2
  class Cli < ::Thor
3
3
  include Thor::Actions
4
4
 
5
+ # Commands
6
+ # ========
7
+
5
8
  desc "version", "Prints the current version number"
6
9
  def version
7
10
  say("Cashflow v#{Cashflow::VERSION}", :green)
@@ -9,96 +12,36 @@ module Cashflow
9
12
 
10
13
  desc "load PATH", "Loads the OFX file at the specified PATH"
11
14
  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)
15
+ account = OFX(ofx_file_path).account
16
+ store_ofx_transactions(account.transactions, account.id)
17
+ say("Successfully loaded the transactions for #{account.id}", :green)
14
18
  end
15
19
 
16
- # Option to factor only up to the day of the month.
17
20
  desc "report", "Analyses the stored transactions and prints a comparison report"
18
- option :span, :type => :numeric, :aliases => "-s", :default => 4
19
- option :day, :type => :numeric, :aliases => "-d"
21
+ option :span, :type => :numeric, :aliases => "-s", :default => 3
22
+ option :account, :type => :string, :aliases => "-a"
20
23
  def report
21
- # Normalize the options.
22
- months_span = options[:span] > 0 ? options[:span] : 4
23
- if options[:day] && options[:day] <= Date.today.day && options[:day] > 0
24
- day_of_month = options[:day]
25
- elsif options[:day] && options[:day] <= 0
26
- day_of_month = 1
27
- else
28
- day_of_month = Date.today.day
29
- end
30
-
31
- # Build report.
32
- report = Report.new(months_span, day_of_month)
33
- report.build_report!
34
-
35
- # Define columns widths.
36
- cols = "%-11s %-11s %-11s %-11s\n"
37
-
38
- # Intro.
39
- say("\n")
40
- say("Using transactions up to and including the #{report.end_date.day.ordinalize} of the month")
41
- say("Transactions begin #{report.start_date.strftime('%B')} #{report.start_date.day.ordinalize}, #{report.start_date.strftime('%Y')}")
42
- say("\n")
43
-
44
- # Table header.
45
- printf(cols, "", "Average", "This month", "Difference")
46
-
47
- # Print the debits & credits comparison.
48
- Transaction::TYPES.each do |type|
49
- # Calculate comparison.
50
- avg = report.send("#{type}_average")
51
- current = report.send("#{type}_current_sum")
52
- diff = report.send("#{type}_diff")
53
- in_or_out = type == :debits ? 'out' : 'in'
54
- is_good = diff <= 0
55
- colour = is_good ? :green : :red
56
-
57
- # Table row.
58
- printf(cols, "Money #{in_or_out}:", currencify(avg), currencify(current), set_color(currencify(diff), colour))
59
- end
60
-
61
- # Calculate net position.
62
- net_position = report.debits_diff + report.credits_diff
63
- is_good = net_position <= 0
64
- colour = is_good ? :green : :red
65
- position = is_good ? 'improvement' : 'decline'
66
-
67
- # Print the net position.
68
- say("\n")
69
- say("Comparison: #{set_color(currencify(net_position) + ' ' + position + ' on average', colour)}")
70
- say("\n")
24
+ Report.new(options).print(self)
71
25
  end
72
26
 
73
- desc "flush", "Flushes the database, removing all stored transactions"
74
- def flush
75
- Cashflow.flush
76
- say("Successfully flushed database", :green)
77
- end
27
+ # Instance methods
28
+ # ================
78
29
 
79
30
  private
80
31
 
81
- def currencify(number)
82
- is_neg = number < 0
83
- number = number / 100.0
84
- int, frac = ("%.2f" % number).split('.')
85
- int.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
86
- int = int.slice(1..-1) if is_neg
87
- "$" + int + "." + frac
88
- end
89
-
90
32
  # Takes a collection of OFX transactions and maps them to
91
33
  # Transaction instances, which are saved to the database.
92
- def map_transactions(transactions)
93
- transactions.map do |record|
94
- Transaction.new({
95
- :id => record.fit_id.to_s,
96
- :type => record.type,
97
- :amount => record.amount_in_pennies,
98
- :date => record.posted_at.to_date
34
+ def store_ofx_transactions(transactions, account_no)
35
+ transactions.each do |record|
36
+ Transaction.create({
37
+ transaction_id: "#{account_no} #{record.fit_id}",
38
+ transaction_type: record.type.to_s,
39
+ account_number: account_no,
40
+ amount: record.amount_in_pennies,
41
+ date: record.posted_at.to_date
99
42
  })
100
43
  end
101
44
  end
102
45
 
103
46
  end
104
- end
47
+ end
@@ -0,0 +1,29 @@
1
+ module Cashflow
2
+ module Config
3
+ class << self
4
+
5
+ def env
6
+ ENV['CASHFLOW_ENV'] || 'production'
7
+ end
8
+
9
+ def root_path
10
+ @@root_path ||= File.expand_path('../../..', __FILE__)
11
+ end
12
+
13
+ def db_config
14
+ {
15
+ 'production' => {
16
+ 'adapter' => 'sqlite3',
17
+ 'database' => '~/.cashflow/cashflow.sqlite3'
18
+ },
19
+
20
+ 'development' => {
21
+ 'adapter' => 'sqlite3',
22
+ 'database' => File.join(Cashflow::Config.root_path, "db/#{env}.sqlite3").to_s
23
+ }
24
+ }
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -1,4 +1,5 @@
1
1
  class String
2
+ # Assumes date represented as YYYY-MM-DD.
2
3
  def to_date
3
4
  year, month, day = self.split("-").map(&:to_i)
4
5
  Date.strptime("{ #{year}, #{month}, #{day} }", "{ %Y, %m, %d }")
@@ -18,4 +19,14 @@ class Fixnum
18
19
  end
19
20
  end
20
21
  end
21
- end
22
+
23
+ # Assumes integer representing amount in cents.
24
+ def to_currency(allow_neg=true)
25
+ is_neg = self < 0
26
+ number = self / 100.0
27
+ int, frac = ("%.2f" % number).split('.')
28
+ int.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
29
+ int.slice!(0) if is_neg
30
+ [ '$', int, '.', frac ].join.prepend(allow_neg && is_neg ? '-' : '')
31
+ end
32
+ end
@@ -1,128 +1,207 @@
1
1
  module Cashflow
2
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_date, :end_date
11
-
12
- def initialize(months_span, day_of_month)
13
- # Set the initial start date to the first of the month,
14
- # however many months ago.
15
- date = Date.today << months_span
16
- date = Date.new(date.year, date.month, 1)
17
- # Get the earliest transaction date since the start date (inclusive).
18
- date = Transaction.all(date).first.date
19
- # If the earliest transaction took place AFTER the target
20
- # day of the month, then move to the following month.
21
- date = date >> 1 if date.day > day_of_month
22
- # Whatever the final date is, ensure it starts at the beginning of the month.
23
- @start_date = Date.new(date.year, date.month, 1)
24
-
25
- # Set the end date.
26
- @end_date = Date.new(Date.today.year, Date.today.month, day_of_month)
27
- end
28
3
 
29
- def debits
30
- @debits ||= Transaction.merged(Transaction.debits(start_date))
31
- end
4
+ # Accessors
5
+ # =========
6
+
7
+ attr_accessor :summary
8
+
9
+ # Instance methods
10
+ # ================
11
+
12
+ def initialize(options)
13
+ if options
14
+ @options = options
15
+
16
+ # Get the start date.
17
+ start_date = get_start_date
18
+
19
+ # Retrieve all transactions since the start date,
20
+ # optionally scoped by a given account number.
21
+ transactions = get_transactions(start_date, options[:account])
22
+
23
+ # In prior months, discard any transactions past the
24
+ # current day of the month.
25
+ transactions = restrict_by_day_of_month(transactions, Date.today.day)
26
+
27
+ # Build a hash to contain the transactions grouped by month.
28
+ by_month = build_hash_by_month
29
+
30
+ # Organise the transactions by month and by type.
31
+ by_month = group_transactions_by_month_and_type(by_month, transactions)
32
+
33
+ # Sum the debits & credits for each month.
34
+ sums = sum_transactions(by_month)
32
35
 
33
- def credits
34
- @credits ||= Transaction.merged(Transaction.credits(start_date))
36
+ # Work out the averages.
37
+ averages = determine_averages(sums)
38
+
39
+ # Work out the difference between the current month and prior average.
40
+ diff = compare_current_to_average(sums[:current], averages)
41
+
42
+ # Build & expose a summary.
43
+ @summary = {
44
+ start_date: start_date,
45
+ sums: sums,
46
+ averages: averages,
47
+ diff: diff
48
+ }
49
+ end
35
50
  end
36
51
 
37
- def build_report!
38
- filter_up_to_day_of_the_month!
39
- separate_current_month!
40
- group_priors_by_month!
41
- sum_all_by_month!
42
- calculate_averages!
43
- calculate_differences!
52
+ # Set the initial start date to the first of the month, [n] months ago.
53
+ def get_start_date
54
+ date = Date.today << (@options[:span])
55
+ Date.new(date.year, date.month, 1)
44
56
  end
45
57
 
46
- private
58
+ def get_transactions(start_date, account_number=nil)
59
+ transactions = Transaction.where("date >= ?", start_date)
60
+ transactions = transactions.where(account_number: account_number) if account_number
61
+ transactions
62
+ end
47
63
 
48
- # Filters the credits and debits to only include the transactions
49
- # that occurred up to the current day of the month.
50
- def filter_up_to_day_of_the_month!
51
- TYPES.each do |type|
52
- filtered = send(type).select do |transaction|
53
- transaction.date.day <= end_date.day
54
- end
55
- send("#{type}=", filtered)
64
+ def restrict_by_day_of_month(transactions, day_of_month)
65
+ transactions.select do |transaction|
66
+ transaction.date.day <= day_of_month
56
67
  end
57
68
  end
58
69
 
59
- # Divide the credits & debits into those for the current month
60
- # and all others prior.
61
- def separate_current_month!
62
- TYPES.each do |type|
63
- this_month = send(type).select do |transaction|
64
- transaction.date.year == Date.today.year &&
65
- transaction.date.month == Date.today.month
66
- end
67
- send("#{type}_current=", this_month)
68
- send("#{type}=", send(type) - this_month)
70
+ def build_hash_by_month
71
+ by_month = { current: { debits: [], credits: [] } }
72
+ today = Date.today
73
+
74
+ @options[:span].times do |n|
75
+ date = today << (n+1)
76
+ key = "#{date.year}-#{date.month}"
77
+ by_month[key] = { debits: [], credits: [] }
69
78
  end
79
+
80
+ # {
81
+ # :current => { debits: [], credits: [] },
82
+ # '2013-10' => { debits: [], credits: [] },
83
+ # '2013-9' => { debits: [], credits: [] },
84
+ # '2013-8' => { debits: [], credits: [] }
85
+ # }
86
+
87
+ by_month
70
88
  end
71
89
 
72
- # Group all prior transactions into months (as a hash).
73
- def group_priors_by_month!
74
- TYPES.each do |type|
75
- by_month = send(type).inject({}) do |hash, transaction|
76
- key = "#{transaction.date.year}-#{transaction.date.month}"
77
- hash[key] ||= []
78
- hash[key] << transaction
79
- hash
80
- end
81
- send("#{type}=", by_month)
90
+ def group_transactions_by_month_and_type(by_month, transactions)
91
+ transactions.each do |transaction|
92
+ this, year, month = Date.today, transaction.date.year, transaction.date.month
93
+ type = "#{transaction.transaction_type}s".to_sym # pluralize by appending "s"
94
+ key = "#{year}-#{month}"
95
+ key = :current if year == this.year && month == this.month
96
+ by_month[key][type] << transaction
82
97
  end
98
+
99
+ by_month
83
100
  end
84
101
 
85
- # Sum all transactions by month.
86
- def sum_all_by_month!
87
- TYPES.each do |type|
88
- # Current month:
89
- current_sum = send("#{type}_current").inject(0) do |sum, transaction|
90
- sum += transaction.amount
91
- sum
92
- end
93
- send("#{type}_current_sum=", current_sum)
102
+ def sum_transactions(by_month)
103
+ sums = {}
94
104
 
95
- # Prior months:
96
- prior_sums = {}
97
- send("#{type}").each do |key, transactions|
98
- prior_sums[key] = transactions.inject(0) do |sum, transaction|
105
+ by_month.each do |month, by_type|
106
+ by_type.each do |type, transactions|
107
+ sums[month] ||= {}
108
+ sums[month][type] = transactions.inject(0) do |sum, transaction|
99
109
  sum += transaction.amount
100
110
  sum
101
111
  end
102
112
  end
103
- send("#{type}_sums=", prior_sums)
104
113
  end
114
+
115
+ # {
116
+ # :current => { debits: -100, credits: 100 },
117
+ # '2013-10' => { debits: -100, credits: 100 },
118
+ # '2013-9' => { debits: -100, credits: 100 },
119
+ # '2013-8' => { debits: -100, credits: 100 }
120
+ # }
121
+
122
+ sums
105
123
  end
106
124
 
107
- # Calculate the average debits & credits based on the prior transactions.
108
- def calculate_averages!
109
- TYPES.each do |type|
125
+ def determine_averages(sums)
126
+ averages = { debits: 0, credits: 0 }
127
+
128
+ sums.each do |month, by_type|
129
+ unless month == :current
130
+ by_type.each do |type, sum|
131
+ averages[type] += sum
132
+ end
133
+ end
134
+ end
135
+
136
+ averages.each do |type, sum|
110
137
  begin
111
- average = send("#{type}_sums").values.inject(0, :+) / send("#{type}_sums").values.length
112
- rescue
113
- average = 0
138
+ averages[type] = sum / @options[:span]
139
+ rescue ZeroDivisionError
140
+ averages[type] = 0
114
141
  end
115
- send("#{type}_average=", average)
116
142
  end
143
+
144
+ # {
145
+ # debits: -100,
146
+ # credits: 100
147
+ # }
148
+
149
+ averages
150
+ end
151
+
152
+ def compare_current_to_average(current, averages)
153
+ diffs = {}
154
+
155
+ averages.each do |type, average|
156
+ diffs[type] = current[type] - average
157
+ end
158
+
159
+ # {
160
+ # debits: 0,
161
+ # credits: 0
162
+ # }
163
+
164
+ diffs
117
165
  end
118
166
 
119
- # Calculate the difference between the current month totals and the prior averages.
120
- def calculate_differences!
121
- TYPES.each do |type|
122
- diff = send("#{type}_average") - send("#{type}_current_sum")
123
- send("#{type}_diff=", diff)
167
+ # Summarise and print the analysis.
168
+ def print(cli)
169
+ # Define columns widths.
170
+ cols = "%-11s %-11s %-11s %-11s\n"
171
+
172
+ # Intro.
173
+ cli.say("\n")
174
+ cli.say("Using transactions up to and including the #{Date.today.day.ordinalize} of the month")
175
+ cli.say("Transactions begin #{summary[:start_date].strftime('%B')} #{summary[:start_date].day.ordinalize}, #{summary[:start_date].strftime('%Y')}")
176
+ cli.say("\n")
177
+
178
+ # Table header.
179
+ printf(cols, "", "Average", "This month", "Difference")
180
+
181
+ # Print the debits & credits comparison.
182
+ %i(debits credits).each do |type|
183
+ current = summary[:sums][:current][type]
184
+ avg = summary[:averages][type]
185
+ diff = summary[:diff][type]
186
+ in_or_out = type == :debits ? 'out' : 'in'
187
+ is_good = diff >= 0
188
+ colour = is_good ? :green : :red
189
+
190
+ # Table row.
191
+ printf(cols, "Money #{in_or_out}:", avg.to_currency, current.to_currency, cli.set_color(diff.to_currency, colour))
124
192
  end
193
+
194
+ # Calculate net position.
195
+ net_position = summary[:diff][:debits] + summary[:diff][:credits]
196
+ is_good = net_position >= 0
197
+ colour = is_good ? :green : :red
198
+ position = is_good ? 'improvement' : 'decline'
199
+
200
+ # Print the net position.
201
+ cli.say("\n")
202
+ cli.say("Comparison: #{cli.set_color(net_position.to_currency(false) + ' ' + position + ' compared to average', colour)}")
203
+ cli.say("\n")
125
204
  end
126
205
 
127
206
  end
128
- end
207
+ end
@@ -1,60 +1,11 @@
1
1
  module Cashflow
2
- class Transaction
3
- attr_accessor :id, :type, :amount, :date
2
+ class Transaction < ActiveRecord::Base
4
3
 
5
- TYPES = %i(debits credits)
4
+ # Validations
5
+ # ===========
6
6
 
7
- class << self
7
+ validates :transaction_id, uniqueness: true
8
+ validates_presence_of :transaction_type, :account_number, :amount, :date
8
9
 
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
10
  end
60
- end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Cashflow
2
- VERSION = "0.0.6".freeze
2
+ VERSION = "0.1.3".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cashflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Fulcher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-04-12 00:00:00.000000000 Z
11
+ date: 2013-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,28 +44,42 @@ dependencies:
44
44
  requirements:
45
45
  - - ~>
46
46
  - !ruby/object:Gem::Version
47
- version: 1.0.2
47
+ version: 1.0.3
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ~>
53
53
  - !ruby/object:Gem::Version
54
- version: 1.0.2
54
+ version: 1.0.3
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: sqlite3
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ~>
60
60
  - !ruby/object:Gem::Version
61
- version: 1.3.7
61
+ version: 1.3.8
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ~>
67
67
  - !ruby/object:Gem::Version
68
- version: 1.3.7
68
+ version: 1.3.8
69
+ - !ruby/object:Gem::Dependency
70
+ name: active_record_migrations
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 4.0.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 4.0.0
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: thor
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +100,14 @@ dependencies:
86
100
  requirements:
87
101
  - - ~>
88
102
  - !ruby/object:Gem::Version
89
- version: 0.3.1
103
+ version: 0.3.2
90
104
  type: :runtime
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - ~>
95
109
  - !ruby/object:Gem::Version
96
- version: 0.3.1
110
+ version: 0.3.2
97
111
  description: Compares your latest income & expense to your average, giving you a quick
98
112
  at-a-glance view of your financial position.
99
113
  email:
@@ -104,20 +118,23 @@ extensions: []
104
118
  extra_rdoc_files: []
105
119
  files:
106
120
  - .gitignore
107
- - .rvmrc
121
+ - .ruby-gemset
122
+ - .ruby-version
108
123
  - Gemfile
109
124
  - LICENSE.txt
110
125
  - README.md
111
126
  - Rakefile
112
127
  - bin/cashflow
113
128
  - cashflow.gemspec
129
+ - db/migrate/20131110065134_create_transaction.rb
130
+ - db/schema.rb
114
131
  - lib/cashflow.rb
115
132
  - lib/cashflow/cli.rb
133
+ - lib/cashflow/config.rb
116
134
  - lib/cashflow/patches.rb
117
135
  - lib/cashflow/report.rb
118
136
  - lib/cashflow/transaction.rb
119
137
  - lib/cashflow/version.rb
120
- - log/cashflow.log
121
138
  - screenshot.png
122
139
  homepage: http://drawingablank.me/blog/cashflow-a-ruby-gem-for-your-financial-position.html
123
140
  licenses:
@@ -139,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
156
  version: '0'
140
157
  requirements: []
141
158
  rubyforge_project:
142
- rubygems_version: 2.0.0
159
+ rubygems_version: 2.1.10
143
160
  signing_key:
144
161
  specification_version: 4
145
162
  summary: Compares your latest income & expense to your average.
data/.rvmrc DELETED
@@ -1 +0,0 @@
1
- rvm use 2.0.0@cashflow --create
data/log/cashflow.log DELETED
File without changes