refactor: financial statements to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-29 00:55:53 +02:00
parent 9a5110aa38
commit 7b81d0c8e5
52 changed files with 2438 additions and 31 deletions

View File

@@ -8,9 +8,11 @@ import { TransactionsByVendorModule } from './modules/TransactionsByVendor/Trans
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module';
import { ARAgingSummaryModule } from './modules/ARAgingSummary/ARAgingSummary.module';
// import { APAgingSummaryModule } from './modules/APAgingSummary/APAgingSummary.module';
import { APAgingSummaryModule } from './modules/APAgingSummary/APAgingSummary.module';
import { InventoryItemDetailsModule } from './modules/InventoryItemDetails/InventoryItemDetails.module';
import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet/InventoryValuationSheet.module';
import { SalesTaxLiabilityModule } from './modules/SalesTaxLiabilitySummary/SalesTaxLiability.module';
import { JournalSheetModule } from './modules/JournalSheet/JournalSheet.module';
@Module({
providers: [],
@@ -24,9 +26,11 @@ import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet
TransactionsByCustomerModule,
TransactionsByReferenceModule,
ARAgingSummaryModule,
// APAgingSummaryModule,
APAgingSummaryModule,
InventoryItemDetailsModule,
InventoryValuationSheetModule,
SalesTaxLiabilityModule,
JournalSheetModule,
],
})
export class FinancialStatementsModule {}

View File

@@ -7,18 +7,22 @@ import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
import { APAgingSummaryController } from './APAgingSummary.controller';
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Module({
imports: [AgingSummaryModule],
imports: [AgingSummaryModule, FinancialSheetCommonModule],
providers: [
APAgingSummaryService,
APAgingSummaryMeta,
APAgingSummaryTableInjectable,
APAgingSummaryExportInjectable,
APAgingSummaryPdfInjectable,
APAgingSummaryRepository,
APAgingSummaryApplication
APAgingSummaryApplication,
TenancyContext,
],
controllers: [APAgingSummaryController],
})
export class APAgingSummaryModule {}

View File

