2
0
mirror of https://github.com/frappe/books.git synced 2024-09-20 11:29:00 +00:00
books/models/baseModels/Invoice/Invoice.ts

199 lines
5.3 KiB
TypeScript
Raw Normal View History

2022-04-14 08:01:33 +00:00
import { LedgerPosting } from 'accounting/ledgerPosting';
import { DocValue } from 'frappe/core/types';
2022-04-14 08:01:33 +00:00
import Doc from 'frappe/model/doc';
import { DefaultMap, FiltersMap, FormulaMap } from 'frappe/model/types';
2022-04-14 08:01:33 +00:00
import Money from 'pesa/dist/types/src/money';
import { getExchangeRate } from '../../../accounting/exchangeRate';
import { Party } from '../Party/Party';
import { Payment } from '../Payment/Payment';
import { Tax } from '../Tax/Tax';
import { TaxSummary } from '../TaxSummary/TaxSummary';
2022-04-14 08:01:33 +00:00
export abstract class Invoice extends Doc {
2022-04-14 08:01:33 +00:00
_taxes: Record<string, Tax> = {};
taxes?: TaxSummary[];
2022-04-14 08:01:33 +00:00
party?: string;
account?: string;
currency?: string;
netTotal?: Money;
baseGrandTotal?: Money;
exchangeRate?: number;
abstract getPosting(): Promise<LedgerPosting>;
get isSales() {
return this.schemaName === 'SalesInvoice';
}
2022-04-14 08:01:33 +00:00
async getPayments() {
const payments = await this.frappe.db.getAll('PaymentFor', {
2022-04-14 08:01:33 +00:00
fields: ['parent'],
filters: { referenceName: this.name! },
2022-04-14 08:01:33 +00:00
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 afterSubmit() {
// post ledger entries
const entries = await this.getPosting();
await entries.post();
// update outstanding amounts
await this.frappe.db.update(this.schemaName, {
2022-04-14 08:01:33 +00:00
name: this.name as string,
outstandingAmount: this.baseGrandTotal!,
2022-04-14 08:01:33 +00:00
});
const party = (await this.frappe.doc.getDoc('Party', this.party!)) as Party;
2022-04-14 08:01:33 +00:00
await party.updateOutstandingAmount();
}
async afterRevert() {
const paymentRefList = await this.getPayments();
for (const paymentFor of paymentRefList) {
const paymentReference = paymentFor.parent;
const payment = (await this.frappe.doc.getDoc(
2022-04-14 08:01:33 +00:00
'Payment',
paymentReference as string
)) as Payment;
const paymentEntries = await payment.getPosting();
for (const entry of paymentEntries) {
await entry.postReverse();
}
// To set the payment status as unsubmitted.
await this.frappe.db.update('Payment', {
2022-04-14 08:01:33 +00:00
name: paymentReference,
submitted: false,
cancelled: true,
});
}
const entries = await this.getPosting();
await entries.postReverse();
}
async getExchangeRate() {
if (!this.currency) return 1.0;
const accountingSettings = await this.frappe.doc.getSingle(
'AccountingSettings'
);
2022-04-14 08:01:33 +00:00
const companyCurrency = accountingSettings.currency;
if (this.currency === companyCurrency) {
return 1.0;
}
return await getExchangeRate({
fromCurrency: this.currency!,
2022-04-14 08:01:33 +00:00
toCurrency: companyCurrency as string,
});
}
async getTaxSummary() {
const taxes: Record<
string,
{
account: string;
rate: number;
amount: Money;
baseAmount: Money;
[key: string]: DocValue;
}
2022-04-14 08:01:33 +00:00
> = {};
for (const row of this.items as Doc[]) {
if (!row.tax) {
continue;
}
const tax = await this.getTax(row.tax as string);
for (const d of tax.details as Doc[]) {
const account = d.account as string;
const rate = d.rate as number;
taxes[account] = taxes[account] || {
account,
rate,
amount: this.frappe.pesa(0),
baseAmount: this.frappe.pesa(0),
2022-04-14 08:01:33 +00:00
};
const amount = (row.amount as Money).mul(rate).div(100);
taxes[account].amount = taxes[account].amount.add(amount);
}
}
return Object.keys(taxes)
.map((account) => {
const tax = taxes[account];
tax.baseAmount = tax.amount.mul(this.exchangeRate!);
2022-04-14 08:01:33 +00:00
return tax;
})
.filter((tax) => !tax.amount.isZero());
}
async getTax(tax: string) {
if (!this._taxes![tax]) {
this._taxes[tax] = await this.frappe.doc.getDoc('Tax', tax);
2022-04-14 08:01:33 +00:00
}
return this._taxes[tax];
}
async getGrandTotal() {
return ((this.taxes ?? []) as Doc[])
.map((doc) => doc.amount as Money)
.reduce((a, b) => a.add(b), this.netTotal!);
2022-04-14 08:01:33 +00:00
}
formulas: FormulaMap = {
account: async () =>
this.getFrom('Party', this.party!, 'defaultAccount') as string,
currency: async () =>
(this.getFrom('Party', this.party!, 'currency') as string) ||
(this.frappe.singles.AccountingSettings!.currency as string),
exchangeRate: async () => await this.getExchangeRate(),
netTotal: async () => this.getSum('items', 'amount', false),
baseNetTotal: async () => this.netTotal!.mul(this.exchangeRate!),
taxes: async () => await this.getTaxSummary(),
grandTotal: async () => await this.getGrandTotal(),
baseGrandTotal: async () =>
(this.grandTotal as Money).mul(this.exchangeRate!),
outstandingAmount: async () => {
if (this.submitted) {
return;
}
return this.baseGrandTotal!;
},
};
defaults: DefaultMap = {
date: () => new Date().toISOString().slice(0, 10),
};
static filters: FiltersMap = {
account: (doc: Doc) => ({
isGroup: false,
accountType: doc.isSales ? 'Receivable' : 'Payable',
}),
numberSeries: (doc: Doc) => ({ referenceType: doc.schemaName }),
};
2022-04-14 08:01:33 +00:00
}