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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +24 -0
  3. data/CHANGELOG.md +9 -0
  4. data/CONTRIBUTING.md +20 -0
  5. data/LICENSE +21 -0
  6. data/QUICKSTART.md +56 -0
  7. data/README.md +74 -0
  8. data/Rakefile +23 -0
  9. data/SECURITY.md +14 -0
  10. data/assets/fin_it_logo.png +0 -0
  11. data/lib/fin_it/account.rb +120 -0
  12. data/lib/fin_it/calculator/currency_conversion.rb +27 -0
  13. data/lib/fin_it/calculator/date_helpers.rb +53 -0
  14. data/lib/fin_it/calculator/variable_hashing.rb +120 -0
  15. data/lib/fin_it/calculator.rb +480 -0
  16. data/lib/fin_it/categories/category.rb +137 -0
  17. data/lib/fin_it/complex_model.rb +169 -0
  18. data/lib/fin_it/dsl/account_builder.rb +35 -0
  19. data/lib/fin_it/dsl/calculated_builder.rb +87 -0
  20. data/lib/fin_it/dsl/config_builder.rb +58 -0
  21. data/lib/fin_it/dsl/model_builder.rb +938 -0
  22. data/lib/fin_it/dsl/model_template_builder.rb +29 -0
  23. data/lib/fin_it/dsl/plan_builder.rb +52 -0
  24. data/lib/fin_it/dsl/project_inheritance_resolver.rb +46 -0
  25. data/lib/fin_it/dsl/variable_builder.rb +41 -0
  26. data/lib/fin_it/dsl.rb +13 -0
  27. data/lib/fin_it/engine.rb +15 -0
  28. data/lib/fin_it/financial_model/account_balances.rb +99 -0
  29. data/lib/fin_it/financial_model/account_hierarchy.rb +158 -0
  30. data/lib/fin_it/financial_model/category_values.rb +179 -0
  31. data/lib/fin_it/financial_model/currency_helpers.rb +14 -0
  32. data/lib/fin_it/financial_model/date_helpers.rb +58 -0
  33. data/lib/fin_it/financial_model/debugging.rb +353 -0
  34. data/lib/fin_it/financial_model/period_flows.rb +121 -0
  35. data/lib/fin_it/financial_model/validation.rb +85 -0
  36. data/lib/fin_it/financial_model/variable_matching.rb +49 -0
  37. data/lib/fin_it/financial_model.rb +395 -0
  38. data/lib/fin_it/model_template.rb +121 -0
  39. data/lib/fin_it/outputs/base_output.rb +51 -0
  40. data/lib/fin_it/outputs/console_output.rb +1528 -0
  41. data/lib/fin_it/outputs/monthly_console_output.rb +145 -0
  42. data/lib/fin_it/outputs/zaxcel_output.rb +1264 -0
  43. data/lib/fin_it/payment_schedule.rb +112 -0
  44. data/lib/fin_it/plan.rb +159 -0
  45. data/lib/fin_it/reports/balance_sheet.rb +638 -0
  46. data/lib/fin_it/reports/base_report.rb +239 -0
  47. data/lib/fin_it/reports/cash_flow_statement.rb +480 -0
  48. data/lib/fin_it/reports/custom_sheet.rb +436 -0
  49. data/lib/fin_it/reports/income_statement.rb +793 -0
  50. data/lib/fin_it/reports/period_comparison.rb +309 -0
  51. data/lib/fin_it/reports/scenario_comparison.rb +296 -0
  52. data/lib/fin_it/temporal_value.rb +349 -0
  53. data/lib/fin_it/transaction_generator/account_resolver.rb +118 -0
  54. data/lib/fin_it/transaction_generator/cache_management.rb +39 -0
  55. data/lib/fin_it/transaction_generator/date_generation.rb +57 -0
  56. data/lib/fin_it/transaction_generator.rb +357 -0
  57. data/lib/fin_it/version.rb +6 -0
  58. data/lib/fin_it.rb +27 -0
  59. data/test/fin_it/calculator_test.rb +109 -0
  60. data/test/fin_it/complex_model_test.rb +198 -0
  61. data/test/fin_it/debugging_test.rb +112 -0
  62. data/test/fin_it/driver_variables_test.rb +109 -0
  63. data/test/fin_it/dsl_test.rb +581 -0
  64. data/test/fin_it/financial_model_test.rb +196 -0
  65. data/test/fin_it/frequency_test.rb +51 -0
  66. data/test/fin_it/outputs/console_output_test.rb +249 -0
  67. data/test/fin_it/plan_test.rb +281 -0
  68. data/test/fin_it/reports/account_balance_test.rb +232 -0
  69. data/test/fin_it/reports/balance_sheet_test.rb +355 -0
  70. data/test/fin_it/reports/cash_flow_statement_test.rb +234 -0
  71. data/test/fin_it/reports/custom_sheet_test.rb +246 -0
  72. data/test/fin_it/reports/income_statement_test.rb +431 -0
  73. data/test/fin_it/reports/period_comparison_test.rb +226 -0
  74. data/test/fin_it/reports/restaurant_model_test.rb +225 -0
  75. data/test/fin_it/reports/scenario_comparison_test.rb +414 -0
  76. data/test/scripts/generate_demo_reports.rb +47 -0
  77. data/test/scripts/startup_saas_demo.rb +62 -0
  78. data/test/test_helper.rb +25 -0
  79. data/test/verify_accounting_equation.rb +91 -0
  80. metadata +264 -0
