diff --git a/packages/server/src/api/controllers/FinancialStatements.ts b/packages/server/src/api/controllers/FinancialStatements.ts index cabbb1235..15b7900f8 100644 --- a/packages/server/src/api/controllers/FinancialStatements.ts +++ b/packages/server/src/api/controllers/FinancialStatements.ts @@ -20,6 +20,7 @@ import InventoryDetailsController from './FinancialStatements/InventoryDetails'; import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference'; import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions'; import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary'; +import SalesTaxLiabilitySummary from './FinancialStatements/SalesTaxLiabilitySummary'; @Service() export default class FinancialStatementsService { @@ -68,40 +69,44 @@ export default class FinancialStatementsService { ); router.use( '/customer-balance-summary', - Container.get(CustomerBalanceSummaryController).router(), + Container.get(CustomerBalanceSummaryController).router() ); router.use( '/vendor-balance-summary', - Container.get(VendorBalanceSummaryController).router(), + Container.get(VendorBalanceSummaryController).router() ); router.use( '/transactions-by-customers', - Container.get(TransactionsByCustomers).router(), + Container.get(TransactionsByCustomers).router() ); router.use( '/transactions-by-vendors', - Container.get(TransactionsByVendors).router(), + Container.get(TransactionsByVendors).router() ); router.use( '/cash-flow', - Container.get(CashFlowStatementController).router(), + Container.get(CashFlowStatementController).router() ); router.use( '/inventory-item-details', - Container.get(InventoryDetailsController).router(), + Container.get(InventoryDetailsController).router() ); router.use( '/transactions-by-reference', - Container.get(TransactionsByReferenceController).router(), + Container.get(TransactionsByReferenceController).router() ); router.use( '/cashflow-account-transactions', - Container.get(CashflowAccountTransactions).router(), + Container.get(CashflowAccountTransactions).router() ); router.use( '/project-profitability-summary', - Container.get(ProjectProfitabilityController).router(), - ) + Container.get(ProjectProfitabilityController).router() + ); + router.use( + '/sales-tax-liability-summary', + Container.get(SalesTaxLiabilitySummary).router() + ); return router; } } diff --git a/packages/server/src/api/controllers/FinancialStatements/SalesTaxLiabilitySummary/index.ts b/packages/server/src/api/controllers/FinancialStatements/SalesTaxLiabilitySummary/index.ts new file mode 100644 index 000000000..9af858699 --- /dev/null +++ b/packages/server/src/api/controllers/FinancialStatements/SalesTaxLiabilitySummary/index.ts @@ -0,0 +1,90 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { query } from 'express-validator'; +import { Inject } from 'typedi'; +import asyncMiddleware from '@/api/middleware/asyncMiddleware'; +import BaseFinancialReportController from '../BaseFinancialReportController'; +import { AbilitySubject, ReportsAction } from '@/interfaces'; +import CheckPolicies from '@/api/middleware/CheckPolicies'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SalesTaxLiabilitySummaryService } from '@/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService'; + +export default class SalesTaxLiabilitySummary extends BaseFinancialReportController { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private salesTaxLiabilitySummaryService: SalesTaxLiabilitySummaryService; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get( + '/', + CheckPolicies( + ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY, + AbilitySubject.Report + ), + this.validationSchema, + asyncMiddleware(this.salesTaxLiabilitySummary.bind(this)) + ); + return router; + } + + /** + * Validation schema. + */ + get validationSchema() { + return [ + query('from_date').optional().isISO8601(), + query('to_date').optional().isISO8601(), + ]; + } + + /* + * + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async salesTaxLiabilitySummary( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId, settings } = req; + const filter = this.matchedQueryData(req); + + try { + const accept = this.accepts(req); + const acceptType = accept.types(['json', 'application/json+table']); + + switch (acceptType) { + case 'application/json+table': + const salesTaxLiabilityTable = + await this.salesTaxLiabilitySummaryService.salesTaxLiabilitySummaryTable( + tenantId, + filter + ); + + return res.status(200).send({ + data: salesTaxLiabilityTable + }); + case 'json': + default: + const salesTaxLiability = + await this.salesTaxLiabilitySummaryService.salesTaxLiability( + tenantId, + filter + ); + return res.status(200).send({ + data: salesTaxLiability, + }); + } + } catch (error) { + next(error); + } + } +} diff --git a/packages/server/src/database/migrations/20230810191606_create_tax_rates.js b/packages/server/src/database/migrations/20230810191606_create_tax_rates.js index e5279151c..d83efd765 100644 --- a/packages/server/src/database/migrations/20230810191606_create_tax_rates.js +++ b/packages/server/src/database/migrations/20230810191606_create_tax_rates.js @@ -11,7 +11,7 @@ exports.up = (knex) => { table.timestamps(); }) .table('items_entries', (table) => { - table.boolean('is_tax_exclusive'); + table.boolean('is_inclusive_tax').defaultTo(false); table .integer('tax_rate_id') .unsigned() @@ -21,7 +21,7 @@ exports.up = (knex) => { table.decimal('tax_rate'); }) .table('sales_invoices', (table) => { - table.boolean('is_tax_exclusive'); + table.boolean('is_inclusive_tax').defaultTo(false); table.decimal('tax_amount_withheld'); }) .createTable('tax_rate_transactions', (table) => { @@ -35,6 +35,13 @@ exports.up = (knex) => { table.integer('reference_id'); table.decimal('tax_amount'); table.integer('tax_account_id').unsigned(); + }) + .table('accounts_transactions', (table) => { + table + .integer('tax_rate_id') + .unsigned() + .references('id') + .inTable('tax_rates'); }); }; diff --git a/packages/server/src/interfaces/FinancialStatements.ts b/packages/server/src/interfaces/FinancialStatements.ts index ca39183e0..fb1d77452 100644 --- a/packages/server/src/interfaces/FinancialStatements.ts +++ b/packages/server/src/interfaces/FinancialStatements.ts @@ -37,6 +37,7 @@ export enum ReportsAction { READ_INVENTORY_ITEM_DETAILS = 'read-inventory-item-details', READ_CASHFLOW_ACCOUNT_TRANSACTION = 'read-cashflow-account-transactions', READ_PROJECT_PROFITABILITY_SUMMARY = 'read-project-profitability-summary', + READ_SALES_TAX_LIABILITY_SUMMARY = 'read-sales-tax-liability-summary', } export interface IFinancialSheetBranchesQuery { diff --git a/packages/server/src/interfaces/Ledger.ts b/packages/server/src/interfaces/Ledger.ts index 8af6ac8b8..57a17f6c5 100644 --- a/packages/server/src/interfaces/Ledger.ts +++ b/packages/server/src/interfaces/Ledger.ts @@ -47,6 +47,7 @@ export interface ILedgerEntry { itemId?: number; branchId?: number; projectId?: number; + taxRateId?: number; entryId?: number; createdAt?: Date; diff --git a/packages/server/src/interfaces/SalesTaxLiabilitySummary.ts b/packages/server/src/interfaces/SalesTaxLiabilitySummary.ts new file mode 100644 index 000000000..cb8e0da33 --- /dev/null +++ b/packages/server/src/interfaces/SalesTaxLiabilitySummary.ts @@ -0,0 +1,33 @@ +export interface SalesTaxLiabilitySummaryQuery { + fromDate: Date; + toDate: Date; + basis: 'cash' | 'accrual'; +} + +export interface SalesTaxLiabilitySummaryAmount { + amount: number; + formattedAmount: string; + currencyCode: string; +} + +export interface SalesTaxLiabilitySummaryTotal { + taxableAmount: SalesTaxLiabilitySummaryAmount; + taxAmount: SalesTaxLiabilitySummaryAmount; +} + +export interface SalesTaxLiabilitySummaryRate { + taxName: string; + taxCode: string; + taxableAmount: SalesTaxLiabilitySummaryAmount; + taxAmount: SalesTaxLiabilitySummaryAmount; +} + +export enum SalesTaxLiabilitySummaryTableRowType { + TaxRate = 'TaxRate', + Total = 'Total', +} + +export interface SalesTaxLiabilitySummaryReportData { + taxRates: SalesTaxLiabilitySummaryRate[]; + total: SalesTaxLiabilitySummaryTotal; +} diff --git a/packages/server/src/interfaces/TaxRate.ts b/packages/server/src/interfaces/TaxRate.ts index 074e46501..0e6481d8d 100644 --- a/packages/server/src/interfaces/TaxRate.ts +++ b/packages/server/src/interfaces/TaxRate.ts @@ -1,6 +1,10 @@ import { Knex } from 'knex'; -export interface ITaxRate {} +export interface ITaxRate { + name: string; + code: string; + rate: number; +} export interface ICommonTaxRateDTO { name: string; diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummary.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummary.ts new file mode 100644 index 000000000..16e605de2 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummary.ts @@ -0,0 +1,95 @@ +import { ITaxRate } from '@/interfaces'; +import { + SalesTaxLiabilitySummaryQuery, + SalesTaxLiabilitySummaryRate, + SalesTaxLiabilitySummaryReportData, + SalesTaxLiabilitySummaryTotal, +} from '@/interfaces/SalesTaxLiabilitySummary'; +import { sumBy } from 'lodash'; +import FinancialSheet from '../FinancialSheet'; + +export class SalesTaxLiabilitySummary extends FinancialSheet { + query: SalesTaxLiabilitySummaryQuery; + taxRates: ITaxRate[]; + payableTaxesById: any; + salesTaxesById: any; + + /** + * Sales tax liability summary constructor. + * @param {SalesTaxLiabilitySummaryQuery} query + * @param {ITaxRate[]} taxRates + * @param payableTaxesById + * @param salesTaxesById + */ + constructor( + query: SalesTaxLiabilitySummaryQuery, + taxRates: ITaxRate[], + payableTaxesById: Record< + string, + { taxRateId: number; credit: number; debit: number } + >, + salesTaxesById: Record< + string, + { taxRateId: number; credit: number; debit: number } + > + ) { + super(); + + this.query = query; + this.taxRates = taxRates; + this.payableTaxesById = payableTaxesById; + this.salesTaxesById = salesTaxesById; + } + + /** + * Retrieves the tax rate liability node. + * @param {ITaxRate} taxRate + * @returns {SalesTaxLiabilitySummaryRate} + */ + private taxRateLiability = ( + taxRate: ITaxRate + ): SalesTaxLiabilitySummaryRate => { + return { + taxName: taxRate.name, + taxCode: taxRate.code, + taxableAmount: this.getAmountMeta(0), + taxAmount: this.getAmountMeta(0), + }; + }; + + /** + * Retrieves the tax rates liability nodes. + * @returns {SalesTaxLiabilitySummaryRate[]} + */ + private taxRatesLiability = (): SalesTaxLiabilitySummaryRate[] => { + return this.taxRates.map(this.taxRateLiability); + }; + + /** + * Retrieves the tax rates total node. + * @param {SalesTaxLiabilitySummaryRate[]} nodes + * @returns {SalesTaxLiabilitySummaryTotal} + */ + private taxRatesTotal = ( + nodes: SalesTaxLiabilitySummaryRate[] + ): SalesTaxLiabilitySummaryTotal => { + const taxableAmount = sumBy(nodes, 'taxableAmount.total'); + const taxAmount = sumBy(nodes, 'taxAmount.total'); + + return { + taxableAmount: this.getTotalAmountMeta(taxableAmount), + taxAmount: this.getTotalAmountMeta(taxAmount), + }; + }; + + /** + * Retrieves the report data. + * @returns {SalesTaxLiabilitySummaryReportData} + */ + public reportData = (): SalesTaxLiabilitySummaryReportData => { + const taxRates = this.taxRatesLiability(); + const total = this.taxRatesTotal(taxRates); + + return { taxRates, total }; + }; +} diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryRepository.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryRepository.ts new file mode 100644 index 000000000..bac9b548a --- /dev/null +++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryRepository.ts @@ -0,0 +1,71 @@ +import { ACCOUNT_TYPE } from '@/data/AccountTypes'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { keyBy } from 'lodash'; +import { Inject, Service } from 'typedi'; + +@Service() +export class SalesTaxLiabilitySummaryRepository { + @Inject() + private tenancy: HasTenancyService; + + /** + * Retrieve tax rates. + * @param tenantId + * @returns + */ + public taxRates = (tenantId: number) => { + const { TaxRate } = this.tenancy.models(tenantId); + + return TaxRate.query().orderBy('name', 'desc'); + }; + + /** + * Retrieve taxes payable sum grouped by tax rate id. + * @param {number} tenantId + * @returns + */ + public async taxesPayableSumGroupedByRateId( + tenantId: number + ): Promise< + Record + > { + const { AccountTransaction } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); + + const receivableAccount = + await accountRepository.findOrCreateAccountReceivable(); + + const groupedTaxesById = await AccountTransaction.query() + .where('account_id', receivableAccount.id) + .groupBy('tax_rate_id') + .select(['tax_rate_id']) + .sum('credit as credit') + .sum('debit as debit'); + + return keyBy(groupedTaxesById, 'taxRateId'); + } + + /** + * Retrieve taxes sales sum grouped by tax rate id. + * @param {number} tenantId + * @returns + */ + public taxesSalesSumGroupedByRateId = async (tenantId: number) => { + const { AccountTransaction, Account } = this.tenancy.models(tenantId); + + const incomeAccounts = await Account.query().whereIn('accountType', [ + ACCOUNT_TYPE.INCOME, + ACCOUNT_TYPE.OTHER_INCOME, + ]); + const incomeAccountsIds = incomeAccounts.map((account) => account.id); + + const groupedTaxesById = await AccountTransaction.query() + .whereIn('account_id', incomeAccountsIds) + .groupBy('tax_rate_id') + .select(['tax_rate_id']) + .sum('credit as credit') + .sum('debit as debit'); + + return keyBy(groupedTaxesById, 'taxRateId'); + }; +} diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService.ts new file mode 100644 index 000000000..8bd9774e6 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService.ts @@ -0,0 +1,64 @@ +import { Inject, Service } from 'typedi'; +import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository'; +import { SalesTaxLiabilitySummaryQuery } from '@/interfaces/SalesTaxLiabilitySummary'; +import { SalesTaxLiabilitySummary } from './SalesTaxLiabilitySummary'; +import { SalesTaxLiabilitySummaryTable } from './SalesTaxLiabilitySummaryTable'; + +@Service() +export class SalesTaxLiabilitySummaryService { + @Inject() + private repostiory: SalesTaxLiabilitySummaryRepository; + + /** + * + * @param tenantId + * @param query + * @returns + */ + public async salesTaxLiability( + tenantId: number, + query: SalesTaxLiabilitySummaryQuery + ) { + const payableByRateId = + await this.repostiory.taxesPayableSumGroupedByRateId(tenantId); + + const salesByRateId = await this.repostiory.taxesSalesSumGroupedByRateId( + tenantId + ); + const taxRates = await this.repostiory.taxRates(tenantId); + + const taxLiabilitySummary = new SalesTaxLiabilitySummary( + query, + taxRates, + payableByRateId, + salesByRateId + ); + return { + data: taxLiabilitySummary.reportData(), + query, + meta: {}, + }; + } + + /** + * + * @param tenantId + * @param query + * @returns + */ + public async salesTaxLiabilitySummaryTable( + tenantId: number, + query: SalesTaxLiabilitySummaryQuery + ) { + const report = await this.salesTaxLiability(tenantId, query); + + const table = new SalesTaxLiabilitySummaryTable(report.data, query); + + return { + table: { + rows: table.tableRows(), + columns: table.tableColumns(), + }, + }; + } +} diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryTable.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryTable.ts new file mode 100644 index 000000000..e469cbdb4 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryTable.ts @@ -0,0 +1,151 @@ +import * as R from 'ramda'; +import { + SalesTaxLiabilitySummaryQuery, + SalesTaxLiabilitySummaryRate, + SalesTaxLiabilitySummaryReportData, + SalesTaxLiabilitySummaryTotal, +} from '@/interfaces/SalesTaxLiabilitySummary'; +import { tableRowMapper } from '@/utils'; +import { ITableColumn, ITableColumnAccessor, ITableRow } from '@/interfaces'; + +enum IROW_TYPE { + TaxRate = 'TaxRate', + Total = 'Total', +} + +export class SalesTaxLiabilitySummaryTable { + data: SalesTaxLiabilitySummaryReportData; + query: SalesTaxLiabilitySummaryQuery; + + /** + * Sales tax liability summary table constructor. + * @param {SalesTaxLiabilitySummaryReportData} data + * @param {SalesTaxLiabilitySummaryQuery} query + */ + constructor( + data: SalesTaxLiabilitySummaryReportData, + query: SalesTaxLiabilitySummaryQuery + ) { + this.data = data; + this.query = query; + } + + /** + * Retrieve the tax rate row accessors. + * @returns {ITableColumnAccessor[]} + */ + private get taxRateRowAccessor() { + return [ + { key: 'taxName', value: 'taxName' }, + { key: 'taxCode', accessor: 'taxCode' }, + { key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' }, + { key: 'taxAmount', accessor: 'taxAmount.formattedAmount' }, + ]; + } + + /** + * Retrieve the tax rate total row accessors. + * @returns {ITableColumnAccessor[]} + */ + private get taxRateTotalRowAccessors() { + return [ + { key: 'taxName', value: '' }, + { key: 'taxCode', accessor: 'taxCode' }, + { key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' }, + { key: 'taxAmount', accessor: 'taxAmount.formattedAmount' }, + ]; + } + + /** + * Maps the tax rate node to table row. + * @param {SalesTaxLiabilitySummaryRate} node + * @returns {ITableRow} + */ + private taxRateTableRowMapper = ( + node: SalesTaxLiabilitySummaryRate + ): ITableRow => { + const columns = this.taxRateRowAccessor; + const meta = { + rowTypes: [IROW_TYPE.TaxRate], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Maps the tax rates nodes to table rows. + * @param {SalesTaxLiabilitySummaryRate[]} nodes + * @returns {ITableRow[]} + */ + private taxRatesTableRowsMapper = ( + nodes: SalesTaxLiabilitySummaryRate[] + ): ITableRow[] => { + return nodes.map(this.taxRateTableRowMapper); + }; + + /** + * Maps the tax rate total node to table row. + * @param {SalesTaxLiabilitySummaryTotal} node + * @returns {ITableRow} + */ + private taxRateTotalRowMapper = (node: SalesTaxLiabilitySummaryTotal) => { + const columns = this.taxRateTotalRowAccessors; + const meta = { + rowTypes: [IROW_TYPE.Total], + id: node.key, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Retrieves the tax rate total row. + * @returns {ITableRow} + */ + private get taxRateTotalRow(): ITableRow { + return this.taxRateTotalRowMapper(this.data.total); + } + + /** + * Retrieves the tax rates rows. + * @returns {ITableRow[]} + */ + private get taxRatesRows(): ITableRow[] { + return this.taxRatesTableRowsMapper(this.data.taxRates); + } + + /** + * Retrieve the table rows. + * @returns {ITableRow[]} + */ + public tableRows(): ITableRow[] { + return R.compose( + R.concat(this.taxRatesRows), + R.prepend(this.taxRateTotalRow) + )([]); + } + + /** + * Retrieve the table columns. + * @returns {ITableColumn[]} + */ + public tableColumns(): ITableColumn[] { + return [ + { + label: 'Tax Name', + key: 'taxName', + }, + { + label: 'Tax Code', + key: 'taxCode', + }, + { + label: 'Taxable Amount', + key: 'taxableAmount', + }, + { + label: 'Tax Rate', + key: 'taxRate', + }, + ]; + } +}