blend_spreadsheet_loan_generator 0.1.0
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 +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
|