2
0
mirror of https://github.com/frappe/books.git synced 2024-11-14 01:14:03 +00:00

refactor naming and added taxes!

This commit is contained in:
Rushabh Mehta 2018-02-16 18:44:38 +05:30
parent 650ad31d12
commit 34b72f2297
30 changed files with 1741 additions and 1346 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
node_modules node_modules
.DS_Store .DS_Store
Thumbs.db Thumbs.db
test.db *test.db
*.log *.log
.cache .cache
/temp /temp

7
dist/css/style.css vendored
View File

@ -6824,7 +6824,6 @@ html {
.hide { .hide {
display: none !important; } display: none !important; }
.page { .page {
max-width: 500px;
padding-bottom: 2rem; } padding-bottom: 2rem; }
.page .page-head { .page .page-head {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
@ -6839,8 +6838,7 @@ html {
text-align: center; text-align: center;
padding: 200px 0px; } padding: 200px 0px; }
.form-body { .form-body {
padding: 1rem; padding: 1rem; }
max-width: 500px; }
.form-body .form-control.font-weight-bold { .form-body .form-control.font-weight-bold {
background-color: lightyellow; } background-color: lightyellow; }
.form-body .alert { .form-body .alert {
@ -6888,8 +6886,9 @@ mark {
margin-top: 0.5rem; } margin-top: 0.5rem; }
.data-table .body-scrollable { .data-table .body-scrollable {
border-bottom: 0px !important; } border-bottom: 0px !important; }
.data-table .body-scrollable tr:first-child .data-table-col {
border-top: 0px !important; }
.data-table thead td { .data-table thead td {
border-bottom: 0px !important;
background-color: #e9ecef !important; } background-color: #e9ecef !important; }
.data-table .data-table-col .edit-cell { .data-table .data-table-col .edit-cell {
padding: 0px !important; } padding: 0px !important; }

2337
dist/js/bundle.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
const frappe = require('frappejs');
const BaseDocument = require('frappejs/model/document');
module.exports = class Account extends BaseDocument {
async validate() {
if (!this.account_type) {
if (this.parent_account) {
this.account_type = await frappe.db.getValue('Account', this.parent_account, 'account_type');
} else {
this.account_type = 'Asset';
}
}
}
}

View File

@ -0,0 +1,15 @@
const BaseForm = require('frappejs/client/view/form');
module.exports = class AccountForm extends BaseForm {
make() {
super.make();
// override controller event
this.controls['parent_account'].getFilters = (query) => {
return {
keywords: ["like", query],
name: ["!=", this.doc.name]
}
}
}
}

View File

@ -0,0 +1,10 @@
const BaseList = require('frappejs/client/view/list');
module.exports = class AccountList extends BaseList {
getFields() {
return ['name', 'account_type'];
}
getRowHTML(data) {
return `<a href="#edit/account/${data.name}">${data.name} (${data.account_type})</a>`;
}
}

View File

@ -0,0 +1,68 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
module.exports = class Invoice extends BaseDocument {
async getRowTax(row) {
if (row.tax) {
let tax = await this.getTax(row.tax);
let taxAmount = [];
for (let d of (tax.details || [])) {
taxAmount.push({account: d.account, rate: d.rate, amount: row.amount * d.rate / 100});
}
return JSON.stringify(taxAmount);
} else {
return '';
}
}
async getTax(tax) {
if (!this._taxes) this._taxes = {};
if (!this._taxes[tax]) this._taxes[tax] = await frappe.getDoc('Tax', tax);
return this._taxes[tax];
}
makeTaxSummary() {
if (!this.taxes) this.taxes = [];
// reset tax amount
this.taxes.map(d => d.amount = 0);
// calculate taxes
for (let row of this.items) {
if (row.taxAmount) {
let taxAmount = JSON.parse(row.taxAmount);
for (let rowTaxDetail of taxAmount) {
let found = false;
// check if added in summary
for (let taxDetail of this.taxes) {
if (taxDetail.account === rowTaxDetail.account) {
taxDetail.amount += rowTaxDetail.amount;
found = true;
}
}
// add new row
if (!found) {
this.taxes.push({
account: rowTaxDetail.account,
rate: rowTaxDetail.rate,
amount: rowTaxDetail.amount
});
}
}
}
}
// clear no taxes
this.taxes = this.taxes.filter(d => d.amount);
}
getGrandTotal() {
this.makeTaxSummary();
let grandTotal = this.netTotal;
if (this.taxes) {
for (let row of this.taxes) {
grandTotal += row.amount;
}
}
return grandTotal;
}
}

View File

@ -1,7 +1,7 @@
const BaseList = require('frappejs/client/view/list'); const BaseList = require('frappejs/client/view/list');
const frappe = require('frappejs'); const frappe = require('frappejs');
class InvoiceList extends BaseList { module.exports = class InvoiceList extends BaseList {
getFields() { getFields() {
return ['name', 'customer', 'total']; return ['name', 'customer', 'total'];
} }
@ -11,7 +11,3 @@ class InvoiceList extends BaseList {
<div class="col-4 text-muted text-right">${frappe.format(data.total, {fieldtype:"Currency"})}</div>`; <div class="col-4 text-muted text-right">${frappe.format(data.total, {fieldtype:"Currency"})}</div>`;
} }
} }
module.exports = {
List: InvoiceList
}

View File

@ -1,5 +1,5 @@
{ module.exports = {
"name": "Invoice Item", "name": "InvoiceItem",
"doctype": "DocType", "doctype": "DocType",
"isSingle": 0, "isSingle": 0,
"isChild": 1, "isChild": 1,
@ -16,7 +16,7 @@
"fieldname": "description", "fieldname": "description",
"label": "Description", "label": "Description",
"fieldtype": "Text", "fieldtype": "Text",
"formula": "doc.getFrom('Item', row.item, 'description')", formula: (row, doc) => doc.getFrom('Item', row.item, 'description'),
"required": 1 "required": 1
}, },
{ {
@ -30,14 +30,27 @@
"label": "Rate", "label": "Rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"required": 1, "required": 1,
"formula": "doc.getFrom('Item', row.item, 'rate')" formula: (row, doc) => doc.getFrom('Item', row.item, 'rate')
},
{
"fieldname": "tax",
"label": "Tax",
"fieldtype": "Link",
"target": "Tax"
}, },
{ {
"fieldname": "amount", "fieldname": "amount",
"label": "Amount", "label": "Amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"disabled": 1, "disabled": 1,
"formula": "row.quantity * row.rate" formula: (row, doc) => row.quantity * row.rate
},
{
"fieldname": "taxAmount",
"label": "Tax Amount",
"hidden": 1,
"fieldtype": "Text",
formula: (row, doc) => doc.getRowTax(row)
} }
] ]
} }

