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 +4 -4
- data/.gitignore +2 -1
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +0 -2
- data/README.md +4 -3
- data/Rakefile +1 -1
- data/cashflow.gemspec +6 -5
- data/db/migrate/20131110065134_create_transaction.rb +11 -0
- data/db/schema.rb +24 -0
- data/lib/cashflow.rb +26 -30
- data/lib/cashflow/cli.rb +20 -77
- data/lib/cashflow/config.rb +29 -0
- data/lib/cashflow/patches.rb +12 -1
- data/lib/cashflow/report.rb +172 -93
- data/lib/cashflow/transaction.rb +6 -55
- data/lib/cashflow/version.rb +1 -1
- metadata +28 -11
- data/.rvmrc +0 -1
- data/log/cashflow.log +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21cd2b9ecd6447a4baca231557a05e16520b3be9
|
4
|
+
data.tar.gz: cfacacd1b4925cf5a5af951b01b1d8d2782f5061
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d48aa2900d9fa8213a83e0697f9d8e534050a3ef0e5cc04049ccedd0f633cf8311ccce172370a4b03708c5f3ff9767bb3e17c1dc1897beefafe978b98c38a607
|
7
|
+
data.tar.gz: 4271b448b3a7765e8d45e12876805526f8e283a11da3a78430cf5cdd929bd0b3864d97cd70d981069b0e4b5c8663d202556b8ffee7e67c266d483c51f10ee9d8
|
data/.gitignore
CHANGED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
cashflow
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.0.0
|
data/Gemfile
CHANGED
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
|
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
|
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
|
-
#
|
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.
|
25
|
-
spec.add_dependency "sqlite3", "~> 1.3.
|
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.
|
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 '
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
13
|
-
|
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 =>
|
19
|
-
option :
|
21
|
+
option :span, :type => :numeric, :aliases => "-s", :default => 3
|
22
|
+
option :account, :type => :string, :aliases => "-a"
|
20
23
|
def report
|
21
|
-
|
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
|
-
|
74
|
-
|
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
|
93
|
-
transactions.
|
94
|
-
Transaction.
|
95
|
-
:
|
96
|
-
:
|
97
|
-
:
|
98
|
-
:
|
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
|
data/lib/cashflow/patches.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/cashflow/report.rb
CHANGED
@@ -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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
86
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
112
|
-
rescue
|
113
|
-
|
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
|
-
#
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
data/lib/cashflow/transaction.rb
CHANGED
@@ -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
|
-
|
4
|
+
# Validations
|
5
|
+
# ===========
|
6
6
|
|
7
|
-
|
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
|
data/lib/cashflow/version.rb
CHANGED
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
- .
|
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.
|
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
|