sie-analysis 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d8c3bea047da8a3e357f6d85c04f744bf52efe6c0865a4f6b2c6d0b9bb2f2d2
4
- data.tar.gz: 49c8590042f39f5c5d1bf2bc2d4278dd699bc9888ca0b2258fadce4afd99ef59
3
+ metadata.gz: 15ee746d937f73cb9a8be5d8ac35133cfaca53d919be9df549ae073b6ef623be
4
+ data.tar.gz: b12af56b12387792f0f68f40cb400c01e7f63b9c98b107359209225a1e7969fa
5
5
  SHA512:
6
- metadata.gz: 1765646441433ec914ad70fdb0cfe936ebcdd0892e573d920fc1d9e1ad0dc9ab0f5b7183909f5f2c5e78efd429cfc563d5a67339e9d4e789bacad9046598ebf8
7
- data.tar.gz: 8ad0b9860cbd8247082b05e27b795bd7b8436a7fd23a7f94753279e9fcd2d52a03ee854c2ae255163801a32a4ca61a4f9f7940bc6c4f0506771e1633ab2df419
6
+ metadata.gz: de5892b81dfad72c9d895cb1a831916750bf7a406099673f91519735778be9f41f418917fa996c086bf7d7d9711aec94c92c6f0128cf65b88d059d1ea06aa49c
7
+ data.tar.gz: 68bf1df9481362cbd6787317016d06373ebf4e2b8e13c508bec62eb1c176f603de1916912ac58548ac32cdfaeb70755374eaa0bb00213776bcd5383645d21897
data/README.md CHANGED
@@ -11,7 +11,7 @@ Requires Ruby 3.2 or newer. From a local checkout, build and install the gem:
11
11
 
12
12
  ```sh
13
13
  gem build sie-analysis.gemspec
14
- gem install ./sie-analysis-1.0.0.gem
14
+ gem install ./sie-analysis-1.1.0.gem
15
15
  ```
16
16
 
17
17
  Load an export and create an analyzer:
@@ -77,7 +77,15 @@ report[:income_statement][:groups].map { |group| group.slice(:code, :label, :amo
77
77
 
78
78
  Use `dimension: :cost_center` for cost centers. `account_movements` accepts the
79
79
  same dimensions, with `dimension_code: "ABC"` to select an object.
80
- `analysis.available_dimensions` lists the available symbols. The export must contain the relevant object balances;
80
+ `analysis.available_dimensions` lists standard names as symbols and other dimensions
81
+ as integers. Supported names are `:cost_center` (1), `:cost_bearer` (2), `:project` (6),
82
+ `:employee` (7), `:customer` (8), `:supplier` (9), and `:invoice` (10).
83
+ Any dimension can also be selected by its SIE number, for example `dimension: 20`.
84
+ Declared dimensions and dimensions referenced in objects, balances, budgets or
85
+ transactions are included, even without monthly balances.
86
+ `document.dimensions` maps declared numbers to names, e.g. `{ 20 => "Department" }`;
87
+ `document.vouchers.first.lines.first.objects` contains pairs such as `[[20, "A"]]`.
88
+ The export must contain the relevant object balances;
81
89
  balance-sheet totals are omitted for dimension reports.
82
90
 
83
91
  ## Inspect the postings on an account
@@ -15,7 +15,12 @@ module SieAnalysis
15
15
  CURRENCY = "SEK"
16
16
  DIMENSIONS = {
17
17
  cost_center: 1,
18
- project: 6
18
+ cost_bearer: 2,
19
+ project: 6,
20
+ employee: 7,
21
+ customer: 8,
22
+ supplier: 9,
23
+ invoice: 10
19
24
  }.freeze
20
25
 
21
26
  # @param document [Parser::Parsed] a decoded and parsed SIE export
@@ -26,7 +31,7 @@ module SieAnalysis
26
31
  # Summarizes all calendar months touched by the inclusive date range.
27
32
  # @param from [Date] first date
28
33
  # @param to [Date] last date
29
- # @param dimension [Symbol, nil] :project or :cost_center
34
+ # @param dimension [Symbol, Integer, nil] standard dimension name or SIE dimension number
30
35
  # @return [Hash] income statement, balance sheet, budget, monthly totals and notes
31
36
  def overview(from:, to:, dimension: nil)
32
37
  start_date, end_date = from, to
@@ -57,7 +62,7 @@ module SieAnalysis
57
62
  # @param month [String] YYYY-MM
58
63
  # @param group_code [String, nil] report group, e.g. "revenue"
59
64
  # @param account_number [Integer, nil] BAS account number
60
- # @param dimension [Symbol, nil] :project or :cost_center
65
+ # @param dimension [Symbol, Integer, nil] standard dimension name or SIE dimension number
61
66
  # @param dimension_code [String, nil] object code; requires dimension
62
67
  # @return [Array<Hash>] account_number, account_name, movement and group/object fields
63
68
  def account_movements(month:, group_code: nil, account_number: nil, dimension: nil, dimension_code: nil)
@@ -111,12 +116,19 @@ module SieAnalysis
111
116
  end
112
117
  end
113
118
 
114
- # @return [Array<Symbol>] supported dimensions with objects declared in the export
119
+ # Includes declared dimensions and dimensions referenced by objects, balances or vouchers.
120
+ # @return [Array<Symbol, Integer>] standard names and other SIE dimension numbers
115
121
  def available_dimensions
116
- dims = []
117
- dims << :cost_center if @parsed.objects.keys.any? { |dim, _code| dim == 1 }
118
- dims << :project if @parsed.objects.keys.any? { |dim, _code| dim == 6 }
119
- dims
122
+ numbers = @parsed.dimensions.keys + @parsed.objects.keys.map(&:first)
123
+ [@parsed.period_changes, @parsed.budgets].each do |periods|
124
+ periods.each_value do |rows|
125
+ numbers.concat(rows.keys.filter_map { |key| key[1] if key.is_a?(Array) })
126
+ end
127
+ end
128
+ @parsed.vouchers.each do |voucher|
129
+ voucher.lines.each { |line| numbers.concat(line.objects.map(&:first)) }
130
+ end
131
+ numbers.uniq.sort.map { |number| DIMENSIONS.key(number) || number }
120
132
  end
121
133
 
122
134
  private
@@ -200,11 +212,16 @@ module SieAnalysis
200
212
 
201
213
  def normalize_dimension(dimension)
202
214
  return if dimension.nil?
215
+ return DIMENSIONS.key(dimension) || dimension if dimension.is_a?(Integer) && dimension.positive?
203
216
  raise ArgumentError, "Unsupported dimension" unless DIMENSIONS.key?(dimension)
204
217
 
205
218
  dimension
206
219
  end
207
220
 
221
+ def dimension_number(dimension)
222
+ dimension.is_a?(Integer) ? dimension : DIMENSIONS.fetch(dimension)
223
+ end
224
+
208
225
  def normalize_year_month(value)
209
226
  text = value.to_s
210
227
  raise ArgumentError, "month must be YYYY-MM" unless text.match?(/\A\d{4}-\d{2}\z/)
@@ -20,7 +20,7 @@ module SieAnalysis
20
20
 
21
21
  def budget_row(month, key, raw, dimension)
22
22
  account, dim, code = dimension_parts(key)
23
- return unless dim == (dimension && Analysis::DIMENSIONS.fetch(dimension))
23
+ return unless dim == (dimension && dimension_number(dimension))
24
24
 
25
25
  group = Groups.for_account(account)
26
26
  return unless income_group?(group)
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SieAnalysis
4
- # Cost-center / project income and account-row builders for Analysis.
4
+ # Income and account-row builders for any SIE dimension.
5
5
  module DimensionStatements
6
6
  private
7
7
 
8
8
  def dimension_income_groups(months, dimension)
9
- dim_no = Analysis::DIMENSIONS.fetch(dimension)
9
+ dim_no = dimension_number(dimension)
10
10
  totals = accumulate_dimension_totals(months, dim_no)
11
11
  groups = dimension_group_rows(totals, dim_no)
12
12
  revenue = totals.values.sum { |values| values[:revenue] }
@@ -56,7 +56,7 @@ module SieAnalysis
56
56
  end
57
57
 
58
58
  def dimension_account_rows(month, dimension)
59
- dim_no = Analysis::DIMENSIONS.fetch(dimension)
59
+ dim_no = dimension_number(dimension)
60
60
  rows = []
61
61
  (@parsed.period_changes[month] || {}).each do |key, raw|
62
62
  account, dim, code = dimension_parts(key)
@@ -7,13 +7,15 @@ module SieAnalysis
7
7
  class Parser
8
8
  Record = Data.define(:tag, :fields)
9
9
  Voucher = Data.define(:series, :number, :date, :description, :lines)
10
- Line = Data.define(:account, :amount)
10
+ # Voucher line with [dimension number, object code] references.
11
+ Line = Data.define(:account, :amount, :objects)
11
12
 
12
13
  # Parsed ledger: monetary values are BigDecimal; voucher dates are Date.
13
14
  # Period keys are YYYYMM strings; object keys are [dimension number, code].
15
+ # Dimensions maps declared dimension numbers to their exported names.
14
16
  Parsed = Data.define(
15
17
  :accounts, :objects, :opening_balances, :closing_balances, :results, :period_changes,
16
- :financial_years, :vouchers, :budgets, :sie_type
18
+ :financial_years, :vouchers, :budgets, :sie_type, :dimensions
17
19
  )
18
20
 
19
21
  # @param text [String] export text already decoded to UTF-8
@@ -28,6 +30,7 @@ module SieAnalysis
28
30
  @text = text.to_s
29
31
  @accounts = {}
30
32
  @objects = {}
33
+ @dimensions = {}
31
34
  @opening = Hash.new(BigDecimal("0"))
32
35
  @closing = Hash.new(BigDecimal("0"))
33
36
  @results = Hash.new(BigDecimal("0"))
@@ -50,7 +53,7 @@ module SieAnalysis
50
53
  Parsed.new(
51
54
  accounts: @accounts, objects: @objects, opening_balances: @opening, closing_balances: @closing,
52
55
  results: @results, period_changes: @periods, financial_years: @years, vouchers: @vouchers,
53
- budgets: @budgets, sie_type: @sie_type
56
+ budgets: @budgets, sie_type: @sie_type, dimensions: @dimensions
54
57
  )
55
58
  rescue ParseError
56
59
  raise
@@ -79,6 +82,7 @@ module SieAnalysis
79
82
  @sie_type = Integer(record.fields.fetch(0))
80
83
  when "KONTO" then apply_account(record.fields)
81
84
  when "OBJEKT" then apply_object(record.fields)
85
+ when "DIM", "UNDERDIM" then apply_dimension(record.fields)
82
86
  when "RAR" then apply_year(record.fields)
83
87
  when "IB" then apply_balance!(@opening, record.fields)
84
88
  when "UB" then apply_balance!(@closing, record.fields)
@@ -102,6 +106,13 @@ module SieAnalysis
102
106
  @objects[[dim, code]] = name if dim && !code.empty?
103
107
  end
104
108
 
109
+ def apply_dimension(fields)
110
+ number = integer_field(fields[0])
111
+ raise ArgumentError unless number&.positive?
112
+
113
+ @dimensions[number] = unquote(fields.fetch(1))
114
+ end
115
+
105
116
  def apply_year(fields)
106
117
  year_no = integer_field(fields[0])
107
118
  first, last = sie_date(fields[1]), sie_date(fields[2])
@@ -189,7 +200,7 @@ module SieAnalysis
189
200
 
190
201
  raise ArgumentError if object_field.start_with?("{") && !valid_budget_objects?(object_field)
191
202
 
192
- @current_voucher[:lines] << Line.new(account:, amount:)
203
+ @current_voucher[:lines] << Line.new(account:, amount:, objects: object_refs_from(object_field))
193
204
  end
194
205
 
195
206
  def finish_voucher!
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SieAnalysis
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sie-analysis
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Standout AB