2
0
mirror of https://github.com/frappe/books.git synced 2025-03-31 23:41:32 +00:00

added ledger

This commit is contained in:
Rushabh Mehta 2018-03-05 22:15:40 +05:30
parent c5b9ae8691
commit b72878110d
16 changed files with 3762 additions and 2657 deletions

View File

@ -0,0 +1,83 @@
const frappe = require('frappejs');
module.exports = class LedgerPosting {
constructor({reference, party, date, description}) {
Object.assign(this, arguments[0]);
this.entries = [];
this.entryMap = {};
}
debit(account, amount) {
const entry = this.getEntry(account);
entry.debit += amount;
}
credit(account, amount) {
const entry = this.getEntry(account);
entry.credit += amount;
}
getEntry(account) {
if (!this.entryMap[account]) {
const entry = {
account: account,
party: this.party,
date: this.date || this.reference.date,
referenceType: this.reference.doctype,
referenceName: this.reference.name,
description: this.description,
debit: 0,
credit: 0
};
this.entries.push(entry);
this.entryMap[account] = entry;
}
return this.entryMap[account];
}
async post() {
this.validateEntries();
await this.insertEntries();
}
async postReverse() {
this.validateEntries();
let temp;
for (let entry of this.entries) {
temp = entry.debit;
entry.debit = entry.credit;
entry.credit = temp;
}
await this.insertEntries();
}
validateEntries() {
let debit = 0, credit = 0;
let debitAccounts = [], creditAccounts = [];
for (let entry of this.entries) {
debit += entry.debit;
credit += entry.credit;
if (debit) {
debitAccounts.push(entry.account);
} else {
creditAccounts.push(entry.account);
}
}
if (debit !== credit) {
throw frappe.errors.ValidationError(frappe._("Debit {0} must be equal to Credit {1}", [debit, credit]));
}
}
async insertEntries() {
for (let entry of this.entries) {
let entryDoc = frappe.newDoc({
doctype: 'AccountingLedgerEntry'
});
Object.assign(entryDoc, entry);
await entryDoc.insert();
}
}
}

67
dist/css/style.css vendored
View File

