financial 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +4 -0
  2. data/.infinity_test +16 -0
  3. data/.rspec +3 -0
  4. data/.rvmrc +1 -0
  5. data/Gemfile +15 -0
  6. data/Gemfile.lock +74 -0
  7. data/README.markdown +157 -0
  8. data/Rakefile +1 -0
  9. data/duck_tales.gif +0 -0
  10. data/example.png +0 -0
  11. data/examples/two_accounts.rb +32 -0
  12. data/features/portuguese_dsl.feature +93 -0
  13. data/features/print_financial_table.feature +318 -0
  14. data/features/support/env.rb +1 -0
  15. data/financial.gemspec +22 -0
  16. data/lib/financial.rb +39 -0
  17. data/lib/financial/account.rb +129 -0
  18. data/lib/financial/account_manager.rb +26 -0
  19. data/lib/financial/balance.rb +11 -0
  20. data/lib/financial/balance_calculation.rb +35 -0
  21. data/lib/financial/cost.rb +34 -0
  22. data/lib/financial/costs.rb +30 -0
  23. data/lib/financial/deposit.rb +30 -0
  24. data/lib/financial/deposits.rb +15 -0
  25. data/lib/financial/dsl.rb +16 -0
  26. data/lib/financial/financial_date.rb +18 -0
  27. data/lib/financial/financial_table.rb +109 -0
  28. data/lib/financial/locale.rb +153 -0
  29. data/lib/financial/locales/en.yml +34 -0
  30. data/lib/financial/locales/pt.yml +67 -0
  31. data/lib/financial/parcels.rb +87 -0
  32. data/lib/financial/per_cent.rb +12 -0
  33. data/lib/financial/print_table.rb +34 -0
  34. data/lib/financial/revenue.rb +40 -0
  35. data/lib/financial/revenues.rb +16 -0
  36. data/lib/financial/rspec_matchers.rb +32 -0
  37. data/lib/financial/tax.rb +4 -0
  38. data/lib/financial/version.rb +3 -0
  39. data/spec/financial/account_manager_spec.rb +38 -0
  40. data/spec/financial/account_spec.rb +399 -0
  41. data/spec/financial/balance_spec.rb +44 -0
  42. data/spec/financial/cost_spec.rb +78 -0
  43. data/spec/financial/costs_spec.rb +18 -0
  44. data/spec/financial/deposit_spec.rb +78 -0
  45. data/spec/financial/deposits_spec.rb +23 -0
  46. data/spec/financial/english_spec.rb +76 -0
  47. data/spec/financial/financial_date_spec.rb +50 -0
  48. data/spec/financial/financial_spec.rb +14 -0
  49. data/spec/financial/financial_table_spec.rb +31 -0
  50. data/spec/financial/locale_spec.rb +37 -0
  51. data/spec/financial/parcels_spec.rb +179 -0
  52. data/spec/financial/per_cent_spec.rb +24 -0
  53. data/spec/financial/portuguese_spec.rb +117 -0
  54. data/spec/financial/print_table_spec.rb +76 -0
  55. data/spec/financial/revenue_spec.rb +89 -0
  56. data/spec/financial/revenues_spec.rb +18 -0
  57. data/spec/financial/tax_spec.rb +42 -0
  58. data/spec/spec_helper.rb +25 -0
  59. metadata +139 -0
