├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── lib ├── cli-runner.js ├── commands │ ├── accounts.js │ ├── balance.js │ ├── print.js │ ├── register.js │ ├── stats.js │ └── version.js ├── commodityParser.js ├── escape-quotes.js └── ledger.js ├── package.json ├── spec ├── accounts.spec.js ├── balance.spec.js ├── data │ ├── drewr.dat │ ├── foreign-currency-transaction.dat │ ├── quoted-transaction.dat │ └── single-transaction.dat ├── register.spec.js └── version.spec.js └── terminology.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | node_js: 4 | - 'stable' 5 | before_install: 6 | - sudo add-apt-repository ppa:mbudde/ledger -y 7 | - sudo apt-get -qq update 8 | - sudo apt-get install -y ledger 9 | before_script: 10 | - npm install -g grunt-cli 11 | env: 12 | matrix: 13 | - LEDGER_BIN=ledger 14 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | grunt.initConfig({ 5 | jshint: { 6 | options: { 7 | curly: true, 8 | eqeqeq: true, 9 | immed: true, 10 | latedef: true, 11 | newcap: true, 12 | noarg: true, 13 | sub: true, 14 | undef: true, 15 | unused: true, 16 | boss: true, 17 | eqnull: true, 18 | globals: { 19 | jQuery: true 20 | } 21 | }, 22 | gruntfile: { 23 | src: 'Gruntfile.js' 24 | }, 25 | lib: { 26 | options: { 27 | globals: { 28 | require: true, 29 | module: true, 30 | console: true 31 | } 32 | }, 33 | src: ['lib/**/*.js'] 34 | }, 35 | spec: { 36 | options: { 37 | globals: { 38 | require: true, 39 | describe: true, 40 | beforeEach: true, 41 | it: true, 42 | expect: true, 43 | process: true 44 | } 45 | }, 46 | src: ['spec/**/*.js'] 47 | } 48 | }, 49 | 50 | 'mochaTest': { 51 | src: ['spec/**/*.spec.js'], 52 | options: { 53 | reporter: 'spec' 54 | } 55 | }, 56 | 57 | watch: { 58 | gruntfile: { 59 | files: '<%= jshint.gruntfile.src %>', 60 | tasks: ['jshint:gruntfile'] 61 | }, 62 | lib: { 63 | files: '<%= jshint.lib.src %>', 64 | tasks: ['jshint:lib'] 65 | }, 66 | spec: { 67 | files: '<%= jshint.spec.src %>', 68 | tasks: ['jshint:spec', 'spec'] 69 | } 70 | } 71 | }); 72 | 73 | grunt.loadNpmTasks('grunt-contrib-jshint'); 74 | grunt.loadNpmTasks('grunt-contrib-watch'); 75 | grunt.loadNpmTasks('grunt-mocha-test'); 76 | 77 | grunt.registerTask('spec', ['mochaTest']); 78 | 79 | // Default task. 80 | grunt.registerTask('default', ['jshint', 'spec']); 81 | }; 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Ben Smith (ben@10consulting.com) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ledger-cli 2 | 3 | API for the Ledger command-line interface ([ledger-cli.org](http://ledger-cli.org/)). 4 | 5 | > Ledger is a powerful, double-entry accounting system that is accessed from the UNIX command-line. 6 | 7 | MIT License 8 | 9 | [![Build Status](https://travis-ci.org/slashdotdash/node-ledger.svg?branch=master)](https://travis-ci.org/slashdotdash/node-ledger) 10 | 11 | ## Dependencies 12 | 13 | * [Ledger 3](http://ledger-cli.org/) 14 | * [Node.js](nodejs.org) and npm 15 | 16 | ### Installing Ledger 17 | 18 | The simplest way to install Ledger 3 is through [Homebrew](http://mxcl.github.com/homebrew/). 19 | 20 | ``` 21 | brew install ledger --HEAD 22 | ``` 23 | 24 | The `--HEAD` option is required to install version 3.x. 25 | 26 | ## Usage 27 | 28 | Install `ledger-cli` and its dependencies with npm. 29 | 30 | ``` 31 | npm install ledger-cli 32 | ``` 33 | 34 | Then require the library and use the exported Ledger class to [execute commands](#available-commands). 35 | 36 | ```js 37 | var Ledger = require('ledger-cli').Ledger; 38 | ``` 39 | 40 | You must provide the path to the Ledger journal file via the `file` option 41 | 42 | ```js 43 | var ledger = new Ledger({ file: 'path/to/ledger/journal/file.dat' }); 44 | ``` 45 | 46 | ### Available commands 47 | 48 | There are five available Ledger commands. 49 | 50 | * `accounts` - Lists all accounts for postings. 51 | * `balance` - Reports the current balance of all accounts. 52 | * `print` - Prints out the full transactions, sorted by date, using the same format as they would appear in a Ledger data file. 53 | * `register` - Displays all the postings occurring in a single account. 54 | * `stats` - Retrieves statistics, like number of unique accounts. 55 | * `version` - Gets the currently installed Ledger version number. 56 | 57 | ### Accounts 58 | 59 | Lists all accounts for postings. It returns a readable object `stream`. 60 | 61 | ```js 62 | ledger.accounts() 63 | .on('data', function(account) { 64 | // account is the name of an account (e.g. 'Assets:Current Account') 65 | }); 66 | ``` 67 | 68 | ### Balance 69 | 70 | The balance command reports the current balance of all accounts. It returns a readable object `stream`. 71 | 72 | ```js 73 | ledger.balance() 74 | .on('data', function(entry) { 75 | // JSON object for each entry 76 | entry = { 77 | total: { 78 | currency: '£', 79 | amount: 1000, 80 | formatted: '£1,000.00' 81 | }, 82 | account: { 83 | fullname: 'Assets:Checking', 84 | shortname: 'Assets:Checking', 85 | depth: 2, 86 | } 87 | }; 88 | }) 89 | .once('end', function(){ 90 | // completed 91 | }) 92 | .once('error', function(error) { 93 | // error 94 | }); 95 | ``` 96 | 97 | ### Print 98 | 99 | The print command formats the full list of transactions, ordered by date, using the same format as they would appear in a Ledger data file. It returns a readable stream. 100 | 101 | ```js 102 | var fs = require('fs'), 103 | out = fs.createWriteStream('output.dat'); 104 | 105 | ledger.print().pipe(out); 106 | ``` 107 | 108 | ### Register 109 | 110 | The register command displays all the postings occurring in a single account. It returns a readable object `stream`. 111 | 112 | ```js 113 | ledger.register() 114 | .on('data', function(entry) { 115 | // JSON object for each entry 116 | entry = { 117 | date: new Date(2014, 1, 1), 118 | cleared: true, 119 | pending: true, 120 | payee: 'Salary', 121 | postings: [{ 122 | commodity: { 123 | currency: '£', 124 | amount: 1000, 125 | formatted: '£1,000.00' 126 | }, 127 | account: 'Assets:Checking' 128 | }] 129 | }; 130 | }) 131 | .once('end', function(){ 132 | // completed 133 | }) 134 | .once('error', function(error) { 135 | // error 136 | }); 137 | ``` 138 | 139 | ### Stats 140 | 141 | The stats command is used to retrieve statistics about the Ledger data file. It requires a Node style callback function that is called with either an error or the stats object. 142 | 143 | ```js 144 | ledger.stats(function(err, stats) { 145 | if (err) { return console.error(err); } 146 | 147 | // stats is a map (e.g. stats['Unique accounts'] = 13) 148 | }); 149 | ``` 150 | 151 | ### Version 152 | 153 | The version command is used to get the Ledger binary version. It requires a Node style callback function that is called with either an error or the version number as a string. 154 | 155 | ```js 156 | ledger.version(function(err, version) { 157 | if (err) { return console.error(err); } 158 | 159 | // version is a string (e.g. '3.0.0-20130529') 160 | }); 161 | ``` 162 | -------------------------------------------------------------------------------- /lib/cli-runner.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | spawn = require('child_process').spawn; 3 | 4 | var Cli = (function() { 5 | function Cli(command, options) { 6 | this.command = command; 7 | this.options = _.defaults({}, options, { debug: false }); 8 | } 9 | 10 | Cli.prototype.exec = function(args) { 11 | var process = this.spawn(args); 12 | 13 | if (this.options.debug) { 14 | this.logging(process); 15 | } 16 | 17 | return process; 18 | }; 19 | 20 | Cli.prototype.spawn = function(args) { 21 | this.log(this.command + ' ' + args.join(' ')); 22 | 23 | var process = spawn(this.command, args); 24 | 25 | process.stdout.setEncoding('utf8'); 26 | process.stderr.setEncoding('utf8'); 27 | 28 | return process; 29 | }; 30 | 31 | Cli.prototype.logging = function(process) { 32 | var log = this.log.bind(this); 33 | 34 | process.stdout.on('data', function(data) { log('stdout: ' + data); }); 35 | process.stderr.on('data', function(error) { log('stderr: '+ error); }); 36 | process.once('close', function(code) { log('child process exited with code ' + code); }); 37 | }; 38 | 39 | Cli.prototype.log = function(msg) { 40 | if (this.options.debug) { 41 | console.log(msg); 42 | } 43 | }; 44 | 45 | return Cli; 46 | })(); 47 | 48 | module.exports.Cli = Cli; -------------------------------------------------------------------------------- /lib/commands/accounts.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Transform = require('stream').Transform; 3 | 4 | var trim = function(str) { 5 | return str.replace(/^\s+|\s+$/g, ''); 6 | }; 7 | 8 | function AccountParser() { 9 | Transform.call(this, { objectMode: true }); 10 | this.buffer = ''; 11 | } 12 | 13 | util.inherits(AccountParser, Transform); 14 | 15 | AccountParser.prototype._transform = function (chunk, encoding, done) { 16 | chunk = this.buffer + chunk.toString(); 17 | var split = chunk.split('\n'); 18 | this.buffer = split.pop(); 19 | 20 | var self = this; 21 | split.forEach(function(line) { self.push(trim(line)); }); 22 | 23 | done(); 24 | }; 25 | 26 | AccountParser.prototype._flush = function (done) { 27 | if (this.buffer) { 28 | this.push(trim(this.buffer)); 29 | this.buffer = null; 30 | } 31 | done(); 32 | }; 33 | 34 | // The accounts command displays the list of accounts used in a Ledger file. 35 | module.exports.run = function(cli) { 36 | var process = cli.exec(['accounts']); 37 | 38 | return process.stdout 39 | .pipe(new AccountParser()); 40 | }; -------------------------------------------------------------------------------- /lib/commands/balance.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Transform = require('stream').Transform, 3 | csv = require('csv-streamify'), 4 | EscapeQuotes = require('../escape-quotes').EscapeQuotes, 5 | CommodityParser = require('../commodityParser').CommodityParser; 6 | 7 | function BalanceParser() { 8 | Transform.call(this, { objectMode: true }); 9 | } 10 | 11 | util.inherits(BalanceParser, Transform); 12 | 13 | BalanceParser.prototype._transform = function (chunk, encoding, done) { 14 | this.parse(chunk); 15 | done(); 16 | }; 17 | 18 | BalanceParser.prototype.parse = function(data) { 19 | try { 20 | var total = CommodityParser.parse(data[0].toString()); 21 | 22 | var balance = { 23 | total: total, 24 | account: { 25 | fullname: data[1], 26 | shortname: data[2], 27 | depth: parseInt(data[3], 10) 28 | } 29 | }; 30 | 31 | this.push(balance); 32 | } catch (ex) { 33 | this.emit('error', 'Failed to parse balance: ' + ex); 34 | } 35 | }; 36 | 37 | // `ledger balance` output format to allow parsing as a CSV string 38 | var format = '%(quoted(display_total)),%(quoted(account)),%(quoted(partial_account)),%(depth)\n%/'; 39 | 40 | // The balance command reports the current balance of all accounts. 41 | module.exports.run = function(cli, options) { 42 | var args = ['balance', '--format', format]; 43 | 44 | options = options || {}; 45 | if (options.collapse) { 46 | args.push('--collapse'); 47 | } 48 | 49 | if (options.market) { 50 | args.push('--market'); 51 | } 52 | 53 | var process = cli.exec(args); 54 | 55 | return process.stdout 56 | .pipe(new EscapeQuotes()) 57 | .pipe(csv({ objectMode: true })) 58 | .pipe(new BalanceParser()); 59 | }; 60 | -------------------------------------------------------------------------------- /lib/commands/print.js: -------------------------------------------------------------------------------- 1 | // The `ledger print` command returns a readable stream that outputs the Ledger file 'pretty printed' 2 | module.exports.run = function(cli) { 3 | // print Ledger transactions sorted by date 4 | var process = cli.exec(['print', '--sort', 'd']); 5 | 6 | return process.stdout; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/commands/register.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Transform = require('stream').Transform, 3 | csv = require('csv-streamify'), 4 | EscapeQuotes = require('../escape-quotes').EscapeQuotes, 5 | CommodityParser = require('../commodityParser').CommodityParser; 6 | 7 | function RegisterParser() { 8 | Transform.call(this, { objectMode: true }); 9 | this.current = null; 10 | } 11 | 12 | util.inherits(RegisterParser, Transform); 13 | 14 | RegisterParser.prototype._transform = function (chunk, encoding, done) { 15 | this.parse(chunk); 16 | done(); 17 | }; 18 | 19 | RegisterParser.prototype._flush = function(done) { 20 | this.emitCurrent(); 21 | done(); 22 | }; 23 | 24 | RegisterParser.prototype.parse = function(data) { 25 | try { 26 | if (data[0].length !== 0) { 27 | this.emitCurrent(); 28 | this.parseCurrent(data); 29 | } 30 | 31 | this.appendPosting(data); 32 | } catch (ex) { 33 | this.emit('error', 'Failed to parse balance: ' + ex); 34 | } 35 | }; 36 | 37 | RegisterParser.prototype.emitCurrent = function() { 38 | if (this.current !== null) { 39 | // emit completed record 40 | this.push(this.current); 41 | this.current = null; 42 | } 43 | }; 44 | 45 | RegisterParser.prototype.parseCurrent = function(data) { 46 | this.current = { 47 | date: this.toDate(data[0]), 48 | effectiveDate: this.toDate(data[1]), 49 | code: data[2], 50 | cleared: data[3] === 'true', 51 | pending: data[4] === 'true', 52 | payee: data[5], 53 | postings: [] 54 | }; 55 | }; 56 | 57 | RegisterParser.prototype.appendPosting = function(data) { 58 | var amount = CommodityParser.parse(data[7].toString()); 59 | 60 | this.current.postings.push({ 61 | account: data[6], 62 | commodity: amount 63 | }); 64 | }; 65 | 66 | RegisterParser.prototype.toDate = function(str) { 67 | if (str.length === 0) { 68 | return null; 69 | } 70 | 71 | var date = str.split('/'); 72 | 73 | return new Date(Date.UTC(date[0], parseInt(date[1], 10) - 1, parseInt(date[2], 10))); 74 | }; 75 | 76 | var initialFormat = [ 77 | '%(quoted(date))', 78 | '%(effective_date ? quoted(effective_date) : "")', 79 | '%(code ? quoted(code) : "")', 80 | '%(cleared ? "true" : "false")', 81 | '%(pending ? "true" : "false")', 82 | '%(quoted(payee))', 83 | '%(quoted(display_account))', 84 | '%(quoted(amount))' 85 | ]; 86 | var subsequentFormat = [ 87 | '%(quoted(display_account))', 88 | '%(quoted(amount))' 89 | ]; 90 | 91 | // The `ledger register` command displays all the postings occurring in a single account, line by line. 92 | module.exports.run = function(cli, opts) { 93 | var format = initialFormat.join(',') + '\n%/,,,,,,' + subsequentFormat.join(',') + '\n%/', 94 | args = ['register'], 95 | options = opts || {}; 96 | 97 | // Allow filtering by a given account name 98 | if (options.account) { 99 | args.push('^' + options.account); 100 | } 101 | 102 | args.push('--format'); 103 | args.push(format); 104 | 105 | var process = cli.exec(args); 106 | 107 | return process.stdout 108 | .pipe(new EscapeQuotes()) 109 | .pipe(csv({ objectMode: true })) 110 | .pipe(new RegisterParser()); 111 | }; -------------------------------------------------------------------------------- /lib/commands/stats.js: -------------------------------------------------------------------------------- 1 | module.exports.run = function(cli, callback) { 2 | var process = cli.exec(['stats']); 3 | 4 | var data = ''; 5 | var errored = false; 6 | 7 | process.stdout.on('data', function(chunk) { 8 | data += chunk; 9 | }); 10 | 11 | process.stdout.once('end', function() { 12 | if (errored) { 13 | return; 14 | } 15 | var stats = null, 16 | split = data.toString().split('\n'), 17 | files = data.match(/Files these postings came from:([^]*?)(\r?\n){2}/); 18 | 19 | split.forEach(function(el){ 20 | var prop = el.trim().match(/^(.*):[\s]+(.*)$/); 21 | if (prop) { 22 | if (stats === null) { 23 | stats = {}; 24 | } 25 | stats[prop[1]] = prop[2]; 26 | } 27 | }); 28 | 29 | if (files) { 30 | if (stats === null) { 31 | stats = {}; 32 | } 33 | 34 | // convert files[1] == paths capture to array and remove empty entries 35 | stats.files = files[1].split('\n').map(function(entry) { 36 | return entry.trim(); 37 | }).filter(Boolean); 38 | } 39 | 40 | if (stats !== null) { 41 | callback(null, stats); 42 | } else { 43 | callback('Failed to parse Ledger stats'); 44 | } 45 | }); 46 | 47 | process.stderr.once('data', function(error) { 48 | errored = true; 49 | callback(error); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /lib/commands/version.js: -------------------------------------------------------------------------------- 1 | // The version command reports the current installed Ledger version. 2 | module.exports.run = function(cli, callback) { 3 | var process = cli.exec(['--version']); 4 | 5 | process.stdout.once('data', function(data) { 6 | var matches = data.toString().match(/Ledger (.*),/); 7 | 8 | if (matches) { 9 | callback(null, matches[1]); 10 | } else { 11 | callback('Failed to match Ledger version'); 12 | } 13 | }); 14 | 15 | process.stderr.once('data', function(error) { 16 | callback(error); 17 | }); 18 | }; -------------------------------------------------------------------------------- /lib/commodityParser.js: -------------------------------------------------------------------------------- 1 | var CommodityParser = { 2 | // Parse an amount from a given string that looks like one of the following 3 | // cases: 4 | // £-1,000.00 5 | // 5 STOCKSYMBOL {USD200} 6 | // -900.00 CAD {USD1.1111111111} [13-Mar-19] 7 | parse: function(data) { 8 | // Strip out unneeded details. 9 | data = data.replace(/{.*}/g, ''); 10 | data = data.replace(/\[.*\]/g, ''); 11 | data = data.trim(); 12 | 13 | // Find the amount first. 14 | var amountMatch = data.match(/-?[0-9,.]+/); 15 | if (amountMatch == null) { 16 | throw ('Could not get amount from string: ' + data); 17 | } 18 | var amountString = amountMatch[0]; 19 | 20 | // Strip commas and parse amount as a float. 21 | var amount = parseFloat(amountString.replace(/,/g, '')); 22 | 23 | // Remove the amount from the data string, and use the rest as the currency. 24 | var currency = data.replace(amountString, '').trim(); 25 | 26 | return { 27 | currency: currency, 28 | amount: amount, 29 | formatted: data 30 | }; 31 | } 32 | }; 33 | 34 | module.exports.CommodityParser = CommodityParser; 35 | -------------------------------------------------------------------------------- /lib/escape-quotes.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Transform = require('stream').Transform; 3 | 4 | // Transform stream to replace \" with "" for valid CSV parsing 5 | function EscapeQuotes() { 6 | Transform.call(this); 7 | } 8 | 9 | util.inherits(EscapeQuotes, Transform); 10 | 11 | EscapeQuotes.prototype._transform = function (chunk, encoding, done) { 12 | this.push(chunk.toString().replace('\\"', '""')); 13 | done(); 14 | }; 15 | 16 | module.exports.EscapeQuotes = EscapeQuotes; -------------------------------------------------------------------------------- /lib/ledger.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Cli = require('./cli-runner').Cli, 3 | accounts = require('./commands/accounts'), 4 | balance = require('./commands/balance'), 5 | print = require('./commands/print'), 6 | register = require('./commands/register'), 7 | stats = require('./commands/stats'), 8 | version = require('./commands/version'); 9 | 10 | var Ledger = (function() { 11 | var config = { 12 | binary: '/usr/local/bin/ledger', 13 | debug: false 14 | }; 15 | 16 | function Ledger(options) { 17 | this.options = _.defaults({}, options, config); 18 | 19 | this.cli = new Cli(this.options.binary, { debug: this.options.debug }); 20 | } 21 | 22 | // version reports the current installed Ledger version. 23 | Ledger.prototype.version = function(callback) { 24 | version.run(this.cli, callback); 25 | }; 26 | 27 | // accounts reports the list of accounts. 28 | Ledger.prototype.accounts = function() { 29 | return accounts.run(this.withLedgerFile(this.cli)); 30 | }; 31 | 32 | // balance reports the current balance of all accounts. 33 | Ledger.prototype.balance = function(options) { 34 | return balance.run(this.withLedgerFile(this.cli), options); 35 | }; 36 | 37 | // register displays all the postings occurring in a single account. 38 | Ledger.prototype.register = function(options) { 39 | return register.run(this.withLedgerFile(this.cli), options); 40 | }; 41 | 42 | // print returns a readable stream that outputs the Ledger file 'pretty printed' 43 | Ledger.prototype.print = function() { 44 | return print.run(this.withLedgerFile(this.cli)); 45 | }; 46 | 47 | // stats returns statistics, like number of unique accounts 48 | Ledger.prototype.stats = function(callback) { 49 | stats.run(this.withLedgerFile(this.cli), callback); 50 | }; 51 | 52 | Ledger.prototype.withLedgerFile = function(cli) { 53 | var file = ['-f', this.options.file]; 54 | 55 | return { 56 | exec: function(args) { 57 | return cli.exec(file.concat(args || [])); 58 | } 59 | }; 60 | }; 61 | 62 | return Ledger; 63 | })(); 64 | 65 | module.exports.Ledger = Ledger; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ledger-cli", 3 | "homepage": "https://github.com/slashdotdash/node-ledger", 4 | "version": "0.3.0", 5 | "description": "API for the ledger command-line interface (ledger-cli.org).", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/slashdotdash/node-ledger.git" 9 | }, 10 | "keywords": [ 11 | "ledger", 12 | "accounting", 13 | "finance" 14 | ], 15 | "author": { 16 | "name": "Ben Smith", 17 | "email": "ben@10consulting.com" 18 | }, 19 | "license": "MIT", 20 | "main": "lib/ledger.js", 21 | "scripts": { 22 | "test": "grunt" 23 | }, 24 | "directories": { 25 | "lib": "lib" 26 | }, 27 | "devDependencies": { 28 | "chai": "^3.5.0", 29 | "grunt": "^1.0.1", 30 | "grunt-contrib-jshint": "^1.1.0", 31 | "grunt-contrib-watch": "^1.0.0", 32 | "grunt-mocha-test": "^0.13.2", 33 | "mocha": "^3.2.0" 34 | }, 35 | "dependencies": { 36 | "lodash": "^4.17.2", 37 | "csv-streamify": "^3.0.4" 38 | }, 39 | "readmeFilename": "README.md" 40 | } 41 | -------------------------------------------------------------------------------- /spec/accounts.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | expect = chai.expect, 3 | Ledger = require('../lib/ledger').Ledger; 4 | 5 | describe('Accounts', function() { 6 | var spec; 7 | var ledgerBinary = null; 8 | 9 | beforeEach(function() { 10 | spec = this; 11 | ledgerBinary = process.env.LEDGER_BIN || '/usr/local/bin/ledger'; 12 | }); 13 | 14 | describe('single transaction, multiple accounts', function() { 15 | var ledger, accounts; 16 | 17 | beforeEach(function(done) { 18 | ledger = new Ledger({ 19 | file: 'spec/data/single-transaction.dat', 20 | binary: ledgerBinary 21 | }); 22 | accounts = []; 23 | 24 | ledger.accounts() 25 | .on('data', function(account) { 26 | accounts.push(account); 27 | }) 28 | .once('error', function(error) { 29 | spec.fail(error); 30 | done(); 31 | }) 32 | .once('end', function(){ 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should return two accounts', function() { 38 | expect(accounts.length).to.equal(2); 39 | }); 40 | 41 | it('should return accounts listed alphabetically', function() { 42 | expect(accounts).to.eql([ 43 | 'Assets:Checking', 44 | 'Income:Salary' 45 | ]); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /spec/balance.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | expect = chai.expect, 3 | Ledger = require('../lib/ledger').Ledger; 4 | 5 | describe('Balance', function() { 6 | var spec; 7 | var ledgerBinary = null; 8 | 9 | beforeEach(function() { 10 | spec = this; 11 | ledgerBinary = process.env.LEDGER_BIN || '/usr/local/bin/ledger'; 12 | }); 13 | 14 | describe('single transaction', function() { 15 | var ledger, balances; 16 | 17 | beforeEach(function(done) { 18 | ledger = new Ledger({ 19 | file: 'spec/data/single-transaction.dat', 20 | binary: ledgerBinary 21 | }); 22 | balances = []; 23 | 24 | ledger.balance() 25 | .on('data', function(entry) { 26 | balances.push(entry); 27 | }) 28 | .once('end', function(){ 29 | done(); 30 | }) 31 | .on('error', function(error) { 32 | spec.fail(error); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should return balance for two accounts', function() { 38 | expect(balances.length).to.equal(2); 39 | }); 40 | 41 | it('should parse first balance', function() { 42 | expect(balances[0]).to.eql({ 43 | total: { 44 | currency: '£', 45 | amount: 1000, 46 | formatted: '£1,000.00' 47 | }, 48 | account: { 49 | fullname: 'Assets:Checking', 50 | shortname: 'Assets:Checking', 51 | depth: 2 52 | } 53 | }); 54 | }); 55 | 56 | it('should parse second balance', function() { 57 | expect(balances[1]).to.eql({ 58 | total: { 59 | currency: '£', 60 | amount: -1000, 61 | formatted: '£-1,000.00' 62 | }, 63 | account: { 64 | fullname: 'Income:Salary', 65 | shortname: 'Income:Salary', 66 | depth: 2 67 | } 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /spec/data/drewr.dat: -------------------------------------------------------------------------------- 1 | ; -*- ledger -*- 2 | 3 | = /^Income/ 4 | (Liabilities:Tithe) £0.12 5 | 6 | ~ Monthly 7 | Assets:Checking £500.00 8 | Income:Salary 9 | 10 | 2003/12/01 * Checking balance 11 | Assets:Checking £1000.00 12 | Equity:Opening Balances 13 | 14 | 2003/12/20 Organic Co-op 15 | Expenses:Food:Groceries £ 37.50 ; [=2004/01/01] 16 | Expenses:Food:Groceries £ 37.50 ; [=2004/02/01] 17 | Expenses:Food:Groceries £ 37.50 ; [=2004/03/01] 18 | Expenses:Food:Groceries £ 37.50 ; [=2004/04/01] 19 | Expenses:Food:Groceries £ 37.50 ; [=2004/05/01] 20 | Expenses:Food:Groceries £ 37.50 ; [=2004/06/01] 21 | Assets:Checking £ -225.00 22 | 23 | 2003/12/28=2004/01/01 Acme Mortgage 24 | Liabilities:Mortgage:Principal £ 200.00 25 | Expenses:Interest:Mortgage £ 500.00 26 | Expenses:Escrow £ 300.00 27 | Assets:Checking £-1,000.00 28 | 29 | 2004/01/02 Grocery Store 30 | Expenses:Food:Groceries £65.00 31 | Assets:Checking 32 | 33 | 2004/01/05 Employer 34 | Assets:Checking £2000.00 35 | Income:Salary 36 | 37 | 2004/01/14 Bank 38 | ; Regular monthly savings transfer 39 | Assets:Savings £ 300.00 40 | Assets:Checking 41 | 42 | 2004/01/19 Grocery Store 43 | Expenses:Food:Groceries £ 44.00 44 | Assets:Checking 45 | 46 | 2004/01/25 Bank 47 | ; Transfer to cover car purchase 48 | Assets:Checking £ 5,500.00 49 | Assets:Savings 50 | ; :nobudget: 51 | 52 | 2004/01/25 Tom’s Used Cars 53 | Expenses:Auto £ 5,500.00 54 | ; :nobudget: 55 | Assets:Checking 56 | 57 | 2004/01/27 Book Store 58 | Expenses:Books £20.00 59 | Liabilities:MasterCard 60 | 61 | 2004/02/01 Sale 62 | Assets:Checking:Business £30.00 63 | Income:Sales -------------------------------------------------------------------------------- /spec/data/foreign-currency-transaction.dat: -------------------------------------------------------------------------------- 1 | 2013/03/19 My Employer 2 | Assets:Checking 50 STOCKSYMBOL {200 USD} @@ 10,000.00 USD 3 | Income:Salary -9,000.00 CAD @@ 10,000.00 USD 4 | -------------------------------------------------------------------------------- /spec/data/quoted-transaction.dat: -------------------------------------------------------------------------------- 1 | 2013/07/08 Payee with double " quote 2 | Assets:Checking £1,000.00 3 | Income:Salary £-1,000.00 -------------------------------------------------------------------------------- /spec/data/single-transaction.dat: -------------------------------------------------------------------------------- 1 | 2013/03/19 My Employer 2 | Assets:Checking £1,000.00 3 | Income:Salary £-1,000.00 -------------------------------------------------------------------------------- /spec/register.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | expect = chai.expect, 3 | Ledger = require('../lib/ledger').Ledger; 4 | 5 | describe('Register', function() { 6 | var spec; 7 | var ledgerBinary = null; 8 | 9 | beforeEach(function() { 10 | spec = this; 11 | ledgerBinary = process.env.LEDGER_BIN || '/usr/local/bin/ledger'; 12 | }); 13 | 14 | describe('single transaction', function() { 15 | var ledger, entries; 16 | 17 | beforeEach(function(done) { 18 | ledger = new Ledger({ 19 | file: 'spec/data/single-transaction.dat', 20 | binary: ledgerBinary 21 | }); 22 | entries = []; 23 | 24 | ledger.register() 25 | .on('data', function(entry) { 26 | entries.push(entry); 27 | }) 28 | .once('end', function(){ 29 | done(); 30 | }) 31 | .once('error', function(error) { 32 | spec.fail(error); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should return entry for single transaction', function() { 38 | expect(entries.length).to.equal(1); 39 | }); 40 | 41 | it('should parse transaction', function() { 42 | var transaction = entries[0]; 43 | expect(transaction.date - new Date(2013, 2, 19)).to.equal(0); 44 | expect(transaction.payee).to.equal('My Employer'); 45 | }); 46 | 47 | it('should parse the first posting', function() { 48 | var posting = entries[0].postings[0]; 49 | expect(posting).to.eql({ 50 | commodity: { 51 | currency: '£', 52 | amount: 1000, 53 | formatted: '£1,000.00' 54 | }, 55 | account: 'Assets:Checking' 56 | }); 57 | }); 58 | 59 | it('should parse the second posting', function() { 60 | var posting = entries[0].postings[1]; 61 | expect(posting).to.eql({ 62 | commodity: { 63 | currency: '£', 64 | amount: -1000, 65 | formatted: '£-1,000.00' 66 | }, 67 | account: 'Income:Salary' 68 | }); 69 | }); 70 | }); 71 | 72 | describe('filtering by account', function() { 73 | var ledger, entries; 74 | 75 | beforeEach(function(done) { 76 | ledger = new Ledger({ 77 | file: 'spec/data/drewr.dat', 78 | binary: ledgerBinary 79 | }); 80 | entries = []; 81 | 82 | ledger.register({account: 'Income'}) 83 | .on('data', function(entry) { 84 | entries.push(entry); 85 | }) 86 | .once('end', function(){ 87 | done(); 88 | }) 89 | .once('error', function(error) { 90 | spec.fail(error); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('should return entries for two matching transactions', function() { 96 | expect(entries.length).to.equal(2); 97 | }); 98 | 99 | it('should parse first transaction', function() { 100 | var transaction = entries[0]; 101 | expect(transaction.date - new Date(2004, 0, 5)).to.equal(0); 102 | expect(transaction.payee).to.equal('Employer'); 103 | expect(transaction.postings.length).to.equal(1); 104 | }); 105 | 106 | it('should parse second transaction', function() { 107 | var transaction = entries[1]; 108 | expect(transaction.date - new Date(2004, 1, 1)).to.equal(0); 109 | expect(transaction.payee).to.equal('Sale'); 110 | expect(transaction.postings.length).to.equal(1); 111 | }); 112 | }); 113 | 114 | // Handle transactions where the payee contains a double quote (') 115 | describe('quoted transaction', function() { 116 | var ledger, entries; 117 | 118 | beforeEach(function(done) { 119 | ledger = new Ledger({ 120 | file: 'spec/data/quoted-transaction.dat', 121 | binary: ledgerBinary 122 | }); 123 | entries = []; 124 | 125 | ledger.register() 126 | .on('data', function(entry) { 127 | entries.push(entry); 128 | }) 129 | .once('end', function(){ 130 | done(); 131 | }) 132 | .once('error', function(error) { 133 | spec.fail(error); 134 | done(); 135 | }); 136 | }); 137 | 138 | it('should return entry for single transaction', function() { 139 | expect(entries.length).to.equal(1); 140 | }); 141 | }); 142 | 143 | describe('foreign currency transaction', function() { 144 | var ledger, entries; 145 | 146 | beforeEach(function(done) { 147 | ledger = new Ledger({ 148 | file: 'spec/data/foreign-currency-transaction.dat', 149 | binary: ledgerBinary 150 | }); 151 | entries = []; 152 | 153 | ledger.register() 154 | .on('data', function(entry) { 155 | entries.push(entry); 156 | }) 157 | .once('end', function(){ 158 | done(); 159 | }) 160 | .once('error', function(error) { 161 | spec.fail(error); 162 | done(); 163 | }); 164 | }); 165 | 166 | it('should return single entry for transaction', function() { 167 | expect(entries.length).to.equal(1); 168 | }); 169 | 170 | it('should parse currency correctly', function() { 171 | var transaction = entries[0]; 172 | var firstPosting = transaction.postings[0]; 173 | var secondPosting = transaction.postings[1]; 174 | 175 | expect(firstPosting.commodity.currency).to.equal('STOCKSYMBOL'); 176 | expect(firstPosting.commodity.amount).to.equal(50); 177 | 178 | expect(secondPosting.commodity.currency).to.equal('CAD'); 179 | expect(secondPosting.commodity.amount).to.equal(-9000); 180 | }); 181 | 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /spec/version.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | expect = chai.expect, 3 | Ledger = require('../lib/ledger').Ledger; 4 | 5 | describe('Ledger', function() { 6 | var ledger, spec; 7 | 8 | beforeEach(function() { 9 | ledger = new Ledger({ 10 | binary: process.env.LEDGER_BIN || '/usr/local/bin/ledger' 11 | }); 12 | spec = this; 13 | }); 14 | 15 | it('should return installed ledger-cli version', function(done) { 16 | ledger.version(function(err, version) { 17 | if (err) { return spec.fail(err); } 18 | 19 | expect(version.substr(0, 5)).to.match(/3\.[0-9]+\.[0-9]+/); 20 | done(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /terminology.md: -------------------------------------------------------------------------------- 1 | # Terminology 2 | 3 | ## Journal 4 | 5 | Any file that contains Ledger data is called a “journal”. A journal mostly contains “transactions”, but it can also contain control directives, comments, and a few other things. 6 | 7 | ## Transaction 8 | 9 | A transaction represents something that you want to enter into your journal. For example, if you just cashed a paycheck from your employer by depositing it into your bank account, this entire action is called an “transaction”. The total cost of every transaction must balance to zero, otherwise Ledger will refuse to read your journal file any further. This guarantee that no amounts can be lost due to balancing errors. 10 | 11 | ## Posting 12 | 13 | Each transaction is made up of two or more postings (there is a special case that allows for just one, using virtual postings). 14 | 15 | ## Account 16 | 17 | An account is any place that accumulates quantities, of any meaning. They can be named anything, the names can even contain spaces, and they can mean whatever you want. Students of accounting will use five top-level names: Equity, Assets, Liabilities, Expenses, Income. All other accounts are specified as children of these accounts. This is not required by Ledger, however, nor does it even know anything about what these names mean. That’s all left up to the user. --------------------------------------------------------------------------------