mirror of
https://github.com/frappe/books.git
synced 2024-12-23 03:19:01 +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:
parent
c3760431a1
commit
b8671cb2ed
@ -38,6 +38,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
formula: (doc) => {
|
||||||
|
if (doc.paymentMethod === 'Cash' && doc.paymentType === 'Pay') {
|
||||||
|
return 'Cash';
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: 'paymentType',
|
fieldname: 'paymentType',
|
||||||
@ -63,7 +68,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
formula: (doc) => {
|
formula: (doc) => {
|
||||||
if (doc.paymentMethod === 'Cash') {
|
if (doc.paymentMethod === 'Cash' && doc.paymentType === 'Receive') {
|
||||||
return 'Cash';
|
return 'Cash';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -107,7 +112,7 @@ export default {
|
|||||||
if (value < 0) {
|
if (value < 0) {
|
||||||
throw new frappe.errors.ValidationError(
|
throw new frappe.errors.ValidationError(
|
||||||
frappe._(
|
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(
|
`Payment amount cannot exceed ${frappe.format(
|
||||||
amount,
|
amount,
|
||||||
'Currency'
|
'Currency'
|
||||||
)}. Amount has been reset to max viable amount.`
|
)}. Amount has been reset.`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else if (value === 0) {
|
} else if (value === 0) {
|
||||||
@ -130,7 +135,7 @@ export default {
|
|||||||
`Payment amount cannot be ${frappe.format(
|
`Payment amount cannot be ${frappe.format(
|
||||||
value,
|
value,
|
||||||
'Currency'
|
'Currency'
|
||||||
)}. Amount has been reset to max viable amount.`
|
)}. Amount has been reset.`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -140,6 +145,7 @@ export default {
|
|||||||
fieldname: 'writeoff',
|
fieldname: 'writeoff',
|
||||||
label: 'Write Off / Refund',
|
label: 'Write Off / Refund',
|
||||||
fieldtype: 'Currency',
|
fieldtype: 'Currency',
|
||||||
|
default: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: 'for',
|
fieldname: 'for',
|
||||||
|
@ -1,14 +1,91 @@
|
|||||||
import BaseDocument from 'frappejs/model/document';
|
|
||||||
import frappe from 'frappejs';
|
import frappe from 'frappejs';
|
||||||
|
import BaseDocument from 'frappejs/model/document';
|
||||||
import LedgerPosting from '../../../accounting/ledgerPosting';
|
import LedgerPosting from '../../../accounting/ledgerPosting';
|
||||||
|
|
||||||
export default class PaymentServer extends BaseDocument {
|
export default class PaymentServer extends BaseDocument {
|
||||||
async change({ changed }) {
|
async change({ changed }) {
|
||||||
if (changed === 'for') {
|
switch (changed) {
|
||||||
this.amount = 0;
|
case 'for': {
|
||||||
for (let paymentReference of this.for) {
|
this.updateAmountOnReferenceUpdate();
|
||||||
this.amount += paymentReference.amount;
|
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}`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,15 +132,16 @@ export default class PaymentServer extends BaseDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async afterSubmit() {
|
|
||||||
const entries = await this.getPosting();
|
|
||||||
await entries.post();
|
|
||||||
}
|
|
||||||
|
|
||||||
async afterRevert() {
|
async afterRevert() {
|
||||||
|
this.updateReferenceOutstandingAmount();
|
||||||
const entries = await this.getPosting();
|
const entries = await this.getPosting();
|
||||||
await entries.postReverse();
|
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 });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,34 +4,40 @@ export default {
|
|||||||
isSingle: 0,
|
isSingle: 0,
|
||||||
isChild: 1,
|
isChild: 1,
|
||||||
keywordFields: [],
|
keywordFields: [],
|
||||||
tableFields: [
|
tableFields: ['referenceType', 'referenceName', 'amount'],
|
||||||
'referenceType',
|
|
||||||
'referenceName',
|
|
||||||
'amount'
|
|
||||||
],
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldname: 'referenceType',
|
fieldname: 'referenceType',
|
||||||
label: 'Reference Type',
|
label: 'Reference Type',
|
||||||
fieldtype: 'AutoComplete',
|
fieldtype: 'Select',
|
||||||
options: ['SalesInvoice', 'PurchaseInvoice'],
|
options: ['SalesInvoice', 'PurchaseInvoice'],
|
||||||
required: 1
|
map: { SalesInvoice: 'Invoice', PurchaseInvoice: 'Bill' },
|
||||||
|
required: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: 'referenceName',
|
fieldname: 'referenceName',
|
||||||
label: 'Reference Name',
|
label: 'Reference Name',
|
||||||
fieldtype: 'DynamicLink',
|
fieldtype: 'DynamicLink',
|
||||||
references: 'referenceType',
|
references: 'referenceType',
|
||||||
required: 1
|
getFilters() {
|
||||||
|
return {
|
||||||
|
outstandingAmount: ['>', 0],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
required: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: 'amount',
|
fieldname: 'amount',
|
||||||
label: 'Amount',
|
label: 'Amount',
|
||||||
fieldtype: 'Currency',
|
fieldtype: 'Currency',
|
||||||
formula: (row, doc) => {
|
formula: (row, doc) => {
|
||||||
return doc.getFrom(row.referenceType, row.referenceName, 'outstandingAmount');
|
return doc.getFrom(
|
||||||
|
row.referenceType,
|
||||||
|
row.referenceName,
|
||||||
|
'outstandingAmount'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
required: 1
|
required: 1,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
@ -4,15 +4,22 @@
|
|||||||
{{ df.label }}
|
{{ df.label }}
|
||||||
</div>
|
</div>
|
||||||
<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"
|
:class="inputClasses"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
class="appearance-none bg-transparent focus:outline-none w-full"
|
class="appearance-none bg-transparent focus:outline-none w-full"
|
||||||
:class="isReadOnly && 'pointer-events-none'"
|
:class="isReadOnly && 'pointer-events-none'"
|
||||||
:value="value"
|
:value="value"
|
||||||
@change="e => triggerChange(e.target.value)"
|
@change="(e) => triggerChange(e.target.value)"
|
||||||
@focus="e => $emit('focus', e)"
|
@focus="(e) => $emit('focus', e)"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
@ -52,16 +59,24 @@ import Base from './Base';
|
|||||||
export default {
|
export default {
|
||||||
name: 'Select',
|
name: 'Select',
|
||||||
extends: Base,
|
extends: Base,
|
||||||
|
methods: {
|
||||||
|
map(v) {
|
||||||
|
if (this.df.map) {
|
||||||
|
return this.df.map[v] ?? v;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
options() {
|
options() {
|
||||||
let options = this.df.options;
|
let options = this.df.options;
|
||||||
return options.map(o => {
|
return options.map((o) => {
|
||||||
if (typeof o === 'string') {
|
if (typeof o === 'string') {
|
||||||
return { label: o, value: o };
|
return { label: this.map(o), value: o };
|
||||||
}
|
}
|
||||||
return o;
|
return o;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user