View File

@ -1,15 +1,16 @@
{ module.exports = {
"name": "Invoice Settings", "name": "InvoiceSettings",
"label": "Invoice Settings",
"doctype": "DocType", "doctype": "DocType",
"isSingle": 1, "isSingle": 1,
"isChild": 0, "isChild": 0,
"keywordFields": [], "keywordFields": [],
"fields": [ "fields": [
{ {
"fieldname": "number_series", "fieldname": "numberSeries",
"label": "Number Series", "label": "Number Series",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Number Series", "target": "NumberSeries",
"required": 1 "required": 1
} }
] ]

22
models/doctype/Tax/Tax.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
"name": "Tax",
"doctype": "DocType",
"isSingle": 0,
"isChild": 0,
"keywordFields": ["name"],
"fields": [
{
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "details",
"label": "Details",
"fieldtype": "Table",
"childtype": "TaxDetail",
"required": 1
}
]
}

View File

@ -0,0 +1,23 @@
module.exports = {
"name": "TaxDetail",
"label": "Tax Detail",
"doctype": "DocType",
"isSingle": 0,
"isChild": 1,
"keywordFields": [],
"fields": [
{
"fieldname": "account",
"label": "Tax Account",
"fieldtype": "Link",
"target": "Account",
"required": 1
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Float",
"required": 1
}
]
}

View File

@ -0,0 +1,28 @@
module.exports = {
"name": "TaxSummary",
"doctype": "DocType",
"isSingle": 0,
"isChild": 1,
"keywordFields": [],
"fields": [
{
"fieldname": "account",
"label": "Tax Account",
"fieldtype": "Link",
"target": "Account",
"required": 1
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Float",
"required": 1
},
{
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"required": 1
}
]
}

View File

@ -1,26 +1,36 @@
const frappe = require('frappejs');
const BaseMeta = require('frappejs/model/meta');
const BaseDocument = require('frappejs/model/document');
class AccountMeta extends BaseMeta {
setupMeta() {
Object.assign(this, require('./account.json'));
}
}
class Account extends BaseDocument {
async validate() {
if (!this.account_type) {
if (this.parent_account) {
this.account_type = await frappe.db.getValue('Account', this.parent_account, 'account_type');
} else {
this.account_type = 'Asset';
}
}
}
}
module.exports = { module.exports = {
Document: Account, "name": "Account",
Meta: AccountMeta "doctype": "DocType",
}; "documentClass": require("./AccountDocument.js"),
"isSingle": 0,
"keywordFields": [
"name",
"account_type"
],
"fields": [
{
"fieldname": "name",
"label": "Account Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "parent_account",
"label": "Parent Account",
"fieldtype": "Link",
"target": "Account"
},
{
"fieldname": "account_type",
"label": "Account Type",
"fieldtype": "Select",
"options": [
"Asset",
"Liability",
"Equity",
"Income",
"Expense"
]
}
]
}

View File

@ -1,35 +0,0 @@
{
"name": "Account",
"doctype": "DocType",
"isSingle": 0,
"keywordFields": [
"name",
"account_type"
],
"fields": [
{
"fieldname": "name",
"label": "Account Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "parent_account",
"label": "Parent Account",
"fieldtype": "Link",
"target": "Account"
},
{
"fieldname": "account_type",
"label": "Account Type",
"fieldtype": "Select",
"options": [
"Asset",
"Liability",
"Equity",
"Income",
"Expense"
]
}
]
}

View File

@ -1,30 +0,0 @@
const BaseList = require('frappejs/client/view/list');
const BaseForm = require('frappejs/client/view/form');
class AccountList extends BaseList {
getFields() {
return ['name', 'account_type'];
}
getRowHTML(data) {
return `<a href="#edit/account/${data.name}">${data.name} (${data.account_type})</a>`;
}
}
class AccountForm extends BaseForm {
make() {
super.make();
// override controller event
this.controls['parent_account'].getFilters = (query) => {
return {
keywords: ["like", query],
name: ["!=", this.doc.name]
}
}
}
}
module.exports = {
Form: AccountForm,
List: AccountList
}

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="js/bundle.js"></script>
</body>
</html>

View File

@ -1,16 +1,17 @@
const BaseMeta = require('frappejs/model/meta');
const BaseDocument = require('frappejs/model/document');
class CustomerMeta extends BaseMeta {
setupMeta() {
Object.assign(this, require('./customer.json'));
}
}
class Customer extends BaseDocument {
}
module.exports = { module.exports = {
Document: Customer, "name": "Customer",
Meta: CustomerMeta "doctype": "DocType",
}; "isSingle": 0,
"istable": 0,
"keywordFields": [
"name"
],
"fields": [
{
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"required": 1
}
]
}

View File

@ -1,17 +0,0 @@
{
"name": "Customer",
"doctype": "DocType",
"isSingle": 0,
"istable": 0,
"keywordFields": [
"name"
],
"fields": [
{
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"required": 1
}
]
}

View File

@ -1,16 +1,53 @@
const BaseMeta = require('frappejs/model/meta');
const BaseDocument = require('frappejs/model/document');
class InvoiceMeta extends BaseMeta {
setupMeta() {
Object.assign(this, require('./invoice.json'));
}
}
class Invoice extends BaseDocument {
}
module.exports = { module.exports = {
Document: Invoice, "name": "Invoice",
Meta: InvoiceMeta "doctype": "DocType",
}; "documentClass": require("./InvoiceDocument.js"),
"isSingle": 0,
"istable": 0,
"keywordFields": ["name", "customer"],
"settings": "InvoiceSettings",
"showTitle": true,
"fields": [
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date"
},
{
"fieldname": "customer",
"label": "Customer",
"fieldtype": "Link",
"target": "Customer",
"required": 1
},
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"childtype": "InvoiceItem",
"required": true
},
{
"fieldname": "netTotal",
"label": "Total",
"fieldtype": "Currency",
formula: (doc) => doc.getSum('items', 'amount'),
"disabled": true
},
{
"fieldname": "taxes",
"label": "Taxes",
"fieldtype": "Table",
"childtype": "TaxSummary",
"disabled": true,
"template": `<div></div>`
},
{
"fieldname": "grandTotal",
"label": "Total",
"fieldtype": "Currency",
formula: (doc) => doc.getGrandTotal(),
"disabled": true
}
]
}

