blend_spreadsheet_loan_generator 0.1.0
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/blend_spreadsheet_loan_generator.gemspec +35 -0
- data/exe/bslg +5 -0
- data/lib/blend_spreadsheet_loan_generator.rb +37 -0
- data/lib/blend_spreadsheet_loan_generator/concerns/csv_concern.rb +29 -0
- data/lib/blend_spreadsheet_loan_generator/concerns/spreadsheet_concern.rb +123 -0
- data/lib/blend_spreadsheet_loan_generator/early_repay.rb +163 -0
- data/lib/blend_spreadsheet_loan_generator/formula.rb +211 -0
- data/lib/blend_spreadsheet_loan_generator/generate.rb +73 -0
- data/lib/blend_spreadsheet_loan_generator/init.rb +36 -0
- data/lib/blend_spreadsheet_loan_generator/linear.rb +20 -0
- data/lib/blend_spreadsheet_loan_generator/loan.rb +127 -0
- data/lib/blend_spreadsheet_loan_generator/normal_interests.rb +21 -0
- data/lib/blend_spreadsheet_loan_generator/realistic_interests.rb +19 -0
- data/lib/blend_spreadsheet_loan_generator/restructure.rb +121 -0
- data/lib/blend_spreadsheet_loan_generator/simple_interests.rb +18 -0
- data/lib/blend_spreadsheet_loan_generator/standard.rb +25 -0
- data/lib/blend_spreadsheet_loan_generator/version.rb +13 -0
- metadata +141 -0
@@ -0,0 +1,211 @@
|
|
1
|
+
module BlendSpreadsheetLoanGenerator
|
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)} - #{period_reimbursed_capitalized_fees(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
|
+
amount_to_capitalize = [
|
70
|
+
'(',
|
71
|
+
remaining_capital_start(line),
|
72
|
+
'+',
|
73
|
+
capitalized_interests_start(line),
|
74
|
+
')'
|
75
|
+
].join(' ')
|
76
|
+
|
77
|
+
"=#{amount_to_capitalize} * #{period_rate(line)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def remaining_capital_start_formula(line:)
|
81
|
+
return excel_float(loan.amount) if line == 2
|
82
|
+
|
83
|
+
"=#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line - 1)}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def remaining_capital_end_formula(line:)
|
87
|
+
"=#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line)}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def total_paid_capital_end_of_period_formula(line:)
|
91
|
+
return "=#{period_capital(line)}" if line == 2
|
92
|
+
|
93
|
+
"=ARRONDI(SOMME(#{column_range(column: period_capital, upto: line)}); 2)"
|
94
|
+
end
|
95
|
+
|
96
|
+
def total_paid_interests_end_of_period_formula(line:)
|
97
|
+
return "=#{period_interests(line)}" if line == 2
|
98
|
+
|
99
|
+
"=ARRONDI(SOMME(#{column_range(column: period_interests, upto: line)}); 2)"
|
100
|
+
end
|
101
|
+
|
102
|
+
def capitalized_interests_start_formula(line:)
|
103
|
+
return excel_float(loan.starting_capitalized_interests) if line == 2
|
104
|
+
|
105
|
+
"=ARRONDI(#{capitalized_interests_end(line - 1)}; 2)"
|
106
|
+
end
|
107
|
+
|
108
|
+
def capitalized_interests_end_formula(line:)
|
109
|
+
term = line - 1
|
110
|
+
if term <= loan.deferred_and_capitalized
|
111
|
+
"=ARRONDI(#{capitalized_interests_start(line)} + #{period_calculated_interests(line)}; 2)"
|
112
|
+
else
|
113
|
+
"=ARRONDI(#{capitalized_interests_start(line)} - #{period_reimbursed_capitalized_interests(line)}; 2)"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def period_reimbursed_capitalized_interests_formula(line:)
|
118
|
+
term = line - 1
|
119
|
+
if term <= loan.total_deferred_duration
|
120
|
+
excel_float(0.0)
|
121
|
+
else
|
122
|
+
"=ARRONDI(MIN(#{period_calculated_capital(line)} - #{period_reimbursed_capitalized_fees(line)}; #{capitalized_interests_start(line)}); 2)"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def period_rate_formula(line:)
|
127
|
+
@interests_formula.period_rate_formula(line: line)
|
128
|
+
end
|
129
|
+
|
130
|
+
def delta_formula(line:)
|
131
|
+
"=#{period_theoric_interests(line)} - #{period_interests(line)}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def accrued_delta_formula(line:)
|
135
|
+
return excel_float(0.0) if line == 2
|
136
|
+
|
137
|
+
"=#{accrued_delta(line - 1)} + #{delta(line)} - #{amount_to_add(line - 1)}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def amount_to_add_formula(line:)
|
141
|
+
"=TRONQUE(#{accrued_delta(line)}; 2)"
|
142
|
+
end
|
143
|
+
|
144
|
+
def period_leap_days_formula(line:)
|
145
|
+
term = line - 1
|
146
|
+
from = loan.due_on + ((term - 2) * loan.period_duration).months
|
147
|
+
to = loan.due_on + ((term - 1) * loan.period_duration).months
|
148
|
+
|
149
|
+
(from...to).sum { |d| d.leap? ? 1 : 0 }
|
150
|
+
end
|
151
|
+
|
152
|
+
def period_non_leap_days_formula(line:)
|
153
|
+
term = line - 1
|
154
|
+
from = loan.due_on + ((term - 2) * loan.period_duration).months
|
155
|
+
to = loan.due_on + ((term - 1) * loan.period_duration).months
|
156
|
+
|
157
|
+
(from...to).sum { |d| d.leap? ? 0 : 1 }
|
158
|
+
end
|
159
|
+
|
160
|
+
def period_fees_formula(line:)
|
161
|
+
term = line - 1
|
162
|
+
if term <= loan.deferred_and_capitalized
|
163
|
+
excel_float(0.0)
|
164
|
+
else
|
165
|
+
"=ARRONDI(#{period_calculated_fees(line)}; 2)"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def period_calculated_fees_formula(line:)
|
170
|
+
amount_to_capitalize = [
|
171
|
+
'(',
|
172
|
+
remaining_capital_start(line),
|
173
|
+
'+',
|
174
|
+
capitalized_interests_start(line),
|
175
|
+
'+',
|
176
|
+
capitalized_fees_start(line),
|
177
|
+
')'
|
178
|
+
].join(' ')
|
179
|
+
|
180
|
+
"=#{amount_to_capitalize} * #{period_fees_rate(line)}"
|
181
|
+
end
|
182
|
+
|
183
|
+
def capitalized_fees_start_formula(line:)
|
184
|
+
return excel_float(loan.starting_capitalized_fees) if line == 2
|
185
|
+
|
186
|
+
"=ARRONDI(#{capitalized_fees_end(line - 1)}; 2)"
|
187
|
+
end
|
188
|
+
|
189
|
+
def capitalized_fees_end_formula(line:)
|
190
|
+
term = line - 1
|
191
|
+
if term <= loan.deferred_and_capitalized
|
192
|
+
"=ARRONDI(#{capitalized_fees_start(line)} + #{period_calculated_fees(line)}; 2)"
|
193
|
+
else
|
194
|
+
"=ARRONDI(#{capitalized_fees_start(line)} - #{period_reimbursed_capitalized_fees(line)}; 2)"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def period_reimbursed_capitalized_fees_formula(line:)
|
199
|
+
term = line - 1
|
200
|
+
if term <= loan.total_deferred_duration
|
201
|
+
excel_float(0.0)
|
202
|
+
else
|
203
|
+
"=ARRONDI(MIN(#{period_calculated_capital(line)}; #{capitalized_fees_start(line)}); 2)"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def period_fees_rate_formula(line:)
|
208
|
+
@interests_formula.period_fees_rate_formula(line: line)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module BlendSpreadsheetLoanGenerator
|
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 :fees_rate, type: :float, default: 0.0, required: true, desc: 'year fees rate'
|
21
|
+
option :starting_capitalized_interests, type: :float, default: 0.0, desc: 'starting capitalized interests (if ongoing loan)'
|
22
|
+
option :starting_capitalized_fees, type: :float, default: 0.0, desc: 'starting capitalized fees (if ongoing loan)'
|
23
|
+
option :target_path, type: :string, default: './', desc: 'where to put the generated csv'
|
24
|
+
|
25
|
+
def call(amount:, duration:, rate:, **options)
|
26
|
+
begin
|
27
|
+
session = GoogleDrive::Session.from_config(
|
28
|
+
File.join(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'], 'config.json')
|
29
|
+
)
|
30
|
+
rescue StandardError => e
|
31
|
+
if ENV['SPREADSHEET_LOAN_GENERATOR_DIR'].blank?
|
32
|
+
puts 'please set SPREADSHEET_LOAN_GENERATOR_DIR'
|
33
|
+
else
|
34
|
+
puts 'Cannot connect to google drive. Did you run slg init CLIENT_ID CLIENT_SECRET ?'
|
35
|
+
end
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
@loan = Loan.new(
|
40
|
+
amount: amount,
|
41
|
+
duration: duration,
|
42
|
+
period_duration: options.fetch(:period_duration),
|
43
|
+
rate: rate,
|
44
|
+
due_on: options.fetch(:due_on),
|
45
|
+
deferred_and_capitalized: options.fetch(:deferred_and_capitalized),
|
46
|
+
deferred: options.fetch(:deferred),
|
47
|
+
type: options.fetch(:type),
|
48
|
+
interests_type: options.fetch(:interests_type),
|
49
|
+
starting_capitalized_interests: options.fetch(:starting_capitalized_interests),
|
50
|
+
starting_capitalized_fees: options.fetch(:starting_capitalized_fees),
|
51
|
+
fees_rate: options.fetch(:fees_rate)
|
52
|
+
)
|
53
|
+
|
54
|
+
spreadsheet = session.create_spreadsheet(loan.name)
|
55
|
+
worksheet = spreadsheet.add_worksheet(loan.type, loan.duration + 2, columns.count + 1, index: 0)
|
56
|
+
|
57
|
+
@formula = Formula.new(loan: loan)
|
58
|
+
|
59
|
+
apply_formulas(worksheet: worksheet)
|
60
|
+
apply_formats(worksheet: worksheet)
|
61
|
+
|
62
|
+
worksheet.save
|
63
|
+
worksheet.reload
|
64
|
+
|
65
|
+
generate_csv(worksheet: worksheet, target_path: options.fetch(:target_path))
|
66
|
+
|
67
|
+
puts worksheet.human_url
|
68
|
+
rescue => e
|
69
|
+
require 'pry'
|
70
|
+
binding.pry
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BlendSpreadsheetLoanGenerator
|
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 BlendSpreadsheetLoanGenerator
|
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)} + #{capitalized_fees_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,127 @@
|
|
1
|
+
module BlendSpreadsheetLoanGenerator
|
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
|
+
:fees_rate,
|
14
|
+
:starting_capitalized_fees
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
amount:,
|
18
|
+
duration:,
|
19
|
+
period_duration:,
|
20
|
+
rate:,
|
21
|
+
due_on:,
|
22
|
+
deferred_and_capitalized:,
|
23
|
+
deferred:,
|
24
|
+
type:,
|
25
|
+
interests_type:,
|
26
|
+
starting_capitalized_interests:,
|
27
|
+
fees_rate:,
|
28
|
+
starting_capitalized_fees:)
|
29
|
+
@amount = amount.to_f
|
30
|
+
@duration = duration.to_i
|
31
|
+
@period_duration = period_duration.to_i
|
32
|
+
@rate = rate.to_f
|
33
|
+
@due_on = due_on.is_a?(Date) ? due_on : Date.parse(due_on)
|
34
|
+
@deferred_and_capitalized = deferred_and_capitalized.to_i
|
35
|
+
@deferred = deferred.to_i
|
36
|
+
@type = type
|
37
|
+
@interests_type = interests_type
|
38
|
+
@fees_rate = fees_rate.to_f
|
39
|
+
@starting_capitalized_interests = starting_capitalized_interests.to_f
|
40
|
+
@starting_capitalized_fees = starting_capitalized_fees.to_f
|
41
|
+
|
42
|
+
print_validation_errors
|
43
|
+
end
|
44
|
+
|
45
|
+
def print_validation_errors
|
46
|
+
puts 'amount < 0' if amount < 0
|
47
|
+
puts 'deferred & deferred_and_capitalized >= duration' if total_deferred_duration >= duration
|
48
|
+
if type == 'standard' && interests_type == 'realistic' && !fully_deferred?
|
49
|
+
puts 'standard & realistic interests do not work together'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def loan_type_formula
|
54
|
+
"BlendSpreadsheetLoanGenerator::#{type.classify}".constantize.new(loan: self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def interests_formula
|
58
|
+
"BlendSpreadsheetLoanGenerator::#{interests_type.classify}Interests".constantize.new(loan: self)
|
59
|
+
end
|
60
|
+
|
61
|
+
def name_type
|
62
|
+
return 'bullet' if bullet?
|
63
|
+
return 'in_fine' if in_fine?
|
64
|
+
|
65
|
+
type
|
66
|
+
end
|
67
|
+
|
68
|
+
def name_period_duration
|
69
|
+
if period_duration.in?([1, 3, 6, 12])
|
70
|
+
{
|
71
|
+
'1' => 'month',
|
72
|
+
'3' => 'quarter',
|
73
|
+
'6' => 'semester',
|
74
|
+
'12' => 'year'
|
75
|
+
}[period_duration.to_s]
|
76
|
+
else
|
77
|
+
period_duration.to_s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def name_deferred
|
82
|
+
return '0' if fully_deferred?
|
83
|
+
|
84
|
+
total_deferred_duration
|
85
|
+
end
|
86
|
+
|
87
|
+
def name_due_on
|
88
|
+
due_on.strftime('%Y%m%d')
|
89
|
+
end
|
90
|
+
|
91
|
+
def name
|
92
|
+
args = []
|
93
|
+
args += ['realistic'] if interests_type == 'realistic'
|
94
|
+
args += [
|
95
|
+
name_type,
|
96
|
+
name_period_duration,
|
97
|
+
amount,
|
98
|
+
(rate * 100).to_s,
|
99
|
+
duration.to_s,
|
100
|
+
name_deferred,
|
101
|
+
name_due_on
|
102
|
+
]
|
103
|
+
|
104
|
+
args.join('_')
|
105
|
+
end
|
106
|
+
|
107
|
+
def fully_deferred?
|
108
|
+
duration > 1 && non_deferred_duration == 1
|
109
|
+
end
|
110
|
+
|
111
|
+
def bullet?
|
112
|
+
fully_deferred? && deferred_and_capitalized == total_deferred_duration
|
113
|
+
end
|
114
|
+
|
115
|
+
def in_fine?
|
116
|
+
fully_deferred? && deferred == total_deferred_duration
|
117
|
+
end
|
118
|
+
|
119
|
+
def non_deferred_duration
|
120
|
+
duration - total_deferred_duration
|
121
|
+
end
|
122
|
+
|
123
|
+
def total_deferred_duration
|
124
|
+
deferred_and_capitalized + deferred
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|