Comparing version 3.0.2 to 4.0.0
{ | ||
"name": "medici", | ||
"version": "3.0.2", | ||
"version": "4.0.0", | ||
"description": "Simple double-entry accounting for Node + Mongoose", | ||
@@ -37,5 +37,5 @@ "main": "src/index.js", | ||
"mocha": "^5.2.0", | ||
"prettier": "^1.18.2", | ||
"prettier": "^1.19.1", | ||
"should": "^11.2.1" | ||
} | ||
} |
@@ -39,5 +39,5 @@ [![Build Status](https://travis-ci.org/koresar/medici.png?branch=master)](https://travis-ci.org/koresar/medici) | ||
const journal = await myBook.entry('Received payment') | ||
.debit('Assets:Cash', 1000) | ||
.credit('Income', 1000, {client: 'Joe Blow'}) | ||
.commit(); | ||
.debit('Assets:Cash', 1000) | ||
.credit('Income', 1000, {client: 'Joe Blow'}) | ||
.commit(); | ||
``` | ||
@@ -54,10 +54,7 @@ | ||
```js | ||
myBook | ||
.balance({ | ||
account: "Assets:Accounts Receivable", | ||
client: "Joe Blow" | ||
}) | ||
.then(balance => { | ||
console.log("Joe Blow owes me", balance); | ||
}); | ||
const balance = await myBook.balance({ | ||
account: "Assets:Accounts Receivable", | ||
client: "Joe Blow" | ||
}); | ||
console.log("Joe Blow owes me", balance); | ||
``` | ||
@@ -72,16 +69,10 @@ | ||
```js | ||
const startDate = moment() | ||
.subtract("months", 1) | ||
.toDate(); // One month ago | ||
const endDate = new Date(); //today | ||
const startDate = moment().subtract("months", 1).toDate(); // One month ago | ||
const endDate = new Date(); // today | ||
myBook | ||
.ledger({ | ||
account: "Income", | ||
start_date: startDate, | ||
end_date: endDate | ||
}) | ||
.then(transactions => { | ||
// Do something with the returned transaction documents | ||
}); | ||
const transactions = await myBook.ledger({ | ||
account: "Income", | ||
start_date: startDate, | ||
end_date: endDate | ||
}); | ||
``` | ||
@@ -96,5 +87,3 @@ | ||
```js | ||
myBook.void("123456", "I made a mistake").then(() => { | ||
// Do something after voiding | ||
}); | ||
await myBook.void("123456", "I made a mistake"); | ||
``` | ||
@@ -292,2 +281,7 @@ | ||
- **v4.0.0** | ||
- Node.js 8 is required now. | ||
- No API changes. | ||
- **v3.0.0** | ||
@@ -294,0 +288,0 @@ |
154
src/book.js
@@ -93,3 +93,3 @@ const mongoose = require("mongoose"); | ||
balance(query) { | ||
async balance(query) { | ||
let pagination; | ||
@@ -139,42 +139,33 @@ | ||
}; | ||
return this.transactionModel | ||
.aggregate([match, project, sort, skip, group]) | ||
.then(function(result) { | ||
result = result.shift(); | ||
if (!result) { | ||
return { | ||
balance: 0, | ||
notes: 0 | ||
}; | ||
} | ||
const total = result.credit - result.debit; | ||
return { | ||
balance: total, | ||
notes: result.count | ||
}; | ||
}); | ||
let result = await this.transactionModel.aggregate([match, project, sort, skip, group]); | ||
result = result.shift(); | ||
if (!result) { | ||
return { | ||
balance: 0, | ||
notes: 0 | ||
}; | ||
} | ||
const total = result.credit - result.debit; | ||
return { | ||
balance: total, | ||
notes: result.count | ||
}; | ||
} else { | ||
return this.transactionModel | ||
.aggregate([match, project, group]) | ||
.then(function(result) { | ||
result = result.shift(); | ||
if (!result) { | ||
return { | ||
balance: 0, | ||
notes: 0 | ||
}; | ||
} | ||
const total = result.credit - result.debit; | ||
return { | ||
balance: total, | ||
notes: result.count | ||
}; | ||
}); | ||
let result2 = await this.transactionModel.aggregate([match, project, group]); | ||
result2 = result2.shift(); | ||
if (!result2) { | ||
return { | ||
balance: 0, | ||
notes: 0 | ||
}; | ||
} | ||
const total = result2.credit - result2.debit; | ||
return { | ||
balance: total, | ||
notes: result2.count | ||
}; | ||
} | ||
} | ||
ledger(query, populate = null) { | ||
async ledger(query, populate = null) { | ||
let pagination; | ||
@@ -196,23 +187,18 @@ | ||
if (pagination) { | ||
return this.transactionModel.count(query).then(count => { | ||
q.skip((pagination.page - 1) * pagination.perPage).limit( | ||
pagination.perPage | ||
); | ||
q.sort({ | ||
datetime: -1, | ||
timestamp: -1 | ||
}); | ||
if (populate) { | ||
for (let pop of Array.from(populate)) { | ||
q.populate(pop); | ||
} | ||
let count = await this.transactionModel.countDocuments(query); | ||
q.skip((pagination.page - 1) * pagination.perPage).limit(pagination.perPage); | ||
q.sort({ | ||
datetime: -1, | ||
timestamp: -1 | ||
}); | ||
if (populate) { | ||
for (let pop of Array.from(populate)) { | ||
q.populate(pop); | ||
} | ||
return q.exec().then(function(results) { | ||
return { | ||
results, | ||
total: count | ||
}; | ||
}); | ||
}); | ||
} | ||
let results = await q.exec(); | ||
return { | ||
results, | ||
total: count | ||
}; | ||
} else { | ||
@@ -229,39 +215,33 @@ q.sort({ | ||
return q.exec().then(function(results) { | ||
return { | ||
results, | ||
total: results.length | ||
}; | ||
}); | ||
let results1 = await q.exec(); | ||
return { | ||
results: results1, | ||
total: results1.length | ||
}; | ||
} | ||
} | ||
void(journal_id, reason) { | ||
return this.journalModel | ||
.findById(journal_id) | ||
.then(journal => journal.void(this, reason)); | ||
async void(journal_id, reason) { | ||
let journal = await this.journalModel.findById(journal_id); | ||
return await journal.void(this, reason); | ||
} | ||
listAccounts() { | ||
return this.transactionModel | ||
.find({ book: this.name }) | ||
.distinct("accounts") | ||
.then(function(results) { | ||
// Make array | ||
const final = []; | ||
for (let result of results) { | ||
const paths = result.split(":"); | ||
const prev = []; | ||
for (let acct of paths) { | ||
prev.push(acct); | ||
final.push(prev.join(":")); | ||
} | ||
async listAccounts() { | ||
try { | ||
let results = await this.transactionModel.find({ book: this.name }).distinct("accounts"); | ||
const final = new Set(); | ||
for (let result of results) { | ||
const paths = result.split(":"); | ||
const prev = []; | ||
for (let acct of paths) { | ||
prev.push(acct); | ||
final.add(prev.join(":")); | ||
} | ||
return Array.from(new Set(final)); // uniques | ||
}) | ||
.catch(err => { | ||
console.error(err); | ||
throw err; | ||
}); | ||
} | ||
return Array.from(final); // uniques | ||
} catch (err) { | ||
console.error("Medici error:", err); | ||
throw err; | ||
} | ||
} | ||
}; |
@@ -22,3 +22,3 @@ module.exports = class Entry { | ||
this.transactions = []; | ||
this.transactionModels = []; | ||
// this.transactionModels = []; | ||
this.journal.approved = true; | ||
@@ -124,3 +124,3 @@ } | ||
commit() { | ||
async commit() { | ||
// First of all, set approved on transactions to approved on journal | ||
@@ -130,3 +130,3 @@ for (let tx of this.transactions) { | ||
} | ||
this.transactionsSaved = 0; | ||
// this.transactionsSaved = 0; | ||
let total = 0.0; | ||
@@ -151,19 +151,18 @@ for (let tx of this.transactions) { | ||
console.error("Journal is invalid. Total is:", total); | ||
return Promise.reject(err); | ||
} else { | ||
return Promise.all( | ||
this.transactions.map(tx => this.saveTransaction(tx)) | ||
).then(() => { | ||
return this.journal | ||
.save() | ||
.then(() => this.journal) | ||
.catch(err => { | ||
this.book.transactionModel.remove({ | ||
_journal: this.journal._id | ||
}); | ||
throw new Error(`Failure to save journal: ${err.message}`); | ||
}); | ||
}); | ||
throw err; | ||
} | ||
try { | ||
await Promise.all(this.transactions.map(tx => this.saveTransaction(tx))); | ||
return await this.journal.save(); | ||
} catch (err) { | ||
console.error(err); | ||
this.book.transactionModel | ||
.deleteMany({ | ||
_journal: this.journal._id | ||
}) | ||
.catch(e => console.error(`Can't delete transactions for journal ${this.journal._id}`, e)); | ||
throw new Error(`Failure to save journal: ${err.message}`); | ||
} | ||
} | ||
}; |
154
src/index.js
@@ -39,7 +39,7 @@ const Book = require("./book"); | ||
}); | ||
transactionSchema.index({ "_journal": 1 }); | ||
transactionSchema.index({ "accounts": 1, "book": 1, "approved": 1, "datetime": -1, "timestamp": -1 }); | ||
transactionSchema.index({ "account_path.0": 1, "book": 1, "approved": 1 }); | ||
transactionSchema.index({ "account_path.0": 1, "account_path.1": 1, "book": 1, "approved": 1 }); | ||
transactionSchema.index({ "account_path.0": 1, "account_path.1": 1, "account_path.2": 1, "book": 1, "approved": 1 }); | ||
transactionSchema.index({ _journal: 1 }); | ||
transactionSchema.index({ accounts: 1, book: 1, approved: 1, datetime: -1, timestamp: -1 }); | ||
transactionSchema.index({ "account_path.0": 1, book: 1, approved: 1 }); | ||
transactionSchema.index({ "account_path.0": 1, "account_path.1": 1, book: 1, approved: 1 }); | ||
transactionSchema.index({ "account_path.0": 1, "account_path.1": 1, "account_path.2": 1, book: 1, approved: 1 }); | ||
mongoose.model("Medici_Transaction", transactionSchema); | ||
@@ -79,5 +79,5 @@ } | ||
journalSchema.methods.void = function(book, reason) { | ||
journalSchema.methods.void = async function(book, reason) { | ||
if (this.voided === true) { | ||
return Promise.reject(new Error("Journal already voided")); | ||
throw new Error("Journal already voided"); | ||
} | ||
@@ -87,7 +87,3 @@ | ||
this.voided = true; | ||
if (!reason) { | ||
this.void_reason = ""; | ||
} else { | ||
this.void_reason = reason; | ||
} | ||
this.void_reason = reason || ""; | ||
@@ -107,71 +103,66 @@ const voidTransaction = trans_id => { | ||
return Promise.all(this._transactions.map(voidTransaction)).then( | ||
transactions => { | ||
let newMemo; | ||
if (this.void_reason) { | ||
newMemo = this.void_reason; | ||
} else { | ||
// It's either VOID, UNVOID, or REVOID | ||
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}`; | ||
} | ||
} | ||
// Ok now create an equal and opposite journal | ||
const entry = book.entry(newMemo, null, this._id); | ||
const valid_fields = [ | ||
"credit", | ||
"debit", | ||
"account_path", | ||
"accounts", | ||
"datetime", | ||
"book", | ||
"memo", | ||
"timestamp", | ||
"voided", | ||
"void_reason", | ||
"_original_journal" | ||
]; | ||
let transactions = await Promise.all(this._transactions.map(voidTransaction)); | ||
let newMemo; | ||
if (this.void_reason) { | ||
newMemo = this.void_reason; | ||
} else { | ||
// It's either VOID, UNVOID, or REVOID | ||
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}`; | ||
} | ||
} | ||
const entry = book.entry(newMemo, null, this._id); | ||
const valid_fields = [ | ||
"credit", | ||
"debit", | ||
"account_path", | ||
"accounts", | ||
"datetime", | ||
"book", | ||
"memo", | ||
"timestamp", | ||
"voided", | ||
"void_reason", | ||
"_original_journal" | ||
]; | ||
function processMetaField(key, val, meta) { | ||
if (key === "_id" || key === "_journal") { | ||
} else if (valid_fields.indexOf(key) === -1) { | ||
return (meta[key] = val); | ||
} | ||
} | ||
function processMetaField(key, val, meta) { | ||
if (key === "_id" || key === "_journal") { | ||
} else if (valid_fields.indexOf(key) === -1) { | ||
return (meta[key] = val); | ||
} | ||
} | ||
for (let trans of transactions) { | ||
trans = trans.toObject(); | ||
const meta = {}; | ||
for (let trans of transactions) { | ||
trans = trans.toObject(); | ||
const meta = {}; | ||
Object.keys(trans).forEach(key => { | ||
const val = trans[key]; | ||
if (key === "meta") { | ||
Object.keys(trans["meta"]).forEach(keyMeta => { | ||
processMetaField(keyMeta, trans["meta"][keyMeta], meta); | ||
}); | ||
} else { | ||
processMetaField(key, val, meta); | ||
} | ||
Object.keys(trans).forEach(key => { | ||
const val = trans[key]; | ||
if (key === "meta") { | ||
Object.keys(trans["meta"]).forEach(keyMeta => { | ||
processMetaField(keyMeta, trans["meta"][keyMeta], meta); | ||
}); | ||
if (trans.credit) { | ||
entry.debit(trans.account_path, trans.credit, meta); | ||
} | ||
if (trans.debit) { | ||
entry.credit(trans.account_path, trans.debit, meta); | ||
} | ||
} else { | ||
processMetaField(key, val, meta); | ||
} | ||
}); | ||
return entry.commit(); | ||
if (trans.credit) { | ||
entry.debit(trans.account_path, trans.credit, meta); | ||
} | ||
); | ||
if (trans.debit) { | ||
entry.credit(trans.account_path, trans.debit, meta); | ||
} | ||
} | ||
return entry.commit(); | ||
}; | ||
journalSchema.pre("save", function(next) { | ||
journalSchema.pre("save", async function(next) { | ||
if (!(this.isModified("approved") && this.approved === true)) { | ||
@@ -181,13 +172,10 @@ return next(); | ||
return mongoose | ||
.model("Medici_Transaction") | ||
.find({ _journal: this._id }) | ||
.then(transactions => | ||
Promise.all( | ||
transactions.map(tx => { | ||
tx.approved = true; | ||
return tx.save(); | ||
}) | ||
).then(() => next()) | ||
); | ||
let transactions = await mongoose.model("Medici_Transaction").find({ _journal: this._id }); | ||
await Promise.all( | ||
transactions.map(tx => { | ||
tx.approved = true; | ||
return tx.save(); | ||
}) | ||
); | ||
return await next(); | ||
}); | ||
@@ -194,0 +182,0 @@ mongoose.model("Medici_Journal", journalSchema); |
28248
534
302