financial 0.0.1

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