@@ -0,0 +1,395 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'money'
4
+ require_relative 'transaction_generator'
5
+ require_relative 'financial_model/debugging'
6
+ require_relative 'financial_model/date_helpers'
7
+ require_relative 'financial_model/validation'
8
+ require_relative 'financial_model/currency_helpers'
9
+ require_relative 'financial_model/variable_matching'
10
+ require_relative 'financial_model/account_balances'
11
+ require_relative 'financial_model/period_flows'
12
+ require_relative 'financial_model/category_values'
13
+ require_relative 'financial_model/account_hierarchy'
14
+
15
+ module FinIt
16
+ # Custom error for balance sheet validation
17
+ class BalanceSheetValidationError < StandardError; end
18
+
19
+ # Custom error for start date validation
20
+ class StartDateValidationError < StandardError; end
21
+
22
+ # Custom error for undefined variables in formulas
23
+ class UndefinedVariableError < StandardError; end
24
+
25
+ # The built financial model - coordinates calculator, accounts, and transactions
26
+ class FinancialModel
27
+ include FinancialModel::Debugging
28
+ include FinancialModel::DateHelpers
29
+ include FinancialModel::Validation
30
+ include FinancialModel::CurrencyHelpers
31
+ include FinancialModel::VariableMatching
32
+ include FinancialModel::AccountBalances
33
+ include FinancialModel::PeriodFlows
34
+ include FinancialModel::CategoryValues
35
+ include FinancialModel::AccountHierarchy
36
+
37
+ attr_reader :calculator, :categories, :config, :accounts, :complex_models, :category_accounts
38
+
39
+ def initialize(calculator, categories, config, accounts = {}, category_accounts = {})
40
+ @calculator = calculator
41
+ @categories = categories
42
+ @config = config
43
+ @accounts = accounts
44
+ @category_accounts = category_accounts # Map category -> account for bidirectional relationship
45
+ @start_date = config[:start_date]
46
+ @complex_models = config[:complex_models] || {}
47
+ @transaction_generator = TransactionGenerator.new(self)
48
+ end
49
+
50
+ # Calculate all values for a specific date
51
+ def calculate_at(date)
52
+ result = {}
53
+
54
+ @calculator.variable_names.each do |name|
55
+ result[name] = @calculator.calculate(name, date: date)
56
+ end
57
+
58
+ result
59
+ end
60
+
61
+ # Calculate for a date range
62
+ def calculate_range(start_date, end_date, frequency: :monthly)
63
+ @calculator.calculate_range(start_date, end_date, frequency: frequency)
64
+ end
65
+
66
+ # Get a specific value
67
+ def get(name, date: nil)
68
+ @calculator.get(name, date: date)
69
+ end
70
+
71
+ # Generate a report
72
+ def to_report(date: nil)
73
+ calculate_at(date || Date.today)
74
+ end
75
+
76
+ # Export to hash
77
+ def to_h
78
+ {
79
+ config: @config,
80
+ categories: @categories,
81
+ values: to_report
82
+ }
83
+ end
84
+
85
+ # Get start date
86
+ def start_date
87
+ @start_date
88
+ end
89
+
90
+ # Generate all transactions up to end_date
91
+ def generate_transactions(end_date)
92
+ @transaction_generator.generate_transactions(end_date)
93
+ end
94
+
95
+ # Query transactions
96
+ def transactions(date_range: nil, account: nil, variable: nil)
97
+ @transaction_generator.transactions(
98
+ date_range: date_range,
99
+ account: account,
100
+ variable: variable
101
+ )
102
+ end
103
+
104
+ # Get variable value for a specific period
105
+ def variable_value(variable_name, date:, period_type: :annual)
106
+ @calculator.calculate(
107
+ variable_name,
108
+ date: date,
109
+ output_currency: @config[:default_currency],
110
+ period_type: period_type
111
+ )
112
+ end
113
+
114
+ # Check if transaction cache is valid
115
+ def transaction_cache_valid?
116
+ @transaction_generator.cache_valid?
117
+ end
118
+
119
+ # Calculate period net income (Income - Expenses) for a date range
120
+ def period_net_income(start_date, end_date, output_currency: nil, filters: {})
121
+ output_currency ||= @config[:default_currency]
122
+ start_date = parse_date(start_date)
123
+ end_date = parse_date(end_date)
124
+
125
+ # Get all income categories
126
+ income_categories = @categories.select { |c| c.type == :income }
127
+
128
+ # Get all expense categories
129
+ expense_categories = @categories.select { |c| c.type == :expense }
130
+
131
+ # Calculate total income for the period using account hierarchy
132
+ total_income = income_categories.sum do |category|
133
+ category_total_via_account(
134
+ category,
135
+ start_date,
136
+ end_date,
137
+ period_type: :annual,
138
+ output_currency: output_currency,
139
+ filters: filters,
140
+ use_balance: false
141
+ )
142
+ end
143
+
144
+ # Calculate total expenses for the period using account hierarchy
145
+ # Note: account flows for expenses are negative (debits), so we need to take absolute value
146
+ total_expenses = expense_categories.sum do |category|
147
+ expense_value = category_total_via_account(
148
+ category,
149
+ start_date,
150
+ end_date,
151
+ period_type: :annual,
152
+ output_currency: output_currency,
153
+ filters: filters,
154
+ use_balance: false
155
+ )
156
+ # Expense account flows are negative, so take absolute value for calculation
157
+ expense_value.abs
158
+ end
159
+
160
+ # Net income = Income - Expenses
161
+ net_income = total_income - total_expenses
162
+
163
+ # Convert to float if Money object
164
+ net_income.is_a?(Money) ? net_income.to_f : net_income.to_f
165
+ end
166
+
167
+ # ========== Plan/Scenario Methods ==========
168
+
169
+ # Get all defined plans
170
+ def plans
171
+ @config[:plans] || {}
172
+ end
173
+
174
+ # Apply a single plan and return a new isolated model
175
+ # @param plan_or_name [Plan, Symbol] The plan object or name of a registered plan
176
+ # @return [FinancialModel] A new model with the plan applied
177
+ def with_plan(plan_or_name)
178
+ plan = resolve_plan(plan_or_name)
179
+ raise ArgumentError, "Plan not found: #{plan_or_name}" unless plan
180
+
181
+ cloned = clone_model
182
+ cloned.apply_plan!(plan)
183
+ cloned
184
+ end
185
+
186
+ # Apply multiple plans in order and return a new isolated model
187
+ # Later plans override earlier ones on conflicts
188
+ # @param plan_names [Array<Symbol, Plan>] The plans to apply
189
+ # @return [FinancialModel] A new model with all plans applied
190
+ def with_plans(*plan_names)
191
+ cloned = clone_model
192
+ plan_names.flatten.each do |plan_or_name|
193
+ plan = resolve_plan(plan_or_name)
194
+ raise ArgumentError, "Plan not found: #{plan_or_name}" unless plan
195
+ cloned.apply_plan!(plan)
196
+ end
197
+ cloned
198
+ end
199
+
200
+ # Apply a plan to this model (mutates this instance)
201
+ # @param plan [Plan] The plan to apply
202
+ # @return [self]
203
+ def apply_plan!(plan)
204
+ plan.overrides.each do |override|
205
+ apply_override(override, plan)
206
+ end
207
+ @transaction_generator.invalidate_cache! if @transaction_generator.respond_to?(:invalidate_cache!)
208
+ self
209
+ end
210
+
211
+ # Create a deep clone of this model for isolation
212
+ # @return [FinancialModel] A new independent model
213
+ def clone_model
214
+ cloned_calculator = @calculator.deep_clone
215
+ cloned_categories = deep_clone_categories(@categories)
216
+ cloned_accounts = deep_clone_accounts(@accounts)
217
+ cloned_category_accounts = rebuild_category_accounts(cloned_categories, cloned_accounts)
218
+
219
+ cloned_config = @config.dup
220
+ cloned_config[:plans] = @config[:plans]&.transform_values(&:dup)
221
+ cloned_config[:complex_models] = @config[:complex_models]&.transform_values(&:dup)
222
+
223
+ FinancialModel.new(
224
+ cloned_calculator,
225
+ cloned_categories,
226
+ cloned_config,
227
+ cloned_accounts,
228
+ cloned_category_accounts
229
+ )
230
+ end
231
+
232
+ private
233
+
234
+ def resolve_plan(plan_or_name)
235
+ case plan_or_name
236
+ when Plan
237
+ plan_or_name
238
+ when Symbol, String
239
+ plans[plan_or_name.to_sym]
240
+ else
241
+ nil
242
+ end
243
+ end
244
+
245
+ def apply_override(override, plan)
246
+ var_name = override[:variable]
247
+ start_date = override[:start_date] || plan.start_date
248
+ end_date = override[:end_date] || plan.end_date
249
+
250
+ case override[:type]
251
+ when :set
252
+ @calculator.set_override(var_name, override[:value],
253
+ start_date: start_date, end_date: end_date)
254
+ when :scale
255
+ @calculator.scale_variable(var_name, override[:factor],
256
+ start_date: start_date, end_date: end_date)
257
+ when :adjust
258
+ @calculator.adjust_variable(var_name, override[:amount],
259
+ start_date: start_date, end_date: end_date)
260
+ when :formula
261
+ @calculator.override_formula(var_name, override[:formula],
262
+ start_date: start_date, end_date: end_date)
263
+ when :opening_balance
264
+ account = @accounts[override[:account]]
265
+ raise ArgumentError, "Account not found: #{override[:account]}" unless account
266
+ account.instance_variable_set(:@opening_balance, override[:amount])
267
+ when :add_variable
268
+ add_variable_from_override(override, plan)
269
+ when :add_calculated
270
+ add_calculated_from_override(override, plan)
271
+ end
272
+ end
273
+
274
+ def add_variable_from_override(override, plan)
275
+ # Find category
276
+ category = find_category_by_name(override[:category])
277
+ return unless category
278
+
279
+ # Create a simple variable builder context
280
+ var_builder = VariableBuilderContext.new
281
+ override[:block]&.call(var_builder)
282
+
283
+ # Add to calculator
284
+ @calculator.define_variable(
285
+ override[:name],
286
+ var_builder.value_amount,
287
+ currency: override[:currency] || @config[:default_currency],
288
+ frequency: var_builder.frequency || :monthly,
289
+ start_date: var_builder.start_date || plan.start_date,
290
+ end_date: var_builder.end_date || plan.end_date
291
+ )
292
+
293
+ # Add to category's variables
294
+ category.variables << {
295
+ name: override[:name],
296
+ type: :financial,
297
+ currency: override[:currency] || @config[:default_currency],
298
+ frequency: var_builder.frequency || :monthly
299
+ }
300
+ end
301
+
302
+ def add_calculated_from_override(override, plan)
303
+ category = find_category_by_name(override[:category])
304
+ return unless category
305
+
306
+ options = override[:options] || {}
307
+ @calculator.define_calculated(
308
+ override[:name],
309
+ override[:formula],
310
+ start_date: options[:start_date] || plan.start_date,
311
+ end_date: options[:end_date] || plan.end_date
312
+ )
313
+
314
+ category.variables << {
315
+ name: override[:name],
316
+ type: :calculated,
317
+ formula: override[:formula]
318
+ }
319
+ end
320
+
321
+ def find_category_by_name(name)
322
+ name = name.to_sym
323
+ @categories.each do |cat|
324
+ return cat if cat.name == name
325
+ found = cat.find_subcategory(name)
326
+ return found if found
327
+ end
328
+ nil
329
+ end
330
+
331
+ def deep_clone_categories(categories)
332
+ categories.map { |cat| cat.deep_clone }
333
+ end
334
+
335
+ def deep_clone_accounts(accounts)
336
+ cloned = {}
337
+ accounts.each do |name, account|
338
+ cloned[name] = account.deep_clone
339
+ end
340
+
341
+ # Rebuild parent-child relationships
342
+ accounts.each do |name, original_account|
343
+ if original_account.parent
344
+ cloned[name].instance_variable_set(:@parent, cloned[original_account.parent.name])
345
+ cloned[original_account.parent.name].children << cloned[name]
346
+ end
347
+ end
348
+
349
+ cloned
350
+ end
351
+
352
+ def rebuild_category_accounts(cloned_categories, cloned_accounts)
353
+ result = {}
354
+ @category_accounts.each do |original_cat, original_account|
355
+ # Find corresponding cloned category
356
+ cloned_cat = find_cloned_category(cloned_categories, original_cat.name)
357
+ if cloned_cat && cloned_accounts[original_account.name]
358
+ result[cloned_cat] = cloned_accounts[original_account.name]
359
+ end
360
+ end
361
+ result
362
+ end
363
+
364
+ def find_cloned_category(categories, name)
365
+ categories.each do |cat|
366
+ return cat if cat.name == name
367
+ found = cat.find_subcategory(name)
368
+ return found if found
369
+ end
370
+ nil
371
+ end
372
+ end
373
+
374
+ # Simple context for building variables from plan overrides
375
+ class VariableBuilderContext
376
+ attr_reader :value_amount, :frequency, :start_date, :end_date
377
+
378
+ def initialize
379
+ @value_amount = 0
380
+ @frequency = :monthly
381
+ end
382
+
383
+ def value(amount, start_date: nil, end_date: nil)
384
+ @value_amount = amount
385
+ @start_date = start_date
386
+ @end_date = end_date
387
+ end
388
+
389
+ def frequency=(freq)
390
+ @frequency = freq
391
+ end
392
+ end
393
+ end
394
+
395
+
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FinIt
4
+ # Template for reusable complex models (e.g., mortgage payment template)
5
+ class ModelTemplate
6
+ attr_reader :name, :variables, :default_debit_accounts, :default_credit_accounts, :calculation_block
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ @variables = []
11
+ @default_debit_accounts = []
12
+ @default_credit_accounts = []
13
+ @calculation_block = nil
14
+ end
15
+
16
+ def add_variable(var_name)
17
+ @variables << var_name.to_sym
18
+ end
19
+
20
+ def set_default_debit_accounts(accounts)
21
+ @default_debit_accounts = Array(accounts).map(&:to_sym)
22
+ end
23
+
24
+ def set_default_credit_accounts(accounts)
25
+ @default_credit_accounts = Array(accounts).map(&:to_sym)
26
+ end
27
+
28
+ def set_calculation(&block)
29
+ @calculation_block = block
30
+ end
31
+
32
+ # Instantiate this template as a ComplexModel
33
+ def instantiate(instance_name, params = {}, accounts: nil, default_currency: 'USD', &block)
34
+ # Extract reserved params
35
+ start_date = params.delete(:start_date) || params.delete('start_date')
36
+ end_date = params.delete(:end_date) || params.delete('end_date')
37
+ frequency = params.delete(:frequency) || params.delete('frequency') || :monthly
38
+ debit_account = params.delete(:debit_account) || params.delete('debit_account') || @default_debit_accounts.first
39
+ credit_account = params.delete(:credit_account) || params.delete('credit_account') || @default_credit_accounts.first
40
+ project = params.delete(:project) || params.delete('project')
41
+
42
+ # Auto-create debit_account if it doesn't exist
43
+ if debit_account && accounts
44
+ debit_account = ensure_account_exists(debit_account, accounts, default_currency: default_currency)
45
+ end
46
+
47
+ # Remaining params become input variables
48
+ input_variables = params
49
+
50
+ # If debit_account is a template variable, add it to input_variables with the resolved account name
51
+ if @variables.include?(:debit_account) && debit_account
52
+ input_variables[:debit_account] = debit_account
53
+ end
54
+
55
+ # If credit_account is a template variable, add it to input_variables
56
+ if @variables.include?(:credit_account) && credit_account
57
+ input_variables[:credit_account] = credit_account
58
+ end
59
+
60
+ # Validate required variables are provided
61
+ missing_vars = @variables - input_variables.keys.map(&:to_sym)
62
+ if missing_vars.any?
63
+ raise ArgumentError, "Missing required variables for #{@name}: #{missing_vars.join(', ')}"
64
+ end
65
+
66
+ # Create ComplexModel with wrapped calculation block
67
+ require_relative 'complex_model'
68
+ ComplexModel.new(
69
+ instance_name,
70
+ start_date: start_date,
71
+ end_date: end_date,
72
+ frequency: frequency,
73
+ debit_account: debit_account,
74
+ credit_account: credit_account,
75
+ project: project
76
+ ) do |date, context|
77
+ # Merge template input variables into context
78
+ full_context = input_variables.merge(context)
79
+ # Call template's calculation block
80
+ @calculation_block.call(date, full_context)
81
+ end.tap do |model|
82
+ # Set input variables
83
+ input_variables.each do |key, value|
84
+ model.input_variable(key, value)
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ # Ensure account exists, creating it if necessary
92
+ # Supports array notation for nested accounts (e.g., [:expense, :loan_interest, :vendor_loan])
93
+ # but only creates the final account for now
94
+ def ensure_account_exists(account_name, accounts_hash, default_currency: 'USD')
95
+ # Handle array notation - extract the final account name
96
+ actual_account_name = if account_name.is_a?(Array)
97
+ account_name.last
98
+ else
99
+ account_name
100
+ end
101
+
102
+ # Convert to symbol if needed
103
+ actual_account_name = actual_account_name.to_sym
104
+
105
+ # Check if account already exists
106
+ return actual_account_name if accounts_hash.key?(actual_account_name)
107
+
108
+ # Create the account as equity type (default for expense accounts)
109
+ require_relative 'account'
110
+ new_account = Account.new(
111
+ actual_account_name,
112
+ type: :equity,
113
+ currency: default_currency,
114
+ opening_balance: 0
115
+ )
116
+
117
+ accounts_hash[actual_account_name] = new_account
118
+ actual_account_name
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FinIt
4
+ module Outputs
5
+ class BaseOutput
6
+ attr_reader :report, :options
7
+
8
+ def initialize(report, options = {})
9
+ @report = report
10
+ @options = options
11
+ end
12
+
13
+ def generate
14
+ raise NotImplementedError, "Subclasses must implement generate"
15
+ end
16
+
17
+ protected
18
+
19
+ def format_number(num)
20
+ return "N/A" if num.nil?
21
+
22
+ if num.is_a?(Money)
23
+ num.format(symbol: false, thousands_separator: ',')
24
+ else
25
+ num.to_i.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
26
+ end
27
+ end
28
+
29
+ def currency_symbol(currency)
30
+ case currency.to_s
31
+ when 'USD' then '$'
32
+ when 'EUR' then '€'
33
+ when 'MXN' then 'MXN'
34
+ when 'GBP' then '£'
35
+ when 'JPY' then '¥'
36
+ else currency.to_s + ' '
37
+ end
38
+ end
39
+
40
+ def format_currency(value, currency = nil)
41
+ currency ||= @report.output_currency
42
+
43
+ if value.is_a?(Money)
44
+ "#{currency_symbol(value.currency.iso_code)}#{format_number(value)}"
45
+ else
46
+ "#{currency_symbol(currency)}#{format_number(value)}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end