View File

@ -1,38 +0,0 @@
{
"name": "Invoice",
"doctype": "DocType",
"isSingle": 0,
"istable": 0,
"keywordFields": [],
"settings": "Invoice Settings",
"showTitle": true,
"fields": [
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date"
},
{
"fieldname": "customer",
"label": "Customer",
"fieldtype": "Link",
"target": "Customer",
"required": 1
},
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"childtype": "Invoice Item",
"required": true
},
{
"fieldname": "total",
"label": "Total",
"fieldtype": "Currency",
"formula": "doc.getSum('items', 'amount')",
"required": true,
"disabled": true
}
]
}

View File

@ -1,16 +0,0 @@
const BaseMeta = require('frappejs/model/meta');
const BaseDocument = require('frappejs/model/document');
class InvoiceItemMeta extends BaseMeta {
setupMeta() {
Object.assign(this, require('./invoice_item.json'));
}
}
class InvoiceItem extends BaseDocument {
}
module.exports = {
Document: InvoiceItem,
Meta: InvoiceItemMeta
};

View File

@ -1,16 +0,0 @@
const BaseMeta = require('frappejs/model/meta');
const BaseDocument = require('frappejs/model/document');
class InvoiceSettingsMeta extends BaseMeta {
setupMeta() {
Object.assign(this, require('./invoice_settings.json'));
}
}
class InvoiceSettings extends BaseDocument {
}
module.exports = {
Document: InvoiceSettings,
Meta: InvoiceSettingsMeta
};

