2
0
mirror of https://github.com/frappe/books.git synced 2024-09-20 03:29:00 +00:00

fix: Commonify Transaction code

for SalesInvoice and PurchaseInvoice
This commit is contained in:
Faris Ansari 2019-12-04 22:51:31 +05:30
parent 4e0af18983
commit 4904632549
16 changed files with 312 additions and 435 deletions

View File

@ -1,10 +1,8 @@
let router = require('@/router').default;
module.exports = {
ledgerLink: {
label: 'Ledger Entries',
condition: doc => doc.submitted,
action: doc => {
action: (doc, router) => {
router.push({
name: 'Report',
params: {

View File

@ -112,9 +112,9 @@ module.exports = {
quickEditFields: [
'party',
'date',
'paymentMethod',
'account',
'paymentType',
'paymentMethod',
'paymentAccount',
'referenceId',
'referenceDate',

View File

@ -22,47 +22,31 @@ module.exports = class PaymentServer extends BaseDocument {
}
async beforeSubmit() {
if (this.for.length > 0) {
for (let row of this.for) {
if (['SalesInvoice', 'PurchaseInvoice'].includes(row.referenceType)) {
let { outstandingAmount, grandTotal } = await frappe.getDoc(
row.referenceType,
row.referenceName
);
if (outstandingAmount === null) {
outstandingAmount = grandTotal;
}
if (this.amount <= 0 || this.amount > outstandingAmount) {
// frappe.call({
// method: 'show-dialog',
// args: {
// title: 'Invalid Payment Entry',
// message: `Payment amount (${this.amount}) should be greater than 0 and less than Outstanding amount (${outstandingAmount})`
// }
// });
throw new Error(
`Payment amount (${this.amount}) should be greater than 0 and less than Outstanding amount (${outstandingAmount})`
);
} else {
await frappe.db.setValue(
row.referenceType,
row.referenceName,
'outstandingAmount',
outstandingAmount - this.amount
);
}
}
}
} else {
// frappe.call({
// method: 'show-dialog',
// args: {
// title: 'Invalid Payment Entry',
// message: `No reference for the payment.`
// }
// });
if (!this.for.length) {
throw new Error(`No reference for the payment.`);
}
for (let row of this.for) {
if (!['SalesInvoice', 'PurchaseInvoice'].includes(row.referenceType)) {
continue;
}
let referenceDoc = await frappe.getDoc(
row.referenceType,
row.referenceName
);
let { outstandingAmount, baseGrandTotal } = referenceDoc;
if (outstandingAmount == null) {
outstandingAmount = baseGrandTotal;
}
if (this.amount <= 0 || this.amount > outstandingAmount) {
throw new Error(
`Payment amount (${this.amount}) should be greater than 0 and less than Outstanding amount (${outstandingAmount})`
);
} else {
let newOutstanding = outstandingAmount - this.amount;
await referenceDoc.set('outstandingAmount', newOutstanding);
await referenceDoc.update();
}
}
}
async afterSubmit() {

View File

@ -1,11 +1,12 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
const { getActions } = require('../Transaction/Transaction');
const InvoiceTemplate = require('../SalesInvoice/InvoiceTemplate.vue').default;
module.exports = {
name: 'PurchaseInvoice',
doctype: 'DocType',
label: 'Purchase Invoice',
documentClass: require('./PurchaseInvoiceDocument'),
printTemplate: InvoiceTemplate,
isSingle: 0,
isChild: 0,
isSubmittable: 1,
@ -30,15 +31,8 @@ module.exports = {
fieldname: 'supplier',
label: 'Supplier',
fieldtype: 'Link',
target: 'Party',
required: 1,
getFilters: (query, control) => {
if (!query) return { supplier: 1 };
return {
keywords: ['like', query],
supplier: 1
};
}
target: 'Supplier',
required: 1
},
{
fieldname: 'account',
@ -46,10 +40,8 @@ module.exports = {
fieldtype: 'Link',
target: 'Account',
formula: doc => doc.getFrom('Party', doc.supplier, 'defaultAccount'),
getFilters: (query, control) => {
if (!query) return { isGroup: 0, accountType: 'Payable' };
getFilters: () => {
return {
keywords: ['like', query],
isGroup: 0,
accountType: 'Payable'
};
@ -61,14 +53,15 @@ module.exports = {
fieldtype: 'Link',
target: 'Currency',
hidden: 1,
formula: doc => doc.getFrom('Party', doc.supplier, 'currency')
formula: doc => doc.getFrom('Party', doc.supplier, 'currency'),
formulaDependsOn: ['supplier']
},
{
fieldname: 'exchangeRate',
label: 'Exchange Rate',
fieldtype: 'Float',
description: '1 USD = [?] INR',
hidden: doc => !doc.isForeignTransaction()
formula: doc => doc.getExchangeRate(),
required: true
},
{
fieldname: 'items',
@ -78,18 +71,18 @@ module.exports = {
required: true
},
{
fieldname: 'baseNetTotal',
label: 'Net Total (INR)',
fieldname: 'netTotal',
label: 'Net Total',
fieldtype: 'Currency',
formula: async doc => await doc.getBaseNetTotal(),
readOnly: 1
formula: doc => doc.getSum('items', 'amount'),
readOnly: 1,
getCurrency: doc => doc.currency
},
{
fieldname: 'netTotal',
label: 'Net Total (USD)',
fieldname: 'baseNetTotal',
label: 'Net Total (Company Currency)',
fieldtype: 'Currency',
hidden: doc => !doc.isForeignTransaction(),
formula: doc => doc.getSum('items', 'amount'),
formula: doc => doc.netTotal * doc.exchangeRate,
readOnly: 1
},
{
@ -97,110 +90,40 @@ module.exports = {
label: 'Taxes',
fieldtype: 'Table',
childtype: 'TaxSummary',
readOnly: 1,
template: (doc, row) => {
return `<div class='row'>
<div class='col-6'></div>
<div class='col-6'>
<div class='row' v-for='row in value'>
<div class='col-6'>{{ row.account }} ({{row.rate}}%)</div>
<div class='col-6 text-right'>
{{ frappe.format(row.amount, 'Currency') }}
</div>
</div>
</div>
</div>`;
}
},
{
fieldname: 'baseGrandTotal',
label: 'Grand Total (INR)',
fieldtype: 'Currency',
formula: async doc => await doc.getBaseGrandTotal(),
formula: doc => doc.getTaxSummary(),
readOnly: 1
},
{
fieldname: 'grandTotal',
label: 'Grand Total (USD)',
label: 'Grand Total',
fieldtype: 'Currency',
hidden: doc => !doc.isForeignTransaction(),
formula: async doc => await doc.getGrandTotal(),
formula: doc => doc.getGrandTotal(),
readOnly: 1,
getCurrency: doc => doc.currency
},
{
fieldname: 'baseGrandTotal',
label: 'Grand Total (Company Currency)',
fieldtype: 'Currency',
formula: doc => doc.grandTotal * doc.exchangeRate,
readOnly: 1
},
{
fieldname: 'outstandingAmount',
label: 'Outstanding Amount',
fieldtype: 'Currency',
formula: doc => {
if (doc.submitted) return;
return doc.baseGrandTotal;
},
readOnly: 1
},
{
fieldname: 'terms',
label: 'Terms',
fieldtype: 'Text'
},
{
fieldname: 'outstandingAmount',
label: 'Outstanding Amount',
fieldtype: 'Currency',
hidden: 1
}
],
layout: [
// section 1
{
columns: [
{ fields: ['supplier', 'account'] },
{ fields: ['date', 'exchangeRate'] }
]
},
// section 2
{
columns: [{ fields: ['items'] }]
},
// section 3
{
columns: [
{
fields: [
'baseNetTotal',
'netTotal',
'taxes',
'baseGrandTotal',
'grandTotal'
]
}
]
},
// section 4
{
columns: [{ fields: ['terms'] }]
}
],
links: [
utils.ledgerLink,
{
label: 'Make Payment',
condition: form =>
form.doc.submitted && form.doc.outstandingAmount != 0.0,
action: async form => {
const payment = await frappe.getNewDoc('Payment');
payment.paymentType = 'Pay';
payment.party = form.doc.supplier;
payment.paymentAccount = form.doc.account;
payment.for = [
{
referenceType: form.doc.doctype,
referenceName: form.doc.name,
amount: form.doc.outstandingAmount
}
];
payment.on('afterInsert', async () => {
form.$formModal.close();
form.$router.push({
path: `/edit/Payment/${payment.name}`
});
});
await form.$formModal.open(payment);
}
}
]
actions: getActions('PurchaseInvoice')
};

View File

@ -1,4 +1,3 @@
const SalesInvoiceDocument = require('../SalesInvoice/SalesInvoiceDocument');
const frappe = require('frappejs');
const TransactionDocument = require('../Transaction/TransactionDocument');
module.exports = class PurchaseInvoice extends SalesInvoiceDocument {};
module.exports = class PurchaseInvoice extends TransactionDocument {};

View File

@ -1,5 +1,5 @@
import { _ } from 'frappejs/utils';
import Badge from '@/components/Badge';
import { getStatusColumn } from '../Transaction/Transaction';
export default {
doctype: 'PurchaseInvoice',
@ -8,24 +8,7 @@ export default {
columns: [
'supplier',
'name',
{
label: 'Status',
fieldname: 'status',
fieldtype: 'Select',
size: 'small',
render(doc) {
let status = 'Pending';
let color = 'orange';
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
status = 'Paid';
color = 'green';
}
return {
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
components: { Badge }
};
}
},
getStatusColumn('PurchaseInvoice'),
'date',
'grandTotal',
'outstandingAmount'

View File

@ -1,66 +1,27 @@
const frappe = require('frappejs');
const TransactionServer = require('../Transaction/TransactionServer');
const PurchaseInvoice = require('./PurchaseInvoiceDocument');
const LedgerPosting = require('../../../accounting/ledgerPosting');
module.exports = class PurchaseInvoiceServer extends PurchaseInvoice {
class PurchaseInvoiceServer extends PurchaseInvoice {
async getPosting() {
let entries = new LedgerPosting({ reference: this, party: this.supplier });
await entries.credit(this.account, this.baseGrandTotal);
for (let item of this.items) {
const baseItemAmount = item.amount * this.exchangeRate;
await entries.debit(item.account, baseItemAmount);
await entries.debit(item.account, item.baseAmount);
}
if (this.taxes) {
for (let tax of this.taxes) {
const baseTaxAmount = tax.amount * this.exchangeRate;
await entries.debit(tax.account, baseTaxAmount);
await entries.debit(tax.account, tax.baseAmount);
}
}
entries.makeRoundOffEntry();
return entries;
}
}
async getPayments() {
let payments = await frappe.db.getAll({
doctype: 'PaymentFor',
fields: ['parent'],
filters: { referenceName: this.name },
orderBy: 'name'
});
if (payments.length != 0) {
return payments;
}
return [];
}
// apply common methods from TransactionServer
Object.assign(PurchaseInvoiceServer.prototype, TransactionServer);
async beforeInsert() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async beforeSubmit() {
const entries = await this.getPosting();
await entries.post();
await frappe.db.setValue(
'PurchaseInvoice',
this.name,
'outstandingAmount',
this.baseGrandTotal
);
}
async afterRevert() {
let paymentRefList = await this.getPayments();
for (let paymentFor of paymentRefList) {
const paymentReference = paymentFor.parent;
const payment = await frappe.getDoc('Payment', paymentReference);
const paymentEntries = await payment.getPosting();
await paymentEntries.postReverse();
// To set the payment status as unsubmitted.
payment.revert();
}
const entries = await this.getPosting();
await entries.postReverse();
}
};
module.exports = PurchaseInvoiceServer;

View File

@ -6,21 +6,14 @@ module.exports = {
isChild: 1,
keywordFields: [],
layout: 'ratio',
tableFields: [
'item',
'tax',
'quantity',
'rate',
'amount'
],
tableFields: ['item', 'tax', 'quantity', 'rate', 'amount'],
fields: [
{
fieldname: 'item',
label: 'Item',
fieldtype: 'Link',
target: 'Item',
required: 1,
width: 2
required: 1
},
{
fieldname: 'description',
@ -40,7 +33,18 @@ 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',
label: 'Rate (Company Currency)',
fieldtype: 'Currency',
formula: (row, doc) => row.rate * doc.exchangeRate,
readOnly: 1
},
{
fieldname: 'account',
@ -65,15 +69,15 @@ 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: 'taxAmount',
label: 'Tax Amount',
hidden: 1,
fieldname: 'baseAmount',
label: 'Amount (Company Currency)',
fieldtype: 'Currency',
readOnly: 1,
fieldtype: 'Text',
formula: (row, doc) => doc.getRowTax(row)
formula: (row, doc) => row.amount * doc.exchangeRate
}
]
};

View File

@ -1,7 +1,4 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
const { openQuickEdit } = require('@/utils');
const router = require('@/router').default;
const { getActions } = require('../Transaction/Transaction');
const InvoiceTemplate = require('./InvoiceTemplate.vue').default;
module.exports = {
@ -127,50 +124,5 @@ module.exports = {
}
],
actions: [
{
label: 'Make Payment',
condition: doc => doc.submitted && doc.outstandingAmount > 0,
action: async function makePayment(doc) {
let payment = await frappe.getNewDoc('Payment');
payment.once('afterInsert', () => {
payment.submit();
});
openQuickEdit({
doctype: 'Payment',
name: payment.name,
hideFields: ['party', 'date', 'account', 'paymentType', 'for'],
defaults: {
party: doc.customer,
account: doc.account,
date: new Date().toISOString().slice(0, 10),
paymentType: 'Receive',
for: [
{
referenceType: doc.doctype,
referenceName: doc.name,
amount: doc.outstandingAmount
}
]
}
});
}
},
{
label: 'Revert',
condition: doc =>
doc.submitted && doc.baseGrandTotal === doc.outstandingAmount,
action(doc) {
doc.revert();
}
},
{
label: 'Print',
condition: doc => doc.submitted,
action(doc) {
router.push(`/print/${doc.doctype}/${doc.name}`);
}
},
utils.ledgerLink
]
actions: getActions('SalesInvoice')
};

View File

@ -1,68 +1,3 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
const { round } = require('frappejs/utils/numberFormat');
const { getExchangeRate } = require('../../../accounting/exchangeRate');
const TransactionDocument = require('../Transaction/TransactionDocument');
module.exports = class SalesInvoice extends BaseDocument {
async getExchangeRate() {
if (!this.currency) return;
let accountingSettings = await frappe.getSingle('AccountingSettings');
const companyCurrency = accountingSettings.currency;
if (this.currency === companyCurrency) {
return 1.0;
}
return await getExchangeRate({
fromCurrency: this.currency,
toCurrency: companyCurrency
});
}
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 (
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) {
if (!this._taxes) this._taxes = {};
if (!this._taxes[tax]) this._taxes[tax] = await frappe.getDoc('Tax', tax);
return this._taxes[tax];
}
async getGrandTotal() {
let grandTotal = this.netTotal;
if (this.taxes) {
for (let row of this.taxes) {
grandTotal += row.amount;
}
}
return grandTotal;
}
};
module.exports = class SalesInvoice extends TransactionDocument {};

View File

@ -1,5 +1,5 @@
import { _ } from 'frappejs/utils';
import Badge from '@/components/Badge';
import { getStatusColumn } from '../Transaction/Transaction';
export default {
doctype: 'SalesInvoice',
@ -8,28 +8,7 @@ export default {
columns: [
'customer',
'name',
{
label: 'Status',
fieldname: 'status',
fieldtype: 'Select',
size: 'small',
render(doc) {
let status = 'Pending';
let color = 'orange';
if (!doc.submitted) {
status = 'Draft';
color = 'gray';
}
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
status = 'Paid';
color = 'green';
}
return {
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
components: { Badge }
};
}
},
getStatusColumn('SalesInvoice'),
'date',
'grandTotal',
'outstandingAmount'

View File

@ -1,7 +1,8 @@
const TransactionServer = require('../Transaction/TransactionServer');
const SalesInvoice = require('./SalesInvoiceDocument');
const LedgerPosting = require('../../../accounting/ledgerPosting');
module.exports = class SalesInvoiceServer extends SalesInvoice {
class SalesInvoiceServer extends SalesInvoice {
async getPosting() {
let entries = new LedgerPosting({ reference: this, party: this.customer });
await entries.debit(this.account, this.baseGrandTotal);
@ -18,52 +19,9 @@ module.exports = class SalesInvoiceServer extends SalesInvoice {
entries.makeRoundOffEntry();
return entries;
}
}
async getPayments() {
let payments = await frappe.db.getAll({
doctype: 'PaymentFor',
fields: ['parent'],
filters: { referenceName: this.name },
orderBy: 'name'
});
if (payments.length != 0) {
return payments;
}
return [];
}
// apply common methods from TransactionServer
Object.assign(SalesInvoiceServer.prototype, TransactionServer);
async beforeUpdate() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async beforeInsert() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async beforeSubmit() {
const entries = await this.getPosting();
await entries.post();
await frappe.db.setValue(
'SalesInvoice',
this.name,
'outstandingAmount',
this.baseGrandTotal
);
}
async afterRevert() {
let paymentRefList = await this.getPayments();
for (let paymentFor of paymentRefList) {
const paymentReference = paymentFor.parent;
const payment = await frappe.getDoc('Payment', paymentReference);
const paymentEntries = await payment.getPosting();
await paymentEntries.postReverse();
// To set the payment status as unsubmitted.
payment.revert();
}
const entries = await this.getPosting();
await entries.postReverse();
}
};
module.exports = SalesInvoiceServer;

View File

@ -0,0 +1,82 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
const { openQuickEdit } = require('@/utils');
const Badge = require('@/components/Badge').default;
module.exports = {
getStatusColumn() {
return {
label: 'Status',
fieldname: 'status',
fieldtype: 'Select',
render(doc) {
let status = 'Pending';
let color = 'orange';
if (!doc.submitted) {
status = 'Draft';
color = 'gray';
}
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
status = 'Paid';
color = 'green';
}
return {
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
components: { Badge }
};
}
};
},
getActions(doctype) {
return [
{
label: 'Make Payment',
condition: doc => doc.submitted && doc.outstandingAmount > 0,
action: async function makePayment(doc) {
let payment = await frappe.getNewDoc('Payment');
payment.once('afterInsert', async () => {
await payment.submit();
});
let isSales = doctype === 'SalesInvoice';
let party = isSales ? doc.customer : doc.supplier;
let paymentType = isSales ? 'Receive' : 'Pay';
let hideAccountField = isSales ? 'account' : 'paymentAccount';
openQuickEdit({
doctype: 'Payment',
name: payment.name,
// hideFields: ['party', 'date', hideAccountField, 'paymentType', 'for'],
defaults: {
party,
[hideAccountField]: doc.account,
date: new Date().toISOString().slice(0, 10),
paymentType,
for: [
{
referenceType: doc.doctype,
referenceName: doc.name,
amount: doc.outstandingAmount
}
]
}
});
}
},
{
label: 'Revert',
condition: doc =>
doc.submitted && doc.baseGrandTotal === doc.outstandingAmount,
action(doc) {
doc.revert();
}
},
{
label: 'Print',
condition: doc => doc.submitted,
action(doc, router) {
router.push(`/print/${doc.doctype}/${doc.name}`);
}
},
utils.ledgerLink
];
}
};

View File

@ -0,0 +1,68 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
const { round } = require('frappejs/utils/numberFormat');
const { getExchangeRate } = require('../../../accounting/exchangeRate');
module.exports = class TransactionDocument extends BaseDocument {
async getExchangeRate() {
if (!this.currency) return;
let accountingSettings = await frappe.getSingle('AccountingSettings');
const companyCurrency = accountingSettings.currency;
if (this.currency === companyCurrency) {
return 1.0;
}
return await getExchangeRate({
fromCurrency: this.currency,
toCurrency: companyCurrency
});
}
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 (
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) {
if (!this._taxes) this._taxes = {};
if (!this._taxes[tax]) this._taxes[tax] = await frappe.getDoc('Tax', tax);
return this._taxes[tax];
}
async getGrandTotal() {
let grandTotal = this.netTotal;
if (this.taxes) {
for (let row of this.taxes) {
grandTotal += row.amount;
}
}
return grandTotal;
}
};

View File

@ -0,0 +1,51 @@
const frappe = require('frappejs');
module.exports = {
async getPayments() {
let payments = await frappe.db.getAll({
doctype: 'PaymentFor',
fields: ['parent'],
filters: { referenceName: this.name },
orderBy: 'name'
});
if (payments.length != 0) {
return payments;
}
return [];
},
async beforeUpdate() {
const entries = await this.getPosting();
await entries.validateEntries();
},
async beforeInsert() {
const entries = await this.getPosting();
await entries.validateEntries();
},
async beforeSubmit() {
const entries = await this.getPosting();
await entries.post();
await frappe.db.setValue(
this.doctype,
this.name,
'outstandingAmount',
this.baseGrandTotal
);
},
async afterRevert() {
let paymentRefList = await this.getPayments();
for (let paymentFor of paymentRefList) {
const paymentReference = paymentFor.parent;
const payment = await frappe.getDoc('Payment', paymentReference);
const paymentEntries = await payment.getPosting();
await paymentEntries.postReverse();
// To set the payment status as unsubmitted.
payment.revert();
}
const entries = await this.getPosting();
await entries.postReverse();
}
};

View File

@ -163,7 +163,7 @@ export default {
return {
label: d.label,
component: d.component,
action: d.action.bind(this, this.doc)
action: d.action.bind(this, this.doc, this.$router)
};
});