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

fix: multiple payment related fixes

- outstanding amount reset on payment cancel
- payment reference type is now select
- map property on select fields for different display values
- multiple payment validations
- sensible binding payment amount and reference amount
This commit is contained in:
18alantom 2021-11-25 14:49:50 +05:30
parent c3760431a1
commit b8671cb2ed
4 changed files with 139 additions and 34 deletions

View File

@ -38,6 +38,11 @@ export default {
}
}
},
formula: (doc) => {
if (doc.paymentMethod === 'Cash' && doc.paymentType === 'Pay') {
return 'Cash';
}
},
},
{
fieldname: 'paymentType',
@ -63,7 +68,7 @@ export default {
}
},
formula: (doc) => {
if (doc.paymentMethod === 'Cash') {
if (doc.paymentMethod === 'Cash' && doc.paymentType === 'Receive') {
return 'Cash';
}
},
@ -107,7 +112,7 @@ export default {
if (value < 0) {
throw new frappe.errors.ValidationError(
frappe._(
`Payment amount cannot be less than zero. Amount has been reset to max viable amount.`
`Payment amount cannot be less than zero. Amount has been reset.`
)
);
}
@ -121,7 +126,7 @@ export default {
`Payment amount cannot exceed ${frappe.format(
amount,
'Currency'
)}. Amount has been reset to max viable amount.`
)}. Amount has been reset.`
)
);
} else if (value === 0) {
@ -130,7 +135,7 @@ export default {
`Payment amount cannot be ${frappe.format(
value,
'Currency'
)}. Amount has been reset to max viable amount.`
)}. Amount has been reset.`
)
);
}
@ -140,6 +145,7 @@ export default {
fieldname: 'writeoff',
label: 'Write Off / Refund',
fieldtype: 'Currency',
default: 0,
},
{
fieldname: 'for',

View File

@ -1,15 +1,92 @@
import BaseDocument from 'frappejs/model/document';
import frappe from 'frappejs';
import BaseDocument from 'frappejs/model/document';
import LedgerPosting from '../../../accounting/ledgerPosting';
export default class PaymentServer extends BaseDocument {
async change({ changed }) {
if (changed === 'for') {
switch (changed) {
case 'for': {
this.updateAmountOnReferenceUpdate();
await this.updateDetailsOnReferenceUpdate();
}
case 'amount': {
this.updateReferenceOnAmountUpdate();
}
}
}
async updateDetailsOnReferenceUpdate() {
const { referenceType, referenceName } = this.for[0];
if (
this.for?.length !== 1 ||
this.party ||
this.paymentType ||
!referenceName ||
!referenceType
) {
return;
}
const doctype = referenceType;
const doc = await frappe.getDoc(doctype, referenceName);
let party;
let paymentType;
if (doctype === 'SalesInvoice') {
party = doc.customer;
paymentType = 'Receive';
} else if (doctype === 'PurchaseInvoice') {
party = doc.supplier;
paymentType = 'Pay';
}
this.party = party;
this.paymentType = paymentType;
}
updateAmountOnReferenceUpdate() {
this.amount = 0;
for (let paymentReference of this.for) {
this.amount += paymentReference.amount;
}
}
updateReferenceOnAmountUpdate() {
if (this.for?.length !== 1) return;
this.for[0].amount = this.amount;
}
async validate() {
this.validateAccounts();
this.validateReferenceAmount();
}
validateAccounts() {
if (this.paymentAccount !== this.account || !this.account) return;
throw new Error(
`To Account and From Account can't be the same: ${this.account}`
);
}
validateReferenceAmount() {
const referenceAmountTotal = this.for
.map(({ amount }) => amount)
.reduce((a, b) => a + b);
if (this.amount + (this.writeoff ?? 0) < referenceAmountTotal) {
const writeoff = frappe.format(this.writeoff, 'Currency');
const payment = frappe.format(this.amount, 'Currency');
const refAmount = frappe.format(referenceAmountTotal, 'Currency');
const writeoffString =
this.writeoff > 0 ? `and writeoff: ${writeoff} ` : '';
throw new Error(
frappe._(
`Amount: ${payment} ${writeoffString}is less than the total amount allocated to references: ${refAmount}`
)
);
}
}
async getPosting() {
@ -55,15 +132,16 @@ export default class PaymentServer extends BaseDocument {
}
}
async afterSubmit() {
const entries = await this.getPosting();
await entries.post();
}
async afterRevert() {
this.updateReferenceOutstandingAmount();
const entries = await this.getPosting();
await entries.postReverse();
}
// Maybe revert outstanding amount of invoice too?
async updateReferenceOutstandingAmount() {
await this.for.forEach(async ({ amount, referenceType, referenceName }) => {
const refDoc = await frappe.getDoc(referenceType, referenceName);
refDoc.update({ outstandingAmount: refDoc.outstandingAmount + amount });
});
}
}

View File

@ -4,34 +4,40 @@ export default {
isSingle: 0,
isChild: 1,
keywordFields: [],
tableFields: [
'referenceType',
'referenceName',
'amount'
],
tableFields: ['referenceType', 'referenceName', 'amount'],
fields: [
{
fieldname: 'referenceType',
label: 'Reference Type',
fieldtype: 'AutoComplete',
fieldtype: 'Select',
options: ['SalesInvoice', 'PurchaseInvoice'],
required: 1
map: { SalesInvoice: 'Invoice', PurchaseInvoice: 'Bill' },
required: 1,
},
{
fieldname: 'referenceName',
label: 'Reference Name',
fieldtype: 'DynamicLink',
references: 'referenceType',
required: 1
getFilters() {
return {
outstandingAmount: ['>', 0],
};
},
required: 1,
},
{
fieldname: 'amount',
label: 'Amount',
fieldtype: 'Currency',
formula: (row, doc) => {
return doc.getFrom(row.referenceType, row.referenceName, 'outstandingAmount');
return doc.getFrom(
row.referenceType,
row.referenceName,
'outstandingAmount'
);
},
required: 1
}
]
required: 1,
},
],
};

View File

@ -4,15 +4,22 @@
{{ df.label }}
</div>
<div
class="relative flex items-center justify-end bg-white focus-within:bg-gray-200"
class="
relative
flex
items-center
justify-end
bg-white
focus-within:bg-gray-200
"
:class="inputClasses"
>
<select
class="appearance-none bg-transparent focus:outline-none w-full"
:class="isReadOnly && 'pointer-events-none'"
:value="value"
@change="e => triggerChange(e.target.value)"
@focus="e => $emit('focus', e)"
@change="(e) => triggerChange(e.target.value)"
@focus="(e) => $emit('focus', e)"
>
<option
v-for="option in options"
@ -52,16 +59,24 @@ import Base from './Base';
export default {
name: 'Select',
extends: Base,
methods: {
map(v) {
if (this.df.map) {
return this.df.map[v] ?? v;
}
return v;
},
},
computed: {
options() {
let options = this.df.options;
return options.map(o => {
return options.map((o) => {
if (typeof o === 'string') {
return { label: o, value: o };
return { label: this.map(o), value: o };
}
return o;
});
}
}
},
},
};
</script>