spreadsheet_loan_generator 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/README.md +36 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/slg +5 -0
- data/lib/spreadsheet_loan_generator/concerns/csv_concern.rb +29 -0
- data/lib/spreadsheet_loan_generator/concerns/spreadsheet_concern.rb +111 -0
- data/lib/spreadsheet_loan_generator/formula.rb +165 -0
- data/lib/spreadsheet_loan_generator/generate.rb +65 -0
- data/lib/spreadsheet_loan_generator/init.rb +36 -0
- data/lib/spreadsheet_loan_generator/linear.rb +20 -0
- data/lib/spreadsheet_loan_generator/loan.rb +122 -0
- data/lib/spreadsheet_loan_generator/normal_interests.rb +15 -0
- data/lib/spreadsheet_loan_generator/realistic_interests.rb +14 -0
- data/lib/spreadsheet_loan_generator/simple_interests.rb +14 -0
- data/lib/spreadsheet_loan_generator/standard.rb +32 -0
- data/lib/spreadsheet_loan_generator/version.rb +13 -0
- data/lib/spreadsheet_loan_generator.rb +33 -0
- data/spreadsheet_loan_generator.gemspec +35 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e8fb0a2c9eea77282750d497e0bd4e15bee2208f387e7f21d974b304870c20e0
|
4
|
+
data.tar.gz: e63010ba2a59739c4515073e36d8f778ff38fc59d697ce524e3a4d25bb7a580a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4ba69336288f6102ea17f5fbc7e7dbbe6f96e026c0af7d94c421630f9c7ca2af104ddf878d46b10a7a5c92bc640d613ef8c0cd6104cfdd57084c46b73a11f789
|
7
|
+
data.tar.gz: 27ef8c9a73208846e86dae009452cc6509ac0d593d6f8c8485744a8c86e84131fe1148efef781498b091dbc4fad93132ef52334c98b7a6bcac3688c1c838b4f4
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# SpreadsheetLoanGenerator
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/spreadsheet_loan_generator`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'spreadsheet_loan_generator'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install spreadsheet_loan_generator
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/spreadsheet_loan_generator.
|
36
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "spreadsheet_loan_generator"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/slg
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
module CsvConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def generate_csv(worksheet:, target_path:)
|
7
|
+
filename = File.join(target_path, "#{loan.name}.csv")
|
8
|
+
CSV.open(filename, 'wb') do |csv|
|
9
|
+
loan.duration.times do |line|
|
10
|
+
row = []
|
11
|
+
columns.each.with_index do |name, column|
|
12
|
+
row << (
|
13
|
+
case name
|
14
|
+
when 'index'
|
15
|
+
worksheet[line + 2, column + 1]
|
16
|
+
when 'due_on'
|
17
|
+
Date.parse(worksheet[line + 2, column + 1]).strftime('%m/%d/%Y')
|
18
|
+
else
|
19
|
+
worksheet[line + 2, column + 1].gsub(',', '.').to_f
|
20
|
+
end
|
21
|
+
)
|
22
|
+
end
|
23
|
+
csv << row
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
module SpreadsheetConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def columns
|
7
|
+
%w[
|
8
|
+
index
|
9
|
+
due_on
|
10
|
+
remaining_capital_start
|
11
|
+
remaining_capital_end
|
12
|
+
period_theoric_interests
|
13
|
+
delta
|
14
|
+
accrued_delta
|
15
|
+
amount_to_add
|
16
|
+
period_interests
|
17
|
+
period_capital
|
18
|
+
total_paid_capital_end_of_period
|
19
|
+
total_paid_interests_end_of_period
|
20
|
+
period_total
|
21
|
+
capitalized_interests_start
|
22
|
+
capitalized_interests_end
|
23
|
+
period_rate
|
24
|
+
period_calculated_capital
|
25
|
+
period_calculated_interests
|
26
|
+
period_reimbursed_capitalized_interests
|
27
|
+
period_leap_days
|
28
|
+
period_non_leap_days
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
def currency_columns
|
33
|
+
%w[
|
34
|
+
remaining_capital_start
|
35
|
+
remaining_capital_end
|
36
|
+
amount_to_add
|
37
|
+
period_interests
|
38
|
+
period_capital
|
39
|
+
total_paid_capital_end_of_period
|
40
|
+
total_paid_interests_end_of_period
|
41
|
+
period_total
|
42
|
+
capitalized_interests_start
|
43
|
+
capitalized_interests_end
|
44
|
+
period_reimbursed_capitalized_interests
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def precise_columns
|
49
|
+
%w[
|
50
|
+
period_theoric_interests
|
51
|
+
period_calculated_interests
|
52
|
+
period_calculated_capital
|
53
|
+
delta
|
54
|
+
accrued_delta
|
55
|
+
period_rate
|
56
|
+
]
|
57
|
+
end
|
58
|
+
|
59
|
+
def column_letter(column)
|
60
|
+
('A'..'ZZ').to_a[columns.index(column)]
|
61
|
+
end
|
62
|
+
|
63
|
+
def apply_formats(worksheet:)
|
64
|
+
precise_columns.each do |column|
|
65
|
+
index = columns.index(column) + 1
|
66
|
+
worksheet.set_number_format(1, index, loan.duration + 1, 1, '0.00000000')
|
67
|
+
end
|
68
|
+
currency_columns.each do |column|
|
69
|
+
index = columns.index(column) + 1
|
70
|
+
worksheet.set_number_format(1, index, loan.duration + 1, 1, '0.00')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def apply_formulas(worksheet:)
|
75
|
+
columns.each.with_index do |title, column|
|
76
|
+
worksheet[1, column + 1] = title
|
77
|
+
end
|
78
|
+
loan.duration.times do |line|
|
79
|
+
columns.each.with_index do |title, column|
|
80
|
+
worksheet[line + 2, column + 1] = @formula.send("#{title}_formula", line: line + 2)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def column_range(column: 'A', upto: , exclude_head: true)
|
86
|
+
start_line = exclude_head ? 2 : 1
|
87
|
+
|
88
|
+
"#{column}#{start_line}:#{column}#{upto}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def index_to_line(index:)
|
92
|
+
index + 1 # first term is on line 2
|
93
|
+
end
|
94
|
+
|
95
|
+
def excel_float(float)
|
96
|
+
float.to_s.gsub('.', ',')
|
97
|
+
end
|
98
|
+
|
99
|
+
# used heavily in formula concern
|
100
|
+
def respond_to_missing?(method_name, include_private = false)
|
101
|
+
columns.include?(method_name.to_s) || super
|
102
|
+
end
|
103
|
+
|
104
|
+
def method_missing(method_name, *args, **kwargs)
|
105
|
+
return super unless respond_to_missing?(method_name)
|
106
|
+
|
107
|
+
"#{column_letter(method_name.to_s)}#{args.first}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class Formula
|
3
|
+
include SpreadsheetConcern
|
4
|
+
|
5
|
+
attr_accessor :loan
|
6
|
+
|
7
|
+
def initialize(loan:)
|
8
|
+
@loan = loan
|
9
|
+
@interests_formula = loan.interests_formula
|
10
|
+
@loan_type_formula = loan.loan_type_formula
|
11
|
+
end
|
12
|
+
|
13
|
+
def index_formula(line:)
|
14
|
+
line - 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def due_on_formula(line:)
|
18
|
+
term = line - 1
|
19
|
+
loan.due_on + ((term - 1) * loan.period_duration).months
|
20
|
+
end
|
21
|
+
|
22
|
+
def period_capital_formula(line:)
|
23
|
+
if line == loan.duration + 1
|
24
|
+
"=ARRONDI(#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line - 1)}; 2)"
|
25
|
+
else
|
26
|
+
"=ARRONDI(#{period_calculated_capital(line)} - #{period_reimbursed_capitalized_interests(line)}; 2)"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def period_interests_formula(line:)
|
31
|
+
term = line - 1
|
32
|
+
if term <= loan.deferred_and_capitalized
|
33
|
+
excel_float(0.0)
|
34
|
+
else
|
35
|
+
"=ARRONDI(#{period_calculated_interests(line)}; 2)"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def period_total_formula(line:)
|
40
|
+
total = [
|
41
|
+
period_capital(line),
|
42
|
+
'+',
|
43
|
+
period_interests(line),
|
44
|
+
'+',
|
45
|
+
period_reimbursed_capitalized_interests(line)
|
46
|
+
].join(' ')
|
47
|
+
"=ARRONDI(#{total}; 2)"
|
48
|
+
end
|
49
|
+
|
50
|
+
def period_calculated_capital_formula(line:)
|
51
|
+
term = line - 1
|
52
|
+
if term <= loan.total_deferred_duration
|
53
|
+
excel_float(0.0)
|
54
|
+
else
|
55
|
+
@loan_type_formula.period_calculated_capital_formula(line: line)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def period_theoric_interests_formula(line:)
|
60
|
+
term = line - 1
|
61
|
+
if term <= loan.deferred_and_capitalized
|
62
|
+
excel_float(0.0)
|
63
|
+
else
|
64
|
+
"=#{period_calculated_interests(line)}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def period_calculated_interests_formula(line:)
|
69
|
+
term = line - 1
|
70
|
+
if loan.type == 'standard' && loan.total_deferred_duration < term
|
71
|
+
return @loan_type_formula.period_calculated_interests_formula(line: line)
|
72
|
+
end
|
73
|
+
|
74
|
+
amount_to_capitalize = [
|
75
|
+
'(',
|
76
|
+
remaining_capital_start(line),
|
77
|
+
'+',
|
78
|
+
capitalized_interests_start(line),
|
79
|
+
')'
|
80
|
+
].join(' ')
|
81
|
+
|
82
|
+
"=#{amount_to_capitalize} * #{period_rate(line)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def remaining_capital_start_formula(line:)
|
86
|
+
return excel_float(loan.amount) if line == 2
|
87
|
+
|
88
|
+
"=#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line - 1)}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def remaining_capital_end_formula(line:)
|
92
|
+
"=#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line)}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def total_paid_capital_end_of_period_formula(line:)
|
96
|
+
return "=#{period_capital(line)}" if line == 2
|
97
|
+
|
98
|
+
"=ARRONDI(SOMME(#{column_range(column: period_capital, upto: line)}); 2)"
|
99
|
+
end
|
100
|
+
|
101
|
+
def total_paid_interests_end_of_period_formula(line:)
|
102
|
+
return "=#{period_interests(line)}" if line == 2
|
103
|
+
|
104
|
+
"=ARRONDI(SOMME(#{column_range(column: period_interests, upto: line)}); 2)"
|
105
|
+
end
|
106
|
+
|
107
|
+
def capitalized_interests_start_formula(line:)
|
108
|
+
return excel_float(loan.starting_capitalized_interests) if line == 2
|
109
|
+
|
110
|
+
"=ARRONDI(#{capitalized_interests_end(line - 1)}; 2)"
|
111
|
+
end
|
112
|
+
|
113
|
+
def capitalized_interests_end_formula(line:)
|
114
|
+
term = line - 1
|
115
|
+
if term <= loan.deferred_and_capitalized
|
116
|
+
"=ARRONDI(#{capitalized_interests_start(line)} + #{period_calculated_interests(line)}; 2)"
|
117
|
+
else
|
118
|
+
"=ARRONDI(#{capitalized_interests_start(line)} - #{period_reimbursed_capitalized_interests(line)}; 2)"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def period_reimbursed_capitalized_interests_formula(line:)
|
123
|
+
term = line - 1
|
124
|
+
if term <= loan.total_deferred_duration
|
125
|
+
excel_float(0.0)
|
126
|
+
else
|
127
|
+
"=ARRONDI(MIN(#{period_calculated_capital(line)}; #{capitalized_interests_start(line)}); 2)"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def period_rate_formula(line:)
|
132
|
+
@interests_formula.period_rate_formula(line: line)
|
133
|
+
end
|
134
|
+
|
135
|
+
def delta_formula(line:)
|
136
|
+
"=#{period_theoric_interests(line)} - #{period_interests(line)}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def accrued_delta_formula(line:)
|
140
|
+
return excel_float(0.0) if line == 2
|
141
|
+
|
142
|
+
"=#{accrued_delta(line - 1)} + #{delta(line)} - #{amount_to_add(line - 1)}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def amount_to_add_formula(line:)
|
146
|
+
"=TRONQUE(#{accrued_delta(line)}; 2)"
|
147
|
+
end
|
148
|
+
|
149
|
+
def period_leap_days_formula(line:)
|
150
|
+
term = line - 1
|
151
|
+
from = loan.due_on + ((term - 2) * loan.period_duration).months
|
152
|
+
to = loan.due_on + ((term - 1) * loan.period_duration).months
|
153
|
+
|
154
|
+
(from...to).sum { |d| d.leap? ? 1 : 0 }
|
155
|
+
end
|
156
|
+
|
157
|
+
def period_non_leap_days_formula(line:)
|
158
|
+
term = line - 1
|
159
|
+
from = loan.due_on + ((term - 2) * loan.period_duration).months
|
160
|
+
to = loan.due_on + ((term - 1) * loan.period_duration).months
|
161
|
+
|
162
|
+
(from...to).sum { |d| d.leap? ? 0 : 1 }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class Generate < Dry::CLI::Command
|
3
|
+
include SpreadsheetConcern
|
4
|
+
include CsvConcern
|
5
|
+
|
6
|
+
attr_accessor :loan
|
7
|
+
|
8
|
+
desc "Generate spreadsheet"
|
9
|
+
|
10
|
+
argument :amount, type: :float, required: true, desc: 'amount borrowed'
|
11
|
+
argument :duration, type: :integer, required: true, desc: 'number of reimbursements'
|
12
|
+
argument :rate, type: :float, required: true, desc: 'year rate'
|
13
|
+
|
14
|
+
option :period_duration, type: :integer, default: 1, desc: 'duration of a period in months'
|
15
|
+
option :due_on, type: :date, default: Date.today, desc: 'date of the pay day of the first period DD/MM/YYYY'
|
16
|
+
option :deferred_and_capitalized, type: :integer, default: 0, desc: 'periods with no capital or interests paid'
|
17
|
+
option :deferred, type: :integer, default: 0, desc: 'periods with only interests paid'
|
18
|
+
option :type, type: :string, default: 'standard', values: %w[standard linear], desc: 'type of amortization'
|
19
|
+
option :interests_type, type: :string, default: 'simple', values: %w[simple realistic normal], desc: 'type of interests calculations'
|
20
|
+
option :starting_capitalized_interests, type: :float, default: 0.0, desc: 'starting capitalized interests (if ongoing loan)'
|
21
|
+
option :target_path, type: :string, default: './', desc: 'where to put the generated csv'
|
22
|
+
|
23
|
+
def call(amount:, duration:, rate:, **options)
|
24
|
+
begin
|
25
|
+
session = GoogleDrive::Session.from_config(
|
26
|
+
File.join(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'], 'config.json')
|
27
|
+
)
|
28
|
+
rescue StandardError => e
|
29
|
+
if ENV['SPREADSHEET_LOAN_GENERATOR_DIR'].blank?
|
30
|
+
puts 'please set SPREADSHEET_LOAN_GENERATOR_DIR'
|
31
|
+
else
|
32
|
+
puts 'Cannot connect to google drive. Did you run slg init CLIENT_ID CLIENT_SECRET ?'
|
33
|
+
end
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
@loan = Loan.new(
|
38
|
+
amount: amount,
|
39
|
+
duration: duration,
|
40
|
+
period_duration: options.fetch(:period_duration),
|
41
|
+
rate: rate,
|
42
|
+
due_on: options.fetch(:due_on),
|
43
|
+
deferred_and_capitalized: options.fetch(:deferred_and_capitalized),
|
44
|
+
deferred: options.fetch(:deferred),
|
45
|
+
type: options.fetch(:type),
|
46
|
+
interests_type: options.fetch(:interests_type),
|
47
|
+
starting_capitalized_interests: options.fetch(:starting_capitalized_interests)
|
48
|
+
)
|
49
|
+
|
50
|
+
spreadsheet = session.create_spreadsheet(loan.name)
|
51
|
+
worksheet = spreadsheet.worksheets.first
|
52
|
+
@formula = Formula.new(loan: loan)
|
53
|
+
|
54
|
+
apply_formulas(worksheet: worksheet)
|
55
|
+
apply_formats(worksheet: worksheet)
|
56
|
+
|
57
|
+
worksheet.save
|
58
|
+
worksheet.reload
|
59
|
+
|
60
|
+
generate_csv(worksheet: worksheet, target_path: options.fetch(:target_path))
|
61
|
+
|
62
|
+
puts worksheet.human_url
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class Init < Dry::CLI::Command
|
3
|
+
|
4
|
+
argument :client_id, type: :string, required: true, desc: 'GCP client_id'
|
5
|
+
argument :client_secret, type: :string, required: true, desc: 'GCP client_secret'
|
6
|
+
|
7
|
+
def call(client_id:, client_secret:)
|
8
|
+
if !ENV.key?('SPREADSHEET_LOAN_GENERATOR_DIR')
|
9
|
+
puts "Please set up the environment variable SPREADSHEET_LOAN_GENERATOR_DIR to a dir that will store this gem's configuration"
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
create_config_dir
|
14
|
+
create_config_file(client_id: client_id, client_secret: client_secret)
|
15
|
+
end
|
16
|
+
|
17
|
+
def config_path
|
18
|
+
File.join(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'], 'config.json')
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_config_dir
|
22
|
+
if !File.exists?(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'])
|
23
|
+
FileUtils.mkdir_p(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_config_file(client_id:, client_secret:)
|
28
|
+
f = File.new(config_path, 'w')
|
29
|
+
f.write({
|
30
|
+
client_id: client_id,
|
31
|
+
client_secret: client_secret
|
32
|
+
}.to_json)
|
33
|
+
f.close
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class Linear
|
3
|
+
include SpreadsheetConcern
|
4
|
+
|
5
|
+
attr_accessor :loan
|
6
|
+
def initialize(loan:)
|
7
|
+
@loan = loan
|
8
|
+
end
|
9
|
+
|
10
|
+
def period_calculated_capital_formula(*)
|
11
|
+
amount =
|
12
|
+
if loan.deferred_and_capitalized.zero?
|
13
|
+
excel_float(loan.amount)
|
14
|
+
else
|
15
|
+
"(#{capitalized_interests_end(loan.deferred_and_capitalized + 1)} + #{excel_float(loan.amount)})"
|
16
|
+
end
|
17
|
+
"=#{amount} / #{loan.non_deferred_duration}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class Loan
|
3
|
+
attr_accessor :amount,
|
4
|
+
:duration,
|
5
|
+
:period_duration,
|
6
|
+
:rate,
|
7
|
+
:due_on,
|
8
|
+
:deferred_and_capitalized,
|
9
|
+
:deferred,
|
10
|
+
:type,
|
11
|
+
:interests_type,
|
12
|
+
:starting_capitalized_interests
|
13
|
+
|
14
|
+
def initialize(
|
15
|
+
amount:,
|
16
|
+
duration:,
|
17
|
+
period_duration:,
|
18
|
+
rate:,
|
19
|
+
due_on:,
|
20
|
+
deferred_and_capitalized:,
|
21
|
+
deferred:,
|
22
|
+
type:,
|
23
|
+
interests_type:,
|
24
|
+
starting_capitalized_interests:)
|
25
|
+
@amount = amount.to_f
|
26
|
+
@duration = duration.to_i
|
27
|
+
@period_duration = period_duration.to_i
|
28
|
+
@rate = rate.to_f
|
29
|
+
@due_on = due_on.is_a?(Date) ? due_on : Date.parse(due_on)
|
30
|
+
@deferred_and_capitalized = deferred_and_capitalized.to_i
|
31
|
+
@deferred = deferred.to_i
|
32
|
+
@type = type
|
33
|
+
@interests_type = interests_type
|
34
|
+
|
35
|
+
@starting_capitalized_interests = starting_capitalized_interests.to_f
|
36
|
+
|
37
|
+
print_validation_errors
|
38
|
+
end
|
39
|
+
|
40
|
+
def print_validation_errors
|
41
|
+
puts 'amount < 0' if amount < 0
|
42
|
+
puts 'deferred & deferred_and_capitalized >= duration' if total_deferred_duration >= duration
|
43
|
+
if type == 'standard' && interests_type == 'realistic' && !fully_deferred?
|
44
|
+
puts 'standard & realistic interests do not work together'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def loan_type_formula
|
49
|
+
"SpreadsheetLoanGenerator::#{type.classify}".constantize.new(loan: self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def interests_formula
|
53
|
+
"SpreadsheetLoanGenerator::#{interests_type.classify}Interests".constantize.new(loan: self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def name_type
|
57
|
+
return 'bullet' if bullet?
|
58
|
+
return 'in_fine' if in_fine?
|
59
|
+
|
60
|
+
type
|
61
|
+
end
|
62
|
+
|
63
|
+
def name_period_duration
|
64
|
+
if period_duration.in?([1, 3, 6, 12])
|
65
|
+
{
|
66
|
+
'1' => 'month',
|
67
|
+
'3' => 'quarter',
|
68
|
+
'6' => 'semester',
|
69
|
+
'12' => 'year'
|
70
|
+
}[period_duration.to_s]
|
71
|
+
else
|
72
|
+
period_duration.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def name_deferred
|
77
|
+
return '0' if fully_deferred?
|
78
|
+
|
79
|
+
total_deferred_duration
|
80
|
+
end
|
81
|
+
|
82
|
+
def name_due_on
|
83
|
+
due_on.strftime('%Y%m%d')
|
84
|
+
end
|
85
|
+
|
86
|
+
def name
|
87
|
+
args = []
|
88
|
+
args += ['realistic'] if interests_type == 'realistic'
|
89
|
+
args += [
|
90
|
+
name_type,
|
91
|
+
name_period_duration,
|
92
|
+
amount,
|
93
|
+
(rate * 100).to_s,
|
94
|
+
duration.to_s,
|
95
|
+
name_deferred,
|
96
|
+
name_due_on
|
97
|
+
]
|
98
|
+
|
99
|
+
args.join('_')
|
100
|
+
end
|
101
|
+
|
102
|
+
def fully_deferred?
|
103
|
+
duration > 1 && non_deferred_duration == 1
|
104
|
+
end
|
105
|
+
|
106
|
+
def bullet?
|
107
|
+
fully_deferred? && deferred_and_capitalized == total_deferred_duration
|
108
|
+
end
|
109
|
+
|
110
|
+
def in_fine?
|
111
|
+
fully_deferred? && deferred == total_deferred_duration
|
112
|
+
end
|
113
|
+
|
114
|
+
def non_deferred_duration
|
115
|
+
duration - total_deferred_duration
|
116
|
+
end
|
117
|
+
|
118
|
+
def total_deferred_duration
|
119
|
+
deferred_and_capitalized + deferred
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class NormalInterests
|
3
|
+
include SpreadsheetConcern
|
4
|
+
|
5
|
+
attr_accessor :loan
|
6
|
+
def initialize(loan:)
|
7
|
+
@loan = loan
|
8
|
+
end
|
9
|
+
|
10
|
+
def period_rate_formula(*)
|
11
|
+
periods_per_year = excel_float(12.0 / loan.period_duration)
|
12
|
+
"=TAUX.NOMINAL(#{excel_float(loan.rate)};#{periods_per_year}) / #{periods_per_year}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class RealisticInterests
|
3
|
+
include SpreadsheetConcern
|
4
|
+
|
5
|
+
attr_accessor :loan
|
6
|
+
def initialize(loan:)
|
7
|
+
@loan = loan
|
8
|
+
end
|
9
|
+
|
10
|
+
def period_rate_formula(line:)
|
11
|
+
"=#{excel_float(loan.rate)} * ((#{period_leap_days(line)} / 366) + (#{period_non_leap_days(line)} / 365))"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class SimpleInterests
|
3
|
+
include SpreadsheetConcern
|
4
|
+
|
5
|
+
attr_accessor :loan
|
6
|
+
def initialize(loan:)
|
7
|
+
@loan = loan
|
8
|
+
end
|
9
|
+
|
10
|
+
def period_rate_formula(*)
|
11
|
+
"=#{excel_float(loan.rate)} * #{loan.period_duration} / 12,0"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SpreadsheetLoanGenerator
|
2
|
+
class Standard
|
3
|
+
include SpreadsheetConcern
|
4
|
+
|
5
|
+
attr_accessor :loan
|
6
|
+
|
7
|
+
def initialize(loan:)
|
8
|
+
@loan = loan
|
9
|
+
end
|
10
|
+
|
11
|
+
def standard_params(line:)
|
12
|
+
amount =
|
13
|
+
if loan.deferred_and_capitalized.zero?
|
14
|
+
excel_float(loan.amount)
|
15
|
+
else
|
16
|
+
"#{capitalized_interests_end(loan.deferred_and_capitalized + 1)} + #{excel_float(loan.amount)}"
|
17
|
+
end
|
18
|
+
term_cell = "#{index(line)} - #{loan.total_deferred_duration}"
|
19
|
+
|
20
|
+
"#{period_rate(line)};#{term_cell};#{loan.non_deferred_duration};#{amount}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def period_calculated_capital_formula(line:)
|
24
|
+
"=-PPMT(#{standard_params(line: line)})"
|
25
|
+
end
|
26
|
+
|
27
|
+
def period_calculated_interests_formula(line:)
|
28
|
+
"=-IPMT(#{standard_params(line: line)})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'dry/cli'
|
2
|
+
require 'google_drive'
|
3
|
+
require 'active_support/all'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'csv'
|
6
|
+
|
7
|
+
require 'spreadsheet_loan_generator/version'
|
8
|
+
|
9
|
+
module SpreadsheetLoanGenerator
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
extend Dry::CLI::Registry
|
13
|
+
|
14
|
+
autoload :SpreadsheetConcern, 'spreadsheet_loan_generator/concerns/spreadsheet_concern'
|
15
|
+
autoload :CsvConcern, 'spreadsheet_loan_generator/concerns/csv_concern'
|
16
|
+
|
17
|
+
autoload :Linear, 'spreadsheet_loan_generator/linear'
|
18
|
+
autoload :Standard, 'spreadsheet_loan_generator/standard'
|
19
|
+
autoload :NormalInterests, 'spreadsheet_loan_generator/normal_interests'
|
20
|
+
autoload :SimpleInterests, 'spreadsheet_loan_generator/simple_interests'
|
21
|
+
autoload :RealisticInterests, 'spreadsheet_loan_generator/realistic_interests'
|
22
|
+
autoload :Formula, 'spreadsheet_loan_generator/formula'
|
23
|
+
|
24
|
+
autoload :Loan, 'spreadsheet_loan_generator/loan'
|
25
|
+
|
26
|
+
autoload :Version, 'spreadsheet_loan_generator/version'
|
27
|
+
autoload :Generate, 'spreadsheet_loan_generator/generate'
|
28
|
+
autoload :Init, 'spreadsheet_loan_generator/init'
|
29
|
+
|
30
|
+
register 'init', Init, aliases: ['i', '-i', '--init']
|
31
|
+
register 'version', Version, aliases: ['v', '-v', '--version']
|
32
|
+
register 'generate', Generate, aliases: ['g', '-g', '--generate']
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'lib/spreadsheet_loan_generator/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'spreadsheet_loan_generator'
|
5
|
+
spec.version = SpreadsheetLoanGenerator::VERSION
|
6
|
+
spec.authors = ['MZiserman']
|
7
|
+
spec.email = ['martinziserman@gmail.com']
|
8
|
+
|
9
|
+
spec.summary = 'Generate spreadsheets amortization schedules from the command line'
|
10
|
+
spec.homepage = 'https://github.com/CapSens/spreadsheet_loan_generator'
|
11
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
12
|
+
|
13
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
14
|
+
|
15
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
16
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
17
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = 'exe'
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.executables << 'slg'
|
27
|
+
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.add_runtime_dependency 'activesupport'
|
31
|
+
spec.add_runtime_dependency 'csv'
|
32
|
+
spec.add_runtime_dependency 'dry-cli', '0.6'
|
33
|
+
spec.add_runtime_dependency 'google_drive'
|
34
|
+
spec.add_development_dependency 'pry'
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spreadsheet_loan_generator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- MZiserman
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: csv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-cli
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.6'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: google_drive
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- martinziserman@gmail.com
|
86
|
+
executables:
|
87
|
+
- slg
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/console
|
98
|
+
- bin/setup
|
99
|
+
- exe/slg
|
100
|
+
- lib/spreadsheet_loan_generator.rb
|
101
|
+
- lib/spreadsheet_loan_generator/concerns/csv_concern.rb
|
102
|
+
- lib/spreadsheet_loan_generator/concerns/spreadsheet_concern.rb
|
103
|
+
- lib/spreadsheet_loan_generator/formula.rb
|
104
|
+
- lib/spreadsheet_loan_generator/generate.rb
|
105
|
+
- lib/spreadsheet_loan_generator/init.rb
|
106
|
+
- lib/spreadsheet_loan_generator/linear.rb
|
107
|
+
- lib/spreadsheet_loan_generator/loan.rb
|
108
|
+
- lib/spreadsheet_loan_generator/normal_interests.rb
|
109
|
+
- lib/spreadsheet_loan_generator/realistic_interests.rb
|
110
|
+
- lib/spreadsheet_loan_generator/simple_interests.rb
|
111
|
+
- lib/spreadsheet_loan_generator/standard.rb
|
112
|
+
- lib/spreadsheet_loan_generator/version.rb
|
113
|
+
- spreadsheet_loan_generator.gemspec
|
114
|
+
homepage: https://github.com/CapSens/spreadsheet_loan_generator
|
115
|
+
licenses: []
|
116
|
+
metadata:
|
117
|
+
allowed_push_host: https://rubygems.org
|
118
|
+
homepage_uri: https://github.com/CapSens/spreadsheet_loan_generator
|
119
|
+
source_code_uri: https://github.com/CapSens/spreadsheet_loan_generator
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: 2.3.0
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
requirements: []
|
135
|
+
rubygems_version: 3.1.4
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: Generate spreadsheets amortization schedules from the command line
|
139
|
+
test_files: []
|