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:
parent
d645ff31b7
commit
049432dbf1
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ module.exports = {
|
||||
router.push({
|
||||
path: `/edit/SalesInvoice/${doc.name}`,
|
||||
query: {
|
||||
doctype: 'SalesInvoice',
|
||||
values: {
|
||||
customer: customer.name
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user