mirror of
https://github.com/frappe/books.git
synced 2025-01-23 07:08:36 +00:00
Merge pull request #789 from frappe/mildred/755-taxes-on-payments
feat: #755 Accounting for taxes on payments
This commit is contained in:
commit
0da912e324
@ -28,6 +28,19 @@ import { TaxSummary } from '../TaxSummary/TaxSummary';
|
|||||||
import { ReturnDocItem } from 'models/inventory/types';
|
import { ReturnDocItem } from 'models/inventory/types';
|
||||||
import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/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 {
|
export abstract class Invoice extends Transactional {
|
||||||
_taxes: Record<string, Tax> = {};
|
_taxes: Record<string, Tax> = {};
|
||||||
taxes?: TaxSummary[];
|
taxes?: TaxSummary[];
|
||||||
@ -242,6 +255,38 @@ export abstract class Invoice extends Transactional {
|
|||||||
return safeParseFloat(exchangeRate.toFixed(2));
|
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() {
|
async getTaxSummary() {
|
||||||
const taxes: Record<
|
const taxes: Record<
|
||||||
string,
|
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 ?? []) {
|
taxes[account] ??= {
|
||||||
if (!item.tax) {
|
account,
|
||||||
continue;
|
rate: details.rate,
|
||||||
}
|
amount: this.fyo.pesa(0),
|
||||||
|
};
|
||||||
|
|
||||||
const tax = await this.getTax(item.tax);
|
taxes[account].amount = taxes[account].amount.add(taxAmount);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Summary = typeof taxes[string] & { idx: number };
|
type Summary = typeof taxes[string] & { idx: number };
|
||||||
|
@ -28,10 +28,12 @@ import { Invoice } from '../Invoice/Invoice';
|
|||||||
import { Party } from '../Party/Party';
|
import { Party } from '../Party/Party';
|
||||||
import { PaymentFor } from '../PaymentFor/PaymentFor';
|
import { PaymentFor } from '../PaymentFor/PaymentFor';
|
||||||
import { PaymentMethod, PaymentType } from './types';
|
import { PaymentMethod, PaymentType } from './types';
|
||||||
|
import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||||
|
|
||||||
type AccountTypeMap = Record<AccountTypeEnum, string[] | undefined>;
|
type AccountTypeMap = Record<AccountTypeEnum, string[] | undefined>;
|
||||||
|
|
||||||
export class Payment extends Transactional {
|
export class Payment extends Transactional {
|
||||||
|
taxes?: TaxSummary[];
|
||||||
party?: string;
|
party?: string;
|
||||||
amount?: Money;
|
amount?: Money;
|
||||||
writeoff?: 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() {
|
async getPosting() {
|
||||||
/**
|
/**
|
||||||
* account : From Account
|
* account : From Account
|
||||||
@ -244,6 +326,20 @@ export class Payment extends Transactional {
|
|||||||
await posting.debit(paymentAccount, amount);
|
await posting.debit(paymentAccount, amount);
|
||||||
await posting.credit(account, 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);
|
await this.applyWriteOffPosting(posting);
|
||||||
return posting;
|
return posting;
|
||||||
}
|
}
|
||||||
@ -546,6 +642,7 @@ export class Payment extends Transactional {
|
|||||||
return this.for![0].referenceType;
|
return this.for![0].referenceType;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
taxes: { formula: async () => await this.getTaxSummary() },
|
||||||
};
|
};
|
||||||
|
|
||||||
validations: ValidationMap = {
|
validations: ValidationMap = {
|
||||||
@ -588,6 +685,7 @@ export class Payment extends Transactional {
|
|||||||
attachment: () =>
|
attachment: () =>
|
||||||
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
|
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
|
||||||
for: () => !!((this.isSubmitted || this.isCancelled) && !this.for?.length),
|
for: () => !!((this.isSubmitted || this.isCancelled) && !this.for?.length),
|
||||||
|
taxes: () => !this.taxes?.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
|
@ -9,6 +9,7 @@ import { Invoice } from '../Invoice/Invoice';
|
|||||||
|
|
||||||
export class TaxSummary extends Doc {
|
export class TaxSummary extends Doc {
|
||||||
account?: string;
|
account?: string;
|
||||||
|
from_account?: string;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
amount?: Money;
|
amount?: Money;
|
||||||
parentdoc?: Invoice;
|
parentdoc?: Invoice;
|
||||||
|
@ -141,6 +141,14 @@
|
|||||||
"computed": true,
|
"computed": true,
|
||||||
"section": "Amounts"
|
"section": "Amounts"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "taxes",
|
||||||
|
"label": "Taxes",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"target": "TaxSummary",
|
||||||
|
"readOnly": true,
|
||||||
|
"section": "Amounts"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "for",
|
"fieldname": "for",
|
||||||
"label": "Payment Reference",
|
"label": "Payment Reference",
|
||||||
|
@ -6,12 +6,20 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "account",
|
"fieldname": "account",
|
||||||
"label": "Tax Account",
|
"label": "Tax Invoice Account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"target": "Account",
|
"target": "Account",
|
||||||
"create": true,
|
"create": true,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_account",
|
||||||
|
"label": "Tax Payment Account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"target": "Account",
|
||||||
|
"create": true,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"label": "Rate",
|
"label": "Rate",
|
||||||
@ -20,5 +28,5 @@
|
|||||||
"placeholder": "0%"
|
"placeholder": "0%"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tableFields": ["account", "rate"]
|
"tableFields": ["account", "payment_account", "rate"]
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,14 @@
|
|||||||
"target": "Account",
|
"target": "Account",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_account",
|
||||||
|
"label": "Tax Invoice Account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"target": "Account",
|
||||||
|
"required": false,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"label": "Tax Rate",
|
"label": "Tax Rate",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user