View File

@ -1,16 +1,45 @@
const BaseMeta = require('frappejs/model/meta');
const BaseDocument = require('frappejs/model/document');
class ItemMeta extends BaseMeta {
setupMeta() {
Object.assign(this, require('./item.json'));
}
}
class Item extends BaseDocument {
}
module.exports = { module.exports = {
Document: Item, "name": "Item",
Meta: ItemMeta "doctype": "DocType",
}; "isSingle": 0,
"keywordFields": [
"name",
"description"
],
"fields": [
{
"fieldname": "name",
"label": "Item Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
},
{
"fieldname": "unit",
"label": "Unit",
"fieldtype": "Select",
"options": [
"No",
"Kg",
"Gram",
"Hour",
"Day"
]
},
{
"fieldname": "tax",
"label": "Tax",
"fieldtype": "Link",
"target": "Tax"
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency"
}
]
}

View File

@ -1,39 +0,0 @@
{
"name": "Item",
"doctype": "DocType",
"isSingle": 0,
"keywordFields": [
"name",
"description"
],
"fields": [
{
"fieldname": "name",
"label": "Item Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
},
{
"fieldname": "unit",
"label": "Unit",
"fieldtype": "Select",
"options": [
"No",
"Kg",
"Gram",
"Hour",
"Day"
]
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency"
}
]
}

11
models/index.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
Account: require('./doctype/Account/Account.js'),
Customer: require('./doctype/Customer/Customer.js'),
Item: require('./doctype/Item/Item.js'),
Invoice: require('./doctype/Invoice/Invoice.js'),
InvoiceItem: require('./doctype/InvoiceItem/InvoiceItem.js'),
InvoiceSettings: require('./doctype/InvoiceSettings/InvoiceSettings.js'),
Tax: require('./doctype/Tax/Tax.js'),
TaxDetail: require('./doctype/TaxDetail/TaxDetail.js'),
TaxSummary: require('./doctype/TaxSummary/TaxSummary.js')
}

View File

