cashflow 0.0.6 → 0.1.3

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 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