sie 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm ruby-1.9.3-p194-falcon@sie --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sie.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Sie
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ gem 'sie'
8
+
9
+ And then execute:
10
+
11
+ $ bundle
12
+
13
+ Or install it yourself as:
14
+
15
+ $ gem install sie
16
+
17
+ ## Developing
18
+
19
+ First time setup:
20
+
21
+ script/bootstrap
22
+
23
+ Running tests:
24
+
25
+ rake
26
+
27
+ Getting the latest code and gems:
28
+
29
+ script/refresh
30
+
31
+ ## Usage
32
+
33
+
34
+ ## Contributing
35
+
36
+ 1. Fork it
37
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
38
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
39
+ 4. Push to the branch (`git push origin my-new-feature`)
40
+ 5. Create new Pull Request
41
+
42
+ ## Credits and license
43
+
44
+ By [Barsoom](http://barsoom.se) under the MIT license:
45
+
46
+ > Copyright (c) 2013 Barsoom AB
47
+ >
48
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
49
+ > of this software and associated documentation files (the "Software"), to deal
50
+ > in the Software without restriction, including without limitation the rights
51
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
52
+ > copies of the Software, and to permit persons to whom the Software is
53
+ > furnished to do so, subject to the following conditions:
54
+ >
55
+ > The above copyright notice and this permission notice shall be included in
56
+ > all copies or substantial portions of the Software.
57
+ >
58
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
59
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
60
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
61
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
62
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
63
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
64
+ > THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ namespace :spec do
4
+ task :unit do
5
+ puts "Running unit tests:"
6
+ system("rspec spec/unit/**_spec.rb") || exit(1)
7
+ end
8
+
9
+ task :integration do
10
+ puts "Running integrated tests:"
11
+ system("rspec spec/integration/**_spec.rb") || exit(1)
12
+ end
13
+ end
14
+
15
+ task :default => [ :"spec:unit", :"spec:integration" ]
data/lib/sie.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "sie/version"
2
+ require "sie/parser"
3
+ require "sie/document"
4
+
5
+ module Sie
6
+ end
@@ -0,0 +1,107 @@
1
+ require "attr_extras"
2
+ require "sie/document/financial_years"
3
+ require "sie/document/voucher_series"
4
+ require "sie/document/renderer"
5
+ require "active_support/core_ext/module/delegation"
6
+
7
+ class Sie::Document
8
+ pattr_initialize :data_source
9
+
10
+ def render
11
+ add_header
12
+ add_financial_years
13
+ add_accounts
14
+ add_balances
15
+ add_vouchers
16
+
17
+ renderer.render
18
+ end
19
+
20
+ private
21
+
22
+ delegate :program, :program_version, :generated_on, :company_name,
23
+ :accounts, :balance_account_numbers, :closing_account_numbers,
24
+ :balance_before, :each_voucher, :from_date, :to_date, :financial_year_start_month,
25
+ to: :data_source
26
+
27
+ def add_header
28
+ add_line("FLAGGA", 0)
29
+ add_line("PROGRAM", program, program_version)
30
+ add_line("FORMAT", "PC8")
31
+ add_line("GEN", generated_on)
32
+ add_line("SIETYP", 4)
33
+ add_line("FNAMN", company_name)
34
+ end
35
+
36
+ def add_financial_years
37
+ financial_years.each_with_index do |date_range, index|
38
+ add_line("RAR", -index, date_range.begin, date_range.end)
39
+ end
40
+ end
41
+
42
+ def add_accounts
43
+ accounts.each do |account|
44
+ number = account.fetch(:number)
45
+ description = account.fetch(:description)
46
+
47
+ add_line("KONTO", number, description)
48
+ end
49
+ end
50
+
51
+ def add_balances
52
+ financial_years.each_with_index do |date_range, index|
53
+ add_balance_rows("IB", -index, balance_account_numbers, date_range.begin)
54
+ add_balance_rows("UB", -index, balance_account_numbers, date_range.end)
55
+ add_balance_rows("RES", -index, closing_account_numbers, date_range.end)
56
+ end
57
+ end
58
+
59
+ def add_balance_rows(label, year_index, account_numbers, date, &block)
60
+ account_numbers.each do |account_number|
61
+ balance = balance_before(account_number, date)
62
+ add_line(label, year_index, account_number, balance)
63
+ end
64
+ end
65
+
66
+ def add_vouchers
67
+ each_voucher do |voucher|
68
+ add_voucher(voucher)
69
+ end
70
+ end
71
+
72
+ def add_voucher(opts)
73
+ creditor = opts.fetch(:creditor)
74
+ type = opts.fetch(:type)
75
+ number = opts.fetch(:number)
76
+ booked_on = opts.fetch(:booked_on)
77
+ description = opts.fetch(:description)
78
+ voucher_lines = opts.fetch(:voucher_lines)
79
+
80
+ add_line("VER", voucher_series(creditor, type), number, booked_on, description)
81
+
82
+ add_array do
83
+ voucher_lines.each do |line|
84
+ account_number = line.fetch(:account_number)
85
+ amount = line.fetch(:amount)
86
+ booked_on = line.fetch(:booked_on)
87
+ description = line.fetch(:description)
88
+
89
+ add_line("TRANS", account_number, Renderer::EMPTY_ARRAY, amount, booked_on, description)
90
+ end
91
+ end
92
+ end
93
+
94
+ delegate :add_line, :add_array, to: :renderer
95
+
96
+ def renderer
97
+ @renderer ||= Renderer.new
98
+ end
99
+
100
+ def financial_years
101
+ FinancialYears.between(financial_year_start_month, from_date, to_date).reverse
102
+ end
103
+
104
+ def voucher_series(creditor, type)
105
+ VoucherSeries.for(creditor, type)
106
+ end
107
+ end
@@ -0,0 +1,24 @@
1
+ class Sie::Document
2
+ class FinancialYears
3
+ def self.between(start_month, from_date, to_date)
4
+ result = []
5
+
6
+ from_date.upto(to_date) do |date|
7
+ next if result.any? { |r| r.cover?(date) }
8
+
9
+ start_of_year = Date.new(date.year, start_month, 1)
10
+ start_of_year = start_of_year <= date ? start_of_year : (start_of_year << 12)
11
+
12
+ a_year_later = start_of_year >> 11
13
+ end_of_year = Date.new(a_year_later.year, a_year_later.month, -1)
14
+
15
+ first_date = [start_of_year, from_date].max
16
+ last_date = [end_of_year, to_date].min
17
+
18
+ result << (first_date..last_date)
19
+ end
20
+
21
+ result
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ class Sie::Document::Renderer
2
+ EMPTY_ARRAY = :empty_array
3
+
4
+ def initialize
5
+ @io = StringIO.new
6
+ @io.set_encoding(Encoding::CP437)
7
+ end
8
+
9
+ def add_line(label, *values)
10
+ append ["##{ label }", *format_values(values)].join(" ")
11
+ end
12
+
13
+ def add_array
14
+ append "{"
15
+ yield
16
+ append "}"
17
+ end
18
+
19
+ def render
20
+ io.rewind
21
+ io.read
22
+ end
23
+
24
+ attr_private :io
25
+
26
+ private
27
+
28
+ def append(text)
29
+ io.puts(text)
30
+ end
31
+
32
+ def format_values(values)
33
+ values.map { |value| format_value(value) }
34
+ end
35
+
36
+ def format_value(value)
37
+ case value
38
+ when Date
39
+ value.strftime("%Y%m%d")
40
+ when EMPTY_ARRAY
41
+ "{}"
42
+ when Numeric
43
+ value.to_s
44
+ else
45
+ '"' + value.to_s.gsub('"', '\"') + '"'
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ class Sie::Document
2
+ class VoucherSeries
3
+ DEBTOR_INVOICE = "KF"
4
+ DEBTOR_PAYMENT = "KI"
5
+ SUPPLIER_INVOICE = "LF"
6
+ SUPPLIER_PAYMENT = "KB"
7
+ OTHER = "LV"
8
+
9
+ def self.for(creditor, type)
10
+ case type
11
+ when :invoice
12
+ creditor ? SUPPLIER_INVOICE : DEBTOR_INVOICE
13
+ when :payment
14
+ creditor ? SUPPLIER_PAYMENT : DEBTOR_PAYMENT
15
+ else
16
+ OTHER
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/sie/parser.rb ADDED
@@ -0,0 +1,40 @@
1
+ require "attr_extras"
2
+ require "sie/parser/line_parser"
3
+
4
+ module Sie
5
+ class Parser
6
+ # TODO: Could this format knowledge be centrailized somewhere, some
7
+ # of this is duplicated in Character.
8
+ BEGINNING_OF_ARRAY = "{"
9
+ END_OF_ARRAY = "}"
10
+ ENTRY = /^#/
11
+
12
+ def parse(io)
13
+ stack = []
14
+ sie_file = SieFile.new
15
+ current = sie_file
16
+
17
+ io.each_line do |line|
18
+ line = line.chomp
19
+
20
+ case line
21
+ when BEGINNING_OF_ARRAY
22
+ stack << current
23
+ current = current.entries.last
24
+ when END_OF_ARRAY
25
+ current = stack.pop
26
+ when ENTRY
27
+ current.entries << parse_line(line)
28
+ end
29
+ end
30
+
31
+ sie_file
32
+ end
33
+
34
+ private
35
+
36
+ def parse_line(line)
37
+ LineParser.new(line).parse
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ module Sie
2
+ class Parser
3
+ class Entry
4
+ attr_reader :label
5
+ attr_accessor :attributes
6
+ attr_accessor :entries
7
+
8
+ def initialize(label)
9
+ @label = label
10
+ @attributes = {}
11
+ @entries = []
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ module Sie
2
+ class Parser
3
+ ENTRY_TYPES = {
4
+ "adress" => [ 'kontakt', 'utdelningsadr', 'postadr', 'tel' ],
5
+ "bkod" => [ 'SNI-kod' ],
6
+ "dim" => [ 'dimensionsnr', 'namn' ],
7
+ "enhet" => [ 'kontonr', 'enhet' ],
8
+ "flagga" => [ 'x' ],
9
+ "fnamn" => [ 'foretagsnamn' ],
10
+ "fnr" => [ 'foretagsid' ],
11
+ "format" => [ 'PC8' ],
12
+ "ftyp" => [ 'foretagstyp' ],
13
+ "gen" => [ 'datum', 'sign' ],
14
+ "ib" => [ 'arsnr', 'konto', 'saldo', 'kvantitet' ],
15
+ "konto" => [ 'kontonr', 'kontonamn' ],
16
+ "kptyp" => [ 'typ' ],
17
+ "ktyp" => [ 'kontonr', 'kontotyp' ],
18
+ "objekt" => [ 'dimensionsnr', 'objektnr', 'objektnamn' ],
19
+ "oib" => [ 'arsnr', 'konto', { name: 'objekt', type: [ 'dimensionsnr', 'objektnr' ] }, 'saldo', 'kvantitet' ],
20
+ "omfattn" => [ 'datum' ],
21
+ "orgnr" => [ 'orgnr', 'forvnr', 'verknr' ],
22
+ "oub" => [ 'arsnr', 'konto', { name: 'objekt', type: [ 'dimensionsnr', 'objektnr' ] }, 'saldo', 'kvantitet' ],
23
+ "pbudget" => [ 'arsnr', 'period', 'konto', { name: 'objekt', type: [ 'dimensionsnr', 'objektnr' ] }, 'saldo', 'kvantitet' ],
24
+ "program" => [ 'programnamn', 'version' ],
25
+ "prosa" => [ 'text' ],
26
+ "psaldo" => [ 'arsnr', 'period', 'konto', { name: 'objekt', type: [ 'dimensionsnr', 'objektnr' ] }, 'saldo', 'kvantitet' ],
27
+ "rar" => [ 'arsnr', 'start', 'slut' ],
28
+ "res" => [ 'ars', 'konto', 'saldo', 'kvantitet' ],
29
+ "sietyp" => [ 'typnr' ],
30
+ "sru" => [ 'konto', 'SRU-kod' ],
31
+ "taxar" => [ 'ar' ],
32
+ "trans" => [ 'kontonr', { name: 'objektlista', type: [ 'dimensionsnr', 'objektnr' ], many: true }, 'belopp', 'transdat', 'transtext', 'kvantitet', 'sign' ],
33
+ "rtrans" => [ 'kontonr', { name: 'objektlista', type: [ 'dimensionsnr', 'objektnr' ], many: true }, 'belopp', 'transdat', 'transtext', 'kvantitet', 'sign' ],
34
+ "btrans" => [ 'kontonr', { name: 'objektlista', type: [ 'dimensionsnr', 'objektnr' ], many: true }, 'belopp', 'transdat', 'transtext', 'kvantitet', 'sign' ],
35
+ "ub" => [ 'arsnr', 'konto', 'saldo', 'kvantitet' ],
36
+ "underdim" => [ 'dimensionsnr', 'namn', 'superdimension' ],
37
+ "valuta" => [ 'valutakod' ],
38
+ "ver" => [ 'serie', 'vernr', 'verdatum', 'vertext', 'regdatum', 'sign' ]
39
+ }
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ require "sie/parser/tokenizer"
2
+ require "sie/parser/entry"
3
+ require "sie/parser/sie_file"
4
+
5
+ module Sie
6
+ class Parser
7
+ class LineParser
8
+ pattr_initialize :line
9
+
10
+ def parse
11
+ tokens = tokenize(line)
12
+ first_token = tokens.shift
13
+
14
+ entry = Entry.new(first_token.label)
15
+ entry_type = first_token.entry_type
16
+
17
+ entry_type.each_with_index do |entry_type, i|
18
+ break if i >= tokens.size
19
+
20
+ if entry_type.is_a?(Hash)
21
+ skip_array(tokens, i)
22
+ next
23
+ else
24
+ label = entry_type
25
+ entry.attributes[label] = tokens[i].value
26
+ end
27
+ end
28
+
29
+ entry
30
+ end
31
+
32
+ private
33
+
34
+ def tokenize(line)
35
+ Tokenizer.new(line).tokenize
36
+ end
37
+
38
+ def skip_array(tokens, i)
39
+ if tokens[i].is_a?(Tokenizer::BeginArrayToken) &&
40
+ !tokens[i+1].is_a?(Tokenizer::EndArrayToken)
41
+ raise "We currently don't support metadata within entries as we haven't had a need for it yet (the data between {} in #{line})."
42
+ end
43
+
44
+ tokens.reject! { |token| token.is_a?(Tokenizer::EndArrayToken) }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ module Sie
2
+ class Parser
3
+ class SieFile
4
+ def entries_with_label(label)
5
+ entries.select { |entry| entry.label == label }
6
+ end
7
+
8
+ def entries
9
+ @entries ||= []
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,90 @@
1
+ require "strscan"
2
+ require "sie/parser/tokenizer/token"
3
+ require "sie/parser/tokenizer/character"
4
+
5
+ module Sie
6
+ class Parser
7
+ class Tokenizer
8
+ pattr_initialize :line
9
+
10
+ def tokenize
11
+ @tokens = []
12
+ @consume = false
13
+ @quoted = false
14
+
15
+ loop do
16
+ move_to_next_character
17
+ break unless current_character.value
18
+
19
+ if consume?
20
+ if quoted?
21
+ consume_quoted_value
22
+ else
23
+ consume_unquoted_value
24
+ end
25
+ else
26
+ add_new_token
27
+ end
28
+ end
29
+
30
+ tokens
31
+ end
32
+
33
+ private
34
+
35
+ attr_query :consume?, :quoted?
36
+ attr_private :consume, :quoted, :tokens, :current_character
37
+
38
+ def move_to_next_character
39
+ @current_character = Character.new(scanner.getch)
40
+ end
41
+
42
+ def consume_quoted_value
43
+ if current_character.quote?
44
+ @quoted = false
45
+ else
46
+ add_to_current_token current_character
47
+ end
48
+ end
49
+
50
+ def consume_unquoted_value
51
+ if current_character.unquoted_data?
52
+ add_to_current_token current_character
53
+ else
54
+ @consume = false
55
+ end
56
+ end
57
+
58
+ def add_new_token
59
+ if current_character.entry?
60
+ @consume = true
61
+ add_token EntryToken.new
62
+ elsif current_character.beginning_of_array?
63
+ add_token BeginArrayToken.new
64
+ elsif current_character.end_of_array?
65
+ add_token EndArrayToken.new
66
+ elsif current_character.quote?
67
+ @consume = @quoted = true
68
+ add_token StringToken.new
69
+ elsif current_character.non_whitespace?
70
+ @consume = true
71
+ add_token StringToken.new(current_character.value)
72
+ elsif current_character.value != " "
73
+ raise "Unhandled character: #{current_character.value}"
74
+ end
75
+ end
76
+
77
+ def add_token(token)
78
+ tokens << token
79
+ end
80
+
81
+ def add_to_current_token(character)
82
+ tokens.last.value += character.value
83
+ end
84
+
85
+ def scanner
86
+ @scanner ||= StringScanner.new(line)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,34 @@
1
+ module Sie
2
+ class Parser
3
+ class Tokenizer
4
+ class Character
5
+ pattr_initialize :value
6
+ attr_reader :value
7
+
8
+ def unquoted_data?
9
+ non_whitespace? && !end_of_array?
10
+ end
11
+
12
+ def entry?
13
+ value == "#"
14
+ end
15
+
16
+ def beginning_of_array?
17
+ value == "{"
18
+ end
19
+
20
+ def end_of_array?
21
+ value == "}"
22
+ end
23
+
24
+ def quote?
25
+ value == '"'
26
+ end
27
+
28
+ def non_whitespace?
29
+ value != " " && value != "\t"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ require "sie/parser/entry_types"
2
+
3
+ module Sie
4
+ class Parser
5
+ class Tokenizer
6
+ class Token
7
+ attr_accessor :value
8
+
9
+ def initialize(value = "")
10
+ @value = value
11
+ end
12
+
13
+ def entry_type
14
+ Sie::Parser::ENTRY_TYPES.fetch(label)
15
+ end
16
+
17
+ def label
18
+ value.sub(/^#/, '').downcase
19
+ end
20
+ end
21
+
22
+ class EntryToken < Token; end
23
+ class BeginArrayToken < Token; end
24
+ class EndArrayToken < Token; end
25
+ class StringToken < Token; end
26
+ class ArrayToken < Token; end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Sie
2
+ VERSION = "1.0.0"
3
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,3 @@
1
+ gem install bundler --no-ri --no-rdoc &&
2
+ bundle &&
3
+ rake
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ export PATH=/opt/app_ruby/bin:$PATH
4
+ export GEM_HOME="$HOME/.gem"
5
+
6
+ bundle install --path /var/lib/jenkins/.bundle/sie 1> /dev/null
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ set -e
3
+ source script/ci/support/env
4
+
5
+ notify_build "bundle exec rake"
data/script/refresh ADDED
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+
3
+ printf "\e[00;34mGetting the latest version\e[00m... "
4
+
5
+ (git status|grep 'nothing to commit' 1> /dev/null)
6
+
7
+ if [ ! $? -eq 0 ]
8
+ then
9
+ set -e
10
+ git stash 1> /dev/null
11
+ git pull --rebase 1> /dev/null
12
+ git stash pop 1> /dev/null
13
+ else
14
+ set -e
15
+ git pull --rebase 1> /dev/null
16
+ fi
17
+
18
+ echo "done"
19
+
20
+ printf "\e[00;34mBundling\e[00m... "
21
+ bundle 1> /dev/null
22
+ echo "done"
23
+ echo
24
+
25
+ rake
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Command to run the specs the correct way when triggered from the turbux vim plugin.
4
+
5
+ command = "rspec #{ARGV.first}"
6
+ exit(1) unless system(command)
data/sie.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sie/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sie"
8
+ spec.version = Sie::VERSION
9
+ spec.authors = ["Barsoom AB"]
10
+ spec.email = ["all@barsoom.se"]
11
+ spec.description = %q{SIE parser and generator}
12
+ spec.summary = %q{Parses and generates SIE files (http://sie.se/)}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "attr_extras"
22
+ spec.add_dependency "activesupport"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec"
27
+ end
@@ -0,0 +1,28 @@
1
+ #FLAGGA 0
2
+ #PROGRAM "fooconomic" "1.0"
3
+ #FORMAT PC8
4
+ #GEN 20130101
5
+ #SIETYP 4
6
+ #ORGNR 555555-5555
7
+ #FNAMN "Foocorp"
8
+ #ADRESS "Foocorp" "" "12345 City" "555-12345"
9
+ #KONTO 1510 "Accounts receivable"
10
+ #KTYP 1510 T
11
+ #SRU 1510 204
12
+ #KONTO 1930 "Bank account"
13
+ #KTYP 1930 T
14
+ #SRU 1930 200
15
+ #KONTO 2440 Supplier
16
+ #KTYP 2440 S
17
+ #SRU 2440 300
18
+ #VER A 1 20130101 "Invoice" 20120105
19
+ {
20
+ #TRANS 1510 {} -200 20130101 "Coffee machine"
21
+ #TRANS 4100 {} 180 20130101 "Coffee machine"
22
+ #TRANS 2611 {} -20 20130101 "VAT"
23
+ }
24
+ #VER A 2 20130102 "Payment" 20120105
25
+ {
26
+ #TRANS 1510 {} -200 20130101 "Payment: Coffee machine"
27
+ #TRANS 1970 {} 200 20130101 "Payment: Coffee machine"
28
+ }
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Sie::Parser do
6
+ it "parses a file" do
7
+ parser = Sie::Parser.new
8
+
9
+ open_file("fixtures/sie_file.se") do |f|
10
+ sie_file = parser.parse(f)
11
+
12
+ expect(sie_file.entries_with_label("fnamn").first.attributes["foretagsnamn"]).to eq("Foocorp")
13
+
14
+ account = sie_file.entries_with_label("konto").first
15
+ expect(account.attributes["kontonr"]).to eq("1510")
16
+ expect(account.attributes["kontonamn"]).to eq("Accounts receivable")
17
+ expect(sie_file.entries_with_label("ver").size).to eq(2)
18
+ end
19
+ end
20
+
21
+ def open_file(fixture_file)
22
+ File.open(File.join(File.dirname(__FILE__), "../#{fixture_file}"))
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ require "sie"
2
+
3
+ RSpec.configure do |config|
4
+ config.order = 'random'
5
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe Sie::Document::FinancialYears, ".between" do
4
+ it "gives us the financial years between from_date and to_date" do
5
+ Sie::Document::FinancialYears.between(
6
+ 1,
7
+ Date.new(2011, 9, 1),
8
+ Date.new(2012, 12, 31)
9
+ ).should == [
10
+ Date.new(2011, 9, 1)..Date.new(2011, 12, 31),
11
+ Date.new(2012, 1, 1)..Date.new(2012, 12, 31),
12
+ ]
13
+ end
14
+
15
+ it "gives us the financial years between from_date and to_date" do
16
+ Sie::Document::FinancialYears.between(
17
+ 1,
18
+ Date.new(2011, 9, 1),
19
+ Date.new(2013, 12, 31)
20
+ ).should == [
21
+ Date.new(2011, 9, 1)..Date.new(2011, 12, 31),
22
+ Date.new(2012, 1, 1)..Date.new(2012, 12, 31),
23
+ Date.new(2013, 1, 1)..Date.new(2013, 12, 31),
24
+ ]
25
+ end
26
+
27
+ it "gives us the financial years between from_date and to_date with a different financial year start month" do
28
+ Sie::Document::FinancialYears.between(
29
+ 5,
30
+ Date.new(2011, 9, 1),
31
+ Date.new(2014, 1, 10)
32
+ ).should == [
33
+ Date.new(2011, 9, 1)..Date.new(2012, 4, 30),
34
+ Date.new(2012, 5, 1)..Date.new(2013, 4, 30),
35
+ Date.new(2013, 5, 1)..Date.new(2014, 1, 10),
36
+ ]
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ describe Sie::Document::VoucherSeries, ".for" do
4
+ subject(:series) { Sie::Document::VoucherSeries.for(creditor, type) }
5
+
6
+ let(:type) { :invoice }
7
+
8
+ context "when on the creditor side" do
9
+ let(:creditor) { true }
10
+
11
+ context "with an invoice" do
12
+ let(:type) { :invoice }
13
+ it { should == "LF" }
14
+ end
15
+
16
+ context "with a payment" do
17
+ let(:type) { :payment }
18
+ it { should == "KB" }
19
+ end
20
+ end
21
+
22
+ context "when on the debtor side" do
23
+ let(:creditor) { false }
24
+
25
+ context "with an invoice" do
26
+ let(:type) { :invoice }
27
+ it { should == "KF" }
28
+ end
29
+
30
+ context "with a payment" do
31
+ let(:type) { :payment }
32
+ it { should == "KI" }
33
+ end
34
+ end
35
+
36
+ context "when neither a payment or invoice" do
37
+ let(:creditor) { false }
38
+ let(:type) { :manual_bookable }
39
+ it { should == "LV" }
40
+ end
41
+ end
@@ -0,0 +1,181 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+ require "sie"
4
+ require "active_support/core_ext/date/calculations"
5
+
6
+ describe Sie::Document, "#render" do
7
+ let(:from_date) { Date.new(2011, 9, 1) }
8
+ let(:to_date) { Date.new(2013, 12, 31) }
9
+ let(:generated_on) { Date.yesterday }
10
+ let(:accounts) {
11
+ [
12
+ number: 1500, description: "Customer ledger"
13
+ ]
14
+ }
15
+ let(:vouchers) {
16
+ [
17
+ {
18
+ creditor: false, type: :invoice, number: 1, booked_on: from_date + 2, description: "Invoice 1",
19
+ voucher_lines: [
20
+ { account_number: "1500", amount: 512.0, booked_on: from_date + 2, description: "Item 1" },
21
+ { account_number: "3100", amount: -512.0, booked_on: from_date + 2, description: "Item 1" },
22
+ ]
23
+ },
24
+ {
25
+ creditor: true, type: :payment, number: 2, booked_on: from_date + 365, description: "Payout 1",
26
+ voucher_lines: [
27
+ { account_number: "2400", amount: 256.0, booked_on: from_date + 365, description: "Payout line 1" },
28
+ { account_number: "1970", amount: -256.0, booked_on: from_date + 365, description: "Payout line 2" },
29
+ ]
30
+ }
31
+ ]
32
+ }
33
+
34
+ class TestDataSource
35
+ attr_accessor :program, :program_version, :generated_on, :company_name,
36
+ :accounts, :balance_account_numbers, :closing_account_numbers,
37
+ :vouchers, :from_date, :to_date, :financial_year_start_month
38
+
39
+ # vouchers is not part of the expected interface so making it private.
40
+ #
41
+ # Sie::Document uses #each_voucher so that you can build documents for huge sets of vouchers
42
+ # by loading them in batches.
43
+ private :vouchers
44
+
45
+ def initialize(hash)
46
+ hash.each do |k, v|
47
+ public_send("#{k}=", v)
48
+ end
49
+ end
50
+
51
+ def each_voucher(&block)
52
+ vouchers.each(&block)
53
+ end
54
+
55
+ def balance_before(account_number, date)
56
+ # Faking a fetch based on date and number
57
+ account_number.to_i + (date.mday * 100).to_f
58
+ end
59
+ end
60
+
61
+ let(:doc) {
62
+ data_source = TestDataSource.new(
63
+ from_date: from_date,
64
+ to_date: to_date,
65
+ accounts: accounts,
66
+ vouchers: vouchers,
67
+ program: "Foonomic",
68
+ program_version: "3.11",
69
+ generated_on: generated_on,
70
+ company_name: "Foocorp",
71
+ financial_year_start_month: 1,
72
+ balance_account_numbers: [ "1500", "2400" ],
73
+ closing_account_numbers: [ "3100" ]
74
+ )
75
+ doc = Sie::Document.new(data_source)
76
+ }
77
+
78
+ let(:sie_file) { Sie::Parser.new.parse(doc.render) }
79
+
80
+ it "adds a header" do
81
+ expect(entry_attribute("flagga", "x")).to eq("0")
82
+ expect(entry_attribute("program", "programnamn")).to eq("Foonomic")
83
+ expect(entry_attribute("program", "version")).to eq("3.11")
84
+ expect(entry_attribute("format", "PC8")).to eq("PC8")
85
+ expect(entry_attribute("gen", "datum")).to eq(generated_on.strftime("%Y%m%d"))
86
+ expect(entry_attribute("sietyp", "typnr")).to eq("4")
87
+ expect(entry_attribute("fnamn", "foretagsnamn")).to eq("Foocorp")
88
+ end
89
+
90
+ it "has accounting years" do
91
+ expect(indexed_entry_attribute("rar", 0, "arsnr")).to eq("0")
92
+ expect(indexed_entry_attribute("rar", 0, "start")).to eq("20130101")
93
+ expect(indexed_entry_attribute("rar", 0, "slut")).to eq("20131231")
94
+ expect(indexed_entry_attribute("rar", 1, "arsnr")).to eq("-1")
95
+ expect(indexed_entry_attribute("rar", 1, "start")).to eq("20120101")
96
+ expect(indexed_entry_attribute("rar", 1, "slut")).to eq("20121231")
97
+ expect(indexed_entry_attribute("rar", 2, "arsnr")).to eq("-2")
98
+ expect(indexed_entry_attribute("rar", 2, "start")).to eq("20110901")
99
+ expect(indexed_entry_attribute("rar", 2, "slut")).to eq("20111231")
100
+ end
101
+
102
+ it "has accounts" do
103
+ expect(indexed_entry_attributes("konto", 0)).to eq("kontonr" => "1500", "kontonamn" => "Customer ledger")
104
+ end
105
+
106
+ it "has balances brought forward (ingående balans)" do
107
+ expect(indexed_entry_attributes("ib", 0)).to eq("arsnr" => "0", "konto" => "1500", "saldo" => "1600.0")
108
+ expect(indexed_entry_attributes("ib", 1)).to eq("arsnr" => "0", "konto" => "2400", "saldo" => "2500.0")
109
+ expect(indexed_entry_attributes("ib", 2)).to eq("arsnr" => "-1", "konto" => "1500", "saldo" => "1600.0")
110
+ expect(indexed_entry_attributes("ib", 3)).to eq("arsnr" => "-1", "konto" => "2400", "saldo" => "2500.0")
111
+ expect(indexed_entry_attributes("ib", 4)).to eq("arsnr" => "-2", "konto" => "1500", "saldo" => "1600.0")
112
+ expect(indexed_entry_attributes("ib", 5)).to eq("arsnr" => "-2", "konto" => "2400", "saldo" => "2500.0")
113
+ end
114
+
115
+ it "has balances carried forward (utgående balans)" do
116
+ expect(indexed_entry_attributes("ub", 0)).to eq("arsnr" => "0", "konto" => "1500", "saldo" => "4600.0")
117
+ expect(indexed_entry_attributes("ub", 1)).to eq("arsnr" => "0", "konto" => "2400", "saldo" => "5500.0")
118
+ expect(indexed_entry_attributes("ub", 2)).to eq("arsnr" => "-1", "konto" => "1500", "saldo" => "4600.0")
119
+ expect(indexed_entry_attributes("ub", 3)).to eq("arsnr" => "-1", "konto" => "2400", "saldo" => "5500.0")
120
+ expect(indexed_entry_attributes("ub", 4)).to eq("arsnr" => "-2", "konto" => "1500", "saldo" => "4600.0")
121
+ expect(indexed_entry_attributes("ub", 5)).to eq("arsnr" => "-2", "konto" => "2400", "saldo" => "5500.0")
122
+ end
123
+
124
+ it "has closing account balances (saldo för resultatkonto)" do
125
+ expect(indexed_entry_attributes("res", 0)).to eq("ars" => "0", "konto" => "3100", "saldo" => "6200.0")
126
+ expect(indexed_entry_attributes("res", 1)).to eq("ars" => "-1", "konto" => "3100", "saldo" => "6200.0")
127
+ expect(indexed_entry_attributes("res", 2)).to eq("ars" => "-2", "konto" => "3100", "saldo" => "6200.0")
128
+ end
129
+
130
+ it "has vouchers" do
131
+ expect(indexed_entry("ver", 0).attributes).to eq(
132
+ "serie" => "KF", "vernr" => "1",
133
+ "verdatum" => "20110903", "vertext" => "Invoice 1"
134
+ )
135
+ expect(indexed_voucher_entries(0)[0].attributes).to eq(
136
+ "kontonr" => "1500", "belopp" => "512.0",
137
+ "transdat" => "20110903", "transtext" => "Item 1"
138
+ )
139
+ expect(indexed_voucher_entries(0)[1].attributes).to eq(
140
+ "kontonr" => "3100", "belopp" => "-512.0",
141
+ "transdat" => "20110903", "transtext" => "Item 1"
142
+ )
143
+
144
+ expect(indexed_entry("ver", 1).attributes).to eq(
145
+ "serie" => "KB", "vernr" => "2",
146
+ "verdatum" => "20120831", "vertext" => "Payout 1"
147
+ )
148
+ expect(indexed_voucher_entries(1)[0].attributes).to eq(
149
+ "kontonr" => "2400", "belopp" => "256.0",
150
+ "transdat" => "20120831", "transtext" => "Payout line 1"
151
+ )
152
+ expect(indexed_voucher_entries(1)[1].attributes).to eq(
153
+ "kontonr" => "1970", "belopp" => "-256.0",
154
+ "transdat" => "20120831", "transtext" => "Payout line 2"
155
+ )
156
+ end
157
+
158
+ private
159
+
160
+ def entry_attribute(label, attribute)
161
+ indexed_entry_attribute(label, 0, attribute)
162
+ end
163
+
164
+ def indexed_entry_attribute(label, index, attribute)
165
+ indexed_entry_attributes(label, index).fetch(attribute) do
166
+ raise "Unknown attribute #{ attribute } in #{ entry.attributes.keys.inspect }"
167
+ end
168
+ end
169
+
170
+ def indexed_entry_attributes(label, index)
171
+ indexed_entry(label, index).attributes
172
+ end
173
+
174
+ def indexed_voucher_entries(index)
175
+ indexed_entry("ver", index).entries
176
+ end
177
+
178
+ def indexed_entry(label, index)
179
+ sie_file.entries_with_label(label)[index] or raise "No entry with label #{ label.inspect } found!"
180
+ end
181
+ end
@@ -0,0 +1,21 @@
1
+ require "spec_helper"
2
+ require "sie/parser/line_parser"
3
+
4
+ describe Sie::Parser::LineParser, "parse" do
5
+ it "parses lines from a sie file" do
6
+ parser = Sie::Parser::LineParser.new('#TRANS 2400 {} -200 20130101 "Foocorp expense"')
7
+ entry = parser.parse
8
+ expect(entry.label).to eq("trans")
9
+ expect(entry.attributes).to eq({
10
+ "kontonr" => "2400",
11
+ "belopp" => "-200",
12
+ "transdat" => "20130101",
13
+ "transtext" => "Foocorp expense"
14
+ })
15
+ end
16
+
17
+ it "fails if you have non empty metadata arrays until there is a need to support that" do
18
+ parser = Sie::Parser::LineParser.new('#TRANS 2400 { 1 "2" } -200 20130101 "Foocorp expense"')
19
+ expect(-> { parser.parse }).to raise_error(/don't support metadata/)
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+ require "sie/parser/tokenizer"
3
+
4
+ describe Sie::Parser::Tokenizer do
5
+ it "tokenizes the given line" do
6
+ tokenizer = Sie::Parser::Tokenizer.new('#TRANS 2400 {} -200 20130101 "Foocorp expense"')
7
+ tokens = tokenizer.tokenize
8
+
9
+ expect(token_table_for(tokens)).to eq([
10
+ [ "EntryToken", "TRANS" ],
11
+ [ "StringToken", "2400" ],
12
+ [ "BeginArrayToken", "" ],
13
+ [ "EndArrayToken", "" ],
14
+ [ "StringToken", "-200" ],
15
+ [ "StringToken", "20130101" ],
16
+ [ "StringToken", "Foocorp expense" ]
17
+ ])
18
+ end
19
+
20
+ it "can parse metadata arrays" do
21
+ tokenizer = Sie::Parser::Tokenizer.new('#TRANS 2400 { 1 "2" } -200 20130101 "Foocorp expense"')
22
+ tokens = tokenizer.tokenize
23
+
24
+ expect(token_table_for(tokens)).to eq([
25
+ [ "EntryToken", "TRANS" ],
26
+ [ "StringToken", "2400" ],
27
+ [ "BeginArrayToken", "" ],
28
+ [ "StringToken", "1" ],
29
+ [ "StringToken", "2" ],
30
+ [ "EndArrayToken", "" ],
31
+ [ "StringToken", "-200" ],
32
+ [ "StringToken", "20130101" ],
33
+ [ "StringToken", "Foocorp expense" ]
34
+ ])
35
+ end
36
+
37
+ def token_table_for(tokens)
38
+ tokens.map { |token|
39
+ [ token.class.name.split("::").last, token.value ]
40
+ }
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+ require "sie/parser"
3
+
4
+ describe Sie::Parser, "parse" do
5
+ it "parses sie data that includes arrays" do
6
+ parser = Sie::Parser.new
7
+ sie_file = parser.parse(<<DATA
8
+ #VER "LF" 2222 20130101 "Foocorp expense"
9
+ {
10
+ #TRANS 2400 {} -200 20130101 "Foocorp expense"
11
+ #TRANS 4100 {} 180 20130101 "Widgets from foocorp"
12
+ #TRANS 2611 {} -20 20130101 "VAT"
13
+ }
14
+ DATA
15
+ )
16
+
17
+ voucher_entry = sie_file.entries.first
18
+ expect(sie_file.entries.size).to eq(1)
19
+ expect(voucher_entry.attributes["verdatum"]).to eq("20130101")
20
+ expect(voucher_entry.entries.size).to eq(3)
21
+ expect(voucher_entry.entries.first.attributes["kontonr"]).to eq("2400")
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sie
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Barsoom AB
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: attr_extras
16
+ requirement: &70103120492320 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70103120492320
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &70103120491900 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70103120491900
36
+ - !ruby/object:Gem::Dependency
37
+ name: bundler
38
+ requirement: &70103120491400 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.3'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70103120491400
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: &70103120490980 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70103120490980
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &70103120490520 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70103120490520
69
+ description: SIE parser and generator
70
+ email:
71
+ - all@barsoom.se
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .rspec
78
+ - .rvmrc
79
+ - Gemfile
80
+ - README.md
81
+ - Rakefile
82
+ - lib/sie.rb
83
+ - lib/sie/document.rb
84
+ - lib/sie/document/financial_years.rb
85
+ - lib/sie/document/renderer.rb
86
+ - lib/sie/document/voucher_series.rb
87
+ - lib/sie/parser.rb
88
+ - lib/sie/parser/entry.rb
89
+ - lib/sie/parser/entry_types.rb
90
+ - lib/sie/parser/line_parser.rb
91
+ - lib/sie/parser/sie_file.rb
92
+ - lib/sie/parser/tokenizer.rb
93
+ - lib/sie/parser/tokenizer/character.rb
94
+ - lib/sie/parser/tokenizer/token.rb
95
+ - lib/sie/version.rb
96
+ - script/bootstrap
97
+ - script/ci/support/env
98
+ - script/ci/tests.sh
99
+ - script/refresh
100
+ - script/turbux_rspec
101
+ - sie.gemspec
102
+ - spec/fixtures/sie_file.se
103
+ - spec/integration/parser_spec.rb
104
+ - spec/spec_helper.rb
105
+ - spec/unit/document/financial_years_spec.rb
106
+ - spec/unit/document/voucher_series_spec.rb
107
+ - spec/unit/document_spec.rb
108
+ - spec/unit/parser/line_parser_spec.rb
109
+ - spec/unit/parser/tokenizer_spec.rb
110
+ - spec/unit/parser_spec.rb
111
+ homepage: ''
112
+ licenses:
113
+ - MIT
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ segments:
125
+ - 0
126
+ hash: 1372446967420835703
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ segments:
134
+ - 0
135
+ hash: 1372446967420835703
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 1.8.6
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Parses and generates SIE files (http://sie.se/)
142
+ test_files:
143
+ - spec/fixtures/sie_file.se
144
+ - spec/integration/parser_spec.rb
145
+ - spec/spec_helper.rb
146
+ - spec/unit/document/financial_years_spec.rb
147
+ - spec/unit/document/voucher_series_spec.rb
148
+ - spec/unit/document_spec.rb
149
+ - spec/unit/parser/line_parser_spec.rb
150
+ - spec/unit/parser/tokenizer_spec.rb
151
+ - spec/unit/parser_spec.rb