@ -1,3 +1,4 @@
@charset "UTF-8";
/*! /*!
* Bootstrap v4.0.0 (https://getbootstrap.com) * Bootstrap v4.0.0 (https://getbootstrap.com)
* Copyright 2011-2018 The Bootstrap Authors * Copyright 2011-2018 The Bootstrap Authors
@ -7129,10 +7130,10 @@ div.CodeMirror-dragcursors {
/* Help users use markselection to safely style text background */ /* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { span.CodeMirror-selectedtext {
background: none; } background: none; }
/* This file is processed by postcss */
/* variables */ /* variables */
.data-table { .data-table {
/* styling */ /* styling */
width: 100%;
position: relative; position: relative;
overflow: auto; } overflow: auto; }
/* resets */ /* resets */
@ -7184,16 +7185,22 @@ span.CodeMirror-selectedtext {
top: 50%; top: 50%;
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
transform: translateY(-50%); } transform: translateY(-50%); }
.data-table .trash-container {
position: absolute;
bottom: 0;
left: 30%;
right: 30%;
height: 70px;
background: palevioletred;
opacity: 0.5; }
.data-table .hide { .data-table .hide {
display: none; } display: none; }
.data-table .toast-message {
position: absolute;
bottom: 16px;
bottom: 1rem;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%); }
.data-table .toast-message span {
display: inline-block;
background-color: rgba(0, 0, 0, 0.8);
color: #dfe2e5;
border-radius: 3px;
padding: 8px 16px;
padding: 0.5rem 1rem; }
.body-scrollable { .body-scrollable {
max-height: 500px; max-height: 500px;
overflow: auto; overflow: auto;
@ -7256,42 +7263,54 @@ span.CodeMirror-selectedtext {
padding: 0.5rem 1rem; } padding: 0.5rem 1rem; }
.data-table-header .data-table-dropdown-list > div:hover { .data-table-header .data-table-dropdown-list > div:hover {
background-color: #f5f7fa; } background-color: #f5f7fa; }
.data-table-header .data-table-col.remove-column { .data-table-header .data-table-cell.remove-column {
background-color: #FD8B8B; background-color: #FD8B8B;
-webkit-transition: 300ms background-color ease-in-out; -webkit-transition: 300ms background-color ease-in-out;
transition: 300ms background-color ease-in-out; } transition: 300ms background-color ease-in-out; }
.data-table-header .data-table-col.sortable-chosen { .data-table-header .data-table-cell.sortable-chosen {
background-color: #f5f7fa; } background-color: #f5f7fa; }
.data-table-col { .data-table-cell {
position: relative; } position: relative; }
.data-table-col .content { .data-table-cell .content {
padding: 8px; padding: 8px;
padding: 0.5rem; padding: 0.5rem;
border: 2px solid transparent; } border: 2px solid transparent; }
.data-table-col .content.ellipsis { .data-table-cell .content.ellipsis {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; } overflow: hidden; }
.data-table-col .edit-cell { .data-table-cell .edit-cell {
display: none; display: none;
padding: 8px; padding: 8px;
padding: 0.5rem; padding: 0.5rem;
background: #fff; background: #fff;
z-index: 1; z-index: 1;
height: 100%; } height: 100%; }
.data-table-col.selected .content { .data-table-cell.selected .content {
border: 2px solid #5292f7; } border: 2px solid #5292f7; }
.data-table-col.editing .content { .data-table-cell.editing .content {
display: none; } display: none; }
.data-table-col.editing .edit-cell { .data-table-cell.editing .edit-cell {
border: 2px solid #5292f7; border: 2px solid #5292f7;
display: block; } display: block; }
.data-table-col.highlight { .data-table-cell.highlight {
background-color: #f5f7fa; } background-color: #f5f7fa; }
.data-table-col:hover .column-resizer { .data-table-cell:hover .column-resizer {
display: inline-block; } display: inline-block; }
.data-table-col:hover .data-table-dropdown-toggle { .data-table-cell:hover .data-table-dropdown-toggle {
display: block; } display: block; }
.data-table-cell .tree-node {
display: inline-block;
position: relative; }
.data-table-cell .toggle {
display: inline-block;
position: absolute;
padding: 0 4px;
cursor: pointer; }
.data-table-cell .toggle:before {
content: '▼'; }
.data-table-cell.tree-close .toggle:before {
content: '►'; }
.data-table-row.row-highlight { .data-table-row.row-highlight {
background-color: #f5f7fa; } background-color: #f5f7fa; }
.noselect { .noselect {
@ -7363,6 +7382,12 @@ html {
padding: 200px 0px; } padding: 200px 0px; }
.form-body { .form-body {
padding: 2rem; } padding: 2rem; }
.form-body .form-check {
margin-bottom: 0.5rem; }
.form-body .form-check .form-check-input {
margin-top: 0.25rem; }
.form-body .form-check .form-check-label {
margin-left: 0.25rem; }
.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 {

5979
dist/js/bundle.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,67 @@
module.exports = {
name: "AccountingLedgerEntry",
label: "Ledger Entry",
naming: "autoincrement",
doctype: "DocType",
isSingle: 0,
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: "date",
label: "Date",
fieldtype: "Date"
},
{
fieldname: "account",
label: "Account",
fieldtype: "Link",
target: "Account",
required: 1
},
{
fieldname: "description",
label: "Description",
fieldtype: "Text"
},
{
fieldname: "party",
label: "Party",
fieldtype: "Link",
target: "Party",
required: 1
},
{
fieldname: "debit",
label: "Debit",
fieldtype: "Currency",
},
{
fieldname: "credit",
label: "Credit",
fieldtype: "Currency",
},
{
fieldname: "againstAccount",
label: "Against Account",
fieldtype: "Text",
required: 1
},
{
fieldname: "referenceType",
label: "Reference Type",
fieldtype: "Data",
},
{
fieldname: "referenceName",
label: "Reference Name",
fieldtype: "Dynamic Link",
references: "referenceType"
},
{
fieldname: "balance",
label: "Balance",
fieldtype: "Currency",
},
]
}

View File

@ -23,9 +23,15 @@ module.exports = {
"fieldname": "customer", "fieldname": "customer",
"label": "Customer", "label": "Customer",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Customer", "target": "Party",
"required": 1 "required": 1
}, },
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account"
},
{ {
"fieldname": "items", "fieldname": "items",
"label": "Items", "label": "Items",
@ -72,5 +78,24 @@ module.exports = {
"label": "Terms", "label": "Terms",
"fieldtype": "Text" "fieldtype": "Text"
} }
],
layout: [
// section 1
{
columns: [
{ fields: [ "customer", "account" ] },
{ fields: [ "date" ] }
]
},
// section 2
{ fields: [ "items" ] },
// section 3
{ fields: [ "netTotal", "taxes", "grandTotal" ] },
// section 4
{ fields: [ "terms" ] },
] ]
} }

View File

@ -0,0 +1,31 @@
const Invoice = require('./invoiceDocument');
const frappe = require('frappejs');
const LedgerPosting = require.main.require('./accounting/ledgerPosting');
module.exports = class InvoiceServer extends Invoice {
getPosting() {
let entries = new LedgerPosting({reference: this, party: this.customer});
entries.debit(this.account, this.grandTotal);
for (let item of this.items) {
entries.credit(item.account, item.amount);
}
if (this.taxes) {
for (let tax of this.taxes) {
entries.credit(tax.account, tax.amount);
}
}
return entries;
}
async afterSubmit() {
console.log('here');
await this.getPosting().post();
}
async afterRevert() {
await this.getPosting().postReverse();
}
}

View File

@ -2,10 +2,6 @@ const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs'); const frappe = require('frappejs');
module.exports = class Invoice extends BaseDocument { module.exports = class Invoice extends BaseDocument {
async afterSubmit() {
// make ledger entry
}
async getRowTax(row) { async getRowTax(row) {
if (row.tax) { if (row.tax) {
let tax = await this.getTax(row.tax); let tax = await this.getTax(row.tax);
@ -62,6 +58,7 @@ module.exports = class Invoice extends BaseDocument {
// clear no taxes // clear no taxes
this.taxes = this.taxes.filter(d => d.amount); this.taxes = this.taxes.filter(d => d.amount);
} }
getGrandTotal() { getGrandTotal() {
this.makeTaxSummary(); this.makeTaxSummary();
let grandTotal = this.netTotal; let grandTotal = this.netTotal;

View File

@ -1,10 +1,11 @@
module.exports = { module.exports = {
"name": "InvoiceItem", name: "InvoiceItem",
"doctype": "DocType", doctype: "DocType",
"isSingle": 0, isSingle: 0,
"isChild": 1, isChild: 1,
"keywordFields": [], keywordFields: [],
"fields": [ layout: 'ratio',
fields: [
{ {
"fieldname": "item", "fieldname": "item",
"label": "Item", "label": "Item",
@ -32,6 +33,14 @@ module.exports = {
"required": 1, "required": 1,
formula: (row, doc) => doc.getFrom('Item', row.item, 'rate') formula: (row, doc) => doc.getFrom('Item', row.item, 'rate')
}, },
{
fieldname: "account",
label: "Account",
hidden: 1,
fieldtype: "Link",
target: "Account",
formula: (row, doc) => doc.getFrom('Item', row.item, 'incomeAccount')
},
{ {
"fieldname": "tax", "fieldname": "tax",
"label": "Tax", "label": "Tax",

View File

@ -1,28 +1,29 @@
module.exports = { module.exports = {
"name": "Item", name: "Item",
"doctype": "DocType", doctype: "DocType",
"isSingle": 0, isSingle: 0,
"keywordFields": [ keywordFields: [
"name", "name",
"description" "description"
], ],
"fields": [ fields: [
{ {
"fieldname": "name", fieldname: "name",
"label": "Item Name", label: "Item Name",
"fieldtype": "Data", fieldtype: "Data",
"required": 1 required: 1
}, },
{ {
"fieldname": "description", fieldname: "description",
"label": "Description", label: "Description",
"fieldtype": "Text" fieldtype: "Text"
}, },
{ {
"fieldname": "unit", fieldname: "unit",
"label": "Unit", label: "Unit",
"fieldtype": "Select", fieldtype: "Select",
"options": [ default: "No",
options: [
"No", "No",
"Kg", "Kg",
"Gram", "Gram",
@ -31,15 +32,48 @@ module.exports = {
] ]
}, },
{ {
"fieldname": "tax", fieldname: "incomeAccount",
"label": "Tax", label: "Income Account",
"fieldtype": "Link", fieldtype: "Link",
"target": "Tax" target: "Account"
}, },
{ {
"fieldname": "rate", fieldname: "expenseAccount",
"label": "Rate", label: "Expense Account",
"fieldtype": "Currency" fieldtype: "Link",
target: "Account"
},
{
fieldname: "tax",
label: "Tax",
fieldtype: "Link",
target: "Tax"
},
{
fieldname: "rate",
label: "Rate",
fieldtype: "Currency"
}
],
layout: [
// section 1
{
columns: [
{ fields: [ "name", "unit" ] },
{ fields: [ "rate" ] }
]
},
// section 2
{ fields: [ "description" ] },
// section 3
{
title: "Accounting",
columns: [
{ fields: [ "incomeAccount", "expenseAccount" ] },
{ fields: [ "tax" ] }
]
} }
] ]
} }

View File

@ -0,0 +1,13 @@
const BaseList = require('frappejs/client/view/list');
const frappe = require('frappejs');
module.exports = class CustomerList extends BaseList {
constructor({doctype, parent, fields, page}) {
super({doctype: 'Party', parent: parent, fields: fields, page: page});
}
getFilters() {
let filters = super.getFilters();
filters.customer = 1;
return filters;
}
}

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
"name": "Customer", "name": "Party",
"doctype": "DocType", "doctype": "DocType",
"isSingle": 0, "isSingle": 0,
"istable": 0, "istable": 0,
@ -12,6 +12,16 @@ module.exports = {
"label": "Name", "label": "Name",
"fieldtype": "Data", "fieldtype": "Data",
"required": 1 "required": 1
},
{
"fieldname": "customer",
"label": "Customer",
"fieldtype": "Check"
},
{
"fieldname": "supplier",
"label": "Supplier",
"fieldtype": "Check"
} }
] ]
} }

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "frappe-accounting", "name": "frappe-accounting",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "server.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "mocha tests", "test": "mocha tests",

View File

@ -7,5 +7,9 @@ server.start({
static: './', static: './',
models: require('./models') models: require('./models')
}).then(() => { }).then(() => {
// set server-side modules
frappe.models.Invoice.documentClass = require('./models/doctype/Invoice/InvoiceServer.js');
frappe.metaCache = {};
frappe.syncDoc(require('./fixtures/invoicePrint')); frappe.syncDoc(require('./fixtures/invoicePrint'));
}); });

View File

@ -7,12 +7,13 @@ client.start({
}).then(() => { }).then(() => {
// require modules // require modules
frappe.registerModels(require('../models')); frappe.registerModels(require('../models'), 'client');
frappe.registerView('List', 'ToDo', require('frappejs/models/doctype/ToDo/ToDoList.js')); frappe.registerView('List', 'ToDo', require('frappejs/models/doctype/ToDo/ToDoList.js'));
frappe.registerView('List', 'Account', require('../models/doctype/Account/AccountList.js')); frappe.registerView('List', 'Account', require('../models/doctype/Account/AccountList.js'));
frappe.registerView('Form', 'Account', require('../models/doctype/Account/AccountForm.js')); frappe.registerView('Form', 'Account', require('../models/doctype/Account/AccountForm.js'));
frappe.registerView('List', 'Invoice', require('../models/doctype/Invoice/InvoiceList.js')); frappe.registerView('List', 'Invoice', require('../models/doctype/Invoice/InvoiceList.js'));
frappe.registerView('List', 'Customer', require('../models/doctype/Party/CustomerList.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');

View File

@ -4,8 +4,8 @@ const helpers = require('frappejs/tests/helpers');
const models = require('../models'); const models = require('../models');
async function makeFixtures() { async function makeFixtures() {
if (!(await frappe.db.exists('Customer', 'Test Customer'))) { if (!(await frappe.db.exists('Party', 'Test Customer'))) {
await frappe.insert({doctype:'Customer', name:'Test Customer'}) await frappe.insert({doctype:'Party', 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 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:'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:'Account', name:'GST', parent_account: 'Liabilities'});