fin_it 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/ARCHITECTURE.md +24 -0
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +20 -0
- data/LICENSE +21 -0
- data/QUICKSTART.md +56 -0
- data/README.md +74 -0
- data/Rakefile +23 -0
- data/SECURITY.md +14 -0
- data/assets/fin_it_logo.png +0 -0
- data/lib/fin_it/account.rb +120 -0
- data/lib/fin_it/calculator/currency_conversion.rb +27 -0
- data/lib/fin_it/calculator/date_helpers.rb +53 -0
- data/lib/fin_it/calculator/variable_hashing.rb +120 -0
- data/lib/fin_it/calculator.rb +480 -0
- data/lib/fin_it/categories/category.rb +137 -0
- data/lib/fin_it/complex_model.rb +169 -0
- data/lib/fin_it/dsl/account_builder.rb +35 -0
- data/lib/fin_it/dsl/calculated_builder.rb +87 -0
- data/lib/fin_it/dsl/config_builder.rb +58 -0
- data/lib/fin_it/dsl/model_builder.rb +938 -0
- data/lib/fin_it/dsl/model_template_builder.rb +29 -0
- data/lib/fin_it/dsl/plan_builder.rb +52 -0
- data/lib/fin_it/dsl/project_inheritance_resolver.rb +46 -0
- data/lib/fin_it/dsl/variable_builder.rb +41 -0
- data/lib/fin_it/dsl.rb +13 -0
- data/lib/fin_it/engine.rb +15 -0
- data/lib/fin_it/financial_model/account_balances.rb +99 -0
- data/lib/fin_it/financial_model/account_hierarchy.rb +158 -0
- data/lib/fin_it/financial_model/category_values.rb +179 -0
- data/lib/fin_it/financial_model/currency_helpers.rb +14 -0
- data/lib/fin_it/financial_model/date_helpers.rb +58 -0
- data/lib/fin_it/financial_model/debugging.rb +353 -0
- data/lib/fin_it/financial_model/period_flows.rb +121 -0
- data/lib/fin_it/financial_model/validation.rb +85 -0
- data/lib/fin_it/financial_model/variable_matching.rb +49 -0
- data/lib/fin_it/financial_model.rb +395 -0
- data/lib/fin_it/model_template.rb +121 -0
- data/lib/fin_it/outputs/base_output.rb +51 -0
- data/lib/fin_it/outputs/console_output.rb +1528 -0
- data/lib/fin_it/outputs/monthly_console_output.rb +145 -0
- data/lib/fin_it/outputs/zaxcel_output.rb +1264 -0
- data/lib/fin_it/payment_schedule.rb +112 -0
- data/lib/fin_it/plan.rb +159 -0
- data/lib/fin_it/reports/balance_sheet.rb +638 -0
- data/lib/fin_it/reports/base_report.rb +239 -0
- data/lib/fin_it/reports/cash_flow_statement.rb +480 -0
- data/lib/fin_it/reports/custom_sheet.rb +436 -0
- data/lib/fin_it/reports/income_statement.rb +793 -0
- data/lib/fin_it/reports/period_comparison.rb +309 -0
- data/lib/fin_it/reports/scenario_comparison.rb +296 -0
- data/lib/fin_it/temporal_value.rb +349 -0
- data/lib/fin_it/transaction_generator/account_resolver.rb +118 -0
- data/lib/fin_it/transaction_generator/cache_management.rb +39 -0
- data/lib/fin_it/transaction_generator/date_generation.rb +57 -0
- data/lib/fin_it/transaction_generator.rb +357 -0
- data/lib/fin_it/version.rb +6 -0
- data/lib/fin_it.rb +27 -0
- data/test/fin_it/calculator_test.rb +109 -0
- data/test/fin_it/complex_model_test.rb +198 -0
- data/test/fin_it/debugging_test.rb +112 -0
- data/test/fin_it/driver_variables_test.rb +109 -0
- data/test/fin_it/dsl_test.rb +581 -0
- data/test/fin_it/financial_model_test.rb +196 -0
- data/test/fin_it/frequency_test.rb +51 -0
- data/test/fin_it/outputs/console_output_test.rb +249 -0
- data/test/fin_it/plan_test.rb +281 -0
- data/test/fin_it/reports/account_balance_test.rb +232 -0
- data/test/fin_it/reports/balance_sheet_test.rb +355 -0
- data/test/fin_it/reports/cash_flow_statement_test.rb +234 -0
- data/test/fin_it/reports/custom_sheet_test.rb +246 -0
- data/test/fin_it/reports/income_statement_test.rb +431 -0
- data/test/fin_it/reports/period_comparison_test.rb +226 -0
- data/test/fin_it/reports/restaurant_model_test.rb +225 -0
- data/test/fin_it/reports/scenario_comparison_test.rb +414 -0
- data/test/scripts/generate_demo_reports.rb +47 -0
- data/test/scripts/startup_saas_demo.rb +62 -0
- data/test/test_helper.rb +25 -0
- data/test/verify_accounting_equation.rb +91 -0
- metadata +264 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ice_cube'
|
|
4
|
+
|
|
5
|
+
module FinIt
|
|
6
|
+
# Handles payment date recurrence logic using ice_cube
|
|
7
|
+
class PaymentSchedule
|
|
8
|
+
attr_reader :schedule, :start_date, :end_date
|
|
9
|
+
|
|
10
|
+
def initialize(frequency:, payment_schedule: {}, start_date: nil, end_date: nil)
|
|
11
|
+
@frequency = frequency
|
|
12
|
+
@payment_schedule = payment_schedule
|
|
13
|
+
@start_date = parse_date(start_date)
|
|
14
|
+
@end_date = parse_date(end_date)
|
|
15
|
+
@schedule = build_schedule
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Check if a given date is a payment date
|
|
19
|
+
def payment_date?(date)
|
|
20
|
+
date = parse_date(date)
|
|
21
|
+
return false if date.nil?
|
|
22
|
+
return false if @start_date && date < @start_date
|
|
23
|
+
return false if @end_date && date > @end_date
|
|
24
|
+
|
|
25
|
+
@schedule.occurs_on?(date)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get the next payment date after a given date
|
|
29
|
+
def next_payment_date(after_date = nil)
|
|
30
|
+
after_date ||= Date.today
|
|
31
|
+
after_date = parse_date(after_date)
|
|
32
|
+
|
|
33
|
+
occurrences = @schedule.occurrences(after_date + 1)
|
|
34
|
+
occurrences.first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get all payment dates in a range
|
|
38
|
+
def payment_dates_in_range(start_date, end_date)
|
|
39
|
+
start_date = parse_date(start_date)
|
|
40
|
+
end_date = parse_date(end_date)
|
|
41
|
+
|
|
42
|
+
@schedule.occurrences_between(start_date, end_date)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_schedule
|
|
48
|
+
schedule = IceCube::Schedule.new(@start_date || Date.today)
|
|
49
|
+
|
|
50
|
+
case @frequency
|
|
51
|
+
when :daily
|
|
52
|
+
schedule.add_recurrence_rule IceCube::Rule.daily
|
|
53
|
+
when :weekly
|
|
54
|
+
day_of_week = @payment_schedule[:day_of_week] || 1 # Monday by default
|
|
55
|
+
schedule.add_recurrence_rule IceCube::Rule.weekly.day(day_of_week)
|
|
56
|
+
when :monthly
|
|
57
|
+
day_of_month = @payment_schedule[:day_of_month] || 1
|
|
58
|
+
if day_of_month == -1 || day_of_month == :last
|
|
59
|
+
# Last day of month
|
|
60
|
+
schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(-1)
|
|
61
|
+
else
|
|
62
|
+
schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(day_of_month)
|
|
63
|
+
end
|
|
64
|
+
when :quarterly
|
|
65
|
+
months = @payment_schedule[:months] || [1, 4, 7, 10]
|
|
66
|
+
day = @payment_schedule[:day] || 1
|
|
67
|
+
months.each do |month|
|
|
68
|
+
schedule.add_recurrence_rule IceCube::Rule.yearly.month_of_year(month).day_of_month(day)
|
|
69
|
+
end
|
|
70
|
+
when :annual, :yearly
|
|
71
|
+
month = @payment_schedule[:month] || 1
|
|
72
|
+
day = @payment_schedule[:day] || 1
|
|
73
|
+
schedule.add_recurrence_rule IceCube::Rule.yearly.month_of_year(month).day_of_month(day)
|
|
74
|
+
else
|
|
75
|
+
# Custom schedule rule provided
|
|
76
|
+
if @payment_schedule[:schedule_rule]
|
|
77
|
+
schedule.add_recurrence_rule @payment_schedule[:schedule_rule]
|
|
78
|
+
else
|
|
79
|
+
# Default to monthly if frequency not recognized
|
|
80
|
+
schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(1)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Apply end date if specified - add to recurrence rules
|
|
85
|
+
if @end_date
|
|
86
|
+
# Update all recurrence rules to have an end date
|
|
87
|
+
schedule.recurrence_rules.each do |rule|
|
|
88
|
+
# Check if rule already has an until time set
|
|
89
|
+
has_until = rule.respond_to?(:until_time) && rule.until_time
|
|
90
|
+
rule.until(@end_date) unless has_until
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
schedule
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_date(date)
|
|
98
|
+
return nil if date.nil?
|
|
99
|
+
return date if date.is_a?(Date)
|
|
100
|
+
|
|
101
|
+
case date
|
|
102
|
+
when String
|
|
103
|
+
Date.parse(date)
|
|
104
|
+
when Time
|
|
105
|
+
date.to_date
|
|
106
|
+
else
|
|
107
|
+
date
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
data/lib/fin_it/plan.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FinIt
|
|
4
|
+
# Represents a scenario plan that can modify a financial model's variables
|
|
5
|
+
# Plans are applied to create isolated copies of models for what-if analysis
|
|
6
|
+
class Plan
|
|
7
|
+
attr_reader :name, :description, :overrides, :start_date, :end_date
|
|
8
|
+
|
|
9
|
+
def initialize(name, description: nil, start_date: nil, end_date: nil)
|
|
10
|
+
@name = name
|
|
11
|
+
@description = description
|
|
12
|
+
@start_date = parse_date(start_date)
|
|
13
|
+
@end_date = parse_date(end_date)
|
|
14
|
+
@overrides = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Override a variable with a new value
|
|
18
|
+
# @param variable_name [Symbol] The variable to override
|
|
19
|
+
# @param value [Numeric, Money] The new value
|
|
20
|
+
# @param start_date [Date, String, nil] When override starts
|
|
21
|
+
# @param end_date [Date, String, nil] When override ends
|
|
22
|
+
def set(variable_name, value, start_date: nil, end_date: nil)
|
|
23
|
+
@overrides << {
|
|
24
|
+
type: :set,
|
|
25
|
+
variable: variable_name.to_sym,
|
|
26
|
+
value: value,
|
|
27
|
+
start_date: parse_date(start_date),
|
|
28
|
+
end_date: parse_date(end_date)
|
|
29
|
+
}
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Multiply variable by factor
|
|
34
|
+
# @param variable_name [Symbol] The variable to scale
|
|
35
|
+
# @param factor [Numeric] The multiplication factor (e.g., 1.10 for +10%)
|
|
36
|
+
# @param start_date [Date, String, nil] When scaling starts
|
|
37
|
+
# @param end_date [Date, String, nil] When scaling ends
|
|
38
|
+
def scale(variable_name, factor, start_date: nil, end_date: nil)
|
|
39
|
+
@overrides << {
|
|
40
|
+
type: :scale,
|
|
41
|
+
variable: variable_name.to_sym,
|
|
42
|
+
factor: factor,
|
|
43
|
+
start_date: parse_date(start_date),
|
|
44
|
+
end_date: parse_date(end_date)
|
|
45
|
+
}
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Add/subtract amount from variable
|
|
50
|
+
# @param variable_name [Symbol] The variable to adjust
|
|
51
|
+
# @param amount [Numeric] The amount to add (negative to subtract)
|
|
52
|
+
# @param start_date [Date, String, nil] When adjustment starts
|
|
53
|
+
# @param end_date [Date, String, nil] When adjustment ends
|
|
54
|
+
def adjust(variable_name, amount, start_date: nil, end_date: nil)
|
|
55
|
+
@overrides << {
|
|
56
|
+
type: :adjust,
|
|
57
|
+
variable: variable_name.to_sym,
|
|
58
|
+
amount: amount,
|
|
59
|
+
start_date: parse_date(start_date),
|
|
60
|
+
end_date: parse_date(end_date)
|
|
61
|
+
}
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Replace formula for calculated variable
|
|
66
|
+
# @param variable_name [Symbol] The calculated variable
|
|
67
|
+
# @param new_formula [String] The new formula
|
|
68
|
+
# @param start_date [Date, String, nil] When formula starts
|
|
69
|
+
# @param end_date [Date, String, nil] When formula ends
|
|
70
|
+
def formula(variable_name, new_formula, start_date: nil, end_date: nil)
|
|
71
|
+
@overrides << {
|
|
72
|
+
type: :formula,
|
|
73
|
+
variable: variable_name.to_sym,
|
|
74
|
+
formula: new_formula,
|
|
75
|
+
start_date: parse_date(start_date),
|
|
76
|
+
end_date: parse_date(end_date)
|
|
77
|
+
}
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Add a new variable to the model
|
|
82
|
+
# @param name [Symbol] Variable name
|
|
83
|
+
# @param category [Symbol] Category to add variable to
|
|
84
|
+
# @param currency [String, nil] Currency code
|
|
85
|
+
# @param block [Block] Variable definition block
|
|
86
|
+
def add_variable(name, category:, currency: nil, &block)
|
|
87
|
+
@overrides << {
|
|
88
|
+
type: :add_variable,
|
|
89
|
+
name: name.to_sym,
|
|
90
|
+
category: category.to_sym,
|
|
91
|
+
currency: currency,
|
|
92
|
+
block: block
|
|
93
|
+
}
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Add a new calculated variable to the model
|
|
98
|
+
# @param name [Symbol] Variable name
|
|
99
|
+
# @param category [Symbol] Category to add to
|
|
100
|
+
# @param formula [String] The formula
|
|
101
|
+
# @param options [Hash] Additional options (start_date, end_date, etc.)
|
|
102
|
+
def add_calculated(name, category:, formula:, **options)
|
|
103
|
+
@overrides << {
|
|
104
|
+
type: :add_calculated,
|
|
105
|
+
name: name.to_sym,
|
|
106
|
+
category: category.to_sym,
|
|
107
|
+
formula: formula,
|
|
108
|
+
options: options
|
|
109
|
+
}
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Set account opening balance
|
|
114
|
+
# @param account_name [Symbol] The account name
|
|
115
|
+
# @param amount [Numeric] The new opening balance
|
|
116
|
+
def set_opening_balance(account_name, amount)
|
|
117
|
+
@overrides << {
|
|
118
|
+
type: :opening_balance,
|
|
119
|
+
account: account_name.to_sym,
|
|
120
|
+
amount: amount
|
|
121
|
+
}
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Create a copy of this plan
|
|
126
|
+
def dup
|
|
127
|
+
new_plan = Plan.new(@name, description: @description,
|
|
128
|
+
start_date: @start_date, end_date: @end_date)
|
|
129
|
+
@overrides.each do |override|
|
|
130
|
+
new_plan.instance_variable_get(:@overrides) << override.dup
|
|
131
|
+
end
|
|
132
|
+
new_plan
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def parse_date(date)
|
|
138
|
+
return nil if date.nil?
|
|
139
|
+
return date if date.is_a?(Date)
|
|
140
|
+
|
|
141
|
+
case date
|
|
142
|
+
when String
|
|
143
|
+
if date =~ /^\d{4}$/
|
|
144
|
+
Date.new(date.to_i, 1, 1)
|
|
145
|
+
elsif date =~ /^\d{4}-\d{2}$/
|
|
146
|
+
Date.parse("#{date}-01")
|
|
147
|
+
else
|
|
148
|
+
Date.parse(date)
|
|
149
|
+
end
|
|
150
|
+
when Integer
|
|
151
|
+
Date.new(date, 1, 1)
|
|
152
|
+
when Time
|
|
153
|
+
date.to_date
|
|
154
|
+
else
|
|
155
|
+
raise ArgumentError, "Cannot parse date: #{date}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|