hledger-forecast 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +22 -0
  3. data/.github/workflows/{ci.yml → test.yml} +5 -5
  4. data/.rubocop.yml +17 -17
  5. data/README.md +8 -172
  6. data/example.journal +14 -14
  7. data/hledger-forecast.gemspec +1 -1
  8. data/lib/hledger_forecast/calculator.rb +5 -1
  9. data/lib/hledger_forecast/cli.rb +5 -18
  10. data/lib/hledger_forecast/generator.rb +52 -35
  11. data/lib/hledger_forecast/settings.rb +42 -27
  12. data/lib/hledger_forecast/summarizer.rb +28 -62
  13. data/lib/hledger_forecast/summarizer_formatter.rb +12 -3
  14. data/lib/hledger_forecast/transactions/default.rb +28 -57
  15. data/lib/hledger_forecast/transactions/trackers.rb +34 -40
  16. data/lib/hledger_forecast/utilities.rb +14 -0
  17. data/lib/hledger_forecast/version.rb +1 -1
  18. data/lib/hledger_forecast.rb +1 -2
  19. data/spec/cli_spec.rb +3 -12
  20. data/spec/computed_amounts_spec.rb +11 -22
  21. data/spec/custom_spec.rb +15 -35
  22. data/spec/half-yearly_spec.rb +6 -13
  23. data/spec/monthly_end_date_spec.rb +8 -18
  24. data/spec/monthly_end_date_transaction_spec.rb +20 -45
  25. data/spec/monthly_spec.rb +11 -28
  26. data/spec/once_spec.rb +6 -13
  27. data/spec/quarterly_spec.rb +5 -12
  28. data/spec/summarizer_spec.rb +11 -42
  29. data/spec/track_spec.rb +19 -49
  30. data/spec/verbose_output_spec.rb +3 -3
  31. data/spec/yearly_spec.rb +5 -12
  32. metadata +10 -18
  33. data/example.yml +0 -98
  34. data/lib/hledger_forecast/csv_parser.rb +0 -106
  35. data/spec/csv_and_yml_comparison_spec.rb +0 -32
  36. data/spec/csv_parser_spec.rb +0 -110
  37. data/spec/modifier_spec.rb +0 -102
  38. data/spec/stubs/forecast.yml +0 -19
@@ -1,41 +1,56 @@
1
1
  module HledgerForecast
2
- # Set the options from a user's confgi
2
+ # Set the options from a user's config
3
3
  class Settings
4
4
  def self.config(forecast, cli_options)
5
5
  settings = {}
6
+ settings[:max_amount] = 0
7
+ settings[:max_category] = 0
6
8
 
7
- settings[:max_amount] = get_max_field_size(forecast, 'amount') + 1 # +1 for the negatives
8
- settings[:max_category] = get_max_field_size(forecast, 'category')
9
+ forecast.each do |row|
10
+ if row['type'] != 'settings'
11
+ category_length = row['category'].length
12
+ settings[:max_category] = category_length if category_length > settings[:max_category]
9
13
 
10
- settings[:currency] = Money::Currency.new(forecast.fetch('settings', {}).fetch('currency', 'USD'))
11
- settings[:show_symbol] = forecast.fetch('settings', {}).fetch('show_symbol', true)
12
- # settings[:sign_before_symbol] = forecast.fetch('settings', {}).fetch('sign_before_symbol', false)
13
- settings[:thousands_separator] = forecast.fetch('settings', {}).fetch('thousands_separator', true)
14
+ amount = if row['amount'].is_a?(Integer) || row['amount'].is_a?(Float)
15
+ ((row['amount'] + 3) * 100).to_s
16
+ else
17
+ row['amount'].to_s
18
+ end
14
19
 
15
- settings.merge!(cli_options) if cli_options
16
-
17
- settings
18
- end
20
+ settings[:max_amount] = amount.length if amount.length > settings[:max_amount]
21
+ end
19
22
 