@@ -1,10 +1,10 @@
import { isEmpty } from 'lodash';
import { Inject } from '@nestjs/common';
import { isEmpty, groupBy } from 'lodash';
import { Bill } from '@/modules/Bills/models/Bill';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { Inject } from '@nestjs/common';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { groupBy } from 'ramda';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { ModelObject } from 'objection';
export class APAgingSummaryRepository {
@Inject(Vendor.name)
@@ -32,19 +32,19 @@ export class APAgingSummaryRepository {
* Due bills by vendor id.
* @param {Record<string, Bill[]>} dueBillsByVendorId
*/
dueBillsByVendorId: Record<number, Bill[]>;
dueBillsByVendorId: Record<string, Bill[]>;
/**
* Overdue bills.
* @param {Bill[]} overdueBills
* @param {Bill[]} overdueBills - overdue bills.
*/
overdueBills: Bill[];
overdueBills: ModelObject<Bill>[];
/**
* Overdue bills by vendor id.
* @param {Record<string, Bill[]>} overdueBillsByVendorId
* @param {Record<string, Bill[]>} overdueBillsByVendorId - Overdue bills by vendor id.
*/
overdueBillsByVendorId: Record<number, Bill[]>;
overdueBillsByVendorId: ModelObject<Bill>[];
/**
* Vendors.

View File

@@ -1,3 +1,4 @@
import { I18nService } from 'nestjs-i18n';
import {
IAPAgingSummaryQuery,
IAPAgingSummaryTable,
@@ -8,18 +9,21 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class APAgingSummaryTableInjectable {
constructor(private readonly APAgingSummarySheet: APAgingSummaryService) {}
constructor(
private readonly APAgingSummarySheet: APAgingSummaryService,
private readonly i18nService: I18nService,
) {}
/**
* Retrieves A/P aging summary in table format.
* @param {IAPAgingSummaryQuery} query -
* @param {IAPAgingSummaryQuery} query -
* @returns {Promise<IAPAgingSummaryTable>}
*/
public async table(
query: IAPAgingSummaryQuery,
): Promise<IAPAgingSummaryTable> {
const report = await this.APAgingSummarySheet.APAgingSummary(query);
const table = new APAgingSummaryTable(report.data, query, {});
const table = new APAgingSummaryTable(report.data, query, this.i18nService);
return {
table: {

View File

@@ -0,0 +1,55 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { IJournalReportQuery } from './JournalSheet.types';
import { Response } from 'express';
import { AcceptType } from '@/constants/accept-type';
import { JournalSheetApplication } from './JournalSheetApplication';
@Controller('/reports/journal')
export class JournalSheetController {
constructor(private readonly journalSheetApp: JournalSheetApplication) {}
@Get('/')
async journalSheet(
@Query() query: IJournalReportQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.journalSheetApp.table(query);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.journalSheetApp.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.journalSheetApp.xlsx(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
return res.send(buffer);
// Retrieves the json format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.journalSheetApp.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
} else {
const sheet = await this.journalSheetApp.sheet(query);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { JournalSheetController } from './JournalSheet.controller';
import { JournalSheetApplication } from './JournalSheetApplication';
import { JournalSheetPdfInjectable } from './JournalSheetPdfInjectable';
import { JournalSheetExportInjectable } from './JournalSheetExport';
import { JournalSheetService } from './JournalSheetService';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
import { JournalSheetRepository } from './JournalSheetRepository';
import { JournalSheetMeta } from './JournalSheetMeta';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
@Module({
imports: [FinancialSheetCommonModule, AccountsModule],
controllers: [JournalSheetController],
providers: [
JournalSheetApplication,
JournalSheetTableInjectable,
JournalSheetService,
JournalSheetExportInjectable,
JournalSheetPdfInjectable,
JournalSheetRepository,
JournalSheetMeta,
TenancyContext,
],
})
export class JournalSheetModule {}

View File

@@ -0,0 +1,135 @@
import { I18nService } from 'nestjs-i18n';
import { sumBy, chain, get, head } from 'lodash';
import moment from 'moment';
import {
IJournalReportEntriesGroup,
IJournalReportQuery,
IJournalSheetEntry,
IJournalTableData,
} from './JournalSheet.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { JournalSheetRepository } from './JournalSheetRepository';
import { Ledger } from '@/modules/Ledger/Ledger';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
export class JournalSheet extends FinancialSheet {
readonly ledger: Ledger;
readonly query: IJournalReportQuery;
readonly repository: JournalSheetRepository;
readonly i18n: I18nService;
/**
* Constructor method.
*/
constructor(
query: IJournalReportQuery,
repository: JournalSheetRepository,
i18n: I18nService,
) {
super();
this.query = query;
this.repository = repository;
this.numberFormat = {
...this.numberFormat,
...this.query.numberFormat,
};
this.i18n = i18n;
}
/**
* Entry mapper.
* @param {ILedgerEntry} entry
*/
entryMapper(entry: ILedgerEntry): IJournalSheetEntry {
const account = this.repository.accountsGraph.getNodeData(entry.accountId);
const contact = this.repository.contactsById.get(entry.contactId);
return {
entryId: entry.id,
index: entry.index,
note: entry.note,
contactName: get(contact, 'displayName'),
contactType: get(contact, 'contactService'),
accountName: account.name,
accountCode: account.code,
transactionNumber: entry.transactionNumber,
currencyCode: this.baseCurrency,
formattedCredit: this.formatNumber(entry.credit),
formattedDebit: this.formatNumber(entry.debit),
credit: entry.credit,
debit: entry.debit,
createdAt: entry.createdAt,
};
}
/**
* maps the journal entries.
* @param {IJournalEntry[]} entries -
*/
entriesMapper(entries: ILedgerEntry[]): Array<IJournalSheetEntry> {
return entries.map(this.entryMapper.bind(this));
}
/**
* Mapping journal entries groups.
* @param {ILedgerEntry[]} entriesGroup -
* @param {ILedgerEntry} key -
* @return {IJournalReportEntriesGroup}
*/
entriesGroupsMapper(
entriesGroup: ILedgerEntry[],
groupEntry: ILedgerEntry,
): IJournalReportEntriesGroup {
const totalCredit = sumBy(entriesGroup, 'credit');
const totalDebit = sumBy(entriesGroup, 'debit');
return {
date: moment(groupEntry.date).toDate(),
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
transactionType: groupEntry.transactionType,
referenceId: groupEntry.transactionId,
referenceTypeFormatted: this.i18n.t(groupEntry.transactionType),
entries: this.entriesMapper(entriesGroup),
currencyCode: this.baseCurrency,
credit: totalCredit,
debit: totalDebit,
formattedCredit: this.formatTotalNumber(totalCredit),
formattedDebit: this.formatTotalNumber(totalDebit),
};
}
/**
* Mapping the journal entries to entries groups.
* @param {IJournalEntry[]} entries
* @return {IJournalReportEntriesGroup[]}
*/
entriesWalker(entries: ILedgerEntry[]): IJournalReportEntriesGroup[] {
return chain(entries)
.groupBy((entry) => `${entry.referenceId}-${entry.referenceType}`)
.map((entriesGroup: ILedgerEntry[], key: string) => {
const headEntry = head(entriesGroup);
return this.entriesGroupsMapper(entriesGroup, headEntry);
})
.value();
}
/**
* Retrieve journal report.
* @return {IJournalReport}
*/
reportData(): IJournalTableData {
return this.entriesWalker(this.ledger.entries);
}
}

View File

@@ -0,0 +1,82 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
export interface IJournalReportQuery {
fromDate: Date | string;
toDate: Date | string;
numberFormat: {
noCents: boolean;
divideOn1000: boolean;
};
transactionType: string;
transactionId: string;
accountsIds: number | number[];
fromRange: number;
toRange: number;
}
export interface IJournalSheetEntry {
entryId: number;
index: number;
credit: number;
debit: number;
formattedDebit: string;
formattedCredit: string;
contactType: string;
contactName: string;
currencyCode: string;
accountName: string;
accountCode: string;
transactionNumber: string;
note: string;
createdAt: Date | string;
}
export interface IJournalReportEntriesGroup {
date: Date;
dateFormatted: string;
entries: IJournalSheetEntry[];
currencyCode: string;
credit: number;
debit: number;
formattedCredit: string;
formattedDebit: string;
transactionType: string;
referenceId: number;
referenceTypeFormatted: string;
}
export interface IJournalReport {
entries: IJournalReportEntriesGroup[];
}
export interface IJournalSheetMeta extends IFinancialSheetCommonMeta {
formattedDateRange: string;
formattedFromDate: string;
formattedToDate: string;
}
export interface IJournalTable extends IFinancialTable {
query: IJournalReportQuery;
meta: IJournalSheetMeta;
}
export type IJournalTableData = IJournalReportEntriesGroup[];
export interface IJournalSheet {
data: IJournalTableData;
query: IJournalReportQuery;
meta: IJournalSheetMeta;
}

View File

@@ -0,0 +1,59 @@
import { JournalSheetService } from './JournalSheetService';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
import { JournalSheetExportInjectable } from './JournalSheetExport';
import { JournalSheetPdfInjectable } from './JournalSheetPdfInjectable';
import { IJournalReportQuery, IJournalTable } from './JournalSheet.types';
export class JournalSheetApplication {
constructor(
private readonly journalSheetTable: JournalSheetTableInjectable,
private readonly journalSheet: JournalSheetService,
private readonly journalExport: JournalSheetExportInjectable,
private readonly journalPdf: JournalSheetPdfInjectable,
) {}
/**
* Retrieves the journal sheet.
* @param {IJournalReportQuery} query
* @returns {}
*/
public sheet(query: IJournalReportQuery) {
return this.journalSheet.journalSheet(query);
}
/**
* Retrieves the journal sheet in table format.
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalTable>}
*/
public table(query: IJournalReportQuery): Promise<IJournalTable> {
return this.journalSheetTable.table(query);
}
/**
* Retrieves the journal sheet in xlsx format.
* @param {IJournalReportQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: IJournalReportQuery) {
return this.journalExport.xlsx(query);
}
/**
* Retrieves the journal sheet in csv format.
* @param {IJournalReportQuery} query
* @returns {Promise<string>}
*/
public csv(query: IJournalReportQuery) {
return this.journalExport.csv(query);
}
/**
* Retrieves the journal sheet in pdf format.
* @param {IJournalReportQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: IJournalReportQuery) {
return this.journalPdf.pdf(query);
}
}

View File

@@ -0,0 +1,39 @@
import { IJournalReportQuery } from './JournalSheet.types';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
import { Injectable } from '@nestjs/common';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class JournalSheetExportInjectable {
constructor(
private readonly journalSheetTable: JournalSheetTableInjectable,
) {}
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {IJournalReportQuery} query - Journal report query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: IJournalReportQuery) {
const table = await this.journalSheetTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the trial balance sheet in CSV format.
* @param {IJournalReportQuery} query - Journal report query.
* @returns {Promise<Buffer>}
*/
public async csv(query: IJournalReportQuery): Promise<string> {
const table = await this.journalSheetTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,31 @@
import moment from 'moment';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import { IJournalReportQuery, IJournalSheetMeta } from './JournalSheet.types';
@Injectable()
export class JournalSheetMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieves the journal sheet meta.
* @param {IJournalReportQuery} query -
* @returns {Promise<IJournalSheetMeta>}
*/
public async meta(
query: IJournalReportQuery,
): Promise<IJournalSheetMeta> {
const common = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
return {
...common,
formattedDateRange,
formattedFromDate,
formattedToDate,
};
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
import { HtmlTableCustomCss } from './constant';
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { IJournalReportQuery } from './JournalSheet.types';
@Injectable()
export class JournalSheetPdfInjectable {
constructor(
private readonly journalSheetTable: JournalSheetTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given journal sheet table to pdf.
* @param {number} tenantId - Tenant ID.
* @param {IBalanceSheetQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: IJournalReportQuery): Promise<Buffer> {
const table = await this.journalSheetTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,132 @@
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { Contact } from '@/modules/Contacts/models/Contact';
import { Ledger } from '@/modules/Ledger/Ledger';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { transformToMap } from '@/utils/transform-to-key';
import { Inject } from '@nestjs/common';
import { ModelObject } from 'objection';
export class JournalSheetRepository {
@Inject(TenancyContext)
private tenancyContext: TenancyContext;
@Inject(AccountRepository)
private accountRepository: AccountRepository;
@Inject(Contact.name)
private contactModel: typeof Contact;
@Inject(AccountTransaction.name)
private accountTransaction: typeof AccountTransaction;
@Inject(AccountTransaction.name)
private accountTransactions: Array<ModelObject<AccountTransaction>>;
/**
*
*/
public filter: any;
/**
*
*/
public accountsGraph: any;
/**
*
*/
public contacts: Array<ModelObject<Contact>>
/**
* Contacts by id map.
*/
public contactsById: Map<number, ModelObject<Contact>>;
/**
*
*/
public ledger: Ledger;
public baseCurrency: string;
setFilter(filter: any) {
this.filter = filter;
}
/**
* Loads the journal sheet data.
*/
async load() {
await this.initBaseCurrency();
await this.initAccountsGraph();
await this.initAccountTransactions();
await this.initContacts();
await this.initLedger();
}
/**
* Initialize base currency.
*/
async initBaseCurrency () {
const metadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = metadata.baseCurrency;
}
/**
* Initialize accounts graph.
*/
async initAccountsGraph() {
// Retrieve all accounts on the storage.
const accountsGraph = await this.accountRepository.getDependencyGraph();
this.accountsGraph = accountsGraph;
}
/**
* Initialize account transactions.
*/
async initAccountTransactions() {
// Retrieve all journal transactions based on the given query.
const transactions = await this.accountTransaction
.query()
.onBuild((query) => {
if (this.filter.fromRange || this.filter.toRange) {
query.modify(
'filterAmountRange',
this.filter.fromRange,
this.filter.toRange,
);
}
query.modify(
'filterDateRange',
this.filter.fromDate,
this.filter.toDate,
);
query.orderBy(['date', 'createdAt', 'indexGroup', 'index']);
if (this.filter.transactionType) {
query.where('reference_type', this.filter.transactionType);
}
if (this.filter.transactionType && this.filter.transactionId) {
query.where('reference_id', this.filter.transactionId);
}
});
this.accountTransactions = transactions;
}
/**
* Initialize contacts.
*/
async initContacts() {
const contacts = await this.contactModel.query();
this.contacts = contacts;
this.contactsById = transformToMap(contacts, 'id');
}
async initLedger(){
this.ledger = Ledger.fromTransactions(this.accountTransactions);
}
}

View File

@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { I18nService } from 'nestjs-i18n';
import { JournalSheet } from './JournalSheet';
import { JournalSheetMeta } from './JournalSheetMeta';
import { getJournalSheetDefaultQuery } from './constant';
import { IJournalReportQuery, IJournalSheet } from './JournalSheet.types';
import { events } from '@/common/events/events';
import { JournalSheetRepository } from './JournalSheetRepository';
@Injectable()
export class JournalSheetService {
constructor(
private readonly journalSheetMeta: JournalSheetMeta,
private readonly journalRepository: JournalSheetRepository,
private readonly eventPublisher: EventEmitter2,
private readonly i18n: I18nService
) {}
/**
* Journal sheet.
* @param {IJournalReportQuery} query - Journal sheet query.
* @returns {Promise<IJournalSheet>}
*/
async journalSheet(query: IJournalReportQuery): Promise<IJournalSheet> {
const filter = {
...getJournalSheetDefaultQuery(),
...query,
};
this.journalRepository.setFilter(query);
await this.journalRepository.load();
// Journal report instance.
const journalSheetInstance = new JournalSheet(
filter,
this.journalRepository,
this.i18n,
);
// Retrieve journal report columns.
const journalSheetData = journalSheetInstance.reportData();
// Retrieve the journal sheet meta.
const meta = await this.journalSheetMeta.meta(filter);
// Triggers `onJournalViewed` event.
await this.eventPublisher.emitAsync(events.reports.onJournalViewed, {
query,
});
return {
data: journalSheetData,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,240 @@
import * as R from 'ramda';
import { first } from 'lodash';
import { I18nService } from 'nestjs-i18n';
import {
IJournalReportEntriesGroup,
IJournalReportQuery,
IJournalSheetEntry,
IJournalTableData,
} from './JournalSheet.types';
import { ROW_TYPE } from './types';
import { FinancialTable } from '../../common/FinancialTable';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialSheet } from '../../common/FinancialSheet';
import {
IColumnMapperMeta,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
export class JournalSheetTable extends R.pipe(
FinancialTable,
FinancialSheetStructure,
)(FinancialSheet) {
data: IJournalTableData;
query: IJournalReportQuery;
i18n: any;
/**
* Constructor method.
* @param {IJournalTableData} data -
* @param {IJournalReportQuery} query -
* @param {I18nService} i18n - I18n service.
*/
constructor(
data: IJournalTableData,
query: IJournalReportQuery,
i18n: I18nService,
) {
super();
this.data = data;
this.query = query;
this.i18n = i18n;
}
/**
* Retrieves the common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private groupColumnsAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'date', accessor: 'dateFormatted' },
{ key: 'transaction_type', accessor: 'referenceTypeFormatted' },
{ key: 'transaction_number', accessor: 'entry.transactionNumber' },
{ key: 'description', accessor: 'entry.note' },
{ key: 'account_code', accessor: 'entry.accountCode' },
{ key: 'account_name', accessor: 'entry.accountName' },
{ key: 'debit', accessor: 'entry.formattedDebit' },
{ key: 'credit', accessor: 'entry.formattedCredit' },
];
};
/**
* Retrieves the group entry accessors.
* @returns {ITableColumnAccessor[]}
*/
private entryColumnsAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'date', accessor: '_empty_' },
{ key: 'transaction_type', accessor: '_empty_' },
{ key: 'transaction_number', accessor: 'transactionNumber' },
{ key: 'description', accessor: 'note' },
{ key: 'account_code', accessor: 'accountCode' },
{ key: 'account_name', accessor: 'accountName' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'credit', accessor: 'formattedCredit' },
];
};
/**
* Retrieves the total entry column accessors.
* @returns {ITableColumnAccessor[]}
*/
private totalEntryColumnAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'date', accessor: '_empty_' },
{ key: 'transaction_type', accessor: '_empty_' },
{ key: 'transaction_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'account_code', accessor: '_empty_' },
{ key: 'account_name', accessor: '_empty_' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'credit', accessor: 'formattedCredit' },
];
};
/**
* Retrieves the total entry column accessors.
* @returns {IColumnMapperMeta[]}
*/
private blankEnrtyColumnAccessors = (): IColumnMapperMeta[] => {
return [
{ key: 'date', value: '' },
{ key: 'transaction_type', value: '' },
{ key: 'transaction_number', value: '' },
{ key: 'description', value: '' },
{ key: 'account_code', value: '' },
{ key: 'account_name', value: '' },
{ key: 'debit', value: '' },
{ key: 'credit', value: '' },
];
};
/**
* Retrieves the common columns.
* @returns {ITableColumn[]}
*/
private commonColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'transaction_type', label: 'Transaction Type' },
{ key: 'transaction_number', label: 'Num.' },
{ key: 'description', label: 'Description' },
{ key: 'account_code', label: 'Acc. Code' },
{ key: 'account_name', label: 'Account' },
{ key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
];
}
/**
* Maps the group and first entry to table row.
* @param {IJournalReportEntriesGroup} group
* @returns {ITableRow}
*/
private firstEntryGroupMapper = (
group: IJournalReportEntriesGroup,
): ITableRow => {
const meta = {
rowTypes: [ROW_TYPE.ENTRY],
};
const computedGroup = { ...group, entry: first(group.entries) };
const columns = this.groupColumnsAccessors();
return tableRowMapper(computedGroup, columns, meta);
};
/**
* Maps the given group entry to table rows.
* @param {IJournalEntry} entry
* @returns {ITableRow}
*/
private entryMapper = (entry: IJournalSheetEntry): ITableRow => {
const columns = this.entryColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.ENTRY],
};
return tableRowMapper(entry, columns, meta);
};
/**
* Maps the given group entries to table rows.
* @param {IJournalReportEntriesGroup} group
* @returns {ITableRow[]}
*/
private entriesMapper = (group: IJournalReportEntriesGroup): ITableRow[] => {
const entries = R.remove(0, 1, group.entries);
return R.map(this.entryMapper, entries);
};
/**
* Maps the given group entry to total table row.
* @param {IJournalReportEntriesGroup} group
* @returns {ITableRow}
*/
public totalEntryMapper = (group: IJournalReportEntriesGroup): ITableRow => {
const total = this.totalEntryColumnAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(group, total, meta);
};
/**
* Retrieves the blank entry row.
* @returns {ITableRow}
*/
private blankEntryMapper = (): ITableRow => {
const columns = this.blankEnrtyColumnAccessors();
const meta = {};
return tableRowMapper({} as ILedgerEntry, columns, meta);
};
/**
* Maps the entry group to table rows.
* @param {IJournalReportEntriesGroup} group -
* @returns {ITableRow}
*/
private groupMapper = (group: IJournalReportEntriesGroup): ITableRow[] => {
const firstRow = this.firstEntryGroupMapper(group);
const lastRows = this.entriesMapper(group);
const totalRow = this.totalEntryMapper(group);
const blankRow = this.blankEntryMapper();
return [firstRow, ...lastRows, totalRow, blankRow];
};
/**
* Maps the given group entries to table rows.
* @param {IJournalReportEntriesGroup[]} entries -
* @returns {ITableRow[]}
*/
private groupsMapper = (
entries: IJournalReportEntriesGroup[],
): ITableRow[] => {
return R.compose(R.flatten, R.map(this.groupMapper))(entries);
};
/**
* Retrieves the table data rows.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
return R.compose(this.groupsMapper)(this.data);
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = this.commonColumns();
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,33 @@
import { JournalSheetService } from './JournalSheetService';
import { IJournalReportQuery, IJournalTable } from './JournalSheet.types';
import { JournalSheetTable } from './JournalSheetTable';
import { I18nService } from 'nestjs-i18n';
export class JournalSheetTableInjectable {
constructor(
private readonly journalSheetService: JournalSheetService,
private readonly i18nService: I18nService,
) {}
/**
* Retrieves the journal sheet in table format.
* @param {IJournalReportQuery} query - Journal report query.
* @returns {Promise<IJournalTable>}
*/
public async table(query: IJournalReportQuery): Promise<IJournalTable> {
const journal = await this.journalSheetService.journalSheet(query);
const table = new JournalSheetTable(
journal.data,
journal.query,
this.i18nService,
);
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
query: journal.query,
meta: journal.meta,
};
}
}

View File

@@ -0,0 +1,29 @@
export const HtmlTableCustomCss = `
table tr.row-type--total td{
font-weight: 600;
}
table tr td:not(:first-child) {
border-left: 1px solid #ececec;
}
table tr:last-child td {
border-bottom: 1px solid #ececec;
}
table .cell--credit,
table .cell--debit,
table .column--credit,
table .column--debit{
text-align: right;
}
`;
export const getJournalSheetDefaultQuery = () => ({
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
fromRange: null,
toRange: null,
accountsIds: [],
numberFormat: {
noCents: false,
divideOn1000: false,
},
});

View File

@@ -0,0 +1,5 @@
export enum ROW_TYPE {
ENTRY = 'ENTRY',
TOTAL = 'TOTAL'
};

View File

@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { SalesTaxLiabiltiySummaryPdf } from './SalesTaxLiabiltiySummaryPdf';
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
import { SalesTaxLiabilitySummaryExportInjectable } from './SalesTaxLiabilitySummaryExportInjectable';
import { SalesTaxLiabilitySummaryService } from './SalesTaxLiabilitySummaryService';
import { SalesTaxLiabilitySummaryApplication } from './SalesTaxLiabilitySummaryApplication';
import { SalesTaxLiabilitySummaryController } from './SalesTaxLiabilitySummary.controller';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
import { SalesTaxLiabilitySummaryMeta } from './SalesTaxLiabilitySummaryMeta';
@Module({
imports: [FinancialSheetCommonModule],
providers: [
SalesTaxLiabiltiySummaryPdf,
SalesTaxLiabilitySummaryTableInjectable,
SalesTaxLiabilitySummaryExportInjectable,
SalesTaxLiabilitySummaryService,
SalesTaxLiabilitySummaryRepository,
SalesTaxLiabilitySummaryMeta,
SalesTaxLiabilitySummaryApplication,
],
controllers: [SalesTaxLiabilitySummaryController],
})
export class SalesTaxLiabilityModule {}

View File

@@ -0,0 +1,61 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
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;
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
}
export interface SalesTaxLiabilitySummaryRate {
id: number;
taxName: string;
taxableAmount: SalesTaxLiabilitySummaryAmount;
taxAmount: SalesTaxLiabilitySummaryAmount;
taxPercentage: any;
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
}
export enum SalesTaxLiabilitySummaryTableRowType {
TaxRate = 'TaxRate',
Total = 'Total',
}
export interface SalesTaxLiabilitySummaryReportData {
taxRates: SalesTaxLiabilitySummaryRate[];
total: SalesTaxLiabilitySummaryTotal;
}
export type SalesTaxLiabilitySummaryPayableById = Record<
string,
{ taxRateId: number; credit: number; debit: number }
>;
export type SalesTaxLiabilitySummarySalesById = Record<
string,
{ taxRateId: number; credit: number; debit: number }
>;
export interface SalesTaxLiabilitySummaryMeta
extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDate: string;
formattedDateRange: string;
}
export interface ISalesTaxLiabilitySummaryTable extends IFinancialTable {
query: SalesTaxLiabilitySummaryQuery;
meta: SalesTaxLiabilitySummaryMeta;
}

View File

@@ -0,0 +1,54 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
import { AcceptType } from '@/constants/accept-type';
import { SalesTaxLiabilitySummaryApplication } from './SalesTaxLiabilitySummaryApplication';
import { Response } from 'express';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('/reports/sales-tax-liability-summary')
@PublicRoute()
export class SalesTaxLiabilitySummaryController {
constructor(
private readonly salesTaxLiabilitySummaryApp: SalesTaxLiabilitySummaryApplication,
) {}
@Get()
public async getSalesTaxLiabilitySummary(
@Query() query: SalesTaxLiabilitySummaryQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.salesTaxLiabilitySummaryApp.table(query);
return res.status(200).send(table);
// Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.salesTaxLiabilitySummaryApp.xlsx(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
return res.send(buffer);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.salesTaxLiabilitySummaryApp.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the json format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.salesTaxLiabilitySummaryApp.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.status(200).send(pdfContent);
} else {
const sheet = await this.salesTaxLiabilitySummaryApp.sheet(query);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,125 @@
import * as R from 'ramda';
import { isEmpty, sumBy } from 'lodash';
import {
SalesTaxLiabilitySummaryQuery,
SalesTaxLiabilitySummaryRate,
SalesTaxLiabilitySummaryReportData,
SalesTaxLiabilitySummaryTotal,
} from './SalesTaxLiability.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ModelObject } from 'objection';
import { TaxRateModel } from '@/modules/TaxRates/models/TaxRate.model';
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
export class SalesTaxLiabilitySummary extends FinancialSheet {
private query: SalesTaxLiabilitySummaryQuery;
private repository: SalesTaxLiabilitySummaryRepository;
/**
* Sales tax liability summary constructor.
* @param {SalesTaxLiabilitySummaryQuery} query
* @param {ITaxRate[]} taxRates
* @param {SalesTaxLiabilitySummaryPayableById} payableTaxesById
* @param {SalesTaxLiabilitySummarySalesById} salesTaxesById
*/
constructor(
query: SalesTaxLiabilitySummaryQuery,
repository: SalesTaxLiabilitySummaryRepository,
) {
super();
this.query = query;
this.repository = repository;
}
/**
* Retrieves the tax rate liability node.
* @param {ITaxRate} taxRate
* @returns {SalesTaxLiabilitySummaryRate}
*/
private taxRateLiability = (
taxRate: ModelObject<TaxRateModel>,
): SalesTaxLiabilitySummaryRate => {
const payableTax = this.repository.taxesPayableByTaxRateId[taxRate.id];
const salesTax = this.repository.accountTransactionsByTaxRateId[taxRate.id];
const payableTaxAmount = payableTax
? payableTax.credit - payableTax.debit
: 0;
const salesTaxAmount = salesTax ? salesTax.credit - salesTax.debit : 0;
// Calculates the tax percentage.
const taxPercentage = R.compose(
R.unless(R.equals(0), R.divide(R.__, salesTaxAmount)),
)(payableTaxAmount);
// Calculates the payable tax amount.
const collectedTaxAmount = payableTax ? payableTax.debit : 0;
return {
id: taxRate.id,
taxName: `${taxRate.name} (${taxRate.rate}%)`,
taxableAmount: this.getAmountMeta(salesTaxAmount),
taxAmount: this.getAmountMeta(payableTaxAmount),
taxPercentage: this.getPercentageTotalAmountMeta(taxPercentage),
collectedTaxAmount: this.getAmountMeta(collectedTaxAmount),
};
};
/**
* Filters the non-transactions tax rates.
* @param {SalesTaxLiabilitySummaryRate[]} nodes
* @returns {SalesTaxLiabilitySummaryRate[]}
*/
private filterNonTransactionsTaxRates = (
nodes: SalesTaxLiabilitySummaryRate[],
): SalesTaxLiabilitySummaryRate[] => {
return nodes.filter((node) => {
const salesTrxs = this.repository.accountTransactionsByTaxRateId[node.id];
const payableTrxs = this.repository.taxesPayableByTaxRateId[node.id];
return !isEmpty(salesTrxs) || !isEmpty(payableTrxs);
});
};
/**
* Retrieves the tax rates liability nodes.
* @returns {SalesTaxLiabilitySummaryRate[]}
*/
private taxRatesLiability = (): SalesTaxLiabilitySummaryRate[] => {
return R.compose(
this.filterNonTransactionsTaxRates,
R.map(this.taxRateLiability),
)(this.repository.taxRates);
};
/**
* Retrieves the tax rates total node.
* @param {SalesTaxLiabilitySummaryRate[]} nodes
* @returns {SalesTaxLiabilitySummaryTotal}
*/
private taxRatesTotal = (
nodes: SalesTaxLiabilitySummaryRate[],
): SalesTaxLiabilitySummaryTotal => {
const taxableAmount = sumBy(nodes, 'taxableAmount.amount');
const taxAmount = sumBy(nodes, 'taxAmount.amount');
const collectedTaxAmount = sumBy(nodes, 'collectedTaxAmount.amount');
return {
taxableAmount: this.getTotalAmountMeta(taxableAmount),
taxAmount: this.getTotalAmountMeta(taxAmount),
collectedTaxAmount: this.getTotalAmountMeta(collectedTaxAmount),
};
};
/**
* 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,62 @@
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
import { SalesTaxLiabilitySummaryExportInjectable } from './SalesTaxLiabilitySummaryExportInjectable';
import { SalesTaxLiabilitySummaryService } from './SalesTaxLiabilitySummaryService';
import { SalesTaxLiabiltiySummaryPdf } from './SalesTaxLiabiltiySummaryPdf';
import { Injectable } from '@nestjs/common';
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
@Injectable()
export class SalesTaxLiabilitySummaryApplication {
constructor(
private readonly salesTaxLiabilitySheet: SalesTaxLiabilitySummaryService,
private readonly salesTaxLiabilityExport: SalesTaxLiabilitySummaryExportInjectable,
private readonly salesTaxLiabilityTable: SalesTaxLiabilitySummaryTableInjectable,
private readonly salesTaxLiabiltiyPdf: SalesTaxLiabiltiySummaryPdf,
) {}
/**
* Retrieves the sales tax liability summary in json format.
* @param {SalesTaxLiabilitySummaryQuery} query -
* @returns {Promise<Buffer>}
*/
public sheet(query: SalesTaxLiabilitySummaryQuery) {
return this.salesTaxLiabilitySheet.salesTaxLiability(query);
}
/**
* Retrieves the sales tax liability summary in table format.
* @param {SalesTaxLiabilitySummaryQuery} query
* @return {Promise<Buffer>}
*/
public table(query: SalesTaxLiabilitySummaryQuery) {
return this.salesTaxLiabilityTable.table(query);
}
/**
* Retrieves the sales tax liability summary in XLSX format.
* @param {SalesTaxLiabilitySummaryQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
return this.salesTaxLiabilityExport.xlsx(query);
}
/**
* Retrieves the sales tax liability summary in CSV format.
* @param {SalesTaxLiabilitySummaryQuery} query
* @returns {Promise<string>}
*/
public csv(query: SalesTaxLiabilitySummaryQuery): Promise<string> {
return this.salesTaxLiabilityExport.csv(query);
}
/**
* Retrieves the sales tax liability summary in PDF format.
* @param {SalesTaxLiabilitySummaryQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
return this.salesTaxLiabiltiyPdf.pdf(query);
}
}

View File

@@ -0,0 +1,39 @@
import { TableSheet } from '../../common/TableSheet';
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class SalesTaxLiabilitySummaryExportInjectable {
constructor(
private readonly salesTaxLiabilityTable: SalesTaxLiabilitySummaryTableInjectable,
) {}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {SalesTaxLiabilitySummaryQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
const table = await this.salesTaxLiabilityTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the cashflow sheet in CSV format.
* @param {SalesTaxLiabilitySummaryQuery} query
* @returns {Promise<string>}
*/
public async csv(query: SalesTaxLiabilitySummaryQuery): Promise<string> {
const table = await this.salesTaxLiabilityTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,31 @@
import * as moment from 'moment';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
@Injectable()
export class SalesTaxLiabilitySummaryMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieves the report meta.
* @param {number} tenantId
* @param {SalesTaxLiabilitySummaryQuery} filter
*/
public async meta(query: SalesTaxLiabilitySummaryQuery) {
const commonMeta = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
const sheetName = 'Sales Tax Liability Summary';
return {
...commonMeta,
sheetName,
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,132 @@
import { ACCOUNT_TYPE } from '@/constants/accounts';
import {
SalesTaxLiabilitySummaryPayableById,
SalesTaxLiabilitySummarySalesById,
} from './SalesTaxLiability.types';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { keyBy } from 'lodash';
import { TaxRateModel } from '@/modules/TaxRates/models/TaxRate.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ModelObject } from 'objection';
@Injectable({ scope: Scope.TRANSIENT })
export class SalesTaxLiabilitySummaryRepository {
@Inject(TaxRateModel.name)
private readonly taxRateModel: typeof TaxRateModel;
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction;
@Inject(Account.name)
private readonly accountModel: typeof Account;
/**
* @param {SalesTaxLiabilitySummarySalesById}
*/
accountTransactionsByTaxRateId: SalesTaxLiabilitySummarySalesById;
/**
* @param {SalesTaxLiabilitySummaryPayableById}
*/
taxesPayableByTaxRateId: SalesTaxLiabilitySummaryPayableById;
/**
* @param {Array<ModelObject<TaxRateModel>>}
*/
taxRates: Array<ModelObject<TaxRateModel>>;
/**
* Load data.
*/
async load() {
await this.initTaxRates();
await this.initTaxesPayableByTaxRateId();
await this.initAccountTransactionsByTaxRateId();
}
/**
* Initialize tax rates.
*/
async initTaxRates() {
const taxRates = await this.getTaxRates();
this.taxRates = taxRates;
}
/**
* Initialize account transactions by tax rate id.
*/
async initAccountTransactionsByTaxRateId() {
const transactionsByTaxRateId = await this.taxesSalesSumGroupedByRateId();
this.accountTransactionsByTaxRateId = transactionsByTaxRateId;
}
/**
* Initialize taxes payable by tax rate id.
*/
async initTaxesPayableByTaxRateId() {
const payableTaxes = await this.getTaxesPayableSumGroupedByRateId();
this.taxesPayableByTaxRateId = payableTaxes;
}
/**
* Retrieve tax rates.
* @param {number} tenantId
* @returns {Promise<TaxRate[]>}
*/
public getTaxRates = () => {
return this.taxRateModel.query().orderBy('name', 'desc');
};
/**
* Retrieve taxes payable sum grouped by tax rate id.
* @returns {Promise<SalesTaxLiabilitySummaryPayableById>}
*/
public async getTaxesPayableSumGroupedByRateId(): Promise<SalesTaxLiabilitySummaryPayableById> {
// Retrieves tax payable accounts.
const taxPayableAccounts = await this.accountModel
.query()
.whereIn('accountType', [ACCOUNT_TYPE.TAX_PAYABLE]);
const payableAccountsIds = taxPayableAccounts.map((account) => account.id);
const groupedTaxesById = await this.accountTransactionModel
.query()
.whereIn('account_id', payableAccountsIds)
.whereNot('tax_rate_id', null)
.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.
* @returns {Promise<SalesTaxLiabilitySummarySalesById>}
*/
public taxesSalesSumGroupedByRateId =
async (): Promise<SalesTaxLiabilitySummarySalesById> => {
const incomeAccounts = await this.accountModel
.query()
.whereIn('accountType', [
ACCOUNT_TYPE.INCOME,
ACCOUNT_TYPE.OTHER_INCOME,
]);
const incomeAccountsIds = incomeAccounts.map((account) => account.id);
const groupedTaxesById = await this.accountTransactionModel
.query()
.whereIn('account_id', incomeAccountsIds)
.whereNot('tax_rate_id', null)
.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,34 @@
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
import { SalesTaxLiabilitySummary } from './SalesTaxLiabilitySummary';
import { SalesTaxLiabilitySummaryMeta } from './SalesTaxLiabilitySummaryMeta';
import { Injectable } from '@nestjs/common';
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
@Injectable()
export class SalesTaxLiabilitySummaryService {
constructor(
private readonly repository: SalesTaxLiabilitySummaryRepository,
private readonly salesTaxLiabilityMeta: SalesTaxLiabilitySummaryMeta,
) {}
/**
* Retrieve sales tax liability summary.
* @param {SalesTaxLiabilitySummaryQuery} query
* @returns
*/
public async salesTaxLiability(query: SalesTaxLiabilitySummaryQuery) {
await this.repository.load();
const taxLiabilitySummary = new SalesTaxLiabilitySummary(
query,
this.repository,
);
const meta = await this.salesTaxLiabilityMeta.meta(query);
return {
data: taxLiabilitySummary.reportData(),
query,
meta,
};
}
}

View File

@@ -0,0 +1,161 @@
import * as R from 'ramda';
import {
SalesTaxLiabilitySummaryQuery,
SalesTaxLiabilitySummaryRate,
SalesTaxLiabilitySummaryReportData,
SalesTaxLiabilitySummaryTotal,
} from './SalesTaxLiability.types';
import { AgingReport } from '../AgingSummary/AgingReport';
import { IROW_TYPE } from './_constants';
import { FinancialTable } from '../../common/FinancialTable';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { ITableRow } from '../../types/Table.types';
import { ITableColumn } from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
export class SalesTaxLiabilitySummaryTable extends R.pipe(
FinancialTable,
FinancialSheetStructure,
)(AgingReport) {
private data: SalesTaxLiabilitySummaryReportData;
private query: SalesTaxLiabilitySummaryQuery;
/**
* Sales tax liability summary table constructor.
* @param {SalesTaxLiabilitySummaryReportData} data
* @param {SalesTaxLiabilitySummaryQuery} query
*/
constructor(
data: SalesTaxLiabilitySummaryReportData,
query: SalesTaxLiabilitySummaryQuery,
) {
super();
this.data = data;
this.query = query;
}
/**
* Retrieve the tax rate row accessors.
* @returns {ITableColumnAccessor[]}
*/
private get taxRateRowAccessor() {
return [
{ key: 'taxName', accessor: 'taxName' },
{ key: 'taxPercentage', accessor: 'taxPercentage.formattedAmount' },
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
{ key: 'collectedTax', accessor: 'collectedTaxAmount.formattedAmount' },
{ key: 'taxAmount', accessor: 'taxAmount.formattedAmount' },
];
}
/**
* Retrieve the tax rate total row accessors.
* @returns {ITableColumnAccessor[]}
*/
private get taxRateTotalRowAccessors() {
return [
{ key: 'taxName', value: 'Total' },
{ key: 'taxPercentage', value: '' },
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
{ key: 'collectedTax', accessor: 'collectedTaxAmount.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],
};
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);
}
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
return R.compose(
R.unless(R.isEmpty, R.append(this.taxRateTotalRow)),
R.concat(this.taxRatesRows),
)([]);
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
return R.compose(this.tableColumnsCellIndexing)([
{
label: 'Tax Name',
key: 'taxName',
},
{
label: 'Tax Percentage',
key: 'taxPercentage',
},
{
label: 'Taxable Amount',
key: 'taxableAmount',
},
{
label: 'Collected Tax',
key: 'collectedTax',
},
{
label: 'Tax Amount',
key: 'taxRate',
},
]);
}
}

View File

@@ -0,0 +1,36 @@
import {
ISalesTaxLiabilitySummaryTable,
SalesTaxLiabilitySummaryQuery,
} from './SalesTaxLiability.types';
import { SalesTaxLiabilitySummaryTable } from './SalesTaxLiabilitySummaryTable';
import { SalesTaxLiabilitySummaryService } from './SalesTaxLiabilitySummaryService';
import { Injectable } from '@nestjs/common';
@Injectable()
export class SalesTaxLiabilitySummaryTableInjectable {
constructor(
private readonly salesTaxLiability: SalesTaxLiabilitySummaryService,
) {}
/**
* Retrieve sales tax liability summary table.
* @param {SalesTaxLiabilitySummaryQuery} query
* @returns {Promise<ISalesTaxLiabilitySummaryTable>}
*/
public async table(
query: SalesTaxLiabilitySummaryQuery,
): Promise<ISalesTaxLiabilitySummaryTable> {
const report = await this.salesTaxLiability.salesTaxLiability(query);
// Creates the sales tax liability summary table.
const table = new SalesTaxLiabilitySummaryTable(report.data, query);
return {
table: {
rows: table.tableRows(),
columns: table.tableColumns(),
},
query: report.query,
meta: report.meta,
};
}
}

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
@Injectable()
export class SalesTaxLiabiltiySummaryPdf {
constructor(
private readonly salesTaxLiabiltiySummaryTable: SalesTaxLiabilitySummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given sales tax liability summary table to pdf.
* @param {ISalesByItemsReportQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
const table = await this.salesTaxLiabiltiySummaryTable.table(
query,
);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
);
}
}

View File

@@ -0,0 +1,4 @@
export enum IROW_TYPE {
TaxRate = 'TaxRate',
Total = 'Total',
}

View File

@@ -13,7 +13,7 @@ import { TransactionsByContactRepository } from './TransactionsByContactReposito
export class TransactionsByContact extends FinancialSheet {
public readonly filter: ITransactionsByContactsFilter;
public readonly i18n: I18nService
public readonly i18n: I18nService;
public readonly repository: TransactionsByContactRepository;
/**
@@ -22,7 +22,7 @@ export class TransactionsByContact extends FinancialSheet {
* @return {Omit<ITransactionsByContactsTransaction, 'runningBalance'>}
*/
protected contactTransactionMapper(
entry: ILedgerEntry
entry: ILedgerEntry,
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
const account = this.repository.accountsGraph.getNodeData(entry.accountId);
const currencyCode = this.baseCurrency;
@@ -37,6 +37,7 @@ export class TransactionsByContact extends FinancialSheet {
// @ts-ignore
// transactionType: this.i18n.t(entry.referenceTypeFormatted),
transactionType: '',
// @ts-ignore
date: entry.date,
createdAt: entry.createdAt,
};
@@ -51,7 +52,7 @@ export class TransactionsByContact extends FinancialSheet {
protected contactTransactionRunningBalance(
openingBalance: number,
accountNormal: 'credit' | 'debit',
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[]
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[],
): any {
let _openingBalance = openingBalance;
@@ -69,10 +70,10 @@ export class TransactionsByContact extends FinancialSheet {
const runningBalance = this.getTotalAmountMeta(
_openingBalance,
transaction.currencyCode
transaction.currencyCode,
);
return { ...transaction, runningBalance };
}
},
);
}
@@ -85,7 +86,7 @@ export class TransactionsByContact extends FinancialSheet {
protected getContactClosingBalance(
customerTransactions: ITransactionsByContactsTransaction[],
contactNormal: 'credit' | 'debit',
openingBalance: number
openingBalance: number,
): number {
const closingBalance = openingBalance;
@@ -124,7 +125,7 @@ export class TransactionsByContact extends FinancialSheet {
*/
protected getContactAmount(
amount: number,
currencyCode: string
currencyCode: string,
): ITransactionsByContactsAmount {
return {
amount,
@@ -153,7 +154,7 @@ export class TransactionsByContact extends FinancialSheet {
* @returns {boolean}
*/
private filterContactByNoneTransaction = (
transactionsByContact: ITransactionsByContactsContact
transactionsByContact: ITransactionsByContactsContact,
): boolean => {
return transactionsByContact.transactions.length > 0;
};
@@ -164,7 +165,7 @@ export class TransactionsByContact extends FinancialSheet {
* @returns {boolean}
*/
private filterContactNoneZero = (
transactionsByContact: ITransactionsByContactsContact
transactionsByContact: ITransactionsByContactsContact,
): boolean => {
return transactionsByContact.closingBalance.amount !== 0;
};
@@ -190,7 +191,7 @@ export class TransactionsByContact extends FinancialSheet {
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
protected contactsFilter = (
nodes: ITransactionsByContactsContact[]
nodes: ITransactionsByContactsContact[],
): ITransactionsByContactsContact[] => {
return nodes.filter(this.contactNodeFilter);
};