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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 740b10d39f33e08cc9522d153db7fd353d90711139f1dea78577ff00294745e9
4
+ data.tar.gz: e99017730cf0b998baf028f90b2237151c3eba8c2d0b1f581b808f7c8ada1438
5
+ SHA512:
6
+ metadata.gz: 4496083db9061a2521f248554d5da3fbe2c63c8bf955b2a788640aae0244670ad0a213e2df8550632524e6089685ddabd47156e07ce5f1a64cbb648d18bffe73
7
+ data.tar.gz: bd42f93b037d7813f854c6228bc1d5b54203c74f32f6788bbbe43bd962f0a496edece072c2ded50efc9de6aeb628f6dd0dd68ec8c99c94535a7e20dfa57d3410
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
14
+
15
+ *.csv
16
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.2
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in blend_spreadsheet_loan_generator.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 12.0'
7
+ gem 'rspec', '~> 3.0'
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]/blend_spreadsheet_loan_generator.
36
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "blend_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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,35 @@
1
+ require_relative 'lib/blend_spreadsheet_loan_generator/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'blend_spreadsheet_loan_generator'
5
+ spec.version = BlendSpreadsheetLoanGenerator::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/blend_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 << 'bslg'
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
data/exe/bslg ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/blend_spreadsheet_loan_generator'
4
+
5
+ Dry::CLI.new(BlendSpreadsheetLoanGenerator).call
@@ -0,0 +1,37 @@
1
+ require 'dry/cli'
2
+ require 'google_drive'
3
+ require 'active_support/all'
4
+ require 'fileutils'
5
+ require 'csv'
6
+
7
+ require 'blend_spreadsheet_loan_generator/version'
8
+
9
+ module BlendSpreadsheetLoanGenerator
10
+ class Error < StandardError; end
11
+
12
+ extend Dry::CLI::Registry
13
+
14
+ autoload :SpreadsheetConcern, 'blend_spreadsheet_loan_generator/concerns/spreadsheet_concern'
15
+ autoload :CsvConcern, 'blend_spreadsheet_loan_generator/concerns/csv_concern'
16
+
17
+ autoload :Linear, 'blend_spreadsheet_loan_generator/linear'
18
+ autoload :Standard, 'blend_spreadsheet_loan_generator/standard'
19
+ autoload :NormalInterests, 'blend_spreadsheet_loan_generator/normal_interests'
20
+ autoload :SimpleInterests, 'blend_spreadsheet_loan_generator/simple_interests'
21
+ autoload :RealisticInterests, 'blend_spreadsheet_loan_generator/realistic_interests'
22
+ autoload :Formula, 'blend_spreadsheet_loan_generator/formula'
23
+
24
+ autoload :Loan, 'blend_spreadsheet_loan_generator/loan'
25
+
26
+ autoload :Version, 'blend_spreadsheet_loan_generator/version'
27
+ autoload :Generate, 'blend_spreadsheet_loan_generator/generate'
28
+ autoload :Restructure, 'blend_spreadsheet_loan_generator/restructure'
29
+ autoload :EarlyRepay, 'blend_spreadsheet_loan_generator/early_repay'
30
+ autoload :Init, 'blend_spreadsheet_loan_generator/init'
31
+
32
+ register 'init', Init, aliases: ['i', '-i', '--init']
33
+ register 'version', Version, aliases: ['v', '-v', '--version']
34
+ register 'generate', Generate, aliases: ['g', '-g', '--generate']
35
+ register 'restructure', Restructure, aliases: ['r', '-r', '--restructure']
36
+ register 'early_repay', EarlyRepay, aliases: ['er', '-er', '--early_repay']
37
+ end
@@ -0,0 +1,29 @@
1
+ module BlendSpreadsheetLoanGenerator
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,123 @@
1
+ module BlendSpreadsheetLoanGenerator
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
+ period_fees
30
+ period_calculated_fees
31
+ capitalized_fees_start
32
+ capitalized_fees_end
33
+ period_reimbursed_capitalized_fees
34
+ period_fees_rate
35
+ ]
36
+ end
37
+
38
+ def currency_columns
39
+ %w[
40
+ remaining_capital_start
41
+ remaining_capital_end
42
+ amount_to_add
43
+ period_interests
44
+ period_capital
45
+ total_paid_capital_end_of_period
46
+ total_paid_interests_end_of_period
47
+ period_total
48
+ capitalized_interests_start
49
+ capitalized_interests_end
50
+ period_reimbursed_capitalized_interests
51
+ period_fees
52
+ capitalized_fees_start
53
+ capitalized_fees_end
54
+ period_reimbursed_capitalized_fees
55
+ ]
56
+ end
57
+
58
+ def precise_columns
59
+ %w[
60
+ period_theoric_interests
61
+ period_calculated_interests
62
+ period_calculated_capital
63
+ delta
64
+ accrued_delta
65
+ period_rate
66
+ period_calculated_fees
67
+ period_fees_rate
68
+ ]
69
+ end
70
+
71
+ def column_letter(column)
72
+ ('A'..'ZZ').to_a[columns.index(column)]
73
+ end
74
+
75
+ def apply_formats(worksheet:)
76
+ precise_columns.each do |column|
77
+ index = columns.index(column) + 1
78
+ worksheet.set_number_format(1, index, loan.duration + 1, 1, '0.00000000')
79
+ end
80
+ currency_columns.each do |column|
81
+ index = columns.index(column) + 1
82
+ worksheet.set_number_format(1, index, loan.duration + 1, 1, '0.00')
83
+ end
84
+ end
85
+
86
+ def apply_formulas(worksheet:)
87
+ columns.each.with_index do |title, column|
88
+ worksheet[1, column + 1] = title
89
+ end
90
+ loan.duration.times do |line|
91
+ columns.each.with_index do |title, column|
92
+ worksheet[line + 2, column + 1] = @formula.send("#{title}_formula", line: line + 2)
93
+ end
94
+ end
95
+ end
96
+
97
+ def column_range(column: 'A', upto: , exclude_head: true)
98
+ start_line = exclude_head ? 2 : 1
99
+
100
+ "#{column}#{start_line}:#{column}#{upto}"
101
+ end
102
+
103
+ def index_to_line(index:)
104
+ index + 1 # first term is on line 2
105
+ end
106
+
107
+ def excel_float(float)
108
+ float.to_s.gsub('.', ',')
109
+ end
110
+
111
+ # used heavily in formula concern
112
+ def respond_to_missing?(method_name, include_private = false)
113
+ columns.include?(method_name.to_s) || super
114
+ end
115
+
116
+ def method_missing(method_name, *args, **kwargs)
117
+ return super unless respond_to_missing?(method_name)
118
+
119
+ "#{column_letter(method_name.to_s)}#{args.first}"
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,163 @@
1
+ module BlendSpreadsheetLoanGenerator
2
+ class EarlyRepay < Dry::CLI::Command
3
+ include SpreadsheetConcern
4
+ include CsvConcern
5
+
6
+ attr_accessor :loan
7
+
8
+ desc "Generate spreadsheet"
9
+
10
+ argument :last_paid_term, type: :integer, required: true, desc: 'last paid term, restructuration starts at last_paid + 1'
11
+ argument :amount_paid, type: :integer, required: true, desc: 'amount early repaid'
12
+ argument :from_path, type: :string, required: true, desc: 'csv to restructure'
13
+ argument :rate, type: :float, required: true, desc: 'year rate post restructuration'
14
+
15
+ option :period_duration, type: :integer, default: 1, desc: 'duration of a period in months'
16
+ option :due_on, type: :date, default: Date.today, desc: 'date of the pay day of the first period DD/MM/YYYY'
17
+ option :deferred_and_capitalized, type: :integer, default: 0, desc: 'periods with no capital or interests paid'
18
+ option :deferred, type: :integer, default: 0, desc: 'periods with only interests paid'
19
+ option :type, type: :string, default: 'standard', values: %w[standard linear], desc: 'type of amortization'
20
+ option :interests_type, type: :string, default: 'simple', values: %w[simple realistic normal], desc: 'type of interests calculations'
21
+ option :fees_rate, type: :float, default: 0.0, required: true, desc: 'year fees rate'
22
+ option :starting_capitalized_interests, type: :float, default: 0.0, desc: 'starting capitalized interests (if ongoing loan)'
23
+ option :starting_capitalized_fees, type: :float, default: 0.0, desc: 'starting capitalized fees (if ongoing loan)'
24
+ option :target_path, type: :string, default: './', desc: 'where to put the generated csv'
25
+
26
+ def call(last_paid_term:, amount_paid:, from_path:, rate:, **options)
27
+ begin
28
+ session = GoogleDrive::Session.from_config(
29
+ File.join(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'], 'config.json')
30
+ )
31
+ rescue StandardError => e
32
+ if ENV['SPREADSHEET_LOAN_GENERATOR_DIR'].blank?
33
+ puts 'please set SPREADSHEET_LOAN_GENERATOR_DIR'
34
+ else
35
+ puts 'Cannot connect to google drive. Did you run slg init CLIENT_ID CLIENT_SECRET ?'
36
+ end
37
+ return
38
+ end
39
+
40
+ f = CSV.open(from_path)
41
+ values = f.to_a
42
+ values.map! { |r| set_types([columns, r].transpose.to_h.with_indifferent_access) }
43
+
44
+ last_paid_line = values.index { |term| term[:index] == last_paid_term.to_i }
45
+
46
+ total_to_be_paid = (
47
+ values[last_paid_line + 1][:remaining_capital_start] +
48
+ values[last_paid_line + 1][:capitalized_interests_start] +
49
+ values[last_paid_line + 1][:capitalized_fees_start]
50
+ )
51
+
52
+ amount_paid = amount_paid.to_f
53
+ duration = (
54
+ if total_to_be_paid == amount_paid
55
+ 1
56
+ else
57
+ values.last[:index] - values[last_paid_line][:index]
58
+ end
59
+ )
60
+
61
+ starting_capitalized_interests = 0.0
62
+ starting_capitalized_fees = 0.0
63
+
64
+ due_on = values[last_paid_line + 1][:due_on] + 1.month
65
+
66
+ capital_paid = amount_paid.to_f - (
67
+ values[last_paid_line + 1][:capitalized_interests_start] +
68
+ values[last_paid_line + 1][:capitalized_fees_start]
69
+ )
70
+
71
+ @loan = Loan.new(
72
+ amount: values[last_paid_line][:remaining_capital_end],
73
+ duration: duration,
74
+ rate: rate,
75
+ fees_rate: options.fetch(:fees_rate),
76
+ period_duration: options.fetch(:period_duration),
77
+ due_on: due_on,
78
+ deferred_and_capitalized: options.fetch(:deferred_and_capitalized),
79
+ deferred: options.fetch(:deferred),
80
+ type: options.fetch(:type),
81
+ interests_type: options.fetch(:interests_type),
82
+ starting_capitalized_interests: starting_capitalized_interests,
83
+ starting_capitalized_fees: starting_capitalized_fees
84
+ )
85
+
86
+ spreadsheet = session.create_spreadsheet(loan.name)
87
+ worksheet = spreadsheet.add_worksheet(loan.type, loan.duration + 2, columns.count + 1, index: 0)
88
+
89
+ @formula = Formula.new(loan: loan)
90
+
91
+ apply_formulas(worksheet: worksheet)
92
+
93
+ worksheet[2, columns.index('remaining_capital_start') + 1] =
94
+ excel_float(values[last_paid_line][:remaining_capital_end])
95
+
96
+ worksheet[2, columns.index('remaining_capital_end') + 1] =
97
+ excel_float(values[last_paid_line][:remaining_capital_end] - capital_paid)
98
+
99
+ worksheet[2, columns.index('period_interests') + 1] =
100
+ excel_float(values[last_paid_line + 1][:period_interests])
101
+
102
+ worksheet[2, columns.index('period_fees') + 1] =
103
+ excel_float(values[last_paid_line + 1][:period_fees])
104
+
105
+ worksheet[2, columns.index('period_capital') + 1] =
106
+ excel_float(capital_paid)
107
+
108
+ worksheet[2, columns.index('capitalized_interests_start') + 1] =
109
+ excel_float(values[last_paid_line + 1][:capitalized_interests_start])
110
+
111
+ worksheet[2, columns.index('capitalized_fees_start') + 1] =
112
+ excel_float(values[last_paid_line + 1][:capitalized_fees_start])
113
+
114
+ worksheet[2, columns.index('period_reimbursed_capitalized_interests') + 1] =
115
+ excel_float(values[last_paid_line + 1][:capitalized_interests_start])
116
+
117
+ worksheet[2, columns.index('period_reimbursed_capitalized_fees') + 1] =
118
+ excel_float(values[last_paid_line + 1][:capitalized_fees_start])
119
+
120
+ apply_formats(worksheet: worksheet)
121
+
122
+ worksheet.save
123
+ worksheet.reload
124
+
125
+ generate_csv(worksheet: worksheet, target_path: options.fetch(:target_path))
126
+
127
+ puts worksheet.human_url
128
+ end
129
+
130
+ def set_types(h)
131
+ h[:index] = h[:index].to_i
132
+
133
+ h[:due_on] = Date.strptime(h[:due_on], '%m/%d/%Y')
134
+ h[:remaining_capital_start] = h[:remaining_capital_start].to_f
135
+ h[:remaining_capital_end] = h[:remaining_capital_end].to_f
136
+ h[:period_theoric_interests] = h[:period_theoric_interests].to_f
137
+ h[:delta] = h[:delta].to_f
138
+ h[:accrued_delta] = h[:accrued_delta].to_f
139
+ h[:amount_to_add] = h[:amount_to_add].to_f
140
+ h[:period_interests] = h[:period_interests].to_f
141
+ h[:period_capital] = h[:period_capital].to_f
142
+ h[:total_paid_capital_end_of_period] = h[:total_paid_capital_end_of_period].to_f
143
+ h[:total_paid_interests_end_of_period] = h[:total_paid_interests_end_of_period].to_f
144
+ h[:period_total] = h[:period_total].to_f
145
+ h[:capitalized_interests_start] = h[:capitalized_interests_start].to_f
146
+ h[:capitalized_interests_end] = h[:capitalized_interests_end].to_f
147
+ h[:period_rate] = h[:period_rate].to_f
148
+ h[:period_calculated_capital] = h[:period_calculated_capital].to_f
149
+ h[:period_calculated_interests] = h[:period_calculated_interests].to_f
150
+ h[:period_reimbursed_capitalized_interests] = h[:period_reimbursed_capitalized_interests].to_f
151
+ h[:period_leap_days] = h[:period_leap_days].to_i
152
+ h[:period_non_leap_days] = h[:period_non_leap_days].to_i
153
+ h[:period_fees] = h[:period_fees].to_f
154
+ h[:period_calculated_fees] = h[:period_calculated_fees].to_f
155
+ h[:capitalized_fees_start] = h[:capitalized_fees_start].to_f
156
+ h[:capitalized_fees_end] = h[:capitalized_fees_end].to_f
157
+ h[:period_reimbursed_capitalized_fees] = h[:period_reimbursed_capitalized_fees].to_f
158
+ h[:period_fees_rate] = h[:period_fees_rate].to_f
159
+
160
+ h
161
+ end
162
+ end
163
+ end