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,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../test_helper"
|
|
4
|
+
|
|
5
|
+
class BalanceSheetTest < 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
|
+
account :savings do
|
|
19
|
+
type :asset
|
|
20
|
+
currency 'USD'
|
|
21
|
+
opening_balance 50_000
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
account :credit_card do
|
|
25
|
+
type :liability
|
|
26
|
+
currency 'USD'
|
|
27
|
+
opening_balance 5_000
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Assets
|
|
31
|
+
category :current_assets, type: :asset do
|
|
32
|
+
variable :cash, currency: 'USD', frequency: :annual, account: :checking do
|
|
33
|
+
value 10_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
34
|
+
description "Cash in checking"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
variable :savings_account, currency: 'USD', frequency: :annual, account: :savings do
|
|
38
|
+
value 50_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
39
|
+
description "Savings account"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
account :equipment_account do
|
|
44
|
+
type :asset
|
|
45
|
+
currency 'USD'
|
|
46
|
+
opening_balance 25_000
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
category :fixed_assets, type: :asset do
|
|
50
|
+
variable :equipment, currency: 'USD', frequency: :annual, account: :equipment_account do
|
|
51
|
+
value 25_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
52
|
+
description "Equipment"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Liabilities
|
|
57
|
+
category :current_liabilities, type: :liability do
|
|
58
|
+
variable :credit_card_debt, currency: 'USD', frequency: :annual, account: :credit_card do
|
|
59
|
+
value 5_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
60
|
+
description "Credit card debt"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
account :loan_account do
|
|
65
|
+
type :liability
|
|
66
|
+
currency 'USD'
|
|
67
|
+
opening_balance 20_000
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
category :long_term_liabilities, type: :liability do
|
|
71
|
+
# Loan account already has opening balance, variable just references it
|
|
72
|
+
variable :loan, currency: 'USD', frequency: :annual, account: :loan_account do
|
|
73
|
+
value 0, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
74
|
+
description "Long-term loan"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Equity
|
|
79
|
+
# Equity account will be auto-created with balance = Assets (85,000) - Liabilities (25,000) = 60,000
|
|
80
|
+
category :equity, type: :equity do
|
|
81
|
+
# Variable will be automatically created for the equity account
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_generate_balance_sheet
|
|
87
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
88
|
+
@model,
|
|
89
|
+
start_date: Date.new(2024, 1, 1),
|
|
90
|
+
end_date: Date.new(2024, 12, 31),
|
|
91
|
+
output_currency: 'USD'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
report_data = report.generate
|
|
95
|
+
assert report_data, "Report should generate"
|
|
96
|
+
assert report_data[:sections], "Report should have sections"
|
|
97
|
+
assert report_data[:sections][:assets], "Report should have assets section"
|
|
98
|
+
assert report_data[:sections][:liabilities], "Report should have liabilities section"
|
|
99
|
+
assert report_data[:sections][:equity], "Report should have equity section"
|
|
100
|
+
assert report_data[:totals], "Report should have totals"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_assets_section
|
|
104
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
105
|
+
@model,
|
|
106
|
+
start_date: Date.new(2024, 1, 1),
|
|
107
|
+
end_date: Date.new(2024, 12, 31),
|
|
108
|
+
output_currency: 'USD'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
report_data = report.generate
|
|
112
|
+
assets_section = report_data[:sections][:assets]
|
|
113
|
+
|
|
114
|
+
assert assets_section[:current_assets], "Should have current assets"
|
|
115
|
+
assert assets_section[:fixed_assets], "Should have fixed assets"
|
|
116
|
+
assert assets_section[:total], "Should have total assets"
|
|
117
|
+
|
|
118
|
+
# Current assets: 10,000 + 50,000 = 60,000
|
|
119
|
+
assert_in_delta 60_000, assets_section[:current_assets], 0.01
|
|
120
|
+
# Fixed assets: 25,000
|
|
121
|
+
assert_in_delta 25_000, assets_section[:fixed_assets], 0.01
|
|
122
|
+
# Total assets: 85,000
|
|
123
|
+
assert_in_delta 85_000, assets_section[:total], 0.01
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_liabilities_section
|
|
127
|
+
report = FinIt::Reports::BalanceSheet.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
|
+
report_data = report.generate
|
|
135
|
+
liabilities_section = report_data[:sections][:liabilities]
|
|
136
|
+
|
|
137
|
+
assert liabilities_section[:current_liabilities], "Should have current liabilities"
|
|
138
|
+
assert liabilities_section[:long_term_liabilities], "Should have long-term liabilities"
|
|
139
|
+
assert liabilities_section[:total], "Should have total liabilities"
|
|
140
|
+
|
|
141
|
+
# Current liabilities: 5,000
|
|
142
|
+
assert_in_delta 5_000, liabilities_section[:current_liabilities], 0.01
|
|
143
|
+
# Long-term liabilities: 20,000
|
|
144
|
+
assert_in_delta 20_000, liabilities_section[:long_term_liabilities], 0.01
|
|
145
|
+
# Total liabilities: 25,000
|
|
146
|
+
assert_in_delta 25_000, liabilities_section[:total], 0.01
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_equity_section
|
|
150
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
151
|
+
@model,
|
|
152
|
+
start_date: Date.new(2024, 1, 1),
|
|
153
|
+
end_date: Date.new(2024, 12, 31),
|
|
154
|
+
output_currency: 'USD'
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
report_data = report.generate
|
|
158
|
+
equity_section = report_data[:sections][:equity]
|
|
159
|
+
|
|
160
|
+
assert equity_section[:total], "Should have total equity"
|
|
161
|
+
# Equity: 60,000
|
|
162
|
+
assert_in_delta 60_000, equity_section[:total], 0.01
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_balance_sheet_equation
|
|
166
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
167
|
+
@model,
|
|
168
|
+
start_date: Date.new(2024, 1, 1),
|
|
169
|
+
end_date: Date.new(2024, 12, 31),
|
|
170
|
+
output_currency: 'USD'
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
report_data = report.generate
|
|
174
|
+
totals = report_data[:totals]
|
|
175
|
+
|
|
176
|
+
# Balance sheet equation: Assets = Liabilities + Equity
|
|
177
|
+
# Assets: 85,000 (60,000 current + 25,000 fixed)
|
|
178
|
+
# Liabilities: 25,000 (5,000 current + 20,000 long-term)
|
|
179
|
+
# Equity: 60,000
|
|
180
|
+
# 85,000 = 25,000 + 60,000 ✓
|
|
181
|
+
|
|
182
|
+
total_assets = totals[:assets]
|
|
183
|
+
total_liabilities = totals[:liabilities]
|
|
184
|
+
total_equity = totals[:equity]
|
|
185
|
+
total_liabilities_and_equity = totals[:total_liabilities_and_equity]
|
|
186
|
+
|
|
187
|
+
assert_in_delta 85_000, total_assets, 0.01, "Total assets should be 85,000"
|
|
188
|
+
assert_in_delta 25_000, total_liabilities, 0.01, "Total liabilities should be 25,000"
|
|
189
|
+
assert_in_delta 60_000, total_equity, 0.01, "Total equity should be 60,000"
|
|
190
|
+
assert_in_delta 85_000, total_liabilities_and_equity, 0.01, "Total liabilities and equity should equal total assets"
|
|
191
|
+
|
|
192
|
+
# Verify the balance sheet equation: Assets = Liabilities + Equity
|
|
193
|
+
assert_in_delta total_assets, total_liabilities_and_equity, 0.01,
|
|
194
|
+
"Balance sheet must balance: Assets (#{total_assets}) = Liabilities + Equity (#{total_liabilities_and_equity})"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_generate_at_specific_date
|
|
198
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
199
|
+
@model,
|
|
200
|
+
start_date: Date.new(2024, 1, 1),
|
|
201
|
+
end_date: Date.new(2024, 12, 31),
|
|
202
|
+
output_currency: 'USD'
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
date_report = report.generate_at(Date.new(2024, 6, 15))
|
|
206
|
+
|
|
207
|
+
assert date_report[:period], "Should have period info"
|
|
208
|
+
assert_equal Date.new(2024, 6, 15), date_report[:period][:start], "Should start on specified date"
|
|
209
|
+
assert_equal Date.new(2024, 6, 15), date_report[:period][:end], "Should end on specified date"
|
|
210
|
+
assert date_report[:report], "Should have report data"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def test_generate_monthly_balance_sheets
|
|
214
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
215
|
+
@model,
|
|
216
|
+
start_date: Date.new(2024, 1, 1),
|
|
217
|
+
end_date: Date.new(2024, 3, 31),
|
|
218
|
+
output_currency: 'USD'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
monthly_reports = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
|
|
222
|
+
|
|
223
|
+
assert_equal 3, monthly_reports.length, "Should generate 3 monthly balance sheets"
|
|
224
|
+
|
|
225
|
+
# Check January report
|
|
226
|
+
jan_report = monthly_reports[0]
|
|
227
|
+
assert jan_report[:period], "Should have period info"
|
|
228
|
+
assert_equal Date.new(2024, 1, 1), jan_report[:period][:start], "January should start on Jan 1"
|
|
229
|
+
assert_equal Date.new(2024, 1, 31), jan_report[:period][:end], "January should end on Jan 31"
|
|
230
|
+
assert jan_report[:report], "Should have report data"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def test_section_value
|
|
234
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
235
|
+
@model,
|
|
236
|
+
start_date: Date.new(2024, 1, 1),
|
|
237
|
+
end_date: Date.new(2024, 12, 31),
|
|
238
|
+
output_currency: 'USD'
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
assets_value = report.section_value(:assets)
|
|
242
|
+
liabilities_value = report.section_value(:liabilities)
|
|
243
|
+
equity_value = report.section_value(:equity)
|
|
244
|
+
|
|
245
|
+
assert assets_value, "Should have assets value"
|
|
246
|
+
assert liabilities_value, "Should have liabilities value"
|
|
247
|
+
assert equity_value, "Should have equity value"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_section_count
|
|
251
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
252
|
+
@model,
|
|
253
|
+
start_date: Date.new(2024, 1, 1),
|
|
254
|
+
end_date: Date.new(2024, 12, 31),
|
|
255
|
+
output_currency: 'USD'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assets_count = report.section_count(:assets)
|
|
259
|
+
liabilities_count = report.section_count(:liabilities)
|
|
260
|
+
equity_count = report.section_count(:equity)
|
|
261
|
+
|
|
262
|
+
assert assets_count > 0, "Should have asset items"
|
|
263
|
+
assert liabilities_count > 0, "Should have liability items"
|
|
264
|
+
assert equity_count > 0, "Should have equity items"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def test_assets_show_individual_variables
|
|
268
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
269
|
+
@model,
|
|
270
|
+
start_date: Date.new(2024, 1, 1),
|
|
271
|
+
end_date: Date.new(2024, 12, 31),
|
|
272
|
+
output_currency: 'USD'
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
report_data = report.generate
|
|
276
|
+
assets_section = report_data[:sections][:assets]
|
|
277
|
+
current_assets_item = assets_section[:items].find { |item| item[:name] == :current_assets }
|
|
278
|
+
|
|
279
|
+
# Should have subcategories showing individual variables
|
|
280
|
+
assert current_assets_item[:subcategories], "Current assets should have subcategories"
|
|
281
|
+
assert_equal 2, current_assets_item[:subcategories].length, "Should have 2 current asset variables"
|
|
282
|
+
|
|
283
|
+
# Check that cash and savings are shown separately
|
|
284
|
+
variable_names = current_assets_item[:subcategories].map { |sub| sub[:name] }
|
|
285
|
+
assert variable_names.include?(:cash), "Should show cash separately"
|
|
286
|
+
assert variable_names.include?(:savings_account), "Should show savings_account separately"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_balance_sheet_validation_raises_error_when_unbalanced
|
|
290
|
+
# Create an unbalanced model
|
|
291
|
+
# Note: This test is tricky because equity account is auto-created to balance
|
|
292
|
+
# To create an unbalanced model, we need to manually set equity account balance incorrectly
|
|
293
|
+
unbalanced_model = FinIt.define(default_currency: 'USD') do
|
|
294
|
+
config do
|
|
295
|
+
start_date 2024
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
account :cash_account do
|
|
299
|
+
type :asset
|
|
300
|
+
currency 'USD'
|
|
301
|
+
opening_balance 100_000
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
account :debt_account do
|
|
305
|
+
type :liability
|
|
306
|
+
currency 'USD'
|
|
307
|
+
opening_balance 50_000
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Equity account will be auto-created with balance = 100,000 - 50,000 = 50,000
|
|
311
|
+
# But we'll manually override it to create imbalance
|
|
312
|
+
category :current_assets, type: :asset do
|
|
313
|
+
variable :cash, currency: 'USD', frequency: :annual, account: :cash_account do
|
|
314
|
+
value 100_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
category :current_liabilities, type: :liability do
|
|
319
|
+
variable :debt, currency: 'USD', frequency: :annual, account: :debt_account do
|
|
320
|
+
value 50_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
category :equity, type: :equity do
|
|
325
|
+
# Equity account auto-created with 50,000 balance
|
|
326
|
+
# But we'll manually add a variable that makes it unbalanced
|
|
327
|
+
variable :owner_equity, currency: 'USD', frequency: :annual do
|
|
328
|
+
value 30_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
329
|
+
# This makes it unbalanced: Assets (100,000) != Liabilities (50,000) + Equity (50,000 + 30,000 = 80,000)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Manually override equity account balance to create imbalance
|
|
335
|
+
# This simulates an unbalanced model
|
|
336
|
+
equity_account = unbalanced_model.accounts[:equity]
|
|
337
|
+
equity_account.instance_variable_set(:@opening_balance, 30_000) if equity_account
|
|
338
|
+
|
|
339
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
340
|
+
unbalanced_model,
|
|
341
|
+
start_date: Date.new(2024, 1, 1),
|
|
342
|
+
end_date: Date.new(2024, 12, 31),
|
|
343
|
+
output_currency: 'USD'
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
error = assert_raises(FinIt::Reports::BalanceSheetError) do
|
|
347
|
+
report.generate
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
assert_match(/does not balance/, error.message, "Error should mention balance issue")
|
|
351
|
+
assert_match(/Assets/, error.message, "Error should mention Assets")
|
|
352
|
+
assert_match(/Liabilities \+ Equity/, error.message, "Error should mention Liabilities + Equity")
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../test_helper"
|
|
4
|
+
|
|
5
|
+
class CashFlowStatementTest < 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
|
+
# Driver variables
|
|
19
|
+
category :drivers, type: :driver do
|
|
20
|
+
variable :sales_volume do
|
|
21
|
+
value 100
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
variable :unit_price do
|
|
25
|
+
value 50
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
variable :expense_rate do
|
|
29
|
+
value 0.30
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Income categories
|
|
34
|
+
category :income, type: :income,
|
|
35
|
+
default_account: :checking,
|
|
36
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
37
|
+
|
|
38
|
+
calculated :product_sales,
|
|
39
|
+
formula: "sales_volume * unit_price"
|
|
40
|
+
|
|
41
|
+
variable :service_revenue, currency: 'USD' do
|
|
42
|
+
value 2_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Expense categories
|
|
47
|
+
category :expenses, type: :expense,
|
|
48
|
+
default_account: :checking,
|
|
49
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
50
|
+
|
|
51
|
+
calculated :cost_of_sales,
|
|
52
|
+
formula: "product_sales * expense_rate"
|
|
53
|
+
|
|
54
|
+
variable :operating_expenses, currency: 'USD' do
|
|
55
|
+
value 1_500, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@test_date = Date.new(2024, 6, 15)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_cash_flow_statement_generates
|
|
64
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
65
|
+
@model,
|
|
66
|
+
start_date: Date.new(2024, 1, 1),
|
|
67
|
+
end_date: Date.new(2024, 6, 30),
|
|
68
|
+
output_currency: 'USD'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
report_data = report.generate
|
|
72
|
+
assert report_data
|
|
73
|
+
assert report_data[:sections]
|
|
74
|
+
assert report_data[:totals]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_cash_flow_has_operating_section
|
|
78
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
79
|
+
@model,
|
|
80
|
+
start_date: Date.new(2024, 1, 1),
|
|
81
|
+
end_date: Date.new(2024, 6, 30),
|
|
82
|
+
output_currency: 'USD'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
report_data = report.generate
|
|
86
|
+
assert report_data[:sections][:operating], "Should have operating section"
|
|
87
|
+
assert report_data[:sections][:operating][:total].is_a?(Numeric), "Operating total should be numeric"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_cash_flow_has_investing_section
|
|
91
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
92
|
+
@model,
|
|
93
|
+
start_date: Date.new(2024, 1, 1),
|
|
94
|
+
end_date: Date.new(2024, 6, 30),
|
|
95
|
+
output_currency: 'USD'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
report_data = report.generate
|
|
99
|
+
assert report_data[:sections][:investing], "Should have investing section"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_cash_flow_has_financing_section
|
|
103
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
104
|
+
@model,
|
|
105
|
+
start_date: Date.new(2024, 1, 1),
|
|
106
|
+
end_date: Date.new(2024, 6, 30),
|
|
107
|
+
output_currency: 'USD'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
report_data = report.generate
|
|
111
|
+
assert report_data[:sections][:financing], "Should have financing section"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_cash_flow_calculates_net_change
|
|
115
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
116
|
+
@model,
|
|
117
|
+
start_date: Date.new(2024, 1, 1),
|
|
118
|
+
end_date: Date.new(2024, 6, 30),
|
|
119
|
+
output_currency: 'USD'
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
report_data = report.generate
|
|
123
|
+
assert report_data[:sections][:net_change_in_cash], "Should have net change in cash"
|
|
124
|
+
|
|
125
|
+
net_change = report_data[:sections][:net_change_in_cash][:total]
|
|
126
|
+
operating = report_data[:sections][:operating][:total] || 0
|
|
127
|
+
investing = report_data[:sections][:investing][:total] || 0
|
|
128
|
+
financing = report_data[:sections][:financing][:total] || 0
|
|
129
|
+
|
|
130
|
+
assert_in_delta(operating + investing + financing, net_change, 0.01, "Net change should equal sum of sections")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_cash_flow_has_beginning_and_ending_cash
|
|
134
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
135
|
+
@model,
|
|
136
|
+
start_date: Date.new(2024, 1, 1),
|
|
137
|
+
end_date: Date.new(2024, 6, 30),
|
|
138
|
+
output_currency: 'USD'
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
report_data = report.generate
|
|
142
|
+
assert report_data[:sections][:beginning_cash], "Should have beginning cash"
|
|
143
|
+
assert report_data[:sections][:ending_cash], "Should have ending cash"
|
|
144
|
+
|
|
145
|
+
beginning = report_data[:sections][:beginning_cash][:total]
|
|
146
|
+
net_change = report_data[:sections][:net_change_in_cash][:total]
|
|
147
|
+
ending = report_data[:sections][:ending_cash][:total]
|
|
148
|
+
|
|
149
|
+
assert_in_delta(beginning + net_change, ending, 0.01, "Ending cash = Beginning + Net Change")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_simple_display_mode
|
|
153
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
154
|
+
@model,
|
|
155
|
+
start_date: Date.new(2024, 1, 1),
|
|
156
|
+
end_date: Date.new(2024, 6, 30),
|
|
157
|
+
output_currency: 'USD',
|
|
158
|
+
display_mode: :simple
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
report_data = report.generate
|
|
162
|
+
assert report_data[:sections][:cash_in], "Simple mode should have cash_in section"
|
|
163
|
+
assert report_data[:sections][:cash_out], "Simple mode should have cash_out section"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_generate_monthly
|
|
167
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
168
|
+
@model,
|
|
169
|
+
start_date: Date.new(2024, 1, 1),
|
|
170
|
+
end_date: Date.new(2024, 6, 30),
|
|
171
|
+
output_currency: 'USD'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
monthly = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
|
|
175
|
+
|
|
176
|
+
assert_equal 3, monthly.length, "Should have 3 monthly periods"
|
|
177
|
+
monthly.each do |period|
|
|
178
|
+
assert period[:period], "Each period should have period info"
|
|
179
|
+
assert period[:report], "Each period should have report data"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_period_summary
|
|
184
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
185
|
+
@model,
|
|
186
|
+
start_date: Date.new(2024, 1, 1),
|
|
187
|
+
end_date: Date.new(2024, 6, 30),
|
|
188
|
+
output_currency: 'USD'
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
summary = report.period_summary(period_type: :monthly)
|
|
192
|
+
|
|
193
|
+
assert summary[:periods], "Should have periods"
|
|
194
|
+
assert summary[:summary], "Should have summary"
|
|
195
|
+
assert summary[:summary][:total_operating], "Summary should have total_operating"
|
|
196
|
+
assert summary[:summary][:total_net_change], "Summary should have total_net_change"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_totals_structure
|
|
200
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
201
|
+
@model,
|
|
202
|
+
start_date: Date.new(2024, 1, 1),
|
|
203
|
+
end_date: Date.new(2024, 6, 30),
|
|
204
|
+
output_currency: 'USD'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
report_data = report.generate
|
|
208
|
+
totals = report_data[:totals]
|
|
209
|
+
|
|
210
|
+
assert totals[:operating], "Totals should have operating"
|
|
211
|
+
assert totals[:investing], "Totals should have investing"
|
|
212
|
+
assert totals[:financing], "Totals should have financing"
|
|
213
|
+
assert totals[:net_change_in_cash], "Totals should have net_change_in_cash"
|
|
214
|
+
assert totals[:beginning_cash], "Totals should have beginning_cash"
|
|
215
|
+
assert totals[:ending_cash], "Totals should have ending_cash"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def test_indirect_method_starts_with_net_income
|
|
219
|
+
report = FinIt::Reports::CashFlowStatement.new(
|
|
220
|
+
@model,
|
|
221
|
+
start_date: Date.new(2024, 1, 1),
|
|
222
|
+
end_date: Date.new(2024, 6, 30),
|
|
223
|
+
output_currency: 'USD',
|
|
224
|
+
method: :indirect
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
report_data = report.generate
|
|
228
|
+
operating_items = report_data[:sections][:operating][:items]
|
|
229
|
+
|
|
230
|
+
# First item should be net income in indirect method
|
|
231
|
+
assert operating_items.any? { |item| item[:name] == :net_income },
|
|
232
|
+
"Indirect method should include net income in operating section"
|
|
233
|
+
end
|
|
234
|
+
end
|