20
- def self.get_max_field_size(block, field)
21
- max_size = 0
22
-
23
- block.each do |period, items|
24
- next if %w[settings].include?(period)
25
-
26
- items.each do |item|
27
- item['transactions'].each do |t|
28
- field_value = if t[field].is_a?(Integer) || t[field].is_a?(Float)
29
- ((t[field] + 3) * 100).to_s
30
- else
31
- t[field].to_s
32
- end
33
- max_size = [max_size, field_value.length].max
34
- end
23
+ if row['type'] == 'settings'
24
+
25
+ settings[:currency] = if row['frequency'] == "currency"
26
+ row['account']
27
+ else
28
+ "USD"
29
+ end
30
+
31
+ settings[:show_symbol] = if row['frequency'] == "show_symbol"
32
+ row['account']
33
+ else
34
+ true
35
+ end
36
+
37
+ settings[:sign_before_symbol] = if row['frequency'] == "sign_before_symbol"
38
+ row['account']
39
+ else
40
+ false
41
+ end
42
+
43
+ settings[:thousands_separator] = if row['frequency'] == "thousands_separator"
44
+ row['account']
45
+ else
46
+ ","
47
+ end
35
48
  end
49
+
50
+ settings.merge!(cli_options) if cli_options
36
51
  end
37
52
 
38
- max_size
53
+ settings
39
54
  end
40
55
  end
41
56
  end
@@ -6,67 +6,46 @@ module HledgerForecast
6
6
  end
7
7
 
8
8
  def summarize(config, cli_options = nil)
9
- @forecast = YAML.safe_load(config)
9
+ @forecast = CSV.parse(config, headers: true)
10
10
  @settings = Settings.config(@forecast, cli_options)
11
11
 
12
- return { output: generate(@forecast), settings: @settings }
12
+ { output: generate(@forecast), settings: @settings }
13
13
  end
14
14
 
15
15
  private
16
16
 
17
17
  def generate(forecast)
18
- output = {}
19
- forecast.each do |period, blocks|
20
- next if %w[settings].include?(period)
21
-
22
- blocks.each do |block|
23
- key = if @settings[:roll_up].nil?
24
- period
25
- else
26
- output.length
27
- end
28
-
29
- output[key] ||= []
30
- output[key] << process_block(period, block)
31
- end
32
- end
33
-
34
- output = filter_out(flatten_and_merge(output))
35
- output = calculate_rolled_up_amount(output) unless @settings[:roll_up].nil?
36
-
37
- output
38
- end
39
-
40
- def process_block(period, block)
41
18
  output = []
42
19
 
43
- output << {
44
- account: block['account'],
45
- from: Date.parse(block['from']),
46
- to: block['to'] ? Date.parse(block['to']) : nil,
47
- type: period,
48
- frequency: block['frequency'],
49
- transactions: []
50
- }
20
+ forecast.each do |row|
21
+ next if row['type'] == 'settings'
22
+ next if row['summary_exclude']
51
23
 
52
- process_transactions(period, block, output)
53
- end
24
+ row['amount'] = Utilities.convert_amount(row['amount'])
54
25
 
55
- def process_transactions(period, block, output)
56
- block['transactions'].each do |t|
57
- amount = Calculator.new.evaluate(t['amount'])
26
+ begin
27
+ annualised_amount = row['roll-up'] ? row['amount'] * row['roll-up'].to_f : row['amount'] * annualise(row['type'])
28
+ rescue StandardError
29
+ puts "\nError: ".bold.red + 'Could not create an annualised ammount. Have you set the roll-up for your custom type transactions?'
30
+ exit
31
+ end
58
32
 
