mirror of
https://github.com/frappe/books.git
synced 2025-04-10 20:11:50 +00:00
feat: #755 Accounting for taxes on payments
When defining taxes, it is possible to define an additional payment account that will be used during payments to move taxes from the original tax account to this new payment tax account. This allows to account for taxes only when payment is received. Now payments can reference tax summary objects that will reference the two accounts to move funds between when the payment is committed. Reuse some of the Invoice code to generate these tax summary objects.
This commit is contained in:
parent
5e4873cda8
commit
2bffcda8ff
@ -28,6 +28,19 @@ import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||
import { ReturnDocItem } from 'models/inventory/types';
|
||||
import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types';
|
||||
|
||||
export type TaxDetail = {
|
||||
account: string;
|
||||
payment_account?: string;
|
||||
rate: number;
|
||||
};
|
||||
|
||||
export type InvoiceTaxItem = {
|
||||
details: TaxDetail;
|
||||
exchangeRate?: number;
|
||||
fullAmount: Money;
|
||||
taxAmount: Money;
|
||||
};
|
||||
|
||||
export abstract class Invoice extends Transactional {
|
||||
_taxes: Record<string, Tax> = {};
|
||||
taxes?: TaxSummary[];
|
||||
@ -242,6 +255,38 @@ export abstract class Invoice extends Transactional {
|
||||
return safeParseFloat(exchangeRate.toFixed(2));
|
||||
}
|
||||
|
||||
async getTaxItems(): Promise<InvoiceTaxItem[]> {
|
||||
const taxItems: InvoiceTaxItem[] = [];
|
||||
for (const item of this.items ?? []) {
|
||||
if (!item.tax) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tax = await this.getTax(item.tax);
|
||||
for (const details of (tax.details ?? []) as TaxDetail[]) {
|
||||
let amount = item.amount!;
|
||||
if (
|
||||
this.enableDiscounting &&
|
||||
!this.discountAfterTax &&
|
||||
!item.itemDiscountedTotal?.isZero()
|
||||
) {
|
||||
amount = item.itemDiscountedTotal!;
|
||||
}
|
||||
|
||||
const taxItem: InvoiceTaxItem = {
|
||||
details,
|
||||
exchangeRate: this.exchangeRate ?? 1,
|
||||
fullAmount: amount,
|
||||
taxAmount: amount.mul(details.rate / 100),
|
||||
};
|
||||
|
||||
taxItems.push(taxItem);
|
||||
}
|
||||
}
|
||||
|
||||
return taxItems;
|
||||
}
|
||||
|
||||
async getTaxSummary() {
|
||||
const taxes: Record<
|
||||
string,
|
||||
@ -252,33 +297,16 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
> = {};
|
||||
|
||||
type TaxDetail = { account: string; rate: number };
|
||||
for (const { details, taxAmount } of await this.getTaxItems()) {
|
||||
const account = details.account;
|
||||
|
||||
for (const item of this.items ?? []) {
|
||||
if (!item.tax) {
|
||||
continue;
|
||||
}
|
||||
taxes[account] ??= {
|
||||
account,
|
||||
rate: details.rate,
|
||||
amount: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
const tax = await this.getTax(item.tax);
|
||||
for (const { account, rate } of (tax.details ?? []) as TaxDetail[]) {
|
||||
taxes[account] ??= {
|
||||
account,
|
||||
rate,
|
||||
amount: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
let amount = item.amount!;
|
||||
if (
|
||||
this.enableDiscounting &&
|
||||
!this.discountAfterTax &&
|
||||
!item.itemDiscountedTotal?.isZero()
|
||||
) {
|
||||
amount = item.itemDiscountedTotal!;
|
||||
}
|
||||
|
||||
const taxAmount = amount.mul(rate / 100);
|
||||
taxes[account].amount = taxes[account].amount.add(taxAmount);
|
||||
}
|
||||
taxes[account].amount = taxes[account].amount.add(taxAmount);
|
||||
}
|
||||
|
||||
type Summary = typeof taxes[string] & { idx: number };
|
||||
|
@ -28,10 +28,12 @@ import { Invoice } from '../Invoice/Invoice';
|
||||
import { Party } from '../Party/Party';
|
||||
import { PaymentFor } from '../PaymentFor/PaymentFor';
|
||||
import { PaymentMethod, PaymentType } from './types';
|
||||
import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||
|
||||
type AccountTypeMap = Record<AccountTypeEnum, string[] | undefined>;
|
||||
|
||||
export class Payment extends Transactional {
|
||||
taxes?: TaxSummary[];
|
||||
party?: string;
|
||||
amount?: Money;
|
||||
writeoff?: Money;
|
||||
@ -221,6 +223,86 @@ export class Payment extends Transactional {
|
||||
);
|
||||
}
|
||||
|
||||
async getTaxSummary() {
|
||||
const taxes: Record<
|
||||
string,
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
account: string;
|
||||
from_account: string;
|
||||
rate: number;
|
||||
amount: Money;
|
||||
}
|
||||
>
|
||||
> = {};
|
||||
|
||||
for (const childDoc of this.for ?? []) {
|
||||
const referenceName = childDoc.referenceName;
|
||||
const referenceType = childDoc.referenceType;
|
||||
|
||||
const refDoc = (await this.fyo.doc.getDoc(
|
||||
childDoc.referenceType!,
|
||||
childDoc.referenceName
|
||||
)) as Invoice;
|
||||
|
||||
if (referenceName && referenceType && !refDoc) {
|
||||
throw new ValidationError(
|
||||
t`${referenceType} of type ${
|
||||
this.fyo.schemaMap?.[referenceType]?.label ?? referenceType
|
||||
} does not exist`
|
||||
);
|
||||
}
|
||||
|
||||
if (!refDoc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const {
|
||||
details,
|
||||
taxAmount,
|
||||
exchangeRate,
|
||||
} of await refDoc.getTaxItems()) {
|
||||
const { account, payment_account } = details;
|
||||
if (!payment_account) {
|
||||
continue;
|
||||
}
|
||||
|
||||
taxes[payment_account] ??= {};
|
||||
taxes[payment_account][account] ??= {
|
||||
account: payment_account,
|
||||
from_account: account,
|
||||
rate: details.rate,
|
||||
amount: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
taxes[payment_account][account].amount = taxes[payment_account][
|
||||
account
|
||||
].amount.add(taxAmount.mul(exchangeRate ?? 1));
|
||||
}
|
||||
}
|
||||
|
||||
type Summary = typeof taxes[string][string] & { idx: number };
|
||||
const taxArr: Summary[] = [];
|
||||
let idx = 0;
|
||||
for (const payment_account in taxes) {
|
||||
for (const account in taxes[payment_account]) {
|
||||
const tax = taxes[payment_account][account];
|
||||
if (tax.amount.isZero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
taxArr.push({
|
||||
...tax,
|
||||
idx,
|
||||
});
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return taxArr;
|
||||
}
|
||||
|
||||
async getPosting() {
|
||||
/**
|
||||
* account : From Account
|
||||
@ -244,6 +326,20 @@ export class Payment extends Transactional {
|
||||
await posting.debit(paymentAccount, amount);
|
||||
await posting.credit(account, amount);
|
||||
|
||||
if (this.taxes) {
|
||||
if (this.paymentType === 'Receive') {
|
||||
for (const tax of this.taxes) {
|
||||
await posting.debit(tax.from_account!, tax.amount!);
|
||||
await posting.credit(tax.account!, tax.amount!);
|
||||
}
|
||||
} else if (this.paymentType === 'Pay') {
|
||||
for (const tax of this.taxes) {
|
||||
await posting.credit(tax.from_account!, tax.amount!);
|
||||
await posting.debit(tax.account!, tax.amount!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.applyWriteOffPosting(posting);
|
||||
return posting;
|
||||
}
|
||||
@ -546,6 +642,7 @@ export class Payment extends Transactional {
|
||||
return this.for![0].referenceType;
|
||||
},
|
||||
},
|
||||
taxes: { formula: async () => await this.getTaxSummary() },
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
@ -588,6 +685,7 @@ export class Payment extends Transactional {
|
||||
attachment: () =>
|
||||
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
|
||||
for: () => !!((this.isSubmitted || this.isCancelled) && !this.for?.length),
|
||||
taxes: () => !this.taxes?.length,
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
|
@ -9,6 +9,7 @@ import { Invoice } from '../Invoice/Invoice';
|
||||
|
||||
export class TaxSummary extends Doc {
|
||||
account?: string;
|
||||
from_account?: string;
|
||||
rate?: number;
|
||||
amount?: Money;
|
||||
parentdoc?: Invoice;
|
||||
|
@ -141,6 +141,14 @@
|
||||
"computed": true,
|
||||
"section": "Amounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes",
|
||||
"label": "Taxes",
|
||||
"fieldtype": "Table",
|
||||
"target": "TaxSummary",
|
||||
"readOnly": true,
|
||||
"section": "Amounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "for",
|
||||
"label": "Payment Reference",
|
||||
|
@ -6,12 +6,20 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": "Tax Account",
|
||||
"label": "Tax Invoice Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_account",
|
||||
"label": "Tax Payment Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"label": "Rate",
|
||||
@ -20,5 +28,5 @@
|
||||
"placeholder": "0%"
|
||||
}
|
||||
],
|
||||
"tableFields": ["account", "rate"]
|
||||
"tableFields": ["account", "payment_account", "rate"]
|
||||
}
|
||||
|
@ -10,6 +10,14 @@
|
||||
"target": "Account",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "from_account",
|
||||
"label": "Tax Invoice Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"required": false,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"label": "Tax Rate",
|
||||
|
Loading…
x
Reference in New Issue
Block a user