From b4041f564f513fa4c1895927b1006fbba6999417 Mon Sep 17 00:00:00 2001 From: Mildred Ki'Lya Date: Sat, 2 Dec 2023 22:57:11 +0100 Subject: [PATCH] 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. --- models/baseModels/Invoice/Invoice.ts | 65 ++++++++++----- models/baseModels/Payment/Payment.ts | 92 ++++++++++++++++++++++ models/baseModels/TaxSummary/TaxSummary.ts | 1 + schemas/app/Payment.json | 8 ++ schemas/app/TaxDetail.json | 12 ++- schemas/app/TaxSummary.json | 8 ++ 6 files changed, 164 insertions(+), 22 deletions(-) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 7c8d451f8..24f6be66c 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -28,6 +28,15 @@ 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 = {}; taxes?: TaxSummary[]; @@ -242,30 +251,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(): Promise { + 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 ( @@ -276,11 +270,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 f1b91b1f9..8795fae3f 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; @@ -221,6 +223,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 @@ -244,6 +320,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 +636,7 @@ export class Payment extends Transactional { return this.for![0].referenceType; }, }, + taxes: { formula: async () => await this.getTaxSummary() }, }; validations: ValidationMap = { @@ -588,6 +679,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/models/baseModels/TaxSummary/TaxSummary.ts b/models/baseModels/TaxSummary/TaxSummary.ts index bb9438e49..ad5bc6b39 100644 --- a/models/baseModels/TaxSummary/TaxSummary.ts +++ b/models/baseModels/TaxSummary/TaxSummary.ts @@ -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; diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index 780c8c55f..9f30e2d8c 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",