2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 23:30:56 +00:00

fix: SalesInvoice

- Make round off entry to balance debit and credit with an allowance of 0.5
- Add Write Off and Round Off Account in AccountingSettings
- Generate Tax Summary without intermediate JSON field
This commit is contained in:
Faris Ansari 2019-12-03 13:40:42 +05:30
parent d645ff31b7
commit 049432dbf1
8 changed files with 157 additions and 180 deletions

View File

@ -1,4 +1,5 @@
const frappe = require('frappejs');
const { round } = require('frappejs/utils/numberFormat');
module.exports = class LedgerPosting {
constructor({ reference, party, date, description }) {
@ -78,35 +79,47 @@ module.exports = class LedgerPosting {
await this.insertEntries();
}
makeRoundOffEntry() {
let { debit, credit } = this.getTotalDebitAndCredit();
let precision = this.getPrecision();
let difference = round(debit - credit, precision);
let absoluteValue = Math.abs(difference);
let allowance = 0.5;
if (absoluteValue === 0) {
return;
}
let roundOffAccount = this.getRoundOffAccount();
if (absoluteValue <= allowance) {
if (difference > 0) {
this.credit(roundOffAccount, absoluteValue);
} else {
this.debit(roundOffAccount, absoluteValue);
}
}
}
validateEntries() {
let { debit, credit } = this.getTotalDebitAndCredit();
if (debit !== credit) {
throw new Error(`Debit ${debit} must be equal to Credit ${credit}`);
}
}
getTotalDebitAndCredit() {
let debit = 0;
let credit = 0;
let debitAccounts = [];
let creditAccounts = [];
for (let entry of this.entries) {
debit += entry.debit;
credit += entry.credit;
if (debit) {
debitAccounts.push(entry.account);
} else {
creditAccounts.push(entry.account);
}
}
debit = Math.floor(debit * 100) / 100;
credit = Math.floor(credit * 100) / 100;
if (debit !== credit) {
frappe.call({
method: 'show-dialog',
args: {
title: 'Invalid Entry',
message: frappe._('Debit {0} must be equal to Credit {1}', [
debit,
credit
])
}
});
throw new Error(`Debit ${debit} must be equal to Credit ${credit}`);
}
let precision = this.getPrecision();
debit = round(debit, precision);
credit = round(credit, precision);
return { debit, credit };
}
async insertEntries() {
@ -123,4 +136,12 @@ module.exports = class LedgerPosting {
await entryDoc.update();
}
}
getPrecision() {
return frappe.SystemSettings.floatPrecision;
}
getRoundOffAccount() {
return frappe.AccountingSettings.roundOffAccount;
}
};

View File

@ -18,9 +18,31 @@ module.exports = {
},
{
label: 'Writeoff Account',
label: 'Write Off Account',
fieldname: 'writeOffAccount',
fieldtype: 'Account'
fieldtype: 'Link',
target: 'Account',
default: 'Write Off',
getFilters() {
return {
isGroup: 0,
rootType: 'Expense'
}
}
},
{
label: 'Round Off Account',
fieldname: 'roundOffAccount',
fieldtype: 'Link',
target: 'Account',
default: 'Rounded Off',
getFilters() {
return {
isGroup: 0,
rootType: 'Expense'
}
}
},
{

View File

@ -17,6 +17,7 @@ module.exports = {
router.push({
path: `/edit/SalesInvoice/${doc.name}`,
query: {
doctype: 'SalesInvoice',
values: {
customer: customer.name
}

View File

@ -57,13 +57,15 @@ module.exports = {
label: 'Customer Currency',
fieldtype: 'Link',
target: 'Currency',
hidden: 1,
formula: doc => doc.getFrom('Party', doc.customer, 'currency')
formula: doc => doc.getFrom('Party', doc.customer, 'currency'),
formulaDependsOn: ['customer']
},
{
fieldname: 'exchangeRate',
label: 'Exchange Rate',
fieldtype: 'Float'
fieldtype: 'Float',
formula: doc => doc.getExchangeRate(),
readOnly: true
},
{
fieldname: 'items',
@ -77,7 +79,8 @@ module.exports = {
label: 'Net Total',
fieldtype: 'Currency',
formula: doc => doc.getSum('items', 'amount'),
readOnly: 1
readOnly: 1,
getCurrency: doc => doc.currency
},
{
fieldname: 'baseNetTotal',
@ -91,14 +94,16 @@ module.exports = {
label: 'Taxes',
fieldtype: 'Table',
childtype: 'TaxSummary',
formula: doc => doc.getTaxSummary(),
readOnly: 1
},
{
fieldname: 'grandTotal',
label: 'Grand Total',
fieldtype: 'Currency',
formula: async doc => await doc.getGrandTotal(),
readOnly: 1
formula: doc => doc.getGrandTotal(),
readOnly: 1,
getCurrency: doc => doc.currency
},
{
fieldname: 'baseGrandTotal',
@ -113,7 +118,7 @@ module.exports = {
fieldtype: 'Currency',
formula: doc => {
if (doc.submitted) return;
return doc.grandTotal;
return doc.baseGrandTotal;
},
readOnly: 1
},
@ -124,41 +129,6 @@ module.exports = {
}
],
layout: [
// section 1
{
columns: [
{ fields: ['customer', 'account'] },
{ fields: ['date', 'exchangeRate'] }
]
},
// section 2
{
columns: [{ fields: ['items'] }]
},
// section 3
{
columns: [
{
fields: [
'baseNetTotal',
'netTotal',
'taxes',
'baseGrandTotal',
'grandTotal'
]
}
]
},
// section 4
{
columns: [{ fields: ['terms'] }]
}
],
actions: [
{
label: 'Make Payment',

View File

@ -1,26 +1,14 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
const { round } = require('frappejs/utils/numberFormat');
const { getExchangeRate } = require('../../../accounting/exchangeRate');
module.exports = class SalesInvoice extends BaseDocument {
async setup() {
this.accountingSettings = await frappe.getSingle('AccountingSettings');
this.onChange({
async customer() {
if (this.customer) {
this.currency = await frappe.db.getValue('Party', this.customer, 'currency');
this.exchangeRate = await this.getExchangeRate();
}
},
async currency() {
this.exchangeRate = await this.getExchangeRate();
}
});
}
async getExchangeRate() {
const companyCurrency = this.accountingSettings.currency;
if (!this.currency) return;
let accountingSettings = await frappe.getSingle('AccountingSettings');
const companyCurrency = accountingSettings.currency;
if (this.currency === companyCurrency) {
return 1.0;
}
@ -30,23 +18,35 @@ module.exports = class SalesInvoice extends BaseDocument {
});
}
async getRowTax(row) {
if (row.tax) {
let tax = await this.getTax(row.tax);
let taxAmount = [];
for (let d of tax.details || []) {
let amount = (row.amount * d.rate) / 100
taxAmount.push({
account: d.account,
rate: d.rate,
amount,
baseAmount: amount * this.exchangeRate,
});
async getTaxSummary() {
let taxes = {};
for (let row of this.items) {
if (row.tax) {
let tax = await this.getTax(row.tax);
for (let d of tax.details) {
let amount = (row.amount * d.rate) / 100;
taxes[d.account] = taxes[d.account] || {
account: d.account,
rate: d.rate,
amount: 0
};
// collect amount
taxes[d.account].amount += amount;
}
}
return JSON.stringify(taxAmount);
} else {
return '';
}
return (
Object.keys(taxes)
.map(account => {
let tax = taxes[account];
tax.baseAmount = round(tax.amount * this.exchangeRate, 2);
return tax;
})
// clear rows with 0 amount
.filter(tax => tax.amount)
);
}
async getTax(tax) {
@ -55,52 +55,7 @@ module.exports = class SalesInvoice extends BaseDocument {
return this._taxes[tax];
}
async makeTaxSummary() {
if (!this.taxes) this.taxes = [];
// reset tax amount
this.taxes.map(d => {
d.amount = 0;
d.rate = 0;
d.baseAmount = 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.rate = rowTaxDetail.rate;
taxDetail.amount = taxDetail.amount + rowTaxDetail.amount;
taxDetail.baseAmount = taxDetail.baseAmount + rowTaxDetail.baseAmount;
found = true;
}
}
// add new row
if (!found) {
this.taxes.push({
account: rowTaxDetail.account,
rate: rowTaxDetail.rate,
amount: rowTaxDetail.amount,
baseAmount: rowTaxDetail.baseAmount
});
}
}
}
}
// clear no taxes
this.taxes = this.taxes.filter(d => d.amount);
}
async getGrandTotal() {
await this.makeTaxSummary();
let grandTotal = this.netTotal;
if (this.taxes) {
for (let row of this.taxes) {

View File

@ -15,6 +15,7 @@ module.exports = class SalesInvoiceServer extends SalesInvoice {
await entries.credit(tax.account, tax.baseAmount);
}
}
entries.makeRoundOffEntry();
return entries;
}
@ -31,6 +32,11 @@ module.exports = class SalesInvoiceServer extends SalesInvoice {
return [];
}
async beforeUpdate() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async beforeInsert() {
const entries = await this.getPosting();
await entries.validateEntries();

View File

@ -32,7 +32,11 @@ module.exports = {
label: 'Rate',
fieldtype: 'Currency',
required: 1,
formula: (row, doc) => doc.getFrom('Item', row.item, 'rate')
formula: async (row, doc) => {
let baseRate = await doc.getFrom('Item', row.item, 'rate');
return baseRate / doc.exchangeRate;
},
getCurrency: (row, doc) => doc.currency
},
{
fieldname: 'baseRate',
@ -65,7 +69,8 @@ module.exports = {
label: 'Amount',
fieldtype: 'Currency',
readOnly: 1,
formula: (row, doc) => row.quantity * row.rate
formula: (row, doc) => row.quantity * row.rate,
getCurrency: (row, doc) => doc.currency
},
{
fieldname: 'baseAmount',
@ -73,14 +78,6 @@ module.exports = {
fieldtype: 'Currency',
readOnly: 1,
formula: (row, doc) => row.amount * doc.exchangeRate
},
{
fieldname: 'taxAmount',
label: 'Tax Amount',
hidden: 1,
readOnly: 1,
fieldtype: 'Text',
formula: (row, doc) => doc.getRowTax(row)
}
]
};

View File

@ -1,28 +1,33 @@
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
}
]
}
name: 'TaxSummary',
doctype: 'DocType',
isChild: 1,
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
},
{
fieldname: 'baseAmount',
label: 'Amount (Company Currency)',
fieldtype: 'Currency',
formula: (row, doc) => row.amount * doc.exchangeRate,
readOnly: 1
}
]
};