@@ -0,0 +1 @@
1
+ require 'aruba/cucumber'
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "financial/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "financial"
7
+ s.version = Financial::VERSION
8
+ s.authors = ["Tomas D'Stefano"]
9
+ s.email = ["tomas_stefano@successoft.com"]
10
+ s.homepage = "https://github.com/tomas-stefano/financial"
11
+ s.summary = %q{Manage your money in the terminal with A Ruby DSL}
12
+ s.description = %q{Manage your money in the terminal with A Ruby DSL}
13
+
14
+ s.rubyforge_project = "financial"
15
+
16
+ s.add_dependency(%q<terminal-table>, ["= 1.4.2"])
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+ end
@@ -0,0 +1,39 @@
1
+ require "financial/version"
2
+ require 'date'
3
+ require 'forwardable'
4
+ require 'terminal-table'
5
+ require 'yaml'
6
+
7
+ module Financial
8
+ autoload 'Account', 'financial/account'
9
+ autoload 'AccountManager', 'financial/account_manager'
10
+ autoload 'Balance', 'financial/balance'
11
+ autoload 'BalanceCalculation', 'financial/balance_calculation'
12
+ autoload 'Cost', 'financial/cost'
13
+ autoload 'Costs', 'financial/costs'
14
+ autoload 'Deposit', 'financial/deposit'
15
+ autoload 'Deposits', 'financial/deposits'
16
+ autoload 'DSL', 'financial/dsl'
17
+ autoload 'FinancialDate', 'financial/financial_date'
18
+ autoload 'Locale', 'financial/locale'
19
+ autoload 'Parcels', 'financial/parcels'
20
+ autoload 'PerCent', 'financial/per_cent'
21
+ autoload 'PrintTable', 'financial/print_table'
22
+ autoload 'Revenue', 'financial/revenue'
23
+ autoload 'Revenues', 'financial/revenues'
24
+ autoload 'RSpecMatchers', 'financial/rspec_matchers'
25
+ autoload 'FinancialTable', 'financial/financial_table'
26
+ autoload 'Tax', 'financial/tax'
27
+
28
+ def self.locale=(locale_name)
29
+ @locale = Locale.new(locale_name)
30
+ end
31
+
32
+ def self.locale
33
+ @locale || Locale.new(:en)
34
+ end
35
+
36
+ def self.account_manager
37
+ AccountManager.instance
38
+ end
39
+ end
@@ -0,0 +1,129 @@
1
+ module Financial
2
+ class Account
3
+ attr_accessor :name, :banner
4
+
5
+ def initialize(*arguments)
6
+ arguments.flatten!
7
+ options = arguments.last.is_a?(Hash) ? arguments.pop : {}
8
+ @name = arguments.shift
9
+ raise RuntimeError, "Pass a name to account" unless @name
10
+ @banner = options[:as] || @name.to_s.split('_').join(' ').capitalize
11
+ @costs = Costs.new
12
+ @revenues = Revenues.new
13
+ @deposits = Deposits.new(@name)
14
+ @balances = []
15
+ @total = 0
16
+ end
17
+
18
+ def total(value=nil)
19
+ return @total unless value
20
+ @total = value
21
+ end
22
+
23
+ def deposits(&block)
24
+ @deposits.instance_eval(&block) if block_given?
25
+ @deposits
26
+ end
27
+
28
+ def costs(&block)
29
+ @costs.instance_eval(&block) if block_given?
30
+ @costs.replace_parcels_with_costs!
31
+ @costs
32
+ end
33
+
34
+ def revenues(&block)
35
+ @revenues.instance_eval(&block) if block_given?
36
+ @revenues
37
+ end
38
+
39
+ def taxes
40
+ revenues.collect { |revenue| revenue.tax }.compact
41
+ end
42
+
43
+ def calculate_balances
44
+ @balances = BalanceCalculation.new(self).calculate!
45
+ end
46
+ alias :balances :calculate_balances
47
+
48
+ def all_costs
49
+ [costs, deposits, taxes].flatten.sort_by { |event| event.date }
50
+ end
51
+
52
+ def all_revenues
53
+ [revenues, received_deposits].flatten
54
+ end
55
+
56
+ def unique_events_dates
57
+ events.collect { |event| event.date }.uniq.sort
58
+ end
59
+
60
+ def events
61
+ [all_costs, all_revenues].flatten
62
+ end
63
+
64
+ def events_in_date(date)
65
+ events.select {|event| event.date == date }
66
+ end
67
+
68
+ def costs_in_date(date)
69
+ all_costs.select { |cost| cost.date == date }
70
+ end
71
+
72
+ def revenues_in_date(date)
73
+ all_revenues.select { |revenue| revenue.date == date }
74
+ end
75
+
76
+ def total_costs
77
+ calculate(:all_costs)
78
+ end
79
+
80
+ def total_revenues
81
+ calculate(:all_revenues)
82
+ end
83
+
84
+ def calculate(method_name)
85
+ objects_instances = self.send(method_name)
86
+ costs_or_revenues_sum = if objects_instances.empty?
87
+ 0
88
+ else
89
+ objects_instances.collect { |object_instance| object_instance.value }.inject(:+)
90
+ end
91
+ self.instance_variable_set("@#{method_name.to_s.gsub('all', 'total')}", costs_or_revenues_sum)
92
+ end
93
+
94
+ # FIXME: Fix a bug that dups all received_deposits
95
+ #
96
+ def received_deposits
97
+ account_manager.accounts.collect { |account| account.deposits}.flatten.select do |deposit|
98
+ deposit.account_to_deposit.equal?(@name)
99
+ end
100
+ end
101
+
102
+ def ==(other_account)
103
+ name == other_account.name
104
+ end
105
+
106
+ # OPTIMIZE ME: if this have hundreds balances will slow things down :\
107
+ #
108
+ def last_balance_in_date(date)
109
+ all_balances_for_date = balances.select { |balance| balance.date <= date }
110
+ if all_balances_for_date.empty?
111
+ total
112
+ else
113
+ all_balances_for_date.last.value
114
+ end
115
+ end
116
+
117
+ def find_balance_in_date(date)
118
+ balances.find { |balance| balance.date == date }
119
+ end
120
+
121
+ def self.find(options)
122
+ Financial.account_manager.accounts.find { |account| account.name == options[:name].to_sym}
123
+ end
124
+
125
+ def account_manager
126
+ Financial.account_manager
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,26 @@
1
+ require 'singleton'
2
+
3
+ module Financial
4
+ class AccountManager
5
+ include Singleton
6
+ attr_accessor :accounts
7
+
8
+ def initialize
9
+ @accounts = []
10
+ end
11
+
12
+ def new_account(args)
13
+ account = Account.new(args)
14
+ @accounts.push(account)
15
+ account
16
+ end
17
+
18
+ def find_account(account_name)
19
+ if account_name == :all or account_name == :todas
20
+ @accounts
21
+ else
22
+ @accounts.select { |a_account| a_account.name.equal?(account_name) }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module Financial
2
+ class Balance
3
+ attr_accessor :value, :date, :name
4
+
5
+ def initialize(balance_value, balance_date)
6
+ @name = Financial.locale.balance_name
7
+ @value = balance_value
8
+ @date = balance_date.is_a?(Date) ? balance_date : Financial::FinancialDate.new(balance_date).date
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ module Financial
2
+ class BalanceCalculation
3
+ attr_accessor :balances, :account, :revenues, :costs, :unique_events_dates
4
+
5
+ def initialize(account_instance)
6
+ @balances = []
7
+ @account = account_instance
8
+ @costs = @account.all_costs
9
+ @revenues = @account.revenues
10
+ @unique_events_dates = account.unique_events_dates
11
+ @total = account.total
12
+ end
13
+
14
+ def calculate!
15
+ unique_events_dates.collect do |date|
16
+ @date = date
17
+ @total -= calculate_costs
18
+ @total += calculate_revenues
19
+ Balance.new(@total, date)
20
+ end
21
+ end
22
+
23
+ def calculate_costs
24
+ costs_in_date = account.costs_in_date(@date)
25
+ return 0 if costs_in_date.empty?
26
+ costs_in_date.collect { |cost| cost.value }.inject(:+)
27
+ end
28
+
29
+ def calculate_revenues
30
+ revenues_in_date = account.revenues_in_date(@date)
31
+ return 0 if revenues_in_date.empty?
32
+ revenues_in_date.collect {|event| event.value }.inject(:+)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ module Financial
2
+ class Cost
3
+ attr_accessor :name, :method_name, :value, :date
4
+
5
+ def initialize(cost_name, *arguments)
6
+ arguments.flatten!
7
+ @name = cost_name.to_s.split('_').join(' ').capitalize
8
+ @method_name = cost_name
9
+ @value = arguments.shift
10
+ @date = Date.today
11
+ end
12
+
13
+ def in_date(user_date=nil)
14
+ @date = Financial::FinancialDate.new(user_date).date
15
+ self
16
+ end
17
+
18
+ def format_value
19
+ "- #{Financial.locale.format_coin(value)}"
20
+ end
21
+
22
+ def +(cost)
23
+ self.value + cost.value
24
+ end
25
+
26
+ def is_a_received_deposit?(account)
27
+ false
28
+ end
29
+
30
+ def ==(cost)
31
+ self.value == cost.value and self.method_name == cost.method_name
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ module Financial
2
+ class Costs < Array
3
+ def parcels(number)
4
+ parcels = Parcels.new(number)
5
+ self.push(parcels)
6
+ parcels
7
+ end
8
+
9
+ def replace_parcels_with_costs!
10
+ costs = self.select { |cost| cost.is_a?(Parcels) }.collect do |parcels|
11
+ self.delete(parcels)
12
+ parcels.to_cost
13
+ end
14
+ costs.flatten.each { |cost| self.push(cost) }
15
+ end
16
+
17
+ def method_missing(meth, *args, &blk)
18
+ unless args.empty?
19
+ cost = Cost.new(meth, args)
20
+ self.push(cost)
21
+ cost
22
+ else
23
+ raise CostWithoutValue, "Cost: #{meth} don't have a value. Pass a value!"
24
+ end
25
+ end
26
+ end
27
+
28
+ class CostWithoutValue < StandardError
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ module Financial
2
+ class Deposit < Cost
3
+ attr_accessor :name, :value
4
+ attr_reader :account_to_deposit, :account_name
5
+
6
+ def initialize(value, _account_name)
7
+ @value = value
8
+ @date = Date.today
9
+ @account_to_deposit = :anonymous
10
+ @account_name = _account_name
11
+ deposit_name!
12
+ end
13
+
14
+ def in_account(account_name)
15
+ @account_to_deposit = account_name
16
+ deposit_name!
17
+ self
18
+ end
19
+
20
+ def deposit_name!
21
+ @name = "#{Financial.locale.deposit_name}: #{@account_to_deposit}"
22
+ end
23
+
24
+ # Return true if is a received deposit
25
+ #
26
+ def is_a_received_deposit?(account)
27
+ (not account.deposits.include?(self)) and (account.name.equal?(account_to_deposit))
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ module Financial
2
+ class Deposits < Array
3
+ attr_reader :account_name
4
+ def initialize(account_name)
5
+ @account_name = account_name
6
+ super()
7
+ end
8
+
9
+ def deposit(value)
10
+ deposit = Deposit.new(value, @account_name)
11
+ self.push(deposit)
12
+ deposit
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module Financial
2
+ module DSL
3
+ def account(*args, &block)
4
+ account = Financial.account_manager.new_account(args)
5
+ account.instance_eval(&block)
6
+ account
7
+ end
8
+
9
+ def print_account(name, &block)
10
+ print_table = PrintTable.new(name)
11
+ print_table.instance_eval(&block)
12
+ print_table.print!
13
+ print_table
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ module Financial
2
+ class FinancialDate
3
+ attr_reader :date, :day, :month, :year
4
+
5
+ extend ::Forwardable
6
+ def_delegator :@date, :day, :day
7
+ def_delegator :@date, :month, :month
8
+ def_delegator :@date, :year, :year
9
+
10
+ def initialize(string_date)
11
+ if string_date
12
+ @date = Date.strptime(string_date, Financial.locale.date_format)
13
+ else
14
+ @date = Date.today
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,109 @@
1
+ module Financial
2
+ class FinancialTable
3
+ include Terminal::Table::TableHelper
4
+ attr_reader :header, :account, :print_table, :locale
5
+
6
+ extend Forwardable
7
+
8
+ def_delegator :@print_table, :initial_date, :initial_date
9
+ def_delegator :@print_table, :final_date, :final_date
10
+ def_delegator :@account, :total, :total
11
+ def_delegator :@account, :events_in_date, :events_in_date
12
+ def_delegator :@account, :find_balance_in_date, :find_balance_in_date
13
+ def_delegator :@locale, :table_headings, :headings
14
+ def_delegator :@locale, :date_to_s, :date_to_s
15
+ def_delegator :@locale, :to_coin, :to_coin
16
+
17
+ def initialize(account_instance, print_table_instance)
18
+ @account = account_instance
19
+ @print_table = print_table_instance
20
+ @locale = Financial.locale
21
+ @header = @locale.header_for(@account, @print_table.initial_date, @print_table.final_date)
22
+ end
23
+
24
+ def positive?
25
+ @positive
26
+ end
27
+
28
+ def to_s
29
+ financial_table = self
30
+ @financial_table = table do
31
+ self.headings = financial_table.headings
32
+ add_row(financial_table.initial_balance)
33
+ add_separator
34
+ financial_table.add_rows(self)
35
+ add_separator
36
+ add_row(financial_table.final_balance)
37
+ end
38
+ end
39
+
40
+ def add_rows(terminal_table)
41
+ initial_date.upto(final_date).each do |date|
42
+ add_rows_in_date(date, :using => terminal_table)
43
+ end
44
+ end
45
+
46
+ def add_rows_in_date(date, options)
47
+ @date = date
48
+ @terminal_table = options[:using]
49
+ @events = events_in_date(date)
50
+ unless @events.empty?
51
+ add_events_to_table
52
+ @terminal_table.add_separator
53
+ add_balance_to_table
54
+ end
55
+ end
56
+
57
+ def add_events_to_table
58
+ @events.each_with_index do |event, index|
59
+ @event = event
60
+ @index = index
61
+ @terminal_table.add_row [row_date, row_name, row_value, '']
62
+ end
63
+ end
64
+
65
+ def add_balance_to_table
66
+ balance = find_balance_in_date(@date)
67
+ @terminal_table.add_row ['', balance.name, hifen, locale.format_coin(balance.value)]
68
+ @terminal_table.add_separator
69
+ end
70
+
71
+ def initial_balance
72
+ [date_to_s(initial_date), locale.initial_balance, hifen, to_coin(total)]
73
+ end
74
+
75
+ def final_balance
76
+ [date_to_s(final_date), locale.final_balance, hifen, to_coin(account.last_balance_in_date(final_date))]
77
+ end
78
+
79
+ def hifen
80
+ '--------'
81
+ end
82
+
83
+ def row_date
84
+ if @index == 0
85
+ date_to_s(@event.date)
86
+ else
87
+ '' # dont pass nothing if is the same date
88
+ end
89
+ end
90
+
91
+ def row_name
92
+ received_deposit.first
93
+ end
94
+
95
+ def row_value
96
+ received_deposit.last
97
+ end
98
+
99
+ def received_deposit
100
+ name = @event.name
101
+ value = @event.format_value
102
+ if @event.is_a_received_deposit?(@account)
103
+ name = "#{locale.received_deposit_name}: #{@event.account_name}"
104
+ value = value.gsub('-', '+')
105
+ end
106
+ [name, value]
107
+ end
108
+ end
109
+ end