From 248c81ae8b8987857f089977a04fd00a9c272635 Mon Sep 17 00:00:00 2001 From: Mildred Ki'Lya Date: Sat, 2 Dec 2023 22:57:11 +0100 Subject: [PATCH] #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 sumary objects. --- models/baseModels/Invoice/Invoice.ts | 65 ++++++++++++++------ models/baseModels/Payment/Payment.ts | 92 ++++++++++++++++++++++++++++ schemas/app/Payment.json | 8 +++ schemas/app/TaxDetail.json | 12 +++- schemas/app/TaxSummary.json | 8 +++ 5 files changed, 163 insertions(+), 22 deletions(-) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 917a1e4d2..69a5a21e0 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -26,6 +26,15 @@ import { Payment } from '../Payment/Payment'; import { Tax } from '../Tax/Tax'; import { TaxSummary } from '../TaxSummary/TaxSummary'; +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 = {}; taxes?: TaxSummary[]; @@ -230,30 +239,15 @@ export abstract class Invoice extends Transactional { return safeParseFloat(exchangeRate.toFixed(2)); } - async getTaxSummary() { - const taxes: Record< - string, - { - account: string; - rate: number; - amount: Money; - } - > = {}; - - type TaxDetail = { account: string; rate: number }; - + async getTaxItems(): InvoiceTaxItem[] { + const taxItems: InvoiceTaxItem[] = [] for (const item of this.items ?? []) { if (!item.tax) { continue; } 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), - }; + for (const details of (tax.details ?? []) as TaxDetail[]) { let amount = item.amount!; if ( @@ -264,11 +258,42 @@ export abstract class Invoice extends Transactional { amount = item.itemDiscountedTotal!; } - const taxAmount = amount.mul(rate / 100); - taxes[account].amount = taxes[account].amount.add(taxAmount); + let 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, + { + account: string; + rate: number; + amount: Money; + } + > = {}; + + for(const { details, taxAmount } of await this.getTaxItems()) { + const account = details.account + + taxes[account] ??= { + account, + rate: details.rate, + amount: this.fyo.pesa(0), + } + + taxes[account].amount = taxes[account].amount.add(taxAmount); + } + type Summary = typeof taxes[string] & { idx: number }; const taxArr: Summary[] = []; let idx = 0; diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 073ca6082..61179eb7f 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -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; export class Payment extends Transactional { + taxes?: TaxSummary[]; party?: string; amount?: Money; writeoff?: Money; @@ -220,6 +222,80 @@ 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 @@ -243,6 +319,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; } @@ -520,6 +610,7 @@ export class Payment extends Transactional { formula: () => this.amount!.sub(this.writeoff!), dependsOn: ['amount', 'writeoff', 'for'], }, + taxes: { formula: async () => await this.getTaxSummary() }, }; validations: ValidationMap = { @@ -562,6 +653,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 = { diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index 71afc80a8..40f505b3f 100644 --- a/schemas/app/Payment.json +++ b/schemas/app/Payment.json @@ -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", diff --git a/schemas/app/TaxDetail.json b/schemas/app/TaxDetail.json index ca74c6355..91e5f2f9a 100644 --- a/schemas/app/TaxDetail.json +++ b/schemas/app/TaxDetail.json @@ -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"] } diff --git a/schemas/app/TaxSummary.json b/schemas/app/TaxSummary.json index d0d2893f5..00823bf75 100644 --- a/schemas/app/TaxSummary.json +++ b/schemas/app/TaxSummary.json @@ -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",