ledgerjournal 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: []