59
- output.last[:transactions] << {
60
- amount: amount,
61
- annualised_amount: amount * (t['roll-up'] || annualise(period)),
62
- rolled_up_amount: 0,
63
- category: t['category'],
64
- exclude: t['summary_exclude'],
65
- description: t['description'],
66
- to: t['to'] ? Calculator.new.evaluate_date(Date.parse(block['from']), t['to']) : nil
33
+ output << {
34
+ account: row['account'],
35
+ from: Date.parse(row['from']),
36
+ to: row['to'] ? Calculator.new.evaluate_date(Date.parse(row['from']), row['to']) : nil,
37
+ type: row['type'],
38
+ frequency: row['frequency'],
39
+ category: row['category'],
40
+ description: row['description'],
41
+ amount: row['amount'],
42
+ annualised_amount: annualised_amount.to_f,
43
+ exclude: row['summary_exclude']
67
44
  }
68
45
  end
69
46
 
47
+ output = calculate_rolled_up_amount(output) unless @settings[:roll_up].nil?
48
+
70
49
  output
71
50
  end
72
51
 
@@ -84,22 +63,9 @@ module HledgerForecast
84
63
  annualise[period]
85
64
  end
86
65
 
87
- def filter_out(data)
88
- data.reject { |item| item[:exclude] == true }
89
- end
90
-
91
- def flatten_and_merge(blocks)
92
- blocks.values.flatten.flat_map do |block|
93
- block[:transactions].map do |transaction|
94
- block.slice(:account, :from, :to, :type, :frequency).merge(transaction)
95
- end
96
- end
97
- end
98
-
99
- def calculate_rolled_up_amount(data)
100
- data.map do |item|
101
- item[:rolled_up_amount] = item[:annualised_amount] / annualise(@settings[:roll_up])
102
- item
66
+ def calculate_rolled_up_amount(forecast)
67
+ forecast.each do |row|
68
+ row[:rolled_up_amount] = row[:annualised_amount] / annualise(@settings[:roll_up])
103
69
  end
104
70
  end
105
71
  end
@@ -41,7 +41,7 @@ module HledgerForecast
41
41
  @table.add_row([{ value: type.capitalize.bold, colspan: 3, alignment: :center }])
42
42
  total = 0
43
43
  items.each do |item|
44
- total += item[:amount]
44
+ total += item[:amount].to_f
45
45
 
46
46
  if @settings[:verbose]
47
47
  @table.add_row [{ value: item[:category], alignment: :left },
@@ -63,7 +63,7 @@ module HledgerForecast
63
63
  def sort(data)
64
64
  data.each do |type, items|
65
65
  data[type] = items.sort_by do |item|
66
- value = item[:amount]
66
+ value = item[:amount].to_f
67
67
  [value >= 0 ? 1 : 0, value >= 0 ? -value : value]
68
68
  end
69
69
  end
@@ -114,12 +114,21 @@ module HledgerForecast
114
114
 
115
115
  def add_total_row_to_table(data, row_to_sum)
116
116
  total = data.reduce(0) do |sum, item|
117
- sum + item[row_to_sum]
117
+ sum + item[row_to_sum].to_f
118
118
  end
119
119
 
120
+ income = data.reduce(0) do |sum, item|
121
+ sum += item[row_to_sum].to_f if item[row_to_sum].to_f < 0
122
+ sum
123
+ end
124
+
125
+ savings = (total / income * 100).to_f.round(2)
126
+
120
127
  @table.add_separator
121
128
  @table.add_row [{ value: "TOTAL".bold, colspan: 2, alignment: :left },
122
129
  { value: format_amount(total).bold, alignment: :right }]
130
+ @table.add_row [{ value: "as a % of income".italic, colspan: 2, alignment: :left },
131
+ { value: "#{savings}%".italic, alignment: :right }]
123
132
  end
124
133
 
125
134
  def format_amount(amount)
@@ -6,15 +6,15 @@ module HledgerForecast
6
6
  # Expenses:Groceries $250.00 ; Food expenses
7
7
  # Assets:Checking
8
8
  class Default
9
- def self.generate(data, options)
10
- new(data, options).generate
9
+ def self.generate(forecast, settings)
10
+ new(forecast, settings).generate
11
11
  end
12
12
 
13
13
  def generate
14
- data.each_value do |blocks|
15
- blocks.each do |block|
16
- process_block(block)
17
- end
14
+ forecast.each do |row|
15
+ next if row[:type] == "settings"
16
+
17
+ process_transactions(row)
18
18
  end
19
19
 
20
20
  output
@@ -22,59 +22,34 @@ module HledgerForecast
22
22
 
23
23
  private
24
24
 
25
- attr_reader :data, :options, :output
25
+ attr_reader :forecast, :settings, :output
26
26
 
27
- def initialize(data, options)
28
- @data = data
29
- @options = options
27
+ def initialize(forecast, settings)
28
+ @forecast = forecast
29
+ @settings = settings
30
30
  @output = []
31
31
  end
32
32
 
33
- def process_block(block)
34
- block[:transactions].each do |to, transactions|
35
- to = get_header(block[:to], to)
36
-
37
- if block[:type] == "custom"
38
- process_custom_transactions(block, to, transactions)
39
- else
40
- process_standard_transactions(block, to, transactions)
41
- end
42
- end
43
- end
44
-
45
- def process_custom_transactions(block, to, transactions)
46
- transactions.each do |t|
47
- frequency = get_periodic_rules(block[:type], t[:frequency])
33
+ def process_transactions(row)
34
+ to = build_to_header(row[:to])
35
+ frequency = get_periodic_rules(row[:type], row[:frequency])
48
36
 
49
- header = build_header(block, to, frequency, t[:description])
50
- footer = build_footer(block)
51
- output << build_transaction(header, [t], footer)
37
+ if @settings[:verbose]
38
+ description = row[:description]
39
+ transactions = [row]
40
+ else
41
+ description = get_descriptions(row[:transactions])
42
+ transactions = row[:transactions]
52
43
  end
53
- end
54
44
 
55
- def process_standard_transactions(block, to, transactions)
56
- if @options[:verbose]
57
- transactions.map do |t|
58
- # Skip transactions that have been marked as tracked
59
- next if t[:track]
60
-
61
- frequency = get_periodic_rules(block[:type], block[:frequency])
62
- header = build_header(block, to, frequency, t[:description])
63
- footer = build_footer(block)
64
- output << build_transaction(header, [t], footer)
65
- end
66
- return
67
- end
45
+ header = build_header(row, frequency, to, description)
46
+ footer = build_footer(row)
68
47
 
69
- block[:descriptions] = get_descriptions(transactions)
70
- frequency = get_periodic_rules(block[:type], block[:frequency])
71
- header = build_header(block, to, frequency, block[:descriptions])
72
- footer = build_footer(block)
73
48
  output << build_transaction(header, transactions, footer)
74
49
  end
75
50
 
76
- def build_header(block, to, frequency, description)
77
- "#{frequency} #{block[:from]}#{to} * #{description}\n"
51
+ def build_header(row, frequency, to, descriptions)
52
+ "#{frequency} #{row[:from]}#{to} * #{descriptions}\n"
78
53
  end
79
54
 
80
55
  def build_footer(block)
@@ -85,11 +60,8 @@ module HledgerForecast
85
60
  { header: header, transactions: write_transactions(transactions), footer: footer }
86
61
  end
87
62
 
88
- def get_header(block, transaction)
89
- return " to #{transaction}" if transaction
90
- return " to #{block}" if block
91
-
92
- return nil
63
+ def build_to_header(to)
64
+ return " to #{to}" if to
93
65
  end
94
66
 
95
67
  def get_descriptions(transactions)
@@ -110,7 +82,6 @@ module HledgerForecast
110
82
  'yearly' => '~ yearly from',
111
83
  'custom' => "~ #{frequency} from"
112
84
  }
113
-
114
85
  map[type]
115
86
  end
116
87
 
@@ -119,11 +90,11 @@ module HledgerForecast
119
90
  # Skip transactions that have been marked as tracked
120
91
  next if t[:track]
121
92
 
122
- t[:amount] = t[:amount].to_s.ljust(options[:max_amount])
123
- t[:category] = t[:category].ljust(options[:max_category])
93
+ t[:amount] = t[:amount].to_s.ljust(@settings[:max_amount] + 5)
94
+ t[:category] = t[:category].to_s.ljust(@settings[:max_category])
124
95
 
125
96
  " #{t[:category]} #{t[:amount]}; #{t[:description]}\n"
126
- end
97
+ end.compact
127
98
  end
128
99
  end
129
100
  end
@@ -9,84 +9,78 @@ module HledgerForecast
9
9
  # Expenses:Groceries $250.00 ; Food expenses
10
10
  # Assets:Checking
11
11
  class Trackers
12
- def self.generate(data, options)
13
- new(data, options).generate
12
+ def self.generate(forecast, options)
13
+ new(forecast, options).generate
14
14
  end
15
15
 
16
16
  def generate
17
- return nil unless tracked?(data)
17
+ return if @options[:no_track]
18
+ return nil unless tracked?(forecast)
18
19
 
19
- data.each_value do |blocks|
20
- blocks.each do |block|
21
- process_tracked(block)
22
- end
20
+ forecast.each do |row|
21
+ process_tracked(row)
23
22
  end
24
23
 
25
24
  output
26
25
  end
27
26
 
28
- def self.track?(transaction, data, options)
27
+ def self.track?(row, options)
29
28
  now = Date.today
30
- transaction['track'] && Date.parse(data['from']) <= now && !exists?(transaction, data['account'],
31
- data['from'], now, options)
29
+ row['track'] && Date.parse(row['from']) <= now && !exists?(row, now, options)
32
30
  end
33
31
 
34
- def self.exists?(transaction, account, from, to, options)
35
-
36
- if !options[:transaction_file]
32
+ def self.exists?(row, now, options)
33
+ unless options[:transaction_file]
37
34
  puts "\nWarning: ".bold.yellow + "For tracked transactions, please specify a file with the `-t` flag"
38
35
  puts "ERROR: ".bold.red + "Tracked transactions ignored for now"
39
36
  return
40
37
  end
41
38
 
42
39
  # Format the money
43
- amount = Formatter.format_money(transaction['amount'], options)
44
- inverse_amount = Formatter.format_money(transaction['amount'] * -1, options)
40
+ amount = Formatter.format_money(row['amount'], options)
41
+ inverse_amount = Formatter.format_money(row['amount'] * -1, options)
45
42
 
46
- category = transaction['category'].gsub('[', '\\[').gsub(']', '\\]').gsub('(', '\\(').gsub(')', '\\)')
43
+ from = Date.parse(row['from'])
44
+ category = row['category'].gsub('[', '\\[').gsub(']', '\\]').gsub('(', '\\(').gsub(')', '\\)')
47
45
 
48
46
  # We run two commands and check to see if category +/- amount or account +/- amount exists
49
- command1 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{to}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{category} (#{amount}|#{inverse_amount})")
50
- command2 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{to}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{account} (#{amount}|#{inverse_amount})")
47
+ command1 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{now}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{category} (#{amount}|#{inverse_amount})")
48
+ command2 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{now}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{row['account']} (#{amount}|#{inverse_amount})")
51
49
 
52
50
  system(command1) || system(command2)
53
51
  end
54
52
 
55
53
  private
56
54
 
57
- attr_reader :data, :options, :output
55
+ attr_reader :forecast, :options, :output
58
56
 
59
- def initialize(data, options)
60
- @data = data
57
+ def initialize(forecast, options)
58
+ @forecast = forecast
61
59
  @options = options
62
60
  @output = []
63
61
  end
64
62
 
65
- def tracked?(data)
66
- data.any? do |_, blocks|
67
- blocks.any? do |block|
68
- block[:transactions].any? do |_, transactions|
69
- transactions.any? { |t| t[:track] }
70
- end
71
- end
63
+ def tracked?(forecast)
64
+ forecast.any? do |row|
65
+ return true if row[:track] == true
72
66
  end
67
+
68
+ return false
73
69
  end
74
70
 
75
- def process_tracked(block)
76
- block[:transactions].each do |_to, transactions|
77
- transactions.each do |t|
78
- next unless t[:track]
71
+ def process_tracked(row)
72
+ row[:transactions].each do |t|
73
+ next if t[:track] == false
79
74
 
80
- category = t[:category].ljust(options[:max_category])
81
- amount = t[:amount].to_s.ljust(options[:max_amount])
75
+ category = t[:category].ljust(options[:max_category])
76
+ amount = t[:amount].to_s.ljust(options[:max_amount])
82
77
 
83
- header = "~ #{Date.new(Date.today.year, Date.today.month,
84
- 1).next_month} * [TRACKED] #{t[:description]}\n"
85
- transactions = " #{category} #{amount}; #{t[:description]}\n"
86
- footer = " #{block[:account]}\n\n"
78
+ header = "~ #{Date.new(Date.today.year, Date.today.month,
79
+ 1).next_month} * [TRACKED] #{t[:description]}\n"
80
+ transactions = " #{category} #{amount}; #{t[:description]}\n"
81
+ footer = " #{row[:account]}\n\n"
87
82
 
88
- output << { header: header, transactions: [transactions], footer: footer }
89
- end
83
+ output << { header: header, transactions: [transactions], footer: footer }
90
84
  end
91
85
  end
92
86
  end
@@ -0,0 +1,14 @@
1
+ module HledgerForecast
2
+ class Utilities
3
+ def self.convert_amount(amount)
4
+ case amount
5
+ when /^-?\d+\.\d+$/ # Detects floating-point numbers (including negatives)
6
+ amount.to_f
7
+ when /^-?\d+$/ # Detects integers (including negatives)
8
+ amount.to_i
9
+ else
10
+ amount
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module HledgerForecast
2
- VERSION = "1.5.1"
2
+ VERSION = "2.0.0".freeze
3
3
  end
@@ -8,7 +8,6 @@ require 'highline'
8
8
  require 'money'
9
9
  require 'optparse'
10
10
  require 'terminal-table'
11
- require 'yaml'
12
11
 
13
12
  Money.locale_backend = nil
14
13
  Money.rounding_mode = BigDecimal::ROUND_HALF_UP
@@ -17,12 +16,12 @@ Money.default_currency = 'USD'
17
16
  require_relative 'hledger_forecast/calculator'
18
17
  require_relative 'hledger_forecast/cli'
19
18
  require_relative 'hledger_forecast/comparator'
20
- require_relative 'hledger_forecast/csv_parser'
21
19
  require_relative 'hledger_forecast/formatter'
22
20
  require_relative 'hledger_forecast/generator'
23
21
  require_relative 'hledger_forecast/settings'
24
22
  require_relative 'hledger_forecast/summarizer'
25
23
  require_relative 'hledger_forecast/summarizer_formatter'
24
+ require_relative 'hledger_forecast/utilities'
26
25
  require_relative 'hledger_forecast/version'
27
26
 
28
27
  require_relative 'hledger_forecast/transactions/default'
data/spec/cli_spec.rb CHANGED
@@ -2,12 +2,12 @@ require_relative '../lib/hledger_forecast'
2
2
 
3
3
  output = <<~JOURNAL
4
4
  ~ monthly from 2023-03-01 * Mortgage, Food
5
- Expenses:Mortgage £2,000.55; Mortgage
6
- Expenses:Food £100.00 ; Food
5
+ Expenses:Mortgage £2,000.55 ; Mortgage
6
+ Expenses:Food £100.00 ; Food
7
7
  Assets:Bank
8
8
 
9
9
  ~ monthly from 2023-03-01 * Savings
10
- Assets:Bank £-1,000.00; Savings
10
+ Assets:Bank £-1,000.00 ; Savings
11
11
  Assets:Savings
12
12
 
13
13
  JOURNAL
@@ -26,15 +26,6 @@ ensure
26
26
  end
27
27
 
28
28
  RSpec.describe 'command' do
29
- it 'uses the CLI to generate an output' do
30
- generated_journal = './test_output.journal'
31
- File.delete(generated_journal) if File.exist?(generated_journal)
32
-
33
- system("./bin/hledger-forecast generate -f ./spec/stubs/forecast.yml -o ./test_output.journal --force")
34
-
35
- expect(File.read(generated_journal)).to eq(output)
36
- end
37
-
38
29
  it 'uses the CLI to generate an output with a CSV config file' do
39
30
  generated_journal = './test_output.journal'
40
31
  File.delete(generated_journal) if File.exist?(generated_journal)
@@ -1,29 +1,18 @@
1
1
  require_relative '../lib/hledger_forecast'
2
2
 
3
- config = <<~YAML
4
- settings:
5
- currency: GBP
6
-
7
- monthly:
8
- - account: "Liabilities:Amex"
9
- from: "2023-05-01"
10
- transactions:
11
- - amount: "=5000/24"
12
- category: "Expenses:House"
13
- description: New Kitchen
14
- - amount: "=25*4.3"
15
- category: "Expenses:Food"
16
- description: Monthly food shop
17
- - amount: "=(102.50+3.25)/2"
18
- category: "Expenses:Food"
19
- description: Random food
20
- YAML
3
+ config = <<~CSV
4
+ type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
5
+ monthly,,Liabilities:Amex,01/05/2023,,New kitchen,Expenses:House,=5000/24,,,
6
+ monthly,,Liabilities:Amex,01/05/2023,,Monthly food shop,Expenses:Food,=25*4.3,,,
7
+ monthly,,Liabilities:Amex,01/05/2023,,Random food,Expenses:Food,=(102.50+3.25)/2,,,
8
+ settings,currency,GBP,,,,,,,,
9
+ CSV
21
10
 
22
11
  output = <<~JOURNAL
23
- ~ monthly from 2023-05-01 * New Kitchen, Monthly food shop, Random food
24
- Expenses:House £208.33 ; New Kitchen
25
- Expenses:Food £107.50 ; Monthly food shop
26
- Expenses:Food £52.88 ; Random food
12
+ ~ monthly from 2023-05-01 * New kitchen, Monthly food shop, Random food
13
+ Expenses:House £208.33 ; New kitchen
14
+ Expenses:Food £107.50 ; Monthly food shop
15
+ Expenses:Food £52.88 ; Random food
27
16
  Liabilities:Amex
28
17
 
29
18
  JOURNAL