@ -3,6 +3,10 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"scripts": {
"test": "mocha tests",
"start": "nodemon server.js"
},
"dependencies": { "dependencies": {
"awesomplete": "^1.1.2", "awesomplete": "^1.1.2",
"body-parser": "^1.18.2", "body-parser": "^1.18.2",

View File

@ -3,7 +3,7 @@ const path = require('path');
server.start({ server.start({
backend: 'sqlite', backend: 'sqlite',
connection_params: {dbPath: 'test.db'}, connectionParams: {dbPath: 'test.db'},
static: './', static: './',
models_path: path.resolve(__dirname, './models') models: require('./models')
}); });

View File

@ -7,26 +7,20 @@ client.start({
}).then(() => { }).then(() => {
// require modules // require modules
frappe.modules.todo = require('frappejs/models/doctype/todo/todo.js'); frappe.registerModels(require('../models'));
frappe.modules.number_series = require('frappejs/models/doctype/number_series/number_series.js');
frappe.modules.account = require('../models/doctype/account/account.js');
frappe.modules.item = require('../models/doctype/item/item.js');
frappe.modules.customer = require('../models/doctype/customer/customer.js');
frappe.modules.invoice = require('../models/doctype/invoice/invoice.js');
frappe.modules.invoice_item = require('../models/doctype/invoice_item/invoice_item.js');
frappe.modules.invoice_settings = require('../models/doctype/invoice_settings/invoice_settings.js');
frappe.modules.todo_client = require('frappejs/models/doctype/todo/todo_client.js'); frappe.registerView('List', 'ToDo', require('frappejs/models/doctype/ToDo/ToDoList.js'));
frappe.modules.account_client = require('../models/doctype/account/account_client.js'); frappe.registerView('List', 'Account', require('../models/doctype/Account/AccountList.js'));
frappe.modules.invoice_client = require('../models/doctype/invoice/invoiceClient.js'); frappe.registerView('Form', 'Account', require('../models/doctype/Account/AccountForm.js'));
frappe.registerView('List', 'Invoice', require('../models/doctype/Invoice/InvoiceList.js'));
frappe.desk.menu.addItem('ToDo', '#list/todo'); frappe.desk.menu.addItem('ToDo', '#list/ToDo');
frappe.desk.menu.addItem('Accounts', '#list/account'); frappe.desk.menu.addItem('Accounts', '#list/Account');
frappe.desk.menu.addItem('Items', '#list/item'); frappe.desk.menu.addItem('Items', '#list/Item');
frappe.desk.menu.addItem('Customers', '#list/customer'); frappe.desk.menu.addItem('Customers', '#list/Customer');
frappe.desk.menu.addItem('Invoice', '#list/invoice'); frappe.desk.menu.addItem('Invoice', '#list/Invoice');
frappe.router.default = '#list/todo'; frappe.router.default = '#list/ToDo';
frappe.router.show(window.location.hash); frappe.router.show(window.location.hash);
}); });

57
tests/testInvoice.js Normal file
View File

@ -0,0 +1,57 @@
const assert = require('assert');
const frappe = require('frappejs');
const helpers = require('frappejs/tests/helpers');
const path = require('path');
const models = require('../models');
async function makeFixtures() {
if (!(await frappe.db.exists('Customer', 'Test Customer'))) {
await frappe.insert({doctype:'Customer', name:'Test Customer'})
await frappe.insert({doctype:'Item', name:'Test Item 1', description:'Test Item Description 1', unit:'No', rate: 100})
await frappe.insert({doctype:'Item', name:'Test Item 2', description:'Test Item Description 2', unit:'No', rate: 200})
await frappe.insert({doctype:'Account', name:'GST', parent_account: 'Liabilities'});
await frappe.insert({doctype:'Tax', name:'GST',
details: [{account: 'GST', rate:10}]
})
}
}
describe('Invoice', () => {
before(async function() {
await helpers.initSqlite({models: models});
await makeFixtures();
});
it('show create an invoice', async () => {
let invoice = await frappe.insert({
doctype:'Invoice',
customer: 'Test Customer',
items: [
{item: 'Test Item 1', quantity: 5},
{item: 'Test Item 2', quantity: 7},
]
});
assert.equal(invoice.items[0].amount, 500);
assert.equal(invoice.items[1].amount, 1400);
assert.equal(invoice.netTotal, 1900);
});
it('show create an invoice with tax', async () => {
let invoice = await frappe.insert({
doctype:'Invoice',
customer: 'Test Customer',
items: [
{item: 'Test Item 1', quantity: 5, tax: 'GST'},
{item: 'Test Item 2', quantity: 7, tax: 'GST'},
]
});
assert.equal(invoice.items[0].amount, 500);
assert.equal(invoice.items[1].amount, 1400);
assert.equal(invoice.netTotal, 1900);
assert.equal(invoice.taxes[0].amount, 190);
assert.equal(invoice.grandTotal, 2090);
});
});