Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

medici

Package Overview
Dependencies
Maintainers
1
Versions
57
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

medici - npm Package Compare versions

Comparing version 0.1.0 to 0.5.0

lib/book.coffee

288

index.js
// Generated by CoffeeScript 1.6.3
(function() {
var Medici, Schema, entry, journalSchema, mongoose, transactionSchema, _;
var Q, Schema, book, entry, err, journalSchema, mongoose, transactionSchema, _;
entry = require('./lib/entry');
book = require('./lib/book');
mongoose = require('mongoose');

@@ -11,25 +13,47 @@

Q = require('q');
_ = require('underscore');
transactionSchema = new Schema({
credit: Number,
debit: Number,
meta: Schema.Types.Mixed,
datetime: Date,
account_path: [String],
accounts: String,
memo: String,
_journal: Schema.Types.ObjectId,
voided: {
type: Boolean,
"default": false
},
void_reason: String,
_original_journal: Schema.Types.ObjectId
});
try {
mongoose.model('Medici_Transaction');
} catch (_error) {
err = _error;
transactionSchema = new Schema({
credit: Number,
debit: Number,
meta: Schema.Types.Mixed,
datetime: Date,
account_path: [String],
accounts: String,
book: String,
memo: String,
_journal: {
type: Schema.Types.ObjectId,
ref: 'Medici_Journal'
},
timestamp: Date,
voided: {
type: Boolean,
"default": false
},
void_reason: String,
_original_journal: Schema.Types.ObjectId
});
mongoose.model('Medici_Transaction', transactionSchema);
}
journalSchema = new Schema({
datetime: Date,
memo: String,
transactions: [Schema.Types.ObjectId],
memo: {
type: String,
"default": ''
},
_transactions: [
{
type: Schema.Types.ObjectId,
ref: 'Medici_Transaction'
}
],
book: String,
voided: {

@@ -39,174 +63,96 @@ type: Boolean,

},
void_reason: String,
_original_journal: Schema.Types.ObjectId
void_reason: String
});
journalSchema.methods["void"] = function(book, reason, callback) {
var err, finished, trans_id, transactions, _i, _len, _ref, _results,
journalSchema.methods["void"] = function(book, reason) {
var deferred, trans_id, voidTransaction, voids, _i, _len, _ref,
_this = this;
deferred = Q.defer();
if (this.voided === true) {
err = new Error('Journal already voided');
return callback(err);
deferred.reject(new Error('Journal already voided'));
}
this.voided = true;
this.void_reason = reason;
transactions = [];
finished = _.after(this.transactions.length, function(err, trans) {
var newMemo, _i, _len;
if (_this.memo.substr(0, 6) === '[VOID]') {
newMemo = _this.memo.replace('[VOID]', '[UNVOID]');
} else if (_this.memo.substr(0, 8) === '[UNVOID]') {
newMemo = _this.memo.replace('[UNVOID]', '[REVOID]');
} else if (_this.memo.substr(0, 8) === '[REVOID]') {
newMemo = _this.memo.replace('[REVOID]', '[UNVOID]');
} else {
newMemo = '[VOID] ' + _this.memo;
}
entry = book.entry(newMemo, null, _this._id);
for (_i = 0, _len = transactions.length; _i < _len; _i++) {
trans = transactions[_i];
if (trans.credit) {
entry.debit(trans.account_path, trans.credit, trans.meta);
if (reason == null) {
this.void_reason = '';
} else {
this.void_reason = reason;
}
console.log('VOID REASON IS:', this.void_reason);
voidTransaction = function(trans_id) {
var d;
d = Q.defer();
mongoose.model('Medici_Transaction').findByIdAndUpdate(trans_id, {
voided: true,
void_reason: _this.void_reason
}, function(err, trans) {
if (err) {
console.error('Failed to void transaction:', err);
return d.reject(err);
} else {
return d.resolve(trans);
}
if (trans.debit) {
entry.credit(trans.account_path, trans.debit, trans.meta);
}
}
return entry.commit(callback);
});
_ref = this.transactions;
_results = [];
});
return d.promise;
};
voids = [];
_ref = this._transactions;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
trans_id = _ref[_i];
_results.push(book.transactionModel.findByIdAndUpdate(trans_id, {
voided: true,
void_reason: reason
}, function(err, trans) {
transactions.push(trans);
return finished(err, trans);
}));
voids.push(voidTransaction(trans_id));
}
return _results;
};
module.exports = Medici = (function() {
function Medici(name) {
this.name = name;
this.transactionModel = mongoose.model(name + '_transaction', transactionSchema);
this.journalModel = mongoose.model(name + '_journal', journalSchema);
}
Medici.prototype.entry = function(memo, date, original_journal) {
if (date == null) {
date = null;
}
if (original_journal == null) {
original_journal = null;
}
return entry.write(this, memo, date, original_journal);
};
Medici.prototype.parseQuery = function(query) {
var account, accounts, acct, i, key, parsed, val, _i, _len, _ref;
parsed = {};
if ((account = query.account)) {
accounts = account.split(':');
for (i = _i = 0, _len = accounts.length; _i < _len; i = ++_i) {
acct = accounts[i];
parsed['account_path.' + i] = acct;
Q.all(voids).then(function(transactions) {
var key, meta, newMemo, trans, val, valid_fields, _j, _len1;
if (_this.void_reason) {
newMemo = _this.void_reason;
} else {
if (_this.memo.substr(0, 6) === '[VOID]') {
newMemo = _this.memo.replace('[VOID]', '[UNVOID]');
} else if (_this.memo.substr(0, 8) === '[UNVOID]') {
newMemo = _this.memo.replace('[UNVOID]', '[REVOID]');
} else if (_this.memo.substr(0, 8) === '[REVOID]') {
newMemo = _this.memo.replace('[REVOID]', '[UNVOID]');
} else {
newMemo = '[VOID] ' + _this.memo;
}
}
if ((query.start_date != null) && (query.end_date != null) && query.start_date instanceof Date && query.end_date instanceof Date) {
parsed['datetime'] = {
$gte: query.start_date,
$lt: query.end_date
};
} else if ((query.start_date != null) && query.start_date instanceof Date) {
parsed['datetime'] = {
$gte: query.start_date
};
} else if ((query.end_date != null) && query.end_date instanceof Date) {
parsed['datetime'] = {
$lt: query.end_date
};
}
if (query.meta != null) {
_ref = query.meta;
for (key in _ref) {
val = _ref[key];
parsed['meta.' + key] = val;
}
}
return parsed;
};
Medici.prototype.balance = function(query, callback) {
var group, match;
query = this.parseQuery(query);
match = {
$match: query
};
group = {
$group: {
_id: '1',
credit: {
$sum: '$credit'
},
debit: {
$sum: '$debit'
entry = book.entry(newMemo, null, _this._id);
valid_fields = ['credit', 'debit', 'account_path', 'accounts', 'datetime', 'book', 'memo', 'timestamp', 'voided', 'void_reason', '_original_journal'];
for (_j = 0, _len1 = transactions.length; _j < _len1; _j++) {
trans = transactions[_j];
trans = trans.toObject();
meta = {};
for (key in trans) {
val = trans[key];
if (key === '_id' || key === '_journal') {
continue;
}
if (valid_fields.indexOf(key) === -1) {
meta[key] = val;
}
}
};
return this.transactionModel.aggregate(match, group, function(err, result) {
var total;
if (err) {
return callback(err);
if (trans.credit) {
entry.debit(trans.account_path, trans.credit, meta);
}
result = result.shift();
total = result.credit - result.debit;
return callback(null, total);
});
};
Medici.prototype.ledger = function(query, callback) {
query = this.parseQuery(query);
return this.transactionModel.find(query).sort({
datetime: -1
}).exec(callback);
};
Medici.prototype["void"] = function(journal_id, reason, callback) {
var _this = this;
return this.journalModel.findById(journal_id, function(err, journal) {
return journal["void"](_this, reason, callback);
});
};
Medici.prototype.listAccounts = function(callback) {
return this.transactionModel.find().distinct('accounts', function(err, results) {
var acct, curPath, final, i, path, res, _i, _j, _len, _len1;
if (err) {
return callback(err);
if (trans.debit) {
entry.credit(trans.account_path, trans.debit, meta);
}
final = {};
for (_i = 0, _len = results.length; _i < _len; _i++) {
res = results[_i];
path = res.split(':');
curPath = final;
for (i = _j = 0, _len1 = path.length; _j < _len1; i = ++_j) {
acct = path[i];
if (curPath[acct] == null) {
curPath[acct] = {};
}
curPath = curPath[acct];
}
}
return callback(null, final);
console.log('did credit or debit');
}
return entry.commit().then(function(entry) {
return deferred.resolve(entry);
}, function(err) {
return deferred.reject(err);
});
};
}, function(err) {
return deferred.reject(err);
});
return deferred.promise;
};
return Medici;
mongoose.model('Medici_Journal', journalSchema);
})();
module.exports = {
book: book
};
}).call(this);
// Generated by CoffeeScript 1.6.3
(function() {
var Entry, _;
var Entry, Q, mongoose, _;
_ = require('underscore');
mongoose = require('mongoose');
Q = require('q');
module.exports = Entry = (function() {

@@ -15,16 +19,14 @@ Entry.write = function(book, memo, date, original_journal) {

}
return new this(book, memo, date);
return new this(book, memo, date, original_journal);
};
function Entry(book, memo, date, original_journal) {
if (date == null) {
date = null;
}
if (original_journal == null) {
original_journal = null;
}
var journalClass;
console.log('constructed entry with original journal:', original_journal);
this.book = book;
this.journal = new this.book.journalModel;
journalClass = mongoose.model('Medici_Journal');
this.journal = new journalClass();
this.journal.memo = memo;
if (this.original_journal) {
if (original_journal) {
console.log('setting journal original to', original_journal);
this.journal._original_journal = original_journal;

@@ -40,7 +42,15 @@ }

Entry.prototype.credit = function(account_path, amount, meta) {
if (meta == null) {
meta = null;
Entry.prototype.credit = function(account_path, amount, extra) {
var key, keys, meta, transaction, val;
if (extra == null) {
extra = null;
}
this.transactions.push({
amount = parseFloat(amount);
if (typeof account_path === 'string') {
account_path = account_path.split(':');
}
if (account_path.length > 3) {
throw "Account path is too deep (maximum 3)";
}
transaction = {
account_path: account_path,

@@ -50,15 +60,37 @@ accounts: account_path.join(':'),

debit: 0.0,
meta: meta,
book: this.book.name,
memo: this.journal.memo,
_journal: this.journal._id,
datetime: this.journal.datetime,
original_journal: this.journal.original_journal
});
_original_journal: this.journal._original_journal,
timestamp: new Date()
};
keys = _.keys(mongoose.model('Medici_Transaction').schema.paths);
meta = {};
for (key in extra) {
val = extra[key];
if (keys.indexOf(key) >= 0) {
transaction[key] = val;
} else {
meta[key] = val;
}
}
transaction.meta = meta;
this.transactions.push(transaction);
return this;
};
Entry.prototype.debit = function(account_path, amount, meta) {
if (meta == null) {
meta = null;
Entry.prototype.debit = function(account_path, amount, extra) {
var key, keys, meta, transaction, val;
if (extra == null) {
extra = null;
}
this.transactions.push({
amount = parseFloat(amount);
if (typeof account_path === 'string') {
account_path = account_path.split(':');
}
if (account_path.length > 3) {
throw "Account path is too deep (maximum 3)";
}
transaction = {
account_path: account_path,

@@ -68,15 +100,45 @@ accounts: account_path.join(':'),

debit: amount,
meta: meta,
_journal: this.journal._id,
book: this.book.name,
memo: this.journal.memo,
datetime: this.journal.datetime,
original_journal: this.journal.original_journal
});
_original_journal: this.journal._original_journal
};
keys = _.keys(mongoose.model('Medici_Transaction').schema.paths);
meta = {};
for (key in extra) {
val = extra[key];
if (keys.indexOf(key) >= 0) {
console.log(key, 'is part of schema, setting to', val);
transaction[key] = val;
} else {
meta[key] = val;
}
}
this.transactions.push(transaction);
transaction.meta = meta;
return this;
};
Entry.prototype.commit = function(callback) {
var err, total, transaction, _i, _len, _ref,
Entry.prototype.saveTransaction = function(transaction) {
var d, model, modelClass;
d = Q.defer();
modelClass = mongoose.model('Medici_Transaction');
model = new modelClass(transaction);
this.journal._transactions.push(model._id);
model.save(function(err, res) {
if (err) {
return d.reject(err);
} else {
return d.resolve(res);
}
});
return d.promise;
};
Entry.prototype.commit = function(success) {
var deferred, err, saves, total, trans, transaction, _i, _j, _len, _len1, _ref, _ref1,
_this = this;
console.log(callback);
this.commitCallback = callback;
deferred = Q.defer();
console.log('committing in commit method...');
this.transactionsSaved = 0;

@@ -90,73 +152,37 @@ total = 0.0;

}
console.log('TOTAL FOR COMMIT:', total);
if (total > 0 || total < 0) {
err = new Error("INVALID_JOURNAL");
return callback(err);
}
return this.journal.save(function(err, journal) {
var key, toSave, trans, val, _j, _len1, _ref1, _results;
console.log('Journal saved', err);
_this.transactionsSaved = 0;
toSave = [];
_ref1 = _this.transactions;
_results = [];
err.code = 400;
console.error('Journal is invalid. Total is:', total);
deferred.reject(err);
} else {
saves = [];
_ref1 = this.transactions;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
trans = _ref1[_j];
transaction = new _this.book.transactionModel;
console.log('Saving transaction', trans);
for (key in trans) {
val = trans[key];
transaction[key] = val;
}
transaction._journal = _this.journal._id;
_results.push(transaction.save(_.bind(_this.transactionSaved, _this)));
saves.push(this.saveTransaction(trans));
}
return _results;
});
};
Entry.prototype.transactionSaved = function(err, transaction) {
this.transactionsSaved++;
if (err) {
return this.revert(err);
Q.all(saves).then(function() {
console.log('saved journal...');
return _this.journal.save(function(err, result) {
if (err) {
mongoose.model('Medici_Transaction').remove({
_journal: _this.journal._id
});
return deferred.reject(new Error('Failure to save journal'));
} else {
deferred.resolve(_this.journal);
if (success != null) {
return success(_this.journal);
}
}
});
}, function(err) {
console.log('could not save all transactions', err);
return deferred.reject(err);
});
}
this.transactionModels.push(transaction);
console.log('Transactions saved so far: ', this.transactionsSaved, ' total transactions:', this.transactions.length);
if (this.transactionsSaved === this.transactions.length) {
console.log('Running commit callback');
return this.finishCommit();
}
return deferred.promise;
};
Entry.prototype.finishCommit = function() {
var trans, _i, _len, _ref,
_this = this;
_ref = this.transactionModels;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
trans = _ref[_i];
this.journal.transactions.push(trans._id);
}
return this.journal.save(function(err) {
if (err) {
return _this.revert(err);
}
return _this.commitSuccessful();
});
};
Entry.prototype.commitSuccessful = function() {
return this.commitCallback(null, this.journal);
};
Entry.prototype.revert = function(error) {
var trans, _i, _len, _ref;
this.journal.remove();
_ref = this.transactionModels;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
trans = _ref[_i];
trans.remove();
}
return this.commitCallback(error);
};
return Entry;

@@ -163,0 +189,0 @@

{
"name": "medici",
"version": "0.1.0",
"version": "0.5.0",
"description": "Simple double-entry accounting for Node + Mongoose",

@@ -18,3 +18,5 @@ "main": "index.js",

],
"author": "Jason Raede",
"author": {
"name": "Jason Raede"
},
"license": "MIT",

@@ -26,4 +28,9 @@ "bugs": {

"underscore": "~1.5.2",
"mongoose": "~3.8.0"
}
"mongoose": "~3.8.0",
"q":"*"
},
"readme": "medici\n======\n\nDouble-entry accounting system for nodejs + mongoose\n",
"readmeFilename": "README.md",
"_id": "medici@0.1.0",
"_from": "medici@"
}

@@ -5,1 +5,149 @@ medici

Double-entry accounting system for nodejs + mongoose
## Basics
To use Medici you will need a working knowledge of JavaScript, Node.js, and Mongoose.
Medici divides itself into "books", each of which store *journal entries* and their child *transactions*. The cardinal rule of double-entry accounting is that "everything must balance out to zero", and that rule is applied to every journal entry written to the book. If the transactions for a journal entry do not balance out to zero, the system will throw a new error with the message `INVALID JOURNAL`.
Books simply represent the physical book in which you would record your transactions - on a technical level, the "book" attribute simply is added as a key-value pair to both the `Medici_Transactions` and `Medici_Journals` collection to allow you to have multiple books if you want to.
Each transaction in Medici is for one account. Accounts are divided into up to three levels, separated by a colon. Transactions to the Assets:Cash account will appear in a query for transactions in the Assets account, but will not appear in a query for transactions in the Assets:Property account. This allows you to query, for example, all expenses, or just "office overhead" expenses (Expenses:Office Overhead).
In theory, the account names are entirely arbitrary, but you will likely want to use traditional accounting sections and subsections like assets, expenses, income, accounts receivable, accounts payable, etc. But, in the end, how you structure the accounts is entirely up to you.
## Coding Standards
Medici is written in CoffeeScript but obviously is compatible with straight JavaScript as well. All database queries return promise objects instead of using the traditional node `function(err, result)`callback.
## Writing journal entries
Writing a journal entry is very simple. First you need a `book` object:
var medici = require('medici');
// The first argument is the book name, which is used to determine which book the transactions and journals are queried from.
var myBook = new medici.book('MyBook');
Now write an entry:
// You can specify a Date object as the second argument in the book.entry() method if you want the transaction to be for a different date than today
myBook.entry('Received payment').debit('Assets:Cash', 1000).credit('Income', 1000, {
client:'Joe Blow'
}).write().then(function(journal) { (do something with written journal)});
You can continue to chain debits and credits to the journal object until you are finished. The `entry.debit()` and `entry.credit()` methods both have the same arguments: (account, amount, meta).
You can use the "meta" field which you can use to store any additional information about the transaction that your application needs. In the example above, the `client` attribute is added to the transaction in the `Income` account, so you can later use it in a balance or transaction query to limit transactions to those for Joe Blow.
## Querying Account Balance
To query account balance, just use the `book.balance()` method:
myBook.balance({
account:'Assets:Accounts Receivable',
client:'Joe Blow'
}).then(function(balance) {
console.log("Joe Blow owes me", balance);
});
Note that the `meta` query parameters are on the same level as the default query parameters (account, _journal, start_date, end_date). Medici parses the query and automatically turns any values that do not match top-level schema properties into meta parameters.
## Retrieving Transactions
To retrieve transactions, use the `book.ledger()` method (here I'm using moment.js for dates):
var startDate = moment().subtract('months', 1).toDate(); // One month ago
var endDate = new Date(); //today
myBook.ledger({
account:'Income'
start_date:startDate
end_date:endDate
}).then(function(transactions) {
// Do something with the returned transaction documents
});
## Voiding Journal Entries
Sometimes you will make an entry that turns out to be inaccurate or that otherwise needs to be voided. Keeping with traditional double-entry accounting, instead of simply deleting that journal entry, Medici instead will mark the entry as "voided", and then add an equal, opposite journal entry to offset the transactions in the original. This gives you a clear picture of all actions taken with your book.
To void a journal entry, you can either call the `void(void_reason)` method on a Medici_Journal document, or use the `book.void(journal_id, void_reason)` method if you know the journal document's ID.
myBook.void("123456", "I made a mistake").then(function() {
// Do something after voiding
})
If you do not specify a void reason, the system will set the memo of the new journal to the original journal's memo prepended with "[VOID]".
## Document Schema
Journals are schemed in Mongoose as follows:
datetime:Date
memo:
type:String
default:''
_transactions:[
type:Schema.Types.ObjectId
ref:'Medici_Transaction'
]
book:String
voided:
type:Boolean
default:false
void_reason:String
Transactions are schemed as follows:
credit:Number
debit:Number
meta:Schema.Types.Mixed
datetime:Date
account_path:[String]
accounts:String
book:String
memo:String
_journal:
type:Schema.Types.ObjectId
ref:'Medici_Journal'
timestamp:Date
voided:
type:Boolean
default:false
void_reason:String
Note that the `book`, `datetime`, `memo`, `voided`, and `void_reason` attributes are duplicates of their counterparts on the Journal document. These attributes will pretty much be needed on every transaction search, so they are added to the Transaction document to avoid having to populate the associated Journal every time.
### Customizing the Transaction document schema
If you need to have related documents for Transactions and want to use Mongoose's `populate` method, or if you need to add additional fields to the schema that the `meta` won't satisfy, you can define your own schema for `Medici_Transaction` and register it before you load the Medici module. If the `Medici_Transaction` schema is already registered with Mongoose, Medici will use the registered schema instead of the default schema. When you specify meta values when querying or writing transactions, the system will check the Transaction schema to see if those values correspond to actual top-level fields, and if so will set those instead of the corresponding `meta` field.
For example, if you want transactions to have a related "person" document, you can define the transaction schema like so:
_person:
type:Schema.Types.ObjectId
ref:'Person'
credit:Number
debit:Number
meta:Schema.Types.Mixed
datetime:Date
account_path:[String]
accounts:String
book:String
memo:String
_journal:
type:Schema.Types.ObjectId
ref:'Medici_Journal'
timestamp:Date
voided:
type:Boolean
default:false
void_reason:String
Then when you query transactions using the `book.ledger()` method, you can specify the related documents to populate as the second argument. E.g., `book.ledger({account:'Assets:Accounts Receivable'}, ['_person']).then()...`

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc