mirror of
https://github.com/frappe/books.git
synced 2024-11-09 23:30:56 +00:00
incr: type Payment
This commit is contained in:
parent
b08a3b9d52
commit
1cc05d218f
@ -424,19 +424,22 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
for (const field in filters) {
|
||||
const value = filters[field];
|
||||
let operator = '=';
|
||||
let comparisonValue = value;
|
||||
let operator: string | number = '=';
|
||||
let comparisonValue = value as string | number | (string | number)[];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
operator = value[0];
|
||||
comparisonValue = value[1];
|
||||
operator = value[0] as string;
|
||||
comparisonValue = value[1] as string | number | (string | number)[];
|
||||
operator = operator.toLowerCase();
|
||||
|
||||
if (operator === 'includes') {
|
||||
operator = 'like';
|
||||
}
|
||||
|
||||
if (operator === 'like' && !comparisonValue.includes('%')) {
|
||||
if (
|
||||
operator === 'like' &&
|
||||
!(comparisonValue as (string | number)[]).includes('%')
|
||||
) {
|
||||
comparisonValue = `%${comparisonValue}%`;
|
||||
}
|
||||
}
|
||||
@ -452,11 +455,14 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
|
||||
filtersArray.map((filter) => {
|
||||
const [field, operator, comparisonValue] = filter;
|
||||
const field = filter[0] as string;
|
||||
const operator = filter[1];
|
||||
const comparisonValue = filter[2];
|
||||
|
||||
if (operator === '=') {
|
||||
builder.where(field as string, comparisonValue);
|
||||
builder.where(field, comparisonValue);
|
||||
} else {
|
||||
builder.where(field as string, operator as string, comparisonValue);
|
||||
builder.where(field, operator as string, comparisonValue as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import { PartyRole } from './types';
|
||||
|
||||
export class Party extends Doc {
|
||||
async updateOutstandingAmounts() {
|
||||
async updateOutstandingAmount() {
|
||||
const role = this.role as PartyRole;
|
||||
switch (role) {
|
||||
case 'Customer':
|
||||
@ -72,7 +72,10 @@ export class Party extends Doc {
|
||||
defaultAccount: (doc: Doc) => {
|
||||
const role = doc.role as PartyRole;
|
||||
if (role === 'Both') {
|
||||
return { isGroup: false, accountType: ['Payable', 'Receivable'] };
|
||||
return {
|
||||
isGroup: false,
|
||||
accountType: ['in', ['Payable', 'Receivable']],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,256 +0,0 @@
|
||||
import frappe, { t } from 'frappe';
|
||||
import utils from '../../../accounting/utils';
|
||||
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
|
||||
|
||||
const paymentTypeMap = {
|
||||
Receive: t`Receive`,
|
||||
Pay: t`Pay`,
|
||||
};
|
||||
|
||||
const paymentMethodMap = {
|
||||
Cash: t`Cash`,
|
||||
Cheque: t`Cheque`,
|
||||
Transfer: t`Transfer`,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'Payment',
|
||||
label: t`Payment`,
|
||||
isSingle: 0,
|
||||
isChild: 0,
|
||||
isSubmittable: 1,
|
||||
keywordFields: [],
|
||||
settings: 'PaymentSettings',
|
||||
fields: [
|
||||
{
|
||||
label: t`Payment No`,
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
readOnly: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'party',
|
||||
label: t`Party`,
|
||||
fieldtype: 'Link',
|
||||
target: 'Party',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'date',
|
||||
label: t`Posting Date`,
|
||||
fieldtype: 'Date',
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
fieldname: 'account',
|
||||
label: t`From Account`,
|
||||
fieldtype: 'Link',
|
||||
target: 'Account',
|
||||
required: 1,
|
||||
getFilters: (query, doc) => {
|
||||
if (doc.paymentType === 'Pay') {
|
||||
if (doc.paymentMethod === 'Cash') {
|
||||
return { accountType: 'Cash', isGroup: 0 };
|
||||
} else {
|
||||
return { accountType: ['in', ['Bank', 'Cash']], isGroup: 0 };
|
||||
}
|
||||
}
|
||||
},
|
||||
formula: (doc) => {
|
||||
if (doc.paymentMethod === 'Cash' && doc.paymentType === 'Pay') {
|
||||
return 'Cash';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: 'paymentType',
|
||||
label: t`Payment Type`,
|
||||
fieldtype: 'Select',
|
||||
placeholder: t`Payment Type`,
|
||||
options: Object.keys(paymentTypeMap),
|
||||
map: paymentTypeMap,
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'paymentAccount',
|
||||
label: t`To Account`,
|
||||
placeholder: t`To Account`,
|
||||
fieldtype: 'Link',
|
||||
target: 'Account',
|
||||
required: 1,
|
||||
getFilters: (query, doc) => {
|
||||
if (doc.paymentType === 'Receive') {
|
||||
if (doc.paymentMethod === 'Cash') {
|
||||
return { accountType: 'Cash', isGroup: 0 };
|
||||
} else {
|
||||
return { accountType: ['in', ['Bank', 'Cash']], isGroup: 0 };
|
||||
}
|
||||
}
|
||||
},
|
||||
formula: (doc) => {
|
||||
if (doc.paymentMethod === 'Cash' && doc.paymentType === 'Receive') {
|
||||
return 'Cash';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: 'numberSeries',
|
||||
label: t`Number Series`,
|
||||
fieldtype: 'Link',
|
||||
target: 'NumberSeries',
|
||||
required: 1,
|
||||
getFilters: () => {
|
||||
return { referenceType: 'Payment' };
|
||||
},
|
||||
default: DEFAULT_NUMBER_SERIES['Payment'],
|
||||
},
|
||||
{
|
||||
fieldname: 'paymentMethod',
|
||||
label: t`Payment Method`,
|
||||
placeholder: t`Payment Method`,
|
||||
fieldtype: 'Select',
|
||||
options: Object.keys(paymentMethodMap),
|
||||
map: paymentMethodMap,
|
||||
default: 'Cash',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'referenceId',
|
||||
label: t`Ref. / Cheque No.`,
|
||||
placeholder: t`Ref. / Cheque No.`,
|
||||
fieldtype: 'Data',
|
||||
required: (doc) => doc.paymentMethod !== 'Cash', // TODO: UNIQUE
|
||||
hidden: (doc) => doc.paymentMethod === 'Cash',
|
||||
},
|
||||
{
|
||||
fieldname: 'referenceDate',
|
||||
label: t`Ref. Date`,
|
||||
placeholder: t`Ref. Date`,
|
||||
fieldtype: 'Date',
|
||||
},
|
||||
{
|
||||
fieldname: 'clearanceDate',
|
||||
label: t`Clearance Date`,
|
||||
placeholder: t`Clearance Date`,
|
||||
fieldtype: 'Date',
|
||||
hidden: (doc) => doc.paymentMethod === 'Cash',
|
||||
},
|
||||
{
|
||||
fieldname: 'amount',
|
||||
label: t`Amount`,
|
||||
fieldtype: 'Currency',
|
||||
required: 1,
|
||||
formula: (doc) => doc.getSum('for', 'amount', false),
|
||||
validate(value, doc) {
|
||||
if (value.isNegative()) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Payment amount cannot be less than zero.`
|
||||
);
|
||||
}
|
||||
|
||||
if (doc.for.length === 0) return;
|
||||
const amount = doc.getSum('for', 'amount', false);
|
||||
|
||||
if (value.gt(amount)) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Payment amount cannot
|
||||
exceed ${frappe.format(amount, 'Currency')}.`
|
||||
);
|
||||
} else if (value.isZero()) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Payment amount cannot
|
||||
be ${frappe.format(value, 'Currency')}.`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: 'writeoff',
|
||||
label: t`Write Off / Refund`,
|
||||
fieldtype: 'Currency',
|
||||
},
|
||||
{
|
||||
fieldname: 'for',
|
||||
label: t`Payment Reference`,
|
||||
fieldtype: 'Table',
|
||||
childtype: 'PaymentFor',
|
||||
required: 0,
|
||||
},
|
||||
{
|
||||
fieldname: 'cancelled',
|
||||
label: t`Cancelled`,
|
||||
fieldtype: 'Check',
|
||||
default: 0,
|
||||
readOnly: 1,
|
||||
},
|
||||
],
|
||||
|
||||
quickEditFields: [
|
||||
'numberSeries',
|
||||
'party',
|
||||
'date',
|
||||
'paymentMethod',
|
||||
'account',
|
||||
'paymentType',
|
||||
'paymentAccount',
|
||||
'referenceId',
|
||||
'referenceDate',
|
||||
'clearanceDate',
|
||||
'amount',
|
||||
'writeoff',
|
||||
'for',
|
||||
],
|
||||
|
||||
layout: [
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
fields: ['party', 'account'],
|
||||
},
|
||||
{
|
||||
fields: ['date', 'paymentAccount'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
fields: ['paymentMethod'],
|
||||
},
|
||||
{
|
||||
fields: ['paymentType'],
|
||||
},
|
||||
{
|
||||
fields: ['referenceId'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
fields: ['referenceDate'],
|
||||
},
|
||||
{
|
||||
fields: ['clearanceDate'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
fields: ['for'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
fields: ['amount', 'writeoff'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
actions: [utils.ledgerLink],
|
||||
links: [utils.ledgerLink],
|
||||
};
|
389
models/baseModels/Payment/Payment.ts
Normal file
389
models/baseModels/Payment/Payment.ts
Normal file
@ -0,0 +1,389 @@
|
||||
import { LedgerPosting } from 'accounting/ledgerPosting';
|
||||
import frappe from 'frappe';
|
||||
import { DocValue } from 'frappe/core/types';
|
||||
import Doc from 'frappe/model/doc';
|
||||
import {
|
||||
Action,
|
||||
DefaultMap,
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
HiddenMap,
|
||||
ListViewSettings,
|
||||
RequiredMap,
|
||||
ValidationMap,
|
||||
} from 'frappe/model/types';
|
||||
import { ValidationError } from 'frappe/utils/errors';
|
||||
import { getLedgerLinkAction } from 'models/helpers';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
import { getIsNullOrUndef } from 'utils';
|
||||
import { Party } from '../Party/Party';
|
||||
import { PaymentMethod, PaymentType } from './types';
|
||||
|
||||
export class Payment extends Doc {
|
||||
async change({ changed }: { changed: string }) {
|
||||
switch (changed) {
|
||||
case 'for': {
|
||||
this.updateAmountOnReferenceUpdate();
|
||||
await this.updateDetailsOnReferenceUpdate();
|
||||
}
|
||||
case 'amount': {
|
||||
this.updateReferenceOnAmountUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateDetailsOnReferenceUpdate() {
|
||||
const forReferences = (this.for ?? []) as Doc[];
|
||||
const { referenceType, referenceName } = forReferences[0];
|
||||
if (
|
||||
forReferences.length !== 1 ||
|
||||
this.party ||
|
||||
this.paymentType ||
|
||||
!referenceName ||
|
||||
!referenceType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaName = referenceType as string;
|
||||
const doc = await frappe.doc.getDoc(schemaName, referenceName as string);
|
||||
|
||||
let party;
|
||||
let paymentType: PaymentType;
|
||||
|
||||
if (schemaName === 'SalesInvoice') {
|
||||
party = doc.customer;
|
||||
paymentType = 'Receive';
|
||||
} else {
|
||||
party = doc.supplier;
|
||||
paymentType = 'Pay';
|
||||
}
|
||||
|
||||
this.party = party;
|
||||
this.paymentType = paymentType;
|
||||
}
|
||||
|
||||
updateAmountOnReferenceUpdate() {
|
||||
this.amount = frappe.pesa(0);
|
||||
for (const paymentReference of this.for as Doc[]) {
|
||||
this.amount = (this.amount as Money).add(
|
||||
paymentReference.amount as Money
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateReferenceOnAmountUpdate() {
|
||||
const forReferences = (this.for ?? []) as Doc[];
|
||||
if (forReferences.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
forReferences[0].amount = this.amount;
|
||||
}
|
||||
|
||||
async validate() {
|
||||
this.validateAccounts();
|
||||
this.validateReferenceAmount();
|
||||
this.validateWriteOff();
|
||||
}
|
||||
|
||||
validateAccounts() {
|
||||
if (this.paymentAccount !== this.account || !this.account) {
|
||||
return;
|
||||
}
|
||||
throw new frappe.errors.ValidationError(
|
||||
`To Account and From Account can't be the same: ${this.account}`
|
||||
);
|
||||
}
|
||||
|
||||
validateReferenceAmount() {
|
||||
const forReferences = (this.for ?? []) as Doc[];
|
||||
if (forReferences.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceAmountTotal = forReferences
|
||||
.map(({ amount }) => amount as Money)
|
||||
.reduce((a, b) => a.add(b), frappe.pesa(0));
|
||||
|
||||
if (
|
||||
(this.amount as Money)
|
||||
.add((this.writeoff as Money) ?? 0)
|
||||
.gte(referenceAmountTotal)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const writeoff = frappe.format(this.writeoff, 'Currency');
|
||||
const payment = frappe.format(this.amount, 'Currency');
|
||||
const refAmount = frappe.format(referenceAmountTotal, 'Currency');
|
||||
|
||||
if ((this.writeoff as Money).gt(0)) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Amount: ${payment} and writeoff: ${writeoff}
|
||||
is less than the total amount allocated to
|
||||
references: ${refAmount}.`
|
||||
);
|
||||
}
|
||||
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Amount: ${payment} is less than the total
|
||||
amount allocated to references: ${refAmount}.`
|
||||
);
|
||||
}
|
||||
|
||||
validateWriteOff() {
|
||||
if ((this.writeoff as Money).isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frappe.singles.AccountingSettings!.writeOffAccount) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Write Off Account not set.
|
||||
Please set Write Off Account in General Settings`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getPosting() {
|
||||
const account = this.account as string;
|
||||
const paymentAccount = this.paymentAccount as string;
|
||||
const amount = this.amount as Money;
|
||||
const writeoff = this.writeoff as Money;
|
||||
const entries = new LedgerPosting({
|
||||
reference: this,
|
||||
party: this.party as string,
|
||||
});
|
||||
|
||||
await entries.debit(paymentAccount as string, amount.sub(writeoff));
|
||||
await entries.credit(account as string, amount.sub(writeoff));
|
||||
|
||||
if (writeoff.isZero()) {
|
||||
return [entries];
|
||||
}
|
||||
|
||||
const writeoffEntry = new LedgerPosting({
|
||||
reference: this,
|
||||
party: this.party as string,
|
||||
});
|
||||
const writeOffAccount = frappe.singles.AccountingSettings!
|
||||
.writeOffAccount as string;
|
||||
|
||||
if (this.paymentType === 'Pay') {
|
||||
await writeoffEntry.debit(account, writeoff);
|
||||
await writeoffEntry.credit(writeOffAccount, writeoff);
|
||||
} else {
|
||||
await writeoffEntry.debit(writeOffAccount, writeoff);
|
||||
await writeoffEntry.credit(account, writeoff);
|
||||
}
|
||||
|
||||
return [entries, writeoffEntry];
|
||||
}
|
||||
|
||||
async beforeSubmit() {
|
||||
const forReferences = (this.for ?? []) as Doc[];
|
||||
if (forReferences.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of forReferences) {
|
||||
if (
|
||||
!['SalesInvoice', 'PurchaseInvoice'].includes(
|
||||
row.referenceType as string
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const referenceDoc = await frappe.doc.getDoc(
|
||||
row.referenceType as string,
|
||||
row.referenceName as string
|
||||
);
|
||||
|
||||
let outstandingAmount = referenceDoc.outstandingAmount as Money;
|
||||
const baseGrandTotal = referenceDoc.baseGrandTotal as Money;
|
||||
const amount = this.amount as Money;
|
||||
|
||||
if (getIsNullOrUndef(outstandingAmount)) {
|
||||
outstandingAmount = baseGrandTotal;
|
||||
}
|
||||
|
||||
if (amount.lte(0) || amount.gt(outstandingAmount)) {
|
||||
let message = frappe.t`Payment amount: ${frappe.format(
|
||||
this.amount,
|
||||
'Currency'
|
||||
)} should be less than Outstanding amount: ${frappe.format(
|
||||
outstandingAmount,
|
||||
'Currency'
|
||||
)}.`;
|
||||
|
||||
if (amount.lte(0)) {
|
||||
const amt = frappe.format(this.amount, 'Currency');
|
||||
message = frappe.t`Payment amount: ${amt} should be greater than 0.`;
|
||||
}
|
||||
|
||||
throw new frappe.errors.ValidationError(message);
|
||||
} else {
|
||||
// update outstanding amounts in invoice and party
|
||||
const newOutstanding = outstandingAmount.sub(amount);
|
||||
await referenceDoc.set('outstandingAmount', newOutstanding);
|
||||
await referenceDoc.update();
|
||||
const party = (await frappe.doc.getDoc(
|
||||
'Party',
|
||||
this.party as string
|
||||
)) as Party;
|
||||
|
||||
await party.updateOutstandingAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async afterSubmit() {
|
||||
const entryList = await this.getPosting();
|
||||
for (const entry of entryList) {
|
||||
await entry.post();
|
||||
}
|
||||
}
|
||||
|
||||
async afterRevert() {
|
||||
this.updateReferenceOutstandingAmount();
|
||||
const entryList = await this.getPosting();
|
||||
for (const entry of entryList) {
|
||||
await entry.postReverse();
|
||||
}
|
||||
}
|
||||
|
||||
async updateReferenceOutstandingAmount() {
|
||||
await (this.for as Doc[]).forEach(
|
||||
async ({ amount, referenceType, referenceName }) => {
|
||||
const refDoc = await frappe.doc.getDoc(
|
||||
referenceType as string,
|
||||
referenceName as string
|
||||
);
|
||||
|
||||
refDoc.setMultiple({
|
||||
outstandingAmount: (refDoc.outstandingAmount as Money).add(
|
||||
amount as Money
|
||||
),
|
||||
});
|
||||
refDoc.update();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
defaults: DefaultMap = { date: () => new Date().toISOString() };
|
||||
|
||||
formulas: FormulaMap = {
|
||||
account: async () => {
|
||||
if (this.paymentMethod === 'Cash' && this.paymentType === 'Pay') {
|
||||
return 'Cash';
|
||||
}
|
||||
},
|
||||
paymentAccount: async () => {
|
||||
if (this.paymentMethod === 'Cash' && this.paymentType === 'Receive') {
|
||||
return 'Cash';
|
||||
}
|
||||
},
|
||||
amount: async () => this.getSum('for', 'amount', false),
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
amount: async (value: DocValue) => {
|
||||
if ((value as Money).isNegative()) {
|
||||
throw new ValidationError(
|
||||
frappe.t`Payment amount cannot be less than zero.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((this.for as Doc[]).length === 0) return;
|
||||
const amount = this.getSum('for', 'amount', false);
|
||||
|
||||
if ((value as Money).gt(amount)) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Payment amount cannot
|
||||
exceed ${frappe.format(amount, 'Currency')}.`
|
||||
);
|
||||
} else if ((value as Money).isZero()) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Payment amount cannot
|
||||
be ${frappe.format(value, 'Currency')}.`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
required: RequiredMap = {
|
||||
referenceId: () => this.paymentMethod !== 'Cash',
|
||||
clearanceDate: () => this.paymentMethod === 'Cash',
|
||||
};
|
||||
|
||||
hidden: HiddenMap = {
|
||||
referenceId: () => this.paymentMethod !== 'Cash',
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
numberSeries: () => {
|
||||
return { referenceType: 'Payment' };
|
||||
},
|
||||
account: (doc: Doc) => {
|
||||
const paymentType = doc.paymentType as PaymentType;
|
||||
const paymentMethod = doc.paymentMethod as PaymentMethod;
|
||||
|
||||
if (paymentType === 'Receive') {
|
||||
return { accountType: 'Receivable', isGroup: false };
|
||||
}
|
||||
|
||||
if (paymentMethod === 'Cash') {
|
||||
return { accountType: 'Cash', isGroup: false };
|
||||
} else {
|
||||
return { accountType: ['in', ['Bank', 'Cash']], isGroup: false };
|
||||
}
|
||||
},
|
||||
paymentAccount: (doc: Doc) => {
|
||||
const paymentType = doc.paymentType as PaymentType;
|
||||
const paymentMethod = doc.paymentMethod as PaymentMethod;
|
||||
|
||||
if (paymentType === 'Pay') {
|
||||
return { accountType: 'Payable', isGroup: false };
|
||||
}
|
||||
|
||||
if (paymentMethod === 'Cash') {
|
||||
return { accountType: 'Cash', isGroup: false };
|
||||
} else {
|
||||
return { accountType: ['in', ['Bank', 'Cash']], isGroup: false };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
static actions: Action[] = [getLedgerLinkAction()];
|
||||
|
||||
static listSettings: ListViewSettings = {
|
||||
columns: [
|
||||
'party',
|
||||
{
|
||||
label: frappe.t`Status`,
|
||||
fieldname: 'status',
|
||||
fieldtype: 'Select',
|
||||
size: 'small',
|
||||
render(doc) {
|
||||
let status = 'Draft';
|
||||
let color = 'gray';
|
||||
if (doc.submitted === 1) {
|
||||
color = 'green';
|
||||
status = 'Submitted';
|
||||
}
|
||||
if (doc.cancelled === 1) {
|
||||
color = 'red';
|
||||
status = 'Cancelled';
|
||||
}
|
||||
|
||||
return {
|
||||
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
|
||||
};
|
||||
},
|
||||
},
|
||||
'paymentType',
|
||||
'date',
|
||||
'amount',
|
||||
],
|
||||
};
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import Badge from '@/components/Badge';
|
||||
import { t } from 'frappe';
|
||||
|
||||
export default {
|
||||
doctype: 'Payment',
|
||||
title: t`Payments`,
|
||||
columns: [
|
||||
'party',
|
||||
{
|
||||
label: t`Status`,
|
||||
fieldname: 'status',
|
||||
fieldtype: 'Select',
|
||||
size: 'small',
|
||||
render(doc) {
|
||||
let status = 'Draft';
|
||||
let color = 'gray';
|
||||
if (doc.submitted === 1) {
|
||||
color = 'green';
|
||||
status = 'Submitted';
|
||||
}
|
||||
if (doc.cancelled === 1) {
|
||||
color = 'red';
|
||||
status = 'Cancelled';
|
||||
}
|
||||
|
||||
return {
|
||||
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
|
||||
components: { Badge },
|
||||
};
|
||||
},
|
||||
},
|
||||
'paymentType',
|
||||
'date',
|
||||
'amount',
|
||||
],
|
||||
};
|
@ -1,208 +0,0 @@
|
||||
import frappe from 'frappe';
|
||||
import Document from 'frappe/model/document';
|
||||
import LedgerPosting from '../../../accounting/ledgerPosting';
|
||||
|
||||
export default class PaymentServer extends Document {
|
||||
async change({ changed }) {
|
||||
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.doc.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 = frappe.pesa(0);
|
||||
for (let paymentReference of this.for) {
|
||||
this.amount = this.amount.add(paymentReference.amount);
|
||||
}
|
||||
}
|
||||
|
||||
updateReferenceOnAmountUpdate() {
|
||||
if (this.for?.length !== 1) return;
|
||||
this.for[0].amount = this.amount;
|
||||
}
|
||||
|
||||
async validate() {
|
||||
this.validateAccounts();
|
||||
this.validateReferenceAmount();
|
||||
this.validateWriteOff();
|
||||
}
|
||||
|
||||
validateAccounts() {
|
||||
if (this.paymentAccount !== this.account || !this.account) return;
|
||||
throw new frappe.errors.ValidationError(
|
||||
`To Account and From Account can't be the same: ${this.account}`
|
||||
);
|
||||
}
|
||||
|
||||
validateReferenceAmount() {
|
||||
if (!this.for?.length) return;
|
||||
|
||||
const referenceAmountTotal = this.for
|
||||
.map(({ amount }) => amount)
|
||||
.reduce((a, b) => a.add(b), frappe.pesa(0));
|
||||
|
||||
if (this.amount.add(this.writeoff ?? 0).gte(referenceAmountTotal)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const writeoff = frappe.format(this.writeoff, 'Currency');
|
||||
const payment = frappe.format(this.amount, 'Currency');
|
||||
const refAmount = frappe.format(referenceAmountTotal, 'Currency');
|
||||
|
||||
if (this.writeoff.gt(0)) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Amount: ${payment} and writeoff: ${writeoff}
|
||||
is less than the total amount allocated to
|
||||
references: ${refAmount}.`
|
||||
);
|
||||
}
|
||||
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Amount: ${payment} is less than the total
|
||||
amount allocated to references: ${refAmount}.`
|
||||
);
|
||||
}
|
||||
|
||||
validateWriteOff() {
|
||||
if (this.writeoff.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frappe.AccountingSettings.writeOffAccount) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Write Off Account not set.
|
||||
Please set Write Off Account in General Settings`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getPosting() {
|
||||
let entries = new LedgerPosting({ reference: this, party: this.party });
|
||||
|
||||
await entries.debit(this.paymentAccount, this.amount.sub(this.writeoff));
|
||||
await entries.credit(this.account, this.amount.sub(this.writeoff));
|
||||
|
||||
if (this.writeoff.isZero()) {
|
||||
return [entries];
|
||||
}
|
||||
|
||||
const writeoffEntry = new LedgerPosting({
|
||||
reference: this,
|
||||
party: this.party,
|
||||
});
|
||||
const { writeOffAccount } = frappe.AccountingSettings;
|
||||
|
||||
if (this.paymentType === 'Pay') {
|
||||
await writeoffEntry.debit(this.account, this.writeoff);
|
||||
await writeoffEntry.credit(writeOffAccount, this.writeoff);
|
||||
} else {
|
||||
await writeoffEntry.debit(writeOffAccount, this.writeoff);
|
||||
await writeoffEntry.credit(this.account, this.writeoff);
|
||||
}
|
||||
|
||||
return [entries, writeoffEntry];
|
||||
}
|
||||
|
||||
async beforeSubmit() {
|
||||
if (!this.for || !this.for.length) {
|
||||
return;
|
||||
}
|
||||
for (let row of this.for) {
|
||||
if (!['SalesInvoice', 'PurchaseInvoice'].includes(row.referenceType)) {
|
||||
continue;
|
||||
}
|
||||
let referenceDoc = await frappe.doc.getDoc(
|
||||
row.referenceType,
|
||||
row.referenceName
|
||||
);
|
||||
let { outstandingAmount, baseGrandTotal } = referenceDoc;
|
||||
if (outstandingAmount == null) {
|
||||
outstandingAmount = baseGrandTotal;
|
||||
}
|
||||
if (this.amount.lte(0) || this.amount.gt(outstandingAmount)) {
|
||||
let message = frappe.t`Payment amount: ${frappe.format(
|
||||
this.amount,
|
||||
'Currency'
|
||||
)} should be less than Outstanding amount: ${frappe.format(
|
||||
outstandingAmount,
|
||||
'Currency'
|
||||
)}.`;
|
||||
|
||||
if (this.amount.lte(0)) {
|
||||
const amt = frappe.format(this.amount, 'Currency');
|
||||
message = frappe.t`Payment amount: ${amt} should be greater than 0.`;
|
||||
}
|
||||
|
||||
throw new frappe.errors.ValidationError(message);
|
||||
} else {
|
||||
// update outstanding amounts in invoice and party
|
||||
let newOutstanding = outstandingAmount.sub(this.amount);
|
||||
await referenceDoc.set('outstandingAmount', newOutstanding);
|
||||
await referenceDoc.update();
|
||||
let party = await frappe.doc.getDoc('Party', this.party);
|
||||
await party.updateOutstandingAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async afterSubmit() {
|
||||
const entryList = await this.getPosting();
|
||||
for (const entry of entryList) {
|
||||
await entry.post();
|
||||
}
|
||||
}
|
||||
|
||||
async afterRevert() {
|
||||
this.updateReferenceOutstandingAmount();
|
||||
const entryList = await this.getPosting();
|
||||
for (const entry of entryList) {
|
||||
await entry.postReverse();
|
||||
}
|
||||
}
|
||||
|
||||
async updateReferenceOutstandingAmount() {
|
||||
await this.for.forEach(async ({ amount, referenceType, referenceName }) => {
|
||||
const refDoc = await frappe.doc.getDoc(referenceType, referenceName);
|
||||
refDoc.setMultiple({
|
||||
outstandingAmount: refDoc.outstandingAmount.add(amount),
|
||||
});
|
||||
refDoc.update();
|
||||
});
|
||||
}
|
||||
}
|
2
models/baseModels/Payment/types.ts
Normal file
2
models/baseModels/Payment/types.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type PaymentType = 'Receive' | 'Pay';
|
||||
export type PaymentMethod = 'Cash' | 'Cheque' | 'Transfer';
|
@ -64,7 +64,7 @@ export interface GetAllOptions {
|
||||
|
||||
export type QueryFilter = Record<
|
||||
string,
|
||||
boolean | string | (string | number)[]
|
||||
boolean | string | (string | number | (string | number)[])[]
|
||||
>;
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user