├── .gitignore ├── TODO.md ├── lib ├── filters │ ├── transactions │ │ ├── index.js │ │ ├── overwrite-ledger.js │ │ ├── format-ledger.js │ │ ├── prepend-accounts.js │ │ ├── parse-csv.js │ │ ├── retrieve-accounts.js │ │ ├── write-ledger.js │ │ └── parse-transactions.js │ └── transaction │ │ ├── index.js │ │ ├── extract-payee.js │ │ ├── extract-date.js │ │ ├── match-existing-transaction.js │ │ ├── parse-ledger.js │ │ ├── classify-account.js │ │ ├── format-transaction.js │ │ ├── extract-amount.js │ │ └── confirm-transaction.js └── import.js ├── spec ├── filters │ └── transaction │ │ ├── parse-ledger.spec.js │ │ ├── extract-payee.spec.js │ │ ├── extract-date.spec.js │ │ ├── format-transaction.spec.js │ │ ├── extract-amount.spec.js │ │ └── classify-account.spec.js ├── import.spec.js └── data │ └── example.dat ├── package.json ├── Gruntfile.js ├── README.md └── bin └── ledger-import /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Append new transactions to ledger `.dat` file and sort by date (using `print --sort d`). 4 | 5 | ledger -f input.dat print --sort d > output.dat 6 | 7 | Include existing accounts 8 | 9 | ledger -f input.dat accounts > accounts.dat -------------------------------------------------------------------------------- /lib/filters/transactions/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | formatLedger: require('./format-ledger'), 3 | overwriteLedger: require('./overwrite-ledger'), 4 | parseCSV: require('./parse-csv'), 5 | parseTransactions: require('./parse-transactions'), 6 | prependAccounts: require('./prepend-accounts'), 7 | retrieveAccounts: require('./retrieve-accounts'), 8 | writeLedger: require('./write-ledger') 9 | }; -------------------------------------------------------------------------------- /lib/filters/transaction/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | classifyAccount: require('./classify-account'), 3 | confirmTransaction: require('./confirm-transaction'), 4 | extractAmount: require('./extract-amount'), 5 | extractDate: require('./extract-date'), 6 | extractPayee: require('./extract-payee'), 7 | formatTransaction: require('./format-transaction'), 8 | matchExistingTransaction: require('./match-existing-transaction'), 9 | parseLedger: require('./parse-ledger') 10 | }; -------------------------------------------------------------------------------- /lib/filters/transaction/extract-payee.js: -------------------------------------------------------------------------------- 1 | var trim = function(str) { 2 | return str.replace(/^[^\w+]/g, ''); 3 | }; 4 | 5 | var extractPayee = function(options) { 6 | if (!options.column) { 7 | throw { 8 | name: 'OptionsError', 9 | message: 'Payee column has not been specified' 10 | }; 11 | } 12 | 13 | return function(input, next) { 14 | var payee = input.data[options.column - 1]; 15 | 16 | if (payee && payee.length > 0) { 17 | input.payee = trim(payee); 18 | 19 | next(null, input); 20 | } else { 21 | next('Failed to extract payee'); 22 | } 23 | }; 24 | }; 25 | 26 | module.exports = extractPayee; -------------------------------------------------------------------------------- /lib/filters/transactions/overwrite-ledger.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var overwriteLedger = function(options) { 4 | if (!options.ledger) { 5 | throw { 6 | name: 'OptionsError', 7 | message: 'Ledger file has not been specified' 8 | }; 9 | } 10 | 11 | return function(input, next) { 12 | var reader = fs.createReadStream(input.outputPath), 13 | writer = fs.createWriteStream(options.ledger); 14 | 15 | reader.pipe(writer); 16 | 17 | writer.once('error', function(err) { 18 | next(err); 19 | }); 20 | 21 | writer.once('finish', function() { 22 | next(null, input); 23 | }); 24 | }; 25 | }; 26 | 27 | module.exports = overwriteLedger; -------------------------------------------------------------------------------- /lib/filters/transactions/format-ledger.js: -------------------------------------------------------------------------------- 1 | var temp = require('temp'), 2 | Ledger = require('ledger-cli').Ledger; 3 | 4 | var formatLedger = function(options) { 5 | return function(input, next) { 6 | var path = input.outputPath, 7 | ledger = new Ledger({ file: path, binary: options.binary }), 8 | writer = temp.createWriteStream(); 9 | 10 | // pretty print the Ledger file 11 | ledger.print().pipe(writer); 12 | 13 | writer.once('error', function(err) { 14 | next(err); 15 | }); 16 | 17 | writer.once('finish', function() { 18 | input.outputPath = writer.path; 19 | 20 | next(null, input); 21 | }); 22 | }; 23 | }; 24 | 25 | module.exports = formatLedger; -------------------------------------------------------------------------------- /spec/filters/transaction/parse-ledger.spec.js: -------------------------------------------------------------------------------- 1 | var parseLedger = require('../../../lib/filters/transaction/parse-ledger'); 2 | 3 | describe('parse Ledger file', function() { 4 | var spec, output; 5 | 6 | beforeEach(function() { 7 | spec = this; 8 | 9 | this.createFilter = function(options, input, done) { 10 | var filter = parseLedger(options); 11 | 12 | filter(input, function(err, result) { 13 | if (err) { 14 | spec.fail(err); 15 | return done(); 16 | } 17 | 18 | output = result; 19 | done(); 20 | }); 21 | }; 22 | }); 23 | 24 | describe('file exists', function() { 25 | beforeEach(function(done) { 26 | this.createFilter({ ledger: 'spec/data/example.dat' }, { }, done); 27 | }); 28 | 29 | it('should parse transactions', function() { 30 | expect(output.transactions.length).toNotBe(0); 31 | }); 32 | }); 33 | }); -------------------------------------------------------------------------------- /lib/filters/transaction/extract-date.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | moment = require('moment'); 3 | 4 | var extractDate = function(options) { 5 | options = _.defaults(options, { format: 'DD/MM/YYYY' }); 6 | 7 | if (!options.column) { 8 | throw { 9 | name: 'OptionsError', 10 | message: 'Date column has not been specified' 11 | }; 12 | } 13 | 14 | return function(input, next) { 15 | var date = input.data[options.column - 1]; 16 | 17 | try { 18 | date = moment(date, options.format, true); 19 | 20 | if (date.isValid()) { 21 | input.date = date.toDate(); 22 | next(null, input); 23 | } else { 24 | next('Failed to parse date "' + date + '" (using format "' + options.format + '").'); 25 | } 26 | } catch(e) { 27 | next('Failed to parse date "' + date + '" (using format "' + options.format + '"). ' + e); 28 | } 29 | }; 30 | }; 31 | 32 | module.exports = extractDate; -------------------------------------------------------------------------------- /lib/filters/transactions/prepend-accounts.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | temp = require('temp'), 3 | _ = require('lodash'); 4 | 5 | var formatAccounts = function(accounts) { 6 | return _.map(accounts, function(account) { return 'account ' + account; }).join('\n'); 7 | }; 8 | 9 | var prependAccounts = function() { 10 | return function(input, next) { 11 | var reader = fs.createReadStream(input.outputPath, { flags: 'r', encoding: 'utf-8' }), 12 | writer = temp.createWriteStream(); 13 | 14 | // write list of accounts (e.g. "account Assets:Current Account") 15 | writer.write(formatAccounts(input.accounts)); 16 | writer.write('\n\n'); 17 | 18 | // write contents of (formatted) Ledger 19 | reader.pipe(writer); 20 | 21 | writer.once('error', function(err) { 22 | next(err); 23 | }); 24 | 25 | writer.once('finish', function() { 26 | input.outputPath = writer.path; 27 | 28 | next(null, input); 29 | }); 30 | }; 31 | }; 32 | 33 | module.exports = prependAccounts; -------------------------------------------------------------------------------- /lib/filters/transactions/parse-csv.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var trim = function(str) { 4 | return str.replace(/^\s+|\s+$/g, ''); 5 | }; 6 | 7 | var parseCSV = function(options) { 8 | return function(input, next) { 9 | var transactions = [], 10 | count = 0; 11 | 12 | input.csv.on('record', function(row, index) { 13 | // trim each column 14 | row = _.map(row, trim); 15 | 16 | // skip empty rows 17 | if (row.length === 0 || _.all(row, function(column) { return column.length === 0; })) { 18 | return; 19 | } 20 | 21 | // skip header 22 | if (options.header && count === 0) { 23 | count++; 24 | return; 25 | } 26 | 27 | // push the CSV row onto the transactions list 28 | transactions.push({ data: row, index: index }); 29 | }) 30 | .once('end', function(count) { 31 | input.transactions = transactions; 32 | input.count = count; 33 | 34 | next(null, input); 35 | }) 36 | .on('error', function(error) { 37 | next(error); 38 | }); 39 | }; 40 | }; 41 | 42 | module.exports = parseCSV; -------------------------------------------------------------------------------- /lib/filters/transactions/retrieve-accounts.js: -------------------------------------------------------------------------------- 1 | var Ledger = require('ledger-cli').Ledger; 2 | 3 | var parseLedgerAccounts = function(ledger, done) { 4 | var accounts = []; 5 | 6 | ledger.accounts() 7 | .on('data', function(entry) { 8 | accounts.push(entry); 9 | }) 10 | .once('end', function() { 11 | // completed 12 | done(null, accounts); 13 | }) 14 | .on('error', function(error) { 15 | // error 16 | done(error); 17 | }); 18 | }; 19 | 20 | var retrieveAccounts = function(options) { 21 | if (!options.ledger) { 22 | throw { 23 | name: 'OptionsError', 24 | message: 'Ledger file has not been specified' 25 | }; 26 | } 27 | 28 | var ledger = new Ledger({ file: options.ledger, binary: options.binary }); 29 | 30 | return function(input, next) { 31 | parseLedgerAccounts(ledger, function(err, accounts) { 32 | if (err) { 33 | return next('Failed to parse accounts in Ledger file "' + options.ledger + '". ' + err); 34 | } 35 | 36 | input.accounts = accounts; 37 | 38 | next(null, input); 39 | }); 40 | }; 41 | }; 42 | 43 | module.exports = retrieveAccounts; -------------------------------------------------------------------------------- /lib/filters/transaction/match-existing-transaction.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | moment = require('moment'); 3 | 4 | var formatPayee = function(payee) { 5 | return payee.trim().replace('\'', ''); 6 | }; 7 | 8 | var mapToDateAndPayee = function(transactions) { 9 | return _.map(transactions, function(transaction) { 10 | return { 11 | date: moment(new Date(transaction.date.getFullYear(), transaction.date.getMonth(), transaction.date.getDate())), 12 | payee: formatPayee(transaction.payee) 13 | }; 14 | }); 15 | }; 16 | 17 | var matchExistingTransaction = function() { 18 | var transactions; 19 | 20 | return function(input, next) { 21 | var date = moment(new Date(input.date.getFullYear(), input.date.getMonth(), input.date.getDate())), 22 | payee = formatPayee(input.payee); 23 | 24 | if (!transactions) { 25 | transactions = mapToDateAndPayee(input.transactions); 26 | } 27 | 28 | var exists = _.any(transactions, function(transaction) { 29 | return payee === transaction.payee && transaction.date.isSame(date); 30 | }); 31 | 32 | input.exists = exists; 33 | 34 | next(null, input); 35 | }; 36 | }; 37 | 38 | module.exports = matchExistingTransaction; -------------------------------------------------------------------------------- /spec/import.spec.js: -------------------------------------------------------------------------------- 1 | var csv = require('csv'), 2 | Import = require('../lib/import').Import; 3 | 4 | xdescribe('Import', function() { 5 | var spec, importer, transactions; 6 | 7 | beforeEach(function(done) { 8 | spec = this; 9 | importer = new Import({ 10 | ledger: 'spec/data/example.dat', 11 | log: true, 12 | header: false, 13 | separator: ',', 14 | currency: '£', 15 | account: 'Liabilities:Credit Card', 16 | date: { 17 | column: 1, 18 | format: 'DD MMM YY' 19 | }, 20 | payee: { 21 | column: 2 22 | }, 23 | amount: { 24 | columns: [ 5, 6 ], 25 | inverse: true 26 | } 27 | }); 28 | transactions = []; 29 | 30 | importer.on('transaction', function(transaction) { 31 | transactions.push(transaction); 32 | }) 33 | .once('end', function() { 34 | done(); 35 | }) 36 | .once('error', function(error) { 37 | spec.fail(error); 38 | done(); 39 | }); 40 | 41 | importer.run(csv().from.string('14 Dec 13,"Payee",MR B SMITH,Household,,1.99')); 42 | }); 43 | 44 | it('should parse a single transaction', function() { 45 | expect(transactions.length).toBe(1); 46 | }); 47 | }); -------------------------------------------------------------------------------- /spec/filters/transaction/extract-payee.spec.js: -------------------------------------------------------------------------------- 1 | var extractPayee = require('../../../lib/filters/transaction/extract-payee'); 2 | 3 | describe('extract payee', function() { 4 | var spec, filter, output; 5 | 6 | beforeEach(function() { 7 | spec = this; 8 | filter = extractPayee({ column: 1 }); 9 | }); 10 | 11 | describe('valid payee', function() { 12 | beforeEach(function(done) { 13 | filter({ data: [ 'Mortgage' ] }, function(err, result) { 14 | if (err) { 15 | spec.fail(err); 16 | return done(); 17 | } 18 | 19 | output = result; 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should parse', function() { 25 | expect(output.payee).toEqual('Mortgage'); 26 | }); 27 | }); 28 | 29 | describe('invalid payee', function() { 30 | var error, output; 31 | 32 | beforeEach(function(done) { 33 | filter({ data: [ '' ] }, function(err, result) { 34 | error = err; 35 | output = result; 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should fail to parse', function() { 41 | expect(error).toNotBe(null); 42 | }); 43 | 44 | it('should not output any data', function() { 45 | expect(output).toBeUndefined(null); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ledger-import", 3 | "homepage": "https://github.com/slashdotdash/node-ledger-import", 4 | "version": "0.0.6", 5 | "description": "Import accounting transactions from CSV files to Ledger format.", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/slashdotdash/node-ledger-import.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/import.js", 21 | "bin": { 22 | "ledger-import": "bin/ledger-import" 23 | }, 24 | "scripts": {}, 25 | "directories": { 26 | "lib": "lib" 27 | }, 28 | "devDependencies": { 29 | "grunt": "~0.4.2", 30 | "grunt-contrib-jshint": "~0.8.0", 31 | "grunt-contrib-watch": "~0.5.3", 32 | "grunt-contrib-jasmine-node": "~0.1.1", 33 | "jasmine-node": "~1.13.0" 34 | }, 35 | "dependencies": { 36 | "classifier": "~0.1.0", 37 | "commander": "2.1.0", 38 | "csv": "~0.3.7", 39 | "ledger-cli": "0.0.11", 40 | "lodash": "~2.4.1", 41 | "moment": "~2.5.1", 42 | "pipes-and-filters": "~0.0.4", 43 | "prompt": "~0.2.12", 44 | "temp": "~0.6.0" 45 | }, 46 | "readmeFilename": "README.md" 47 | } 48 | -------------------------------------------------------------------------------- /spec/filters/transaction/extract-date.spec.js: -------------------------------------------------------------------------------- 1 | var extractDate = require('../../../lib/filters/transaction/extract-date'); 2 | 3 | describe('extract date', function() { 4 | var spec, filter, output; 5 | 6 | beforeEach(function() { 7 | spec = this; 8 | filter = extractDate({ format: 'DD MMM YY', column: 1 }); 9 | }); 10 | 11 | describe('valid date', function() { 12 | beforeEach(function(done) { 13 | filter({ data: [ '14 Dec 13' ] }, function(err, result) { 14 | if (err) { 15 | spec.fail(err); 16 | return done(); 17 | } 18 | 19 | output = result; 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should parse the date given a date format string', function() { 25 | expect(output.date).toEqual(new Date(2013, 11, 14)); 26 | }); 27 | }); 28 | 29 | describe('invalid date', function() { 30 | var error, output; 31 | 32 | beforeEach(function(done) { 33 | filter({ data: [ 'invalid' ] }, function(err, result) { 34 | error = err; 35 | output = result; 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should fail to parse an invalid date', function() { 41 | expect(error).toNotBe(null); 42 | }); 43 | 44 | it('should not output any data', function() { 45 | expect(output).toBeUndefined(null); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /lib/filters/transaction/parse-ledger.js: -------------------------------------------------------------------------------- 1 | var Ledger = require('ledger-cli').Ledger; 2 | 3 | var parseLedgerTransactions = function(ledger, done) { 4 | var transactions = []; 5 | 6 | ledger.register() 7 | .on('data', function(entry) { 8 | transactions.push(entry); 9 | }) 10 | .once('end', function(){ 11 | // completed 12 | done(null, transactions); 13 | }) 14 | .on('error', function(error) { 15 | // error 16 | done(error); 17 | }); 18 | }; 19 | 20 | var parseLedger = function(options) { 21 | if (!options.ledger) { 22 | throw { 23 | name: 'OptionsError', 24 | message: 'Ledger file has not been specified' 25 | }; 26 | } 27 | 28 | var ledger = new Ledger({ file: options.ledger, binary: options.binary }), 29 | cached; 30 | 31 | return function(input, next) { 32 | if (cached) { 33 | input.transactions = cached; 34 | 35 | return next(null, input); 36 | } 37 | 38 | parseLedgerTransactions(ledger, function(err, transactions) { 39 | if (err) { 40 | return next('Failed to parse transactions in Ledger file "' + options.ledger + '". ' + err); 41 | } 42 | 43 | // cache transactions for subsequent calls to this filter 44 | cached = transactions; 45 | 46 | input.transactions = transactions; 47 | 48 | next(null, input); 49 | }); 50 | }; 51 | }; 52 | 53 | module.exports = parseLedger; -------------------------------------------------------------------------------- /lib/filters/transaction/classify-account.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Bayesian = require('classifier').Bayesian; 3 | 4 | var trainClassifier = function(classifier, transactions, account) { 5 | _.each(transactions, function(entry) { 6 | var payee = entry.payee; 7 | 8 | var posting = _.find(entry.postings, function(p) { 9 | return (p.account !== account); 10 | }); 11 | 12 | if (posting) { 13 | classifier.train(payee, posting.account); 14 | } 15 | }); 16 | }; 17 | 18 | // Use Naive Bayes classification to match account from payee name 19 | var classifyAccount = function(options) { 20 | options = _.defaults(options, { threshold: 0.5 }); 21 | 22 | if (!options.account) { 23 | throw { 24 | name: 'OptionsError', 25 | message: 'Account has not been specified' 26 | }; 27 | } 28 | 29 | var classifier = new Bayesian(), 30 | trained = false; 31 | 32 | return function(input, next) { 33 | if (!trained) { 34 | trainClassifier(classifier, input.transactions, options.account); 35 | trained = true; 36 | } 37 | 38 | var payee = input.payee, 39 | classification = classifier.classify(payee); 40 | 41 | if (classification) { 42 | input.account = classification; 43 | } else { 44 | input.account = null; 45 | input.warning = 'Failed to classify payee "' + payee + '"'; 46 | } 47 | 48 | next(null, input); 49 | }; 50 | }; 51 | 52 | module.exports = classifyAccount; -------------------------------------------------------------------------------- /lib/filters/transactions/write-ledger.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | temp = require('temp'), 3 | _ = require('lodash'); 4 | 5 | // format the transactions for output to a Ledger .dat file 6 | var format = function(transactions) { 7 | return _.map(transactions, function(transaction) { 8 | return transaction.formatted; 9 | }).join('\n\n'); 10 | }; 11 | 12 | var writeLedger = function(options) { 13 | if (!options.ledger) { 14 | throw { 15 | name: 'OptionsError', 16 | message: 'Ledger output file has not been specified' 17 | }; 18 | } 19 | 20 | // automatically track and cleanup files at exit 21 | temp.track(); 22 | 23 | return function(input, next) { 24 | temp.open('ledger', function(err, tmp) { 25 | if (err) { 26 | return next(err); 27 | } 28 | 29 | var reader = fs.createReadStream(options.ledger, { flags: 'r', encoding: 'utf-8' }), 30 | writer = fs.createWriteStream(tmp.path, { flags: 'w', encoding: 'utf-8' }); 31 | 32 | reader.pipe(writer, { end: false }); 33 | 34 | // append transactions after pipe'ing source Ledger file to tmp output file 35 | reader.once('end', function() { 36 | writer.write('\n' + format(input.transactions)); 37 | writer.end(); 38 | }); 39 | 40 | writer.once('error', function(err) { 41 | next(err); 42 | }); 43 | 44 | writer.once('finish', function() { 45 | input.outputPath = tmp.path; 46 | 47 | next(null, input); 48 | }); 49 | }); 50 | }; 51 | }; 52 | 53 | module.exports = writeLedger; -------------------------------------------------------------------------------- /lib/filters/transaction/format-transaction.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | 3 | var numberWithSeparator = function(amount, separator) { 4 | var regex = /(\d+)(\d{3})/; 5 | 6 | return amount.toString().replace(/^\d+/, function(w) { 7 | while (regex.test(w)) { 8 | w = w.replace(regex, '$1' + separator + '$2'); 9 | } 10 | return w; 11 | }); 12 | }; 13 | 14 | var formatMoney = function(amount, currency) { 15 | return currency + numberWithSeparator(amount.toFixed(2), ','); 16 | }; 17 | 18 | var formatTransaction = function(options) { 19 | if (!options.account) { 20 | throw { 21 | name: 'OptionsError', 22 | message: 'Account has not been specified' 23 | }; 24 | } 25 | 26 | if (!options.currency) { 27 | throw { 28 | name: 'OptionsError', 29 | message: 'Currency has not been specified' 30 | }; 31 | } 32 | 33 | return function(input, next) { 34 | var destination, source, amount; 35 | 36 | if (input.amount > 0) { 37 | destination = options.account; 38 | source = input.account; 39 | amount = input.amount; 40 | } else { 41 | destination = input.account; 42 | source = options.account; 43 | amount = -1 * input.amount; 44 | } 45 | 46 | var formatted = [ 47 | moment(input.date).format('YYYY/MM/DD') + ' ' + input.payee, 48 | '\t' + destination + '\t\t' + formatMoney(amount, options.currency), 49 | '\t' + source 50 | ].join('\n'); 51 | 52 | input.formatted = formatted; 53 | 54 | next(null, input); 55 | }; 56 | }; 57 | 58 | module.exports = formatTransaction; -------------------------------------------------------------------------------- /spec/filters/transaction/format-transaction.spec.js: -------------------------------------------------------------------------------- 1 | var formatTransaction = require('../../../lib/filters/transaction/format-transaction'); 2 | 3 | describe('format transaction', function() { 4 | var spec, filter, output; 5 | 6 | beforeEach(function() { 7 | spec = this; 8 | filter = formatTransaction({ account: 'Assets:Current Account', currency: '£' }); 9 | }); 10 | 11 | describe('from source account', function() { 12 | beforeEach(function(done) { 13 | filter({ date: new Date(2014, 0, 20), payee: 'Mortgage payment', account: 'Expenses:Mortgage', amount: -1000 }, function(err, result) { 14 | if (err) { 15 | spec.fail(err); 16 | return done(); 17 | } 18 | 19 | output = result; 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should format the transaction', function() { 25 | expect(output.formatted).toEqual([ 26 | '2014/01/20 Mortgage payment', 27 | '\tExpenses:Mortgage\t\t£1,000.00', 28 | '\tAssets:Current Account' 29 | ].join('\n')); 30 | }); 31 | }); 32 | 33 | describe('to source account', function() { 34 | beforeEach(function(done) { 35 | filter({ date: new Date(2014, 0, 20), payee: 'Salary', account: 'Income:Salary', amount: 1234.56 }, function(err, result) { 36 | if (err) { 37 | spec.fail(err); 38 | return done(); 39 | } 40 | 41 | output = result; 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should format the transaction', function() { 47 | expect(output.formatted).toEqual([ 48 | '2014/01/20 Salary', 49 | '\tAssets:Current Account\t\t£1,234.56', 50 | '\tIncome:Salary' 51 | ].join('\n')); 52 | }); 53 | }); 54 | }); -------------------------------------------------------------------------------- /lib/import.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | EventEmitter = require('events').EventEmitter, 3 | Pipeline = require('pipes-and-filters'), 4 | _ = require('lodash'), 5 | filters = require('./filters/transactions'); 6 | 7 | var Import = (function() { 8 | function Import(options) { 9 | this.options = _.defaults({}, options); 10 | 11 | EventEmitter.call(this); 12 | } 13 | 14 | util.inherits(Import, EventEmitter); 15 | 16 | // Execute the import from the given `csv` 17 | Import.prototype.run = function(csv) { 18 | var self = this, 19 | pipeline = Pipeline.create('input'); 20 | 21 | // parsing CSV and transactions 22 | pipeline.use(filters.parseCSV(_.pick(this.options, 'header'))); 23 | pipeline.use(filters.parseTransactions(_.merge(this.options, { emitter: this }))); 24 | 25 | // exit early if there are no transactions 26 | pipeline.breakIf(function(input) { 27 | return input.transactions.length === 0; 28 | }); 29 | 30 | // output new transactions to destination Ledger file 31 | pipeline.use(filters.retrieveAccounts(_.pick(this.options, 'ledger', 'binary'))); 32 | pipeline.use(filters.writeLedger(_.pick(this.options, 'ledger'))); 33 | pipeline.use(filters.formatLedger(_.pick(this.options, 'ledger', 'binary'))); 34 | pipeline.use(filters.prependAccounts()); 35 | pipeline.use(filters.overwriteLedger(_.pick(this.options, 'ledger'))); 36 | 37 | pipeline.once('break', function() { 38 | self.emit('end', { transactions: [] }); 39 | }); 40 | 41 | pipeline.once('error', function(err) { 42 | self.emit('error', err); 43 | }); 44 | 45 | pipeline.once('end', function(result) { 46 | self.emit('end', { transactions: result.transactions }); 47 | }); 48 | 49 | pipeline.execute({ csv: csv }); 50 | }; 51 | 52 | return Import; 53 | })(); 54 | 55 | exports.Import = Import; -------------------------------------------------------------------------------- /spec/filters/transaction/extract-amount.spec.js: -------------------------------------------------------------------------------- 1 | var extractAmount = require('../../../lib/filters/transaction/extract-amount'); 2 | 3 | describe('extract amount', function() { 4 | var spec, output; 5 | 6 | beforeEach(function() { 7 | spec = this; 8 | 9 | this.createFilter = function(options, data, done) { 10 | var filter = extractAmount(options); 11 | 12 | filter({ data: data }, function(err, result) { 13 | if (err) { 14 | spec.fail(err); 15 | return done(); 16 | } 17 | 18 | output = result; 19 | done(); 20 | }); 21 | }; 22 | }); 23 | 24 | describe('single amount column', function() { 25 | describe('valid amount', function() { 26 | beforeEach(function(done) { 27 | this.createFilter({ column: 1 }, [ '1.99' ], done); 28 | }); 29 | 30 | it('should parse', function() { 31 | expect(output.amount).toEqual(1.99); 32 | }); 33 | }); 34 | 35 | describe('inverse', function() { 36 | beforeEach(function(done) { 37 | this.createFilter({ column: 1, inverse: true }, [ '1.99' ], done); 38 | }); 39 | 40 | it('should parse', function() { 41 | expect(output.amount).toEqual(-1.99); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('multiple amount columns', function() { 47 | describe('valid amount', function() { 48 | beforeEach(function(done) { 49 | this.createFilter({ columns: [ 1, 2 ] }, [ '1.99', '' ], done); 50 | }); 51 | 52 | it('should parse', function() { 53 | expect(output.amount).toEqual(1.99); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('amounts with thousand separator', function() { 59 | beforeEach(function(done) { 60 | this.createFilter({ column: 1 }, [ '-1,097.43' ], done); 61 | }); 62 | 63 | it('should parse', function() { 64 | expect(output.amount).toEqual(-1097.43); 65 | }); 66 | }); 67 | }); -------------------------------------------------------------------------------- /lib/filters/transaction/extract-amount.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var clean = function(str) { 4 | return str.replace(/[^0-9\-\.]/g, ''); 5 | }; 6 | 7 | 8 | var parseAmount = function(options, input, amount, next) { 9 | if (!amount) { 10 | return next('Failed to extract amount - it was empty'); 11 | } 12 | 13 | try { 14 | if (_.isString(amount)) { 15 | amount = parseFloat(clean(amount), 10); 16 | } 17 | } catch(ex) { 18 | return next('Failed to parse amount "' + amount + '". ' + ex); 19 | } 20 | 21 | if (options.inverse) { 22 | amount = amount * -1; 23 | } 24 | 25 | input.amount = amount; 26 | 27 | return next(null, input); 28 | }; 29 | 30 | var extractFromSingleColumn = function(options, input, next) { 31 | var amount = input.data[options.column - 1]; 32 | 33 | parseAmount(options, input, amount, next); 34 | }; 35 | 36 | var extractFromMultipleColumns = function(options, input, next) { 37 | var amounts = _.map(options.columns, function(column) { 38 | var amount = input.data[column - 1]; 39 | if (_.isString(amount)) { 40 | amount = parseFloat(clean(input.data[column - 1]), 10); 41 | } 42 | return amount; 43 | }); 44 | 45 | var amount = _.find(amounts, function(number) { 46 | return _.isNumber(number) && !_.isNaN(number); 47 | }); 48 | 49 | if (amount) { 50 | parseAmount(options, input, amount, next); 51 | } else { 52 | next('Failed to find an amount from columns ' + options.columns.join(', ')); 53 | } 54 | 55 | }; 56 | 57 | var extractAmount = function(options) { 58 | if (!options.column && !options.columns) { 59 | throw { 60 | name: 'OptionsError', 61 | message: 'Amount column has not been specified' 62 | }; 63 | } 64 | 65 | return function(input, next) { 66 | if (options.column) { 67 | return extractFromSingleColumn(options, input, next); 68 | } 69 | 70 | if (options.columns) { 71 | return extractFromMultipleColumns(options, input, next); 72 | } 73 | 74 | next('Failed to extract amount'); 75 | }; 76 | }; 77 | 78 | module.exports = extractAmount; -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | // Task configuration. 6 | jshint: { 7 | options: { 8 | curly: true, 9 | eqeqeq: true, 10 | immed: true, 11 | latedef: true, 12 | newcap: true, 13 | noarg: true, 14 | sub: true, 15 | undef: true, 16 | unused: true, 17 | boss: true, 18 | eqnull: true, 19 | indent: 2, 20 | globals: { 21 | module: true, 22 | exports: true, 23 | require: true, 24 | process: true, 25 | console: true 26 | } 27 | }, 28 | gruntfile: { 29 | src: 'Gruntfile.js' 30 | }, 31 | lib: { 32 | src: ['lib/**/*.js'] 33 | }, 34 | spec: { 35 | src: ['spec/**/*.js'], 36 | options: { 37 | globals: { 38 | module: true, 39 | exports: true, 40 | require: true, 41 | console: true, 42 | describe: true, 43 | xdescribe: true, 44 | it: true, 45 | xit: true, 46 | expect: true, 47 | beforeEach: true 48 | } 49 | } 50 | } 51 | }, 52 | 53 | 'jasmine-node': { 54 | run: { 55 | spec: 'spec' 56 | }, 57 | executable: './node_modules/.bin/jasmine-node' 58 | }, 59 | 60 | watch: { 61 | gruntfile: { 62 | files: '<%= jshint.gruntfile.src %>', 63 | tasks: ['jshint:gruntfile'] 64 | }, 65 | lib: { 66 | files: '<%= jshint.lib.src %>', 67 | tasks: ['jshint:lib'] 68 | }, 69 | spec: { 70 | files: '<%= jshint.spec.src %>', 71 | tasks: ['jshint:spec', 'jasmine-node'] 72 | } 73 | } 74 | }); 75 | 76 | grunt.loadNpmTasks('grunt-contrib-jshint'); 77 | grunt.loadNpmTasks('grunt-contrib-watch'); 78 | grunt.loadNpmTasks('grunt-contrib-jasmine-node'); 79 | 80 | // Default task. 81 | grunt.registerTask('default', ['jshint', 'jasmine-node']); 82 | 83 | grunt.registerTask('spec', ['jasmine-node']); 84 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ledger Import 2 | 3 | Import accounting transactions from a CSV file into Ledger, using naive Bayesian learning to identify accounts from each payee. Heavily inspired by the Reckon Ruby gem. 4 | 5 | > Ledger is a powerful, double-entry accounting system that is accessed from the UNIX command-line. 6 | 7 | ## Dependencies 8 | 9 | * [Ledger 3](http://ledger-cli.org/) 10 | * [Node.js](nodejs.org) and npm 11 | 12 | ### Installing Ledger 13 | 14 | The simplest way to install Ledger 3 is through [Homebrew](http://mxcl.github.com/homebrew/). 15 | 16 | brew install ledger --HEAD 17 | 18 | The `--HEAD` option is required to install version 3.x. 19 | 20 | ## Usage 21 | 22 | Install `ledger-import` and its dependencies with npm. 23 | 24 | npm install -g ledger-import 25 | 26 | Then run the import tool, providing the relevant command line arguments for the CSV you are attempting to parse. 27 | 28 | ledger-import --file /path/to/transactions.csv --account 'Assets:Current Account' --ledger /path/to/ledger.dat --currency '£' --contains-header --date-column 1 --date-format 'DD/MM/YYYY' --payee-column 2 --amount-column 3 29 | 30 | ### Command line help 31 | 32 | $ ledger-import 33 | 34 | Usage: ledger-import [options] 35 | 36 | Options: 37 | 38 | -h, --help output usage information 39 | -V, --version output the version number 40 | -f, --file The CSV file to parse 41 | -a, --account The Ledger Account this file is for 42 | -i, --inverse Use the negative of each amount 43 | -v, --verbose Run verbosely 44 | -l, --ledger An existing ledger file to learn accounts from 45 | -c, --currency Currency symbol to use, defaults to £ ($, EUR) 46 | --contains-header The first row of the CSV is a header and should be skipped 47 | --csv-separator Separator for parsing the CSV, default is comma. 48 | --date-column Column containing the date in the CSV file, the first column is column 1 49 | --date-format Force the date format 50 | --payee-column Column containing the payee (description) in the CSV file 51 | --amount-column Column containing the amount in the CSV file 52 | --amount-columns Multiple columns containing the amount in the CSV file 53 | -------------------------------------------------------------------------------- /lib/filters/transactions/parse-transactions.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Pipeline = require('pipes-and-filters'), 3 | filters = require('../transaction'); 4 | 5 | var transactionExists = function(input) { 6 | // if transacton already exists in source Ledger file, skip 7 | return input.exists; 8 | }; 9 | 10 | var transactionIsSkipped = function(input) { 11 | // skipped transaction 12 | return input.skip; 13 | }; 14 | 15 | // Create the transaction processing pipeline (processes an individual transaction) 16 | var createPipeline = function(options) { 17 | var pipeline = Pipeline.create('transaction parser'); 18 | 19 | pipeline.use(filters.parseLedger(_.pick(options, 'ledger', 'binary'))); 20 | pipeline.use(filters.extractDate(options.date)); 21 | pipeline.use(filters.extractPayee(options.payee)); 22 | pipeline.use(filters.extractAmount(options.amount)); 23 | pipeline.use(filters.matchExistingTransaction()); 24 | pipeline.breakIf(transactionExists); 25 | pipeline.use(filters.classifyAccount(_.pick(options, 'account'))); 26 | pipeline.use(filters.confirmTransaction()); 27 | pipeline.breakIf(transactionIsSkipped); 28 | pipeline.use(filters.formatTransaction(_.pick(options, 'account', 'currency'))); 29 | 30 | return pipeline; 31 | }; 32 | 33 | var parseTransactions = function(options) { 34 | var pipeline = createPipeline(options); 35 | 36 | return function(input, next) { 37 | var transactions = [], 38 | queue = input.transactions; 39 | 40 | var parseNext = function parseNext(err, result) { 41 | if (err) { 42 | return next(err); 43 | } 44 | 45 | if (result) { 46 | // include parsed transaction on pipeline completion 47 | transactions.push(result); 48 | } 49 | 50 | var data = queue.shift(); 51 | 52 | if (data) { 53 | pipeline.execute(data, parseNext); 54 | } else { 55 | // done, with success 56 | input.transactions = transactions; 57 | 58 | pipeline.removeListener('break', transactionSkipped); 59 | 60 | next(null, input); 61 | } 62 | }; 63 | 64 | // continue processing the next transaction in the queue when one is skipped 65 | var transactionSkipped = function() { 66 | options.emitter.emit('skipped'); 67 | parseNext(); 68 | }; 69 | 70 | pipeline.on('break', transactionSkipped); 71 | 72 | parseNext(); 73 | }; 74 | }; 75 | 76 | module.exports = parseTransactions; -------------------------------------------------------------------------------- /lib/filters/transaction/confirm-transaction.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | moment = require('moment'), 3 | prompt = require('prompt'); 4 | 5 | var promptForAccount = function(input, next) { 6 | var accounts = _(input.transactions) 7 | .map(function(transaction) { return transaction.postings; }) 8 | .flatten() 9 | .map(function(posting) { return posting.account; }) 10 | .uniq() 11 | .sortBy(function(account) { return account; }) 12 | .value(); 13 | 14 | console.log('Choose the account:'); 15 | _.each(accounts, function(account, i) { console.log((i + 1) + ': ' + account); }); 16 | 17 | (function promptUser() { 18 | prompt.get(['account'], function(err, response) { 19 | if (err) { 20 | return next(err); 21 | } 22 | 23 | var index = parseInt(response.account, 10); 24 | 25 | // ensure the user enters a valid number 26 | if (_.isNaN(index)) { 27 | console.error('please enter a number'); 28 | return promptUser(); 29 | } 30 | 31 | input.account = accounts[index - 1]; 32 | 33 | next(null, input); 34 | }); 35 | })(); 36 | }; 37 | 38 | // prompt user to confirm the transaction details 39 | var confirmTransaction = function() { 40 | prompt.message = ''; 41 | prompt.delimiter = ''; 42 | 43 | prompt.start(); 44 | 45 | return function(input, next) { 46 | var date = new Date(input.date.getFullYear(), input.date.getMonth(), input.date.getDate()), 47 | payee = input.payee, 48 | account = input.account, 49 | amount = input.amount, 50 | destination = amount > 0 ? 'source' : 'recipient'; 51 | 52 | console.log(''); 53 | console.log(moment(date).format('YYYY-MM-DD'), payee, amount); 54 | 55 | prompt.get({ 56 | properties: { 57 | response: { 58 | description: 'Confirm "' + account + '" was the ' + destination + '? ([Y]es/[n]o/[q]uit/[s]kip): '.green 59 | } 60 | } 61 | }, function(err, result) { 62 | if (err) { 63 | return next(err); 64 | } 65 | 66 | switch((result.response || 'y').substr(0, 1).toLowerCase()) { 67 | case 'y': 68 | next(null, input); 69 | break; 70 | case 'n': 71 | promptForAccount(input, next); 72 | break; 73 | case 'q': 74 | process.exit(1); 75 | break; 76 | case 's': 77 | input.skip = true; 78 | next(null, input); 79 | break; 80 | } 81 | }); 82 | }; 83 | }; 84 | 85 | module.exports = confirmTransaction; -------------------------------------------------------------------------------- /spec/filters/transaction/classify-account.spec.js: -------------------------------------------------------------------------------- 1 | var classifyAccount = require('../../../lib/filters/transaction/classify-account'); 2 | 3 | describe('classify account', function() { 4 | var spec, output, transactions; 5 | 6 | beforeEach(function() { 7 | spec = this; 8 | 9 | this.createFilter = function(options, input, done) { 10 | var filter = classifyAccount(options); 11 | 12 | filter(input, function(err, result) { 13 | if (err) { 14 | spec.fail(err); 15 | return done(); 16 | } 17 | 18 | output = result; 19 | done(); 20 | }); 21 | }; 22 | 23 | transactions = [ 24 | { 25 | date: new Date(), 26 | payee: 'Salary', 27 | postings: [ 28 | { 29 | account: 'Assets:Current Account', 30 | commodity: { 31 | currency: '£', 32 | amount: 1000, 33 | formatted: '£1,000.00' 34 | } 35 | }, 36 | { 37 | account: 'Income:Salary', 38 | commodity: { 39 | currency: '£', 40 | amount: -1000, 41 | formatted: '£-1,000.00' 42 | } 43 | } 44 | ] 45 | }, 46 | { 47 | date: new Date(), 48 | payee: 'Mortgage payment', 49 | postings: [ 50 | { 51 | account: 'Expenses:Mortgage', 52 | commodity: { 53 | currency: '£', 54 | amount: 500, 55 | formatted: '£500.00' 56 | } 57 | }, 58 | { 59 | account: 'Assets:Current Account', 60 | commodity: { 61 | currency: '£', 62 | amount: -500, 63 | formatted: '£-500.00' 64 | } 65 | } 66 | ] 67 | } 68 | ]; 69 | }); 70 | 71 | describe('account exists', function() { 72 | beforeEach(function(done) { 73 | this.createFilter({ account: 'Assets:Current Account' }, { transactions: transactions, payee: 'Salary' }, done); 74 | }); 75 | 76 | it('should classify to correct account', function() { 77 | expect(output.account).toEqual('Income:Salary'); 78 | }); 79 | }); 80 | 81 | xdescribe('unknown payee', function() { 82 | beforeEach(function(done) { 83 | this.createFilter({ account: 'Assets:Current Account', threshold: 0.5 }, { transactions: transactions, payee: 'Foo' }, done); 84 | }); 85 | 86 | it('should not classify the account', function() { 87 | expect(output.account).toEqual(null); 88 | }); 89 | 90 | it('should provide error message', function() { 91 | expect(output.error).toEqual('Failed to classify payee "Foo"'); 92 | }); 93 | }); 94 | }); -------------------------------------------------------------------------------- /bin/ledger-import: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var _ = require('lodash'), 4 | program = require('commander'), 5 | csv = require('csv'), 6 | Import = require('../lib/import').Import; 7 | 8 | var list = function(val) { 9 | return _.map(val.split(','), function(i) { return parseInt(i, 10); }); 10 | }; 11 | 12 | program 13 | .version('0.0.6') 14 | .usage('[options]') 15 | .option('-f, --file ', 'The CSV file to parse') 16 | .option('-a, --account ', 'The Ledger Account this file is for') 17 | .option('-i, --inverse', 'Use the negative of each amount', false) 18 | .option('-v, --verbose', 'Run verbosely', false) 19 | .option('-l, --ledger ', 'An existing ledger file to learn accounts from') 20 | .option('-c, --currency ', 'Currency symbol to use, defaults to £ ($, EUR)', '£') 21 | .option('--contains-header', 'The first row of the CSV is a header and should be skipped') 22 | .option('--csv-separator ', 'Separator for parsing the CSV, default is comma.', ',') 23 | .option('--date-column ', 'Column containing the date in the CSV file, the first column is column 1', parseInt) 24 | .option('--date-format ', 'Force the date format') 25 | .option('--payee-column ', 'Column containing the payee (description) in the CSV file', parseInt) 26 | .option('--amount-column ', 'Column containing the amount in the CSV file', parseInt) 27 | .option('--amount-columns ', 'Multiple columns containing the amount in the CSV file', list) 28 | .option('--ledger-binary ', 'Path to the Ledger executable (e.g. /user/local/bin/ledger)') 29 | .parse(process.argv); 30 | 31 | var file = program.file, 32 | delimiter = program.csvSeparator, 33 | escape = '"'; 34 | 35 | var options = { 36 | ledger: program.ledger, 37 | binary: program.ledgerBinary, 38 | log: program.verbose, 39 | header: program.containsHeader, 40 | currency: program.currency, 41 | account: program.account, 42 | date: { 43 | column: program.dateColumn, 44 | format: program.dateFormat 45 | }, 46 | payee: { 47 | column: program.payeeColumn 48 | } 49 | }; 50 | 51 | if (program.amountColumn) { 52 | options.amount = { 53 | column: program.amountColumn, 54 | inverse: program.inverse 55 | }; 56 | } else if (program.amountColumns) { 57 | options.amount = { 58 | columns: program.amountColumns, 59 | inverse: program.inverse 60 | }; 61 | } 62 | 63 | if (!file) { 64 | program.help(); 65 | } 66 | 67 | var csv = csv().from.path(file, { delimiter: delimiter, escape: escape }); 68 | 69 | new Import(options) 70 | .on('skipped', function() { 71 | process.stdout.write('.'); 72 | }) 73 | .once('error', function(err) { 74 | console.error(err); 75 | process.exit(1); 76 | }) 77 | .once('end', function() { 78 | console.log('completed!'); 79 | process.exit(0); 80 | }) 81 | .run(csv); -------------------------------------------------------------------------------- /spec/data/example.dat: -------------------------------------------------------------------------------- 1 | account Assets:Current Account 2 | account Assets:Savings 3 | account Equity:Opening Balances 4 | account Expenses:Books 5 | account Expenses:Car 6 | account Expenses:Cash 7 | account Expenses:Clothing 8 | account Expenses:Computer 9 | account Expenses:Council Tax 10 | account Expenses:Food 11 | account Expenses:Holiday 12 | account Expenses:Home 13 | account Expenses:Leisure 14 | account Expenses:Mobile Phone 15 | account Expenses:Mortgage 16 | account Expenses:Utilities:Energy 17 | account Expenses:Utilities:Water 18 | account Income:Interest 19 | account Income:Salary 20 | account Liabilities:Mastercard 21 | 22 | 2013/01/01 * Opening Balance 23 | Assets:Current Account £ 100.00 24 | Assets:Savings £ 500.00 25 | Liabilities:Mastercard £ -250.00 26 | Equity:Opening Balances 27 | 28 | 2013/01/01 Salary 29 | Assets:Current Account £1,000.00 30 | Income:Salary 31 | 32 | 2013/01/02 Mortgage payment 33 | Expenses:Mortgage £446.52 34 | Assets:Current Account 35 | 36 | 2013/01/04 Tesco Store 37 | Expenses:Food £26.50 38 | Liabilities:Mastercard 39 | 40 | 2013/01/05 Savings 41 | Assets:Savings £100.00 42 | Assets:Current Account 43 | 44 | 2013/01/07 Southern Water 45 | Expenses:Utilities:Water £25.20 46 | Assets:Current Account 47 | 48 | 2013/01/08 Local Council Tax 49 | Expenses:Council Tax £64.00 50 | Assets:Current Account 51 | 52 | 2013/01/09 Gas & Electricity Supply 53 | Expenses:Utilities:Energy £39.82 54 | Assets:Current Account 55 | 56 | 2013/01/13 Clothes 57 | Expenses:Clothing £37.50 58 | Liabilities:Mastercard 59 | 60 | 2013/01/13 Home Improvements 61 | Expenses:Home £20.18 62 | Liabilities:Mastercard 63 | 64 | 2013/01/16 Introduction to Accounting Book 65 | Expenses:Books £31.73 66 | Liabilities:Mastercard 67 | 68 | 2013/01/17 Ski Holiday 69 | Expenses:Holiday £314.37 70 | Liabilities:Mastercard 71 | 72 | 2013/01/24 ATM 73 | Expenses:Cash £40.00 74 | Assets:Current Account 75 | 76 | 2013/01/30 Credit Card Payment 77 | Liabilities:Mastercard £680.28 78 | Assets:Current Account 79 | 80 | 2013/02/01 Salary 81 | Assets:Current Account £1,000.00 82 | Income:Salary 83 | 84 | 2013/02/02 Mortgage payment 85 | Expenses:Mortgage £446.52 86 | Assets:Current Account 87 | 88 | 2013/02/05 Savings 89 | Assets:Savings £100.00 90 | Assets:Current Account 91 | 92 | 2013/02/07 Southern Water 93 | Expenses:Utilities:Water £25.20 94 | Assets:Current Account 95 | 96 | 2013/02/07 Tesco Store 97 | Expenses:Food £47.89 98 | Liabilities:Mastercard 99 | 100 | 2013/02/08 Local Council Tax 101 | Expenses:Council Tax £64.00 102 | Assets:Current Account 103 | 104 | 2013/02/09 Gas & Electricity Supply 105 | Expenses:Utilities:Energy £39.82 106 | Assets:Current Account 107 | 108 | 2013/02/18 Tesco Store 109 | Expenses:Food £32.12 110 | Liabilities:Mastercard 111 | 112 | 2013/03/01 Salary 113 | Assets:Current Account £1,000.00 114 | Income:Salary 115 | 116 | 2013/03/02 Mortgage payment 117 | Expenses:Mortgage £446.52 118 | Assets:Current Account 119 | 120 | 2013/03/05 Savings 121 | Assets:Savings £100.00 122 | Assets:Current Account 123 | 124 | 2013/03/07 Southern Water 125 | Expenses:Utilities:Water £25.20 126 | Assets:Current Account 127 | 128 | 2013/03/08 Local Council Tax 129 | Expenses:Council Tax £64.00 130 | Assets:Current Account 131 | 132 | 2013/03/09 Gas & Electricity Supply 133 | Expenses:Utilities:Energy £39.82 134 | Assets:Current Account 135 | 136 | 2013/03/11 Tesco Store 137 | Expenses:Food £36.21 138 | Liabilities:Mastercard 139 | 140 | 2013/04/01 Salary 141 | Assets:Current Account £1,000.00 142 | Income:Salary 143 | 144 | 2013/04/02 Mortgage payment 145 | Expenses:Mortgage £446.52 146 | Assets:Current Account 147 | 148 | 2013/04/05 Savings 149 | Assets:Savings £100.00 150 | Assets:Current Account 151 | 152 | 2013/04/06 Tesco Store 153 | Expenses:Food £23.74 154 | Liabilities:Mastercard 155 | 156 | 2013/04/07 Southern Water 157 | Expenses:Utilities:Water £25.20 158 | Assets:Current Account 159 | 160 | 2013/04/08 Local Council Tax 161 | Expenses:Council Tax £64.00 162 | Assets:Current Account 163 | 164 | 2013/04/09 Gas & Electricity Supply 165 | Expenses:Utilities:Energy £39.82 166 | Assets:Current Account 167 | 168 | 2013/04/21 Tesco Store 169 | Expenses:Food £42.60 170 | Liabilities:Mastercard 171 | 172 | 2013/05/01 Salary 173 | Assets:Current Account £1,000.00 174 | Income:Salary 175 | 176 | 2013/05/02 Mortgage payment 177 | Expenses:Mortgage £446.52 178 | Assets:Current Account 179 | 180 | 2013/05/05 Savings 181 | Assets:Savings £100.00 182 | Assets:Current Account 183 | 184 | 2013/05/07 Southern Water 185 | Expenses:Utilities:Water £25.20 186 | Assets:Current Account 187 | 188 | 2013/05/08 Local Council Tax 189 | Expenses:Council Tax £64.00 190 | Assets:Current Account 191 | 192 | 2013/05/08 Tesco Store 193 | Expenses:Food £24.82 194 | Liabilities:Mastercard 195 | 196 | 2013/05/09 Gas & Electricity Supply 197 | Expenses:Utilities:Energy £39.82 198 | Assets:Current Account --------------------------------------------------------------------------------