mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
feat(server): wip sales tax liability summary report
This commit is contained in:
@@ -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 };
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
};
|
||||
}
|
||||
@@ -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(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user