mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
added ledger
This commit is contained in:
parent
c5b9ae8691
commit
b72878110d
83
accounting/ledgerPosting.js
Normal file
83
accounting/ledgerPosting.js
Normal 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
67
dist/css/style.css
vendored
@ -1,3 +1,4 @@
|
||||
@charset "UTF-8";
|
||||
/*!
|
||||
* Bootstrap v4.0.0 (https://getbootstrap.com)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
@ -7129,10 +7130,10 @@ div.CodeMirror-dragcursors {
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext {
|
||||
background: none; }
|
||||
/* This file is processed by postcss */
|
||||
/* variables */
|
||||
.data-table {
|
||||
/* styling */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: auto; }
|
||||
/* resets */
|
||||
@ -7184,16 +7185,22 @@ span.CodeMirror-selectedtext {
|
||||
top: 50%;
|
||||
-webkit-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 {
|
||||
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 {
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
@ -7256,42 +7263,54 @@ span.CodeMirror-selectedtext {
|
||||
padding: 0.5rem 1rem; }
|
||||
.data-table-header .data-table-dropdown-list > div:hover {
|
||||
background-color: #f5f7fa; }
|
||||
.data-table-header .data-table-col.remove-column {
|
||||
.data-table-header .data-table-cell.remove-column {
|
||||
background-color: #FD8B8B;
|
||||
-webkit-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; }
|
||||
.data-table-col {
|
||||
.data-table-cell {
|
||||
position: relative; }
|
||||
.data-table-col .content {
|
||||
.data-table-cell .content {
|
||||
padding: 8px;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid transparent; }
|
||||
.data-table-col .content.ellipsis {
|
||||
.data-table-cell .content.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden; }
|
||||
.data-table-col .edit-cell {
|
||||
.data-table-cell .edit-cell {
|
||||
display: none;
|
||||
padding: 8px;
|
||||
padding: 0.5rem;
|
||||
background: #fff;
|
||||
z-index: 1;
|
||||
height: 100%; }
|
||||
.data-table-col.selected .content {
|
||||
.data-table-cell.selected .content {
|
||||
border: 2px solid #5292f7; }
|
||||
.data-table-col.editing .content {
|
||||
.data-table-cell.editing .content {
|
||||
display: none; }
|
||||
.data-table-col.editing .edit-cell {
|
||||
.data-table-cell.editing .edit-cell {
|
||||
border: 2px solid #5292f7;
|
||||
display: block; }
|
||||
.data-table-col.highlight {
|
||||
.data-table-cell.highlight {
|
||||
background-color: #f5f7fa; }
|
||||
.data-table-col:hover .column-resizer {
|
||||
.data-table-cell:hover .column-resizer {
|
||||
display: inline-block; }
|
||||
.data-table-col:hover .data-table-dropdown-toggle {
|
||||
.data-table-cell:hover .data-table-dropdown-toggle {
|
||||
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 {
|
||||
background-color: #f5f7fa; }
|
||||
.noselect {
|
||||
@ -7363,6 +7382,12 @@ html {
|
||||
padding: 200px 0px; }
|
||||
.form-body {
|
||||
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 {
|
||||
background-color: lightyellow; }
|
||||
.form-body .alert {
|
||||
|
5979
dist/js/bundle.js
vendored
5979
dist/js/bundle.js
vendored
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
},
|
||||
]
|
||||
}
|
@ -23,9 +23,15 @@ module.exports = {
|
||||
"fieldname": "customer",
|
||||
"label": "Customer",
|
||||
"fieldtype": "Link",
|
||||
"target": "Customer",
|
||||
"target": "Party",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": "Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "items",
|
||||
"label": "Items",
|
||||
@ -72,5 +78,24 @@ module.exports = {
|
||||
"label": "Terms",
|
||||
"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" ] },
|
||||
]
|
||||
}
|
31
models/doctype/Invoice/InvoiceServer.js
Normal file
31
models/doctype/Invoice/InvoiceServer.js
Normal 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();
|
||||
}
|
||||
}
|
@ -2,10 +2,6 @@ const BaseDocument = require('frappejs/model/document');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
module.exports = class Invoice extends BaseDocument {
|
||||
async afterSubmit() {
|
||||
// make ledger entry
|
||||
}
|
||||
|
||||
async getRowTax(row) {
|
||||
if (row.tax) {
|
||||
let tax = await this.getTax(row.tax);
|
||||
@ -62,6 +58,7 @@ module.exports = class Invoice extends BaseDocument {
|
||||
// clear no taxes
|
||||
this.taxes = this.taxes.filter(d => d.amount);
|
||||
}
|
||||
|
||||
getGrandTotal() {
|
||||
this.makeTaxSummary();
|
||||
let grandTotal = this.netTotal;
|
||||
|
@ -1,10 +1,11 @@
|
||||
module.exports = {
|
||||
"name": "InvoiceItem",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"isChild": 1,
|
||||
"keywordFields": [],
|
||||
"fields": [
|
||||
name: "InvoiceItem",
|
||||
doctype: "DocType",
|
||||
isSingle: 0,
|
||||
isChild: 1,
|
||||
keywordFields: [],
|
||||
layout: 'ratio',
|
||||
fields: [
|
||||
{
|
||||
"fieldname": "item",
|
||||
"label": "Item",
|
||||
@ -32,6 +33,14 @@ module.exports = {
|
||||
"required": 1,
|
||||
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",
|
||||
"label": "Tax",
|
||||
|
@ -1,28 +1,29 @@
|
||||
module.exports = {
|
||||
"name": "Item",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"keywordFields": [
|
||||
name: "Item",
|
||||
doctype: "DocType",
|
||||
isSingle: 0,
|
||||
keywordFields: [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
fields: [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"label": "Item Name",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
fieldname: "name",
|
||||
label: "Item Name",
|
||||
fieldtype: "Data",
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"label": "Description",
|
||||
"fieldtype": "Text"
|
||||
fieldname: "description",
|
||||
label: "Description",
|
||||
fieldtype: "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "unit",
|
||||
"label": "Unit",
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
fieldname: "unit",
|
||||
label: "Unit",
|
||||
fieldtype: "Select",
|
||||
default: "No",
|
||||
options: [
|
||||
"No",
|
||||
"Kg",
|
||||
"Gram",
|
||||
@ -31,15 +32,48 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
{
|
||||
"fieldname": "tax",
|
||||
"label": "Tax",
|
||||
"fieldtype": "Link",
|
||||
"target": "Tax"
|
||||
fieldname: "incomeAccount",
|
||||
label: "Income Account",
|
||||
fieldtype: "Link",
|
||||
target: "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"label": "Rate",
|
||||
"fieldtype": "Currency"
|
||||
fieldname: "expenseAccount",
|
||||
label: "Expense Account",
|
||||
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" ] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
13
models/doctype/Party/CustomerList.js
Normal file
13
models/doctype/Party/CustomerList.js
Normal 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;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
"name": "Customer",
|
||||
"name": "Party",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"istable": 0,
|
||||
@ -12,6 +12,16 @@ module.exports = {
|
||||
"label": "Name",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer",
|
||||
"label": "Customer",
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier",
|
||||
"label": "Supplier",
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
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')
|
||||
models: {
|
||||
Account: require('./doctype/Account/Account.js'),
|
||||
AccountingLedgerEntry: require('./doctype/AccountingLedgerEntry/AccountingLedgerEntry.js'),
|
||||
Party: require('./doctype/Party/Party.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')
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frappe-accounting",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"main": "server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "mocha tests",
|
||||
|
@ -7,5 +7,9 @@ server.start({
|
||||
static: './',
|
||||
models: require('./models')
|
||||
}).then(() => {
|
||||
// set server-side modules
|
||||
frappe.models.Invoice.documentClass = require('./models/doctype/Invoice/InvoiceServer.js');
|
||||
frappe.metaCache = {};
|
||||
|
||||
frappe.syncDoc(require('./fixtures/invoicePrint'));
|
||||
});
|
@ -7,12 +7,13 @@ client.start({
|
||||
}).then(() => {
|
||||
|
||||
// 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', 'Account', require('../models/doctype/Account/AccountList.js'));
|
||||
frappe.registerView('Form', 'Account', require('../models/doctype/Account/AccountForm.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('Accounts', '#list/Account');
|
||||
|
@ -4,8 +4,8 @@ const helpers = require('frappejs/tests/helpers');
|
||||
const models = require('../models');
|
||||
|
||||
async function makeFixtures() {
|
||||
if (!(await frappe.db.exists('Customer', 'Test Customer'))) {
|
||||
await frappe.insert({doctype:'Customer', name:'Test Customer'})
|
||||
if (!(await frappe.db.exists('Party', '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 2', description:'Test Item Description 2', unit:'No', rate: 200})
|
||||
await frappe.insert({doctype:'Account', name:'GST', parent_account: 'Liabilities'});
|
||||
|
Loading…
Reference in New Issue
Block a user