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,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../test_helper"
|
|
4
|
+
|
|
5
|
+
class CustomSheetTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@model = FinIt.define(default_currency: 'USD') do
|
|
8
|
+
config do
|
|
9
|
+
start_date 2024
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
account :checking do
|
|
13
|
+
type :asset
|
|
14
|
+
currency 'USD'
|
|
15
|
+
opening_balance 10_000
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
category :income, type: :income,
|
|
19
|
+
default_account: :checking,
|
|
20
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
21
|
+
variable :sales do
|
|
22
|
+
value 5_000
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
category :expenses, type: :expense,
|
|
27
|
+
default_account: :checking,
|
|
28
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
29
|
+
variable :rent do
|
|
30
|
+
value 1_000
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@end_date = Date.new(2024, 12, 31)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_static_content_header
|
|
39
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test Sheet") do
|
|
40
|
+
header "Main Title", level: 1
|
|
41
|
+
header "Subtitle", level: 2
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
data = sheet.generate
|
|
45
|
+
|
|
46
|
+
assert_equal "CustomSheet", data[:report_type]
|
|
47
|
+
assert_equal "Test Sheet", data[:title]
|
|
48
|
+
assert_equal 2, data[:sections].length
|
|
49
|
+
assert_equal :header, data[:sections][0][:type]
|
|
50
|
+
assert_equal "Main Title", data[:sections][0][:text]
|
|
51
|
+
assert_equal 1, data[:sections][0][:level]
|
|
52
|
+
assert_equal 2, data[:sections][1][:level]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_static_content_paragraph
|
|
56
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
57
|
+
paragraph "This is a test paragraph."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
data = sheet.generate
|
|
61
|
+
assert_equal :paragraph, data[:sections][0][:type]
|
|
62
|
+
assert_equal "This is a test paragraph.", data[:sections][0][:text]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_static_content_bullet_list
|
|
66
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
67
|
+
bullet_list ["Item 1", "Item 2", "Item 3"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
data = sheet.generate
|
|
71
|
+
assert_equal :bullet_list, data[:sections][0][:type]
|
|
72
|
+
assert_equal 3, data[:sections][0][:items].length
|
|
73
|
+
assert_equal "Item 1", data[:sections][0][:items][0]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_static_content_numbered_list
|
|
77
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
78
|
+
numbered_list ["First", "Second", "Third"]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
data = sheet.generate
|
|
82
|
+
assert_equal :numbered_list, data[:sections][0][:type]
|
|
83
|
+
assert_equal 3, data[:sections][0][:items].length
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_static_content_table
|
|
87
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
88
|
+
table(
|
|
89
|
+
headers: ["Name", "Value"],
|
|
90
|
+
rows: [["Item A", "$100"], ["Item B", "$200"]],
|
|
91
|
+
title: "Sample Table"
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
data = sheet.generate
|
|
96
|
+
assert_equal :table, data[:sections][0][:type]
|
|
97
|
+
assert_equal ["Name", "Value"], data[:sections][0][:headers]
|
|
98
|
+
assert_equal 2, data[:sections][0][:rows].length
|
|
99
|
+
assert_equal "Sample Table", data[:sections][0][:title]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_static_content_key_value
|
|
103
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
104
|
+
key_value "Revenue", "$100,000"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
data = sheet.generate
|
|
108
|
+
assert_equal :key_value, data[:sections][0][:type]
|
|
109
|
+
assert_equal "Revenue", data[:sections][0][:label]
|
|
110
|
+
assert_equal "$100,000", data[:sections][0][:value]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_static_content_spacer
|
|
114
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
115
|
+
spacer rows: 3
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
data = sheet.generate
|
|
119
|
+
assert_equal :spacer, data[:sections][0][:type]
|
|
120
|
+
assert_equal 3, data[:sections][0][:rows]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_static_content_horizontal_rule
|
|
124
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
125
|
+
horizontal_rule
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
data = sheet.generate
|
|
129
|
+
assert_equal :hr, data[:sections][0][:type]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_static_content_note
|
|
133
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test") do
|
|
134
|
+
note "Important info", style: :warning
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
data = sheet.generate
|
|
138
|
+
assert_equal :note, data[:sections][0][:type]
|
|
139
|
+
assert_equal "Important info", data[:sections][0][:text]
|
|
140
|
+
assert_equal :warning, data[:sections][0][:style]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_dynamic_model_metric
|
|
144
|
+
@model.generate_transactions(@end_date)
|
|
145
|
+
|
|
146
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test", model: @model) do
|
|
147
|
+
model_metric :net_income, label: "Net Income"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
data = sheet.generate
|
|
151
|
+
|
|
152
|
+
# Dynamic section is resolved to key_value
|
|
153
|
+
assert_equal :key_value, data[:sections][0][:type]
|
|
154
|
+
assert_equal "Net Income", data[:sections][0][:label]
|
|
155
|
+
assert_match(/\$/, data[:sections][0][:value]) # Should be formatted currency
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_dynamic_account_balance
|
|
159
|
+
@model.generate_transactions(@end_date)
|
|
160
|
+
|
|
161
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test", model: @model) do
|
|
162
|
+
account_balance :checking, label: "Cash Balance"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
data = sheet.generate
|
|
166
|
+
|
|
167
|
+
assert_equal :key_value, data[:sections][0][:type]
|
|
168
|
+
assert_equal "Cash Balance", data[:sections][0][:label]
|
|
169
|
+
assert_match(/\$/, data[:sections][0][:value])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_dynamic_metrics_table
|
|
173
|
+
@model.generate_transactions(@end_date)
|
|
174
|
+
|
|
175
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test", model: @model) do
|
|
176
|
+
metrics_table(
|
|
177
|
+
title: "Key Metrics",
|
|
178
|
+
metrics: [:net_income, :total_revenue, :total_expenses]
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
data = sheet.generate
|
|
183
|
+
|
|
184
|
+
assert_equal :table, data[:sections][0][:type]
|
|
185
|
+
assert_equal "Key Metrics", data[:sections][0][:title]
|
|
186
|
+
assert_equal ["Metric", "Value"], data[:sections][0][:headers]
|
|
187
|
+
assert_equal 3, data[:sections][0][:rows].length
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_model_required_for_dynamic_content
|
|
191
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test")
|
|
192
|
+
|
|
193
|
+
assert_raises(ArgumentError) do
|
|
194
|
+
sheet.model_metric :net_income
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_combined_static_and_dynamic_content
|
|
199
|
+
@model.generate_transactions(@end_date)
|
|
200
|
+
|
|
201
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Financial Summary", model: @model) do
|
|
202
|
+
header "2024 Financial Report", level: 1
|
|
203
|
+
paragraph "This report summarizes the financial performance for 2024."
|
|
204
|
+
spacer rows: 1
|
|
205
|
+
model_metric :net_income, label: "Net Income"
|
|
206
|
+
account_balance :checking, label: "Cash Position"
|
|
207
|
+
spacer rows: 1
|
|
208
|
+
bullet_list ["Prepared by Finance Team", "Internal Draft"]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
data = sheet.generate
|
|
212
|
+
|
|
213
|
+
assert_equal "Financial Summary", data[:title]
|
|
214
|
+
assert_equal 7, data[:sections].length
|
|
215
|
+
assert_equal :header, data[:sections][0][:type]
|
|
216
|
+
assert_equal :paragraph, data[:sections][1][:type]
|
|
217
|
+
assert_equal :spacer, data[:sections][2][:type]
|
|
218
|
+
assert_equal :key_value, data[:sections][3][:type]
|
|
219
|
+
assert_equal :key_value, data[:sections][4][:type]
|
|
220
|
+
assert_equal :spacer, data[:sections][5][:type]
|
|
221
|
+
assert_equal :bullet_list, data[:sections][6][:type]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def test_console_output
|
|
225
|
+
@model.generate_transactions(@end_date)
|
|
226
|
+
|
|
227
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test Report", model: @model) do
|
|
228
|
+
header "Summary", level: 1
|
|
229
|
+
model_metric :net_income, label: "Net Income"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
output = capture_io do
|
|
233
|
+
sheet.output(FinIt::Outputs::ConsoleOutput)
|
|
234
|
+
end.first
|
|
235
|
+
|
|
236
|
+
assert_match(/TEST REPORT/, output)
|
|
237
|
+
assert_match(/SUMMARY/, output)
|
|
238
|
+
assert_match(/Net Income/, output)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_currency_alias
|
|
242
|
+
sheet = FinIt::Reports::CustomSheet.build(title: "Test", output_currency: 'EUR')
|
|
243
|
+
assert_equal 'EUR', sheet.currency
|
|
244
|
+
assert_equal 'EUR', sheet.output_currency
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../test_helper"
|
|
4
|
+
|
|
5
|
+
class IncomeStatementTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@model = FinIt.define(default_currency: 'USD') do
|
|
8
|
+
config do
|
|
9
|
+
start_date 2024
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
account :checking do
|
|
13
|
+
type :asset
|
|
14
|
+
currency 'USD'
|
|
15
|
+
opening_balance 10_000
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
category :income, type: :income do
|
|
19
|
+
variable :salary, currency: 'USD', frequency: :annual, account: :checking do
|
|
20
|
+
value 100_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
21
|
+
description "Annual salary"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
variable :bonus, currency: 'USD', frequency: :annual, account: :checking do
|
|
25
|
+
value 20_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
26
|
+
description "Annual bonus"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
category :expenses, type: :expense do
|
|
31
|
+
variable :rent, currency: 'USD', frequency: :monthly, account: :checking do
|
|
32
|
+
value 2_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
33
|
+
description "Monthly rent"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
variable :utilities, currency: 'USD', frequency: :monthly, account: :checking do
|
|
37
|
+
value 300, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
38
|
+
description "Monthly utilities"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_generate_monthly_reports
|
|
45
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
46
|
+
@model,
|
|
47
|
+
start_date: Date.new(2024, 1, 1),
|
|
48
|
+
end_date: Date.new(2024, 3, 31),
|
|
49
|
+
output_currency: 'USD'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
monthly_reports = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
|
|
53
|
+
|
|
54
|
+
assert_equal 3, monthly_reports.length, "Should generate 3 monthly reports"
|
|
55
|
+
|
|
56
|
+
# Check January report
|
|
57
|
+
jan_report = monthly_reports[0]
|
|
58
|
+
assert jan_report[:period], "Should have period info"
|
|
59
|
+
assert_equal Date.new(2024, 1, 1), jan_report[:period][:start], "January should start on Jan 1"
|
|
60
|
+
assert_equal Date.new(2024, 1, 31), jan_report[:period][:end], "January should end on Jan 31"
|
|
61
|
+
assert jan_report[:report], "Should have report data"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_generate_yearly_report
|
|
65
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
66
|
+
@model,
|
|
67
|
+
start_date: Date.new(2024, 1, 1),
|
|
68
|
+
end_date: Date.new(2024, 12, 31),
|
|
69
|
+
output_currency: 'USD'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
yearly_report = report.generate_yearly(2024)
|
|
73
|
+
|
|
74
|
+
assert yearly_report[:period], "Should have period info"
|
|
75
|
+
assert_equal Date.new(2024, 1, 1), yearly_report[:period][:start], "Year should start on Jan 1"
|
|
76
|
+
assert_equal Date.new(2024, 12, 31), yearly_report[:period][:end], "Year should end on Dec 31"
|
|
77
|
+
assert yearly_report[:report], "Should have report data"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_generate_at_specific_date
|
|
81
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
82
|
+
@model,
|
|
83
|
+
start_date: Date.new(2024, 1, 1),
|
|
84
|
+
end_date: Date.new(2024, 12, 31),
|
|
85
|
+
output_currency: 'USD'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
date_report = report.generate_at(Date.new(2024, 6, 15))
|
|
89
|
+
|
|
90
|
+
assert date_report[:period], "Should have period info"
|
|
91
|
+
assert_equal Date.new(2024, 6, 15), date_report[:period][:start], "Should start on specified date"
|
|
92
|
+
assert_equal Date.new(2024, 6, 15), date_report[:period][:end], "Should end on specified date"
|
|
93
|
+
assert date_report[:report], "Should have report data"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_get_periods_monthly
|
|
97
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
98
|
+
@model,
|
|
99
|
+
start_date: Date.new(2024, 1, 1),
|
|
100
|
+
end_date: Date.new(2024, 3, 31),
|
|
101
|
+
output_currency: 'USD'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
periods = report.get_periods(frequency: :monthly)
|
|
105
|
+
|
|
106
|
+
assert_equal 3, periods.length, "Should have 3 monthly periods"
|
|
107
|
+
assert_equal Date.new(2024, 1, 1), periods[0], "First period should be Jan 1"
|
|
108
|
+
assert_equal Date.new(2024, 2, 1), periods[1], "Second period should be Feb 1"
|
|
109
|
+
assert_equal Date.new(2024, 3, 1), periods[2], "Third period should be Mar 1"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_get_periods_quarterly
|
|
113
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
114
|
+
@model,
|
|
115
|
+
start_date: Date.new(2024, 1, 1),
|
|
116
|
+
end_date: Date.new(2024, 12, 31),
|
|
117
|
+
output_currency: 'USD'
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
periods = report.get_periods(frequency: :quarterly)
|
|
121
|
+
|
|
122
|
+
assert_equal 4, periods.length, "Should have 4 quarterly periods"
|
|
123
|
+
assert_equal Date.new(2024, 1, 1), periods[0], "First period should be Q1"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_section_value
|
|
127
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
128
|
+
@model,
|
|
129
|
+
start_date: Date.new(2024, 1, 1),
|
|
130
|
+
end_date: Date.new(2024, 12, 31),
|
|
131
|
+
output_currency: 'USD'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# section_value uses the report's end_date by default, which is Dec 31
|
|
135
|
+
# But for annual income, we need to use a date range, not a single date
|
|
136
|
+
# Use the full year range for section_value
|
|
137
|
+
income_value = report.section_value(:income, date: Date.new(2024, 12, 31))
|
|
138
|
+
expenses_value = report.section_value(:expenses, date: Date.new(2024, 12, 31))
|
|
139
|
+
net_income_value = report.section_value(:net_income, date: Date.new(2024, 12, 31))
|
|
140
|
+
|
|
141
|
+
# Actually, section_value creates a single-day report, which won't work for annual income
|
|
142
|
+
# Instead, check the report totals directly
|
|
143
|
+
report_data = report.generate
|
|
144
|
+
income_value = report_data[:totals][:income]
|
|
145
|
+
expenses_value = report_data[:totals][:expenses]
|
|
146
|
+
net_income_value = report_data[:totals][:net_income]
|
|
147
|
+
|
|
148
|
+
assert income_value, "Should have income value"
|
|
149
|
+
assert expenses_value, "Should have expenses value"
|
|
150
|
+
assert net_income_value, "Should have net income value"
|
|
151
|
+
|
|
152
|
+
# Income should be positive, expenses negative (or positive as absolute)
|
|
153
|
+
assert income_value > 0, "Income should be positive"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_section_value_at_specific_date
|
|
157
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
158
|
+
@model,
|
|
159
|
+
start_date: Date.new(2024, 1, 1),
|
|
160
|
+
end_date: Date.new(2024, 12, 31),
|
|
161
|
+
output_currency: 'USD'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
income_value = report.section_value(:income, date: Date.new(2024, 6, 15))
|
|
165
|
+
|
|
166
|
+
assert income_value, "Should have income value at specific date"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def test_section_count
|
|
170
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
171
|
+
@model,
|
|
172
|
+
start_date: Date.new(2024, 1, 1),
|
|
173
|
+
end_date: Date.new(2024, 12, 31),
|
|
174
|
+
output_currency: 'USD'
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
income_count = report.section_count(:income)
|
|
178
|
+
expenses_count = report.section_count(:expenses)
|
|
179
|
+
|
|
180
|
+
assert income_count > 0, "Should have income items"
|
|
181
|
+
assert expenses_count > 0, "Should have expense items"
|
|
182
|
+
# Count includes subcategories (individual variables), so 1 category + 2 variables = 3
|
|
183
|
+
assert_equal 3, income_count, "Should have 1 income category with 2 variables shown separately"
|
|
184
|
+
assert_equal 3, expenses_count, "Should have 1 expense category with 2 variables shown separately"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_income_shows_individual_variables
|
|
188
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
189
|
+
@model,
|
|
190
|
+
start_date: Date.new(2024, 1, 1),
|
|
191
|
+
end_date: Date.new(2024, 12, 31),
|
|
192
|
+
output_currency: 'USD'
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
report_data = report.generate
|
|
196
|
+
income_section = report_data[:sections][:income]
|
|
197
|
+
income_item = income_section[:items].first
|
|
198
|
+
|
|
199
|
+
# Should have subcategories showing individual variables
|
|
200
|
+
assert income_item[:subcategories], "Income category should have subcategories"
|
|
201
|
+
assert_equal 2, income_item[:subcategories].length, "Should have 2 income variables"
|
|
202
|
+
|
|
203
|
+
# Check that salary and bonus are shown separately
|
|
204
|
+
variable_names = income_item[:subcategories].map { |sub| sub[:name] }
|
|
205
|
+
assert variable_names.include?(:salary), "Should show salary separately"
|
|
206
|
+
assert variable_names.include?(:bonus), "Should show bonus separately"
|
|
207
|
+
|
|
208
|
+
# Check values
|
|
209
|
+
salary_item = income_item[:subcategories].find { |sub| sub[:name] == :salary }
|
|
210
|
+
bonus_item = income_item[:subcategories].find { |sub| sub[:name] == :bonus }
|
|
211
|
+
|
|
212
|
+
assert_in_delta 100_000, salary_item[:value], 0.01, "Salary should be $100,000"
|
|
213
|
+
assert_in_delta 20_000, bonus_item[:value], 0.01, "Bonus should be $20,000"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_period_summary_monthly
|
|
217
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
218
|
+
@model,
|
|
219
|
+
start_date: Date.new(2024, 1, 1),
|
|
220
|
+
end_date: Date.new(2024, 3, 31),
|
|
221
|
+
output_currency: 'USD'
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
summary = report.period_summary(period_type: :monthly)
|
|
225
|
+
|
|
226
|
+
assert summary, "Should have summary"
|
|
227
|
+
assert_equal :monthly, summary[:frequency], "Should be monthly frequency"
|
|
228
|
+
assert_equal 3, summary[:periods].length, "Should have 3 periods"
|
|
229
|
+
assert summary[:summary], "Should have summary totals"
|
|
230
|
+
assert summary[:summary][:total_income], "Should have total income"
|
|
231
|
+
assert summary[:summary][:total_expenses], "Should have total expenses"
|
|
232
|
+
assert summary[:summary][:total_net_income], "Should have total net income"
|
|
233
|
+
assert_equal 3, summary[:summary][:period_count], "Should have 3 periods"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_period_summary_quarterly
|
|
237
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
238
|
+
@model,
|
|
239
|
+
start_date: Date.new(2024, 1, 1),
|
|
240
|
+
end_date: Date.new(2024, 12, 31),
|
|
241
|
+
output_currency: 'USD'
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
summary = report.period_summary(period_type: :quarterly)
|
|
245
|
+
|
|
246
|
+
assert summary, "Should have summary"
|
|
247
|
+
assert_equal :quarterly, summary[:frequency], "Should be quarterly frequency"
|
|
248
|
+
assert_equal 4, summary[:periods].length, "Should have 4 quarters"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_period_summary_net_income_calculation
|
|
252
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
253
|
+
@model,
|
|
254
|
+
start_date: Date.new(2024, 1, 1),
|
|
255
|
+
end_date: Date.new(2024, 3, 31),
|
|
256
|
+
output_currency: 'USD'
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
summary = report.period_summary(period_type: :monthly)
|
|
260
|
+
|
|
261
|
+
# Net income should be income - expenses
|
|
262
|
+
total_income = summary[:summary][:total_income]
|
|
263
|
+
total_expenses = summary[:summary][:total_expenses]
|
|
264
|
+
total_net_income = summary[:summary][:total_net_income]
|
|
265
|
+
|
|
266
|
+
# Convert to floats for comparison
|
|
267
|
+
income_f = total_income.is_a?(Money) ? total_income.to_f : total_income
|
|
268
|
+
expenses_f = total_expenses.is_a?(Money) ? total_expenses.to_f : total_expenses
|
|
269
|
+
net_f = total_net_income.is_a?(Money) ? total_net_income.to_f : total_net_income
|
|
270
|
+
|
|
271
|
+
expected_net = income_f - expenses_f
|
|
272
|
+
assert_in_delta expected_net, net_f, 0.01, "Net income should be income minus expenses"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def test_display_item_with_variable
|
|
276
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
277
|
+
@model,
|
|
278
|
+
start_date: Date.new(2024, 1, 1),
|
|
279
|
+
end_date: Date.new(2024, 12, 31),
|
|
280
|
+
output_currency: 'USD'
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
report.display_item :salary_display,
|
|
284
|
+
variable: :salary,
|
|
285
|
+
before: :income,
|
|
286
|
+
format: :currency
|
|
287
|
+
|
|
288
|
+
report_data = report.generate
|
|
289
|
+
display_items = report_data[:display_items]
|
|
290
|
+
|
|
291
|
+
assert display_items, "Should have display items"
|
|
292
|
+
assert_equal 1, display_items.length, "Should have 1 display item"
|
|
293
|
+
|
|
294
|
+
item = display_items.first
|
|
295
|
+
assert_equal :salary_display, item[:name]
|
|
296
|
+
assert_equal :income, item[:before]
|
|
297
|
+
assert_equal :currency, item[:format]
|
|
298
|
+
assert item[:value] > 0, "Should have a value"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def test_display_item_with_formula
|
|
302
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
303
|
+
@model,
|
|
304
|
+
start_date: Date.new(2024, 1, 1),
|
|
305
|
+
end_date: Date.new(2024, 12, 31),
|
|
306
|
+
output_currency: 'USD'
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
report.display_item :cogs_percentage,
|
|
310
|
+
formula: "cogs / revenue * 100",
|
|
311
|
+
after: :gross_margin,
|
|
312
|
+
format: :percentage
|
|
313
|
+
|
|
314
|
+
report_data = report.generate
|
|
315
|
+
display_items = report_data[:display_items]
|
|
316
|
+
|
|
317
|
+
assert display_items, "Should have display items"
|
|
318
|
+
item = display_items.find { |di| di[:name] == :cogs_percentage }
|
|
319
|
+
assert item, "Should have cogs_percentage item"
|
|
320
|
+
assert_equal :gross_margin, item[:after]
|
|
321
|
+
assert_equal :percentage, item[:format]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def test_display_item_with_category
|
|
325
|
+
model_with_drivers = FinIt.define(default_currency: 'USD') do
|
|
326
|
+
config do
|
|
327
|
+
start_date 2024
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
category :drivers, type: :driver do
|
|
331
|
+
variable :tables do
|
|
332
|
+
value 20
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
category :income, type: :income do
|
|
337
|
+
variable :salary, currency: 'USD', frequency: :annual do
|
|
338
|
+
value 100_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
344
|
+
model_with_drivers,
|
|
345
|
+
start_date: Date.new(2024, 1, 1),
|
|
346
|
+
end_date: Date.new(2024, 12, 31),
|
|
347
|
+
output_currency: 'USD'
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
report.display_item :tables_display,
|
|
351
|
+
category: :drivers,
|
|
352
|
+
before: :income,
|
|
353
|
+
format: :number
|
|
354
|
+
|
|
355
|
+
report_data = report.generate
|
|
356
|
+
display_items = report_data[:display_items]
|
|
357
|
+
|
|
358
|
+
assert display_items, "Should have display items"
|
|
359
|
+
item = display_items.find { |di| di[:name] == :tables_display }
|
|
360
|
+
assert item, "Should have tables_display item"
|
|
361
|
+
assert_equal 20, item[:value], "Should have value from driver category"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def test_display_items_in_period_comparison
|
|
365
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
366
|
+
@model,
|
|
367
|
+
start_date: Date.new(2024, 1, 1),
|
|
368
|
+
end_date: Date.new(2024, 3, 31),
|
|
369
|
+
output_currency: 'USD'
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
report.display_item :salary_display,
|
|
373
|
+
variable: :salary,
|
|
374
|
+
before: :income,
|
|
375
|
+
format: :currency
|
|
376
|
+
|
|
377
|
+
monthly_reports = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
|
|
378
|
+
|
|
379
|
+
monthly_reports.each do |period_report|
|
|
380
|
+
report_data = period_report[:report]
|
|
381
|
+
display_items = report_data[:display_items]
|
|
382
|
+
assert display_items, "Each period should have display items"
|
|
383
|
+
assert display_items.any? { |di| di[:name] == :salary_display }, "Should have salary_display in each period"
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def test_display_item_last_column_only
|
|
388
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
389
|
+
@model,
|
|
390
|
+
start_date: Date.new(2024, 1, 1),
|
|
391
|
+
end_date: Date.new(2024, 3, 31),
|
|
392
|
+
output_currency: 'USD'
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
report.display_item :summary_metric,
|
|
396
|
+
variable: :salary,
|
|
397
|
+
column: :gross_margin,
|
|
398
|
+
format: :currency,
|
|
399
|
+
last_column_only: true
|
|
400
|
+
|
|
401
|
+
report_data = report.generate
|
|
402
|
+
display_items = report_data[:display_items]
|
|
403
|
+
|
|
404
|
+
item = display_items.find { |di| di[:name] == :summary_metric }
|
|
405
|
+
assert item, "Should have summary_metric item"
|
|
406
|
+
assert_equal true, item[:last_column_only], "Should be marked as last_column_only"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def test_display_item_validation
|
|
410
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
411
|
+
@model,
|
|
412
|
+
start_date: Date.new(2024, 1, 1),
|
|
413
|
+
end_date: Date.new(2024, 12, 31),
|
|
414
|
+
output_currency: 'USD'
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Should raise error if no source specified
|
|
418
|
+
assert_raises(ArgumentError) do
|
|
419
|
+
report.display_item :invalid_item, before: :income
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Should raise error if multiple positions specified
|
|
423
|
+
assert_raises(ArgumentError) do
|
|
424
|
+
report.display_item :invalid_item,
|
|
425
|
+
variable: :salary,
|
|
426
|
+
before: :income,
|
|
427
|
+
after: :expenses
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|