2
0
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:
Mildred Ki'Lya 2023-12-02 22:57:11 +01:00
parent 5e4873cda8
commit 2bffcda8ff
6 changed files with 178 additions and 27 deletions

View File

@ -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 };

View File

@ -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 = {

View File

@ -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;

View File

@ -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",

View File

@ -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"]
}

View File

@ -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",