feat(server): wip sales tax liability summary report

This commit is contained in:
Ahmed Bouhuolia
2023-08-31 02:19:18 +02:00
parent 6535424d0f
commit 6baec8dd96
11 changed files with 535 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,7 @@ export interface ILedgerEntry {
itemId?: number;
branchId?: number;
projectId?: number;
taxRateId?: number;
entryId?: number;
createdAt?: Date;

View File

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

View File

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

View File

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

View File

@@ -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<string, { taxRateId: number; credit: number; debit: number }>
> {
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');
};
}

View File

@@ -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(),
},
};
}
}

View File

@@ -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',
},
];
}
}