ledgerjournal 0.5.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 54a88d089772a52d3f2679f0ca80c2d3fed563d7cbadd44531ace686018045df
4
+ data.tar.gz: f78a221028fd29e7a2553b55431b10fe7ff2ea0cac4fbe4cfb7d62db73f5da3f
5
+ SHA512:
6
+ metadata.gz: aea34667a60c6560425fa18032ae15b525db1459b741d50e67afed0d52fba9fcce3205163b39ecf01e4d258e59dff21674fdf945f86762df8d9af9296b4a92b5
7
+ data.tar.gz: 20a2e543860cd3cc7e72ccd9a805d7bdd20e5cbf485f519afd7be24e84d2949de7fdfbc049fdb6a1ef8995d8bcae0c0d0790e36c6843e2577d8d693b6733fe75
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea
10
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6
6
+ - 2.7
7
+ before_install:
8
+ - gem install bundler -v 2.1.4
9
+ - sudo apt-get -y install ledger
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ledgerjournal.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ledgerjournal (0.5.0)
5
+ nokogiri (~> 1.10)
6
+ open4 (~> 1.3)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ mini_portile2 (2.4.0)
12
+ minitest (5.14.0)
13
+ nokogiri (1.10.9)
14
+ mini_portile2 (~> 2.4.0)
15
+ open4 (1.3.4)
16
+ rake (12.3.3)
17
+
18
+ PLATFORMS
19
+ ruby
20
+
21
+ DEPENDENCIES
22
+ ledgerjournal!
23
+ minitest (~> 5.0)
24
+ rake (~> 12.0)
25
+
26
+ BUNDLED WITH
27
+ 2.1.4
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Ralf Ebert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # ledgerjournal
2
+
3
+ [![Build Status](https://travis-ci.org/ralfebert/ledgerjournal.svg?branch=master)](https://travis-ci.org/github/ralfebert/ledgerjournal)
4
+
5
+ ledgerjournal is a Ruby gem to read and write [ledger](https://www.ledger-cli.org/) accounting files.
6
+ For parsing, it uses the xml output from ledger. For outputting, it formats the ledger data to String in custom Ruby code.
7
+ The ledger binary needs to be installed to parse and pretty-print.
8
+
9
+ ## Usage
10
+
11
+ Parsing a leger file:
12
+
13
+ ```ruby
14
+ journal = Ledger::Journal.new(path: "example_journal_en.txt")
15
+ journal.transactions.each do |tx|
16
+ puts tx.date, tx.payee
17
+ end
18
+ ```
19
+
20
+ Creating a ledger:
21
+
22
+ ```ruby
23
+ journal = Ledger::Journal.new(path: "example_journal_en.txt")
24
+
25
+ journal.transactions << Ledger::Transaction.new(
26
+ date: Date.new(2020, 1, 2),
27
+ state: :cleared,
28
+ payee: 'Example Payee',
29
+ metadata: { "Foo" => "Bar", "Description" => "Example Transaction" },
30
+ postings: [
31
+ Ledger::Posting.new(account: "Expenses:Unknown", currency: "EUR", amount: BigDecimal('1234.56'), metadata: { "Foo" => "Bar", "Description" => "Example Posting" }),
32
+ Ledger::Posting.new(account: "Assets:Checking", currency: "EUR", amount: BigDecimal('-1234.56'))
33
+ ]
34
+ )
35
+
36
+ puts(journal.to_s)
37
+ ```
38
+
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem 'ledgerjournal'
46
+ ```
47
+
48
+ Or install it yourself as:
49
+
50
+ $ gem install ledgerjournal
51
+
52
+ ## Development
53
+
54
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
55
+
56
+ ## Contributing
57
+
58
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ralfebert/ledgerjournal.
59
+
60
+ ## License
61
+
62
+ libledger is released under the MIT License. See LICENSE.md.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ledgerjournal"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,30 @@
1
+ require_relative 'lib/ledgerjournal/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "ledgerjournal"
5
+ spec.version = Ledger::VERSION
6
+ spec.licenses = ['MIT']
7
+ spec.authors = ["Ralf Ebert"]
8
+ spec.email = ["ralf.ebert@gmail.com"]
9
+
10
+ spec.summary = %q{Library to read and write ledger accounting files.}
11
+ spec.description = %q{ledgerjournal is a Ruby gem to read and write ledger accounting files.
12
+ For parsing, it uses the xml output from ledger. For outputting, it formats the ledger data to String in custom Ruby code.
13
+ The ledger binary needs to be installed to parse and pretty-print.}
14
+ spec.homepage = "https://github.com/ralfebert/ledgerjournal"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/ralfebert/ledgerjournal.git"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency 'nokogiri', '~> 1.10'
28
+ spec.add_dependency 'open4', '~> 1.3'
29
+
30
+ end
@@ -0,0 +1,72 @@
1
+ require 'shellwords'
2
+ require 'nokogiri'
3
+ require 'open4'
4
+
5
+ module Ledger
6
+
7
+ # Represents a ledger journal
8
+ # @see https://www.ledger-cli.org/3.0/doc/ledger3.html#Journal-Format
9
+ class Journal
10
+
11
+ # @return [Array<Ledger::Transaction>] list of transactions in journal
12
+ attr_accessor :transactions
13
+ # @return [String] path path to ledger file
14
+ attr_reader :path
15
+
16
+ # Creates a new ledger journal or loads transactions from a ledger journal file.
17
+ # @param [String] path path to a ledger journal to load
18
+ # @param [String] ledger_args when loading from a path, you can pass in arguments to the ledger command (for example to filter transactions)
19
+ def initialize(path: nil, ledger_args: nil)
20
+ @transactions = []
21
+ if path
22
+ @path = path
23
+ raise Error.new("#{@path} not found") unless File.exist?(@path)
24
+ args = ["-f #{@path.shellescape}", ledger_args].compact.join(" ")
25
+ read_ledger(ledger_args: args)
26
+ end
27
+ end
28
+
29
+ # @param [Ledger::Journal] other
30
+ # @return [Boolean] true if the other journal contains equal transactions
31
+ def ==(other)
32
+ self.transactions == other.transactions
33
+ end
34
+
35
+ # @return [String] returns the transactions in the journal formatted as string
36
+ # @param [Boolean] pretty_print calls ledger to format the journal if true
37
+ def to_s(pretty_print: true)
38
+ str = self.transactions.map(&:to_s).join("\n\n")
39
+ if pretty_print
40
+ begin
41
+ str = Ledger.defaults.run("-f - print", stdin: str)
42
+ str = str.lines.map { |line| line.rstrip }.join("\n") + "\n"
43
+ rescue => error
44
+ # return an unformatted string if an error occurs
45
+ puts "Couldn't format transaction log: #{error}"
46
+ end
47
+ end
48
+ return str
49
+ end
50
+
51
+ # If the journal was opened from a file, save/overwrite the file with the transactions from this journal.
52
+ def save!
53
+ raise Error.new("Journal was not read from path, cannot save") unless @path
54
+ File.write(@path, self.to_s)
55
+ end
56
+
57
+ # returns all transactions that have a posting for the given account
58
+ # @param [String] account account name
59
+ def transactions_with_account(account)
60
+ @transactions.select {|tx| tx.postings.map(&:account).include?(account) }
61
+ end
62
+
63
+ private
64
+ def read_ledger(ledger_args: "")
65
+ xml_result = Ledger.defaults.run(ledger_args + " xml")
66
+ xml = Nokogiri::XML(xml_result)
67
+ @transactions = xml.css("transaction").map { |tx_xml| Transaction.parse_xml(tx_xml) }
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,79 @@
1
+ module Ledger
2
+
3
+ # Options for interaction with ledger-cli
4
+ class Options
5
+
6
+ # @param [String] date_format like '%Y/%m/%d' to pass to ledger-cli
7
+ # @param [Boolean] decimal_comma pass true to use ',' as decimal comma separator
8
+ def initialize(date_format:, decimal_comma:)
9
+ @date_format = date_format
10
+ @decimal_comma = decimal_comma
11
+ end
12
+
13
+ # Returns default options by locale. Currently supported are :en or :de.
14
+ # @param [Symbol] locale as symbol
15
+ # @return [Ledger::Options]
16
+ def self.for_locale(locale)
17
+ case locale.to_sym
18
+ when :en
19
+ Options.new(date_format: '%Y/%m/%d', decimal_comma: false)
20
+ when :de
21
+ Options.new(date_format: '%d.%m.%Y', decimal_comma: true)
22
+ else
23
+ raise Error.new("Unknown locale for ledger options: #{locale}, supported are :en, :de")
24
+ end
25
+ end
26
+
27
+ # @param [String] string decimal amount as String (like '12.34')
28
+ # @return [BigDecimal] the given string as BigDecimal, parsed according the decimal_comma setting
29
+ def parse_amount(string)
30
+ BigDecimal(if @decimal_comma then string.gsub(".", "").gsub(",", ".") else string end)
31
+ end
32
+
33
+ # Formats the given value as String
34
+ # @param [Date, BigDecimal] value to parse
35
+ # @return [String] string representation according to the options
36
+ def format(value)
37
+ if value.instance_of? Date
38
+ return value.strftime(@date_format)
39
+ elsif value.instance_of? BigDecimal
40
+ str = '%.2f' % value
41
+ str.gsub!('.', ',') if @decimal_comma
42
+ return str
43
+ else
44
+ raise Error.new("Unknown value type #{value.class}")
45
+ end
46
+ end
47
+
48
+ # Runs ledger-cli
49
+ # @param [String] args command line arguments to pass
50
+ # @param [String] stdin stdin text to pass
51
+ # @return [String] stdout result of calling ledger
52
+ def run(args, stdin: nil)
53
+ output = ""
54
+ error = ""
55
+ begin
56
+ Open4.spawn("ledger #{cmdline_options} #{args}", :stdin => stdin, :stdout => output, :stderr => error)
57
+ rescue => e
58
+ raise Error.new("#{e}: #{error}")
59
+ end
60
+ return output
61
+ end
62
+
63
+ private
64
+ def cmdline_options
65
+ args = ["--args-only", "--date-format #{@date_format}", "--input-date-format #{@date_format}"]
66
+ args << "--decimal-comma" if @decimal_comma
67
+ return args.join(" ")
68
+ end
69
+
70
+ end
71
+
72
+ @defaults = Options.for_locale(:en)
73
+
74
+ class <<self
75
+ # @attr [Ledger::Options] defaults options to use when interacting with ledger-cli, by default settings for locale :en are used
76
+ attr_accessor :defaults
77
+ end
78
+
79
+ end
@@ -0,0 +1,63 @@
1
+ require 'date'
2
+ require 'bigdecimal'
3
+
4
+ module Ledger
5
+
6
+ # @attr [String] account
7
+ # @attr [String] currency
8
+ # @attr [BigDecimal] amount
9
+ # @attr [BigDecimal] balance_assignment if a balance_assignment is set, ledger-cli checks the balance of the account after this posting
10
+ # @attr [Hash<String, String>] metadata
11
+ class Posting
12
+
13
+ attr_accessor :account, :currency, :amount, :balance_assignment, :metadata
14
+
15
+ def initialize(account:, currency:, amount:, balance_assignment: nil, metadata: {})
16
+ @account = account
17
+ @currency = currency
18
+ @amount = amount
19
+ @balance_assignment = balance_assignment
20
+ @metadata = metadata
21
+ end
22
+
23
+ def self.parse_xml(xml)
24
+ balance_assignment = nil
25
+ currency = xml.xpath('post-amount/amount/commodity/symbol').text
26
+ if xml_balance = xml.xpath('balance-assignment').first
27
+ balance_currency = xml_balance.xpath('commodity/symbol').text
28
+ raise Error.new("Posting currency #{currency} doesn't match assignment currency #{balance_currency}") if currency != balance_currency
29
+ balance_assignment = Ledger.defaults.parse_amount(xml_balance.xpath('quantity').text)
30
+ end
31
+ Posting.new(
32
+ account: xml.xpath('account/name').text,
33
+ currency: currency,
34
+ amount: Ledger.defaults.parse_amount(xml.xpath('post-amount/amount/quantity').text),
35
+ balance_assignment: balance_assignment,
36
+ metadata: Hash[xml.xpath("metadata/value").collect {|m| [m['key'], m.xpath("string").text]}]
37
+ )
38
+ end
39
+
40
+ def ==(other)
41
+ self.class == other.class && self.all_fields == other.all_fields
42
+ end
43
+
44
+ def to_s
45
+ posting_line = "#{self.account} #{self.currency} #{Ledger.defaults.format(self.amount)}"
46
+ if self.balance_assignment
47
+ posting_line += " = #{self.currency} #{Ledger.defaults.format(self.balance_assignment)}"
48
+ end
49
+ lines = [posting_line]
50
+ unless self.metadata.empty?
51
+ lines += metadata.to_a.sort {|a,b| a[0].casecmp(b[0]) }.collect{|m| "; #{m[0]}: #{m[1]}" }
52
+ end
53
+ return lines.join("\n")
54
+ end
55
+
56
+ protected
57
+ def all_fields
58
+ self.instance_variables.map { |variable| self.instance_variable_get variable }
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,64 @@
1
+ require 'date'
2
+
3
+ module Ledger
4
+
5
+ # @attr [Date] date
6
+ # @attr [Symbol] state state of transaction, can be :cleared or :pending
7
+ # @attr [String] payee
8
+ # @attr [Hash<String, String>] metadata
9
+ # @attr [Array<Ledger::Posting>] postings
10
+ class Transaction
11
+
12
+ attr_accessor :date, :state, :payee, :metadata, :postings
13
+
14
+ def initialize(date:, state: :cleared, payee:, metadata: {}, postings:)
15
+ @date = date
16
+ @state = state
17
+ @payee = payee
18
+ @metadata = metadata
19
+ @postings = postings
20
+ end
21
+
22
+ def self.parse_xml(xml)
23
+ Transaction.new(
24
+ date: Date.strptime(xml.xpath('date').text, '%Y/%m/%d'),
25
+ payee: xml.xpath('payee').text,
26
+ state: xml['state'].to_sym,
27
+ metadata: Hash[xml.xpath("metadata/value").collect { |m| [m['key'], m.xpath("string").text] }],
28
+ postings: xml.xpath('postings/posting').map { |posting_xml| Posting.parse_xml(posting_xml) }
29
+ )
30
+ end
31
+
32
+ def ==(other)
33
+ self.class == other.class && self.all_fields == other.all_fields
34
+ end
35
+
36
+ def to_s
37
+ date_str = Ledger.defaults.format(self.date)
38
+ states = {:pending => "!", :cleared => "*"}
39
+ lines = ["#{date_str} #{states[self.state]} #{self.payee}"]
40
+
41
+ unless self.metadata.empty?
42
+ lines += metadata.to_a.sort { |a, b| a[0].casecmp(b[0]) }.collect { |m| " ; #{m[0]}: #{m[1]}" }
43
+ end
44
+
45
+ lines += self.postings.map { |posting| posting.to_s.lines.map { |line| " " + line }.join }
46
+
47
+ return lines.join("\n") + "\n"
48
+ end
49
+
50
+ # @param [String] account
51
+ # @return [Ledger::Posting]
52
+ def posting_for_account(account)
53
+ postings.select { |posting| posting.account == account }.first
54
+ end
55
+
56
+ protected
57
+
58
+ def all_fields
59
+ self.instance_variables.map { |variable| self.instance_variable_get(variable) }
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,4 @@
1
+ module Ledger
2
+ # ledgerjournal version
3
+ VERSION = "0.5.0"
4
+ end
@@ -0,0 +1,13 @@
1
+ require "ledgerjournal/version"
2
+ require "ledgerjournal/options"
3
+ require "ledgerjournal/journal"
4
+ require "ledgerjournal/transaction"
5
+ require "ledgerjournal/posting"
6
+
7
+ # Ledger provides classes to read and write ledger accounting files.
8
+ module Ledger
9
+
10
+ # Error class for errors handling ledger files
11
+ class Error < StandardError; end
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ledgerjournal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Ralf Ebert
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: open4
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ description: |-
42
+ ledgerjournal is a Ruby gem to read and write ledger accounting files.
43
+ For parsing, it uses the xml output from ledger. For outputting, it formats the ledger data to String in custom Ruby code.
44
+ The ledger binary needs to be installed to parse and pretty-print.
45
+ email:
46
+ - ralf.ebert@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - ".gitignore"
52
+ - ".travis.yml"
53
+ - Gemfile
54
+ - Gemfile.lock
55
+ - LICENSE.md
56
+ - README.md
57
+ - Rakefile
58
+ - bin/console
59
+ - bin/setup
60
+ - ledgerjournal.gemspec
61
+ - lib/ledgerjournal.rb
62
+ - lib/ledgerjournal/journal.rb
63
+ - lib/ledgerjournal/options.rb
64
+ - lib/ledgerjournal/posting.rb
65
+ - lib/ledgerjournal/transaction.rb
66
+ - lib/ledgerjournal/version.rb
67
+ homepage: https://github.com/ralfebert/ledgerjournal
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/ralfebert/ledgerjournal
72
+ source_code_uri: https://github.com/ralfebert/ledgerjournal.git
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.3.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.0.4
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Library to read and write ledger accounting files.
92
+ test_files: []