Merge pull request #303 from bigcapitalhq/journal-sheet-export

Export general ledger & Journal to CSV and XLSX
This commit is contained in:
Ahmed Bouhuolia
2024-01-10 17:45:04 +02:00
committed by GitHub
39 changed files with 1544 additions and 301 deletions

View File

@@ -2,15 +2,16 @@ import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator'; import { query, ValidationChain } from 'express-validator';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import GeneralLedgerService from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerService';
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { GeneralLedgerApplication } from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication';
@Service() @Service()
export default class GeneralLedgerReportController extends BaseFinancialReportController { export default class GeneralLedgerReportController extends BaseFinancialReportController {
@Inject() @Inject()
generalLedgetService: GeneralLedgerService; private generalLedgerApplication: GeneralLedgerApplication;
/** /**
* Router constructor. * Router constructor.
@@ -61,20 +62,43 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
* @param {Response} res - * @param {Response} res -
*/ */
async generalLedger(req: Request, res: Response, next: NextFunction) { async generalLedger(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req; const { tenantId } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const accept = this.accepts(req);
try { const acceptType = accept.types([
const { data, query, meta } = ACCEPT_TYPE.APPLICATION_JSON,
await this.generalLedgetService.generalLedger(tenantId, filter); ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
// Retrieves the table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.generalLedgerApplication.table(tenantId, filter);
return res.status(200).send({ return res.status(200).send(table);
meta: this.transfromToResponse(meta), // Retrieves the csv format.
data: this.transfromToResponse(data), } else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
query: this.transfromToResponse(query), const buffer = await this.generalLedgerApplication.csv(tenantId, filter);
});
} catch (error) { res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
next(error); res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.generalLedgerApplication.xlsx(tenantId, filter);
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 {
const sheet = await this.generalLedgerApplication.sheet(tenantId, filter);
return res.status(200).send(sheet);
} }
} }
} }

View File

@@ -3,14 +3,15 @@ import { Request, Response, Router, NextFunction } from 'express';
import { castArray } from 'lodash'; import { castArray } from 'lodash';
import { query, oneOf } from 'express-validator'; import { query, oneOf } from 'express-validator';
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import JournalSheetService from '@/services/FinancialStatements/JournalSheet/JournalSheetService';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { JournalSheetApplication } from '@/services/FinancialStatements/JournalSheet/JournalSheetApplication';
@Service() @Service()
export default class JournalSheetController extends BaseFinancialReportController { export default class JournalSheetController extends BaseFinancialReportController {
@Inject() @Inject()
journalService: JournalSheetService; private journalSheetApp: JournalSheetApplication;
/** /**
* Router constructor. * Router constructor.
@@ -57,28 +58,49 @@ export default class JournalSheetController extends BaseFinancialReportControlle
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
async journal(req: Request, res: Response, next: NextFunction) { private async journal(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req; const { tenantId } = req;
let filter = this.matchedQueryData(req); let filter = this.matchedQueryData(req);
filter = { filter = {
...filter, ...filter,
accountsIds: castArray(filter.accountsIds), accountsIds: castArray(filter.accountsIds),
}; };
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
try { // Retrieves the json table format.
const { data, query, meta } = await this.journalService.journalSheet( if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
tenantId, const table = await this.journalSheetApp.table(tenantId, filter);
filter return res.status(200).send(table);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.journalSheetApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.journalSheetApp.xlsx(tenantId, filter);
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 {
const sheet = await this.journalSheetApp.sheet(tenantId, filter);
return res.status(200).send({ return res.status(200).send(sheet);
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
} catch (error) {
next(error);
} }
} }
} }

View File

@@ -531,7 +531,6 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
* @returns
*/ */
public sendPaymentReceiveByMail = async ( public sendPaymentReceiveByMail = async (
req: Request, req: Request,
@@ -546,6 +545,7 @@ export default class PaymentReceivesController extends BaseController {
includeOptionals: false, includeOptionals: false,
} }
); );
try { try {
await this.paymentReceiveApplication.notifyPaymentByMail( await this.paymentReceiveApplication.notifyPaymentByMail(
tenantId, tenantId,

View File

@@ -1,3 +1,4 @@
import { IFinancialTable } from "./Table";
export interface IGeneralLedgerSheetQuery { export interface IGeneralLedgerSheetQuery {
@@ -36,6 +37,7 @@ export interface IGeneralLedgerSheetAccountTransaction {
referenceType?: string, referenceType?: string,
date: Date|string, date: Date|string,
dateFormatted: string;
}; };
export interface IGeneralLedgerSheetAccountBalance { export interface IGeneralLedgerSheetAccountBalance {
@@ -56,6 +58,8 @@ export interface IGeneralLedgerSheetAccount {
closingBalance: IGeneralLedgerSheetAccountBalance, closingBalance: IGeneralLedgerSheetAccountBalance,
} }
export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];
export interface IAccountTransaction { export interface IAccountTransaction {
id: number, id: number,
index: number, index: number,
@@ -78,4 +82,11 @@ export interface IGeneralLedgerMeta {
isCostComputeRunning: boolean, isCostComputeRunning: boolean,
organizationName: string, organizationName: string,
baseCurrency: string, baseCurrency: string,
fromDate: string;
toDate: string;
}; };
export interface IGeneralLedgerTableData extends IFinancialTable {
meta: IGeneralLedgerMeta;
query: IGeneralLedgerSheetQuery;
}

View File

@@ -1,36 +1,52 @@
import { IJournalEntry } from './Journal'; import { IJournalEntry } from './Journal';
import { IFinancialTable } from './Table';
export interface IJournalReportQuery { export interface IJournalReportQuery {
fromDate: Date | string, fromDate: Date | string;
toDate: Date | string, toDate: Date | string;
numberFormat: { numberFormat: {
noCents: boolean, noCents: boolean;
divideOn1000: boolean, divideOn1000: boolean;
}, };
transactionType: string, transactionType: string;
transactionId: string, transactionId: string;
accountsIds: number | number[], accountsIds: number | number[];
fromRange: number, fromRange: number;
toRange: number, toRange: number;
} }
export interface IJournalReportEntriesGroup { export interface IJournalReportEntriesGroup {
id: string, id: string;
entries: IJournalEntry[], date: Date;
currencyCode: string, dateFormatted: string;
credit: number, entries: IJournalEntry[];
debit: number, currencyCode: string;
formattedCredit: string, credit: number;
formattedDebit: string, debit: number;
formattedCredit: string;
formattedDebit: string;
} }
export interface IJournalReport { export interface IJournalReport {
entries: IJournalReportEntriesGroup[], entries: IJournalReportEntriesGroup[];
} }
export interface IJournalSheetMeta { export interface IJournalSheetMeta {
isCostComputeRunning: boolean, isCostComputeRunning: boolean;
organizationName: string, organizationName: string;
baseCurrency: string, baseCurrency: 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

@@ -2,6 +2,9 @@ import { Model } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
export default class Contact extends TenantModel { export default class Contact extends TenantModel {
email: string;
displayName: string;
/** /**
* Table name * Table name
*/ */

View File

@@ -10,6 +10,7 @@ import {
IContact, IContact,
} from '@/interfaces'; } from '@/interfaces';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import moment from 'moment';
/** /**
* General ledger sheet. * General ledger sheet.
@@ -88,8 +89,10 @@ export default class GeneralLedgerSheet extends FinancialSheet {
const newEntry = { const newEntry = {
date: entry.date, date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
entryId: entry.id, entryId: entry.id,
transactionNumber: entry.transactionNumber,
referenceType: entry.referenceType, referenceType: entry.referenceType,
referenceId: entry.referenceId, referenceId: entry.referenceId,
referenceTypeFormatted: this.i18n.__(entry.referenceTypeFormatted), referenceTypeFormatted: this.i18n.__(entry.referenceTypeFormatted),

View File

@@ -0,0 +1,66 @@
import { Inject } from 'typedi';
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from '@/interfaces';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
import { GeneralLedgerService } from './GeneralLedgerService';
export class GeneralLedgerApplication {
@Inject()
private GLTable: GeneralLedgerTableInjectable;
@Inject()
private GLExport: GeneralLedgerExportInjectable;
@Inject()
private GLSheet: GeneralLedgerService;
/**
* Retrieves the G/L sheet in json format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
*/
public sheet(tenantId: number, query: IGeneralLedgerSheetQuery) {
return this.GLSheet.generalLedger(tenantId, query);
}
/**
* Retrieves the G/L sheet in table format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public table(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<IGeneralLedgerTableData> {
return this.GLTable.table(tenantId, query);
}
/**
* Retrieves the G/L sheet in xlsx format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {}
*/
public xlsx(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<Buffer> {
return this.GLExport.xlsx(tenantId, query);
}
/**
* Retrieves the G/L sheet in csv format.
* @param {number} tenantId -
* @param {IGeneralLedgerSheetQuery} query -
*/
public csv(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<string> {
return this.GLExport.csv(tenantId, query);
}
}

View File

@@ -0,0 +1,43 @@
import { IGeneralLedgerSheetQuery } from '@/interfaces';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { Inject, Service } from 'typedi';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
@Service()
export class GeneralLedgerExportInjectable {
@Inject()
private generalLedgerTable: GeneralLedgerTableInjectable;
/**
* Retrieves the general ledger sheet in XLSX format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(tenantId: number, query: IGeneralLedgerSheetQuery) {
const table = await this.generalLedgerTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the general ledger sheet in CSV format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<string> {
const table = await this.generalLedgerTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -15,7 +15,7 @@ const ERRORS = {
}; };
@Service() @Service()
export default class GeneralLedgerService { export class GeneralLedgerService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@@ -64,7 +64,7 @@ export default class GeneralLedgerService {
* @param {number} tenantId - * @param {number} tenantId -
* @returns {IGeneralLedgerMeta} * @returns {IGeneralLedgerMeta}
*/ */
reportMetadata(tenantId: number): IGeneralLedgerMeta { reportMetadata(tenantId: number, filter): IGeneralLedgerMeta {
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService const isCostComputeRunning = this.inventoryService
@@ -78,11 +78,15 @@ export default class GeneralLedgerService {
group: 'organization', group: 'organization',
key: 'base_currency', key: 'base_currency',
}); });
const fromDate = moment(filter.fromDate).format('YYYY MMM DD');
const toDate = moment(filter.toDate).format('YYYY MMM DD');
return { return {
isCostComputeRunning: parseBoolean(isCostComputeRunning, false), isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
organizationName, organizationName,
baseCurrency baseCurrency,
fromDate,
toDate
}; };
} }
@@ -166,7 +170,7 @@ export default class GeneralLedgerService {
return { return {
data: reportData, data: reportData,
query: filter, query: filter,
meta: this.reportMetadata(tenantId), meta: this.reportMetadata(tenantId, filter),
}; };
} }
} }

View File

@@ -0,0 +1,256 @@
import * as R from 'ramda';
import {
IColumnMapperMeta,
IGeneralLedgerMeta,
IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountTransaction,
IGeneralLedgerSheetData,
IGeneralLedgerSheetQuery,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { FinancialTable } from '../FinancialTable';
import { tableRowMapper } from '@/utils';
import { ROW_TYPE } from './utils';
export class GeneralLedgerTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private data: IGeneralLedgerSheetData;
private query: IGeneralLedgerSheetQuery;
private meta: IGeneralLedgerMeta;
/**
* Creates an instance of `GeneralLedgerTable`.
* @param {IGeneralLedgerSheetData} data
* @param {IGeneralLedgerSheetQuery} query
*/
constructor(
data: IGeneralLedgerSheetData,
query: IGeneralLedgerSheetQuery,
meta: IGeneralLedgerMeta
) {
super();
this.data = data;
this.query = query;
this.meta = meta;
}
/**
* Retrieves the common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private accountColumnsAccessors(): ITableColumnAccessor[] {
return [
{ key: 'date', accessor: 'name' },
{ key: 'account_name', accessor: '_empty_' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'amount.formattedAmount' },
{ key: 'running_balance', accessor: 'closingBalance.formattedAmount' },
];
}
/**
* Retrieves the transaction column accessors.
* @returns {ITableColumnAccessor[]}
*/
private transactionColumnAccessors(): ITableColumnAccessor[] {
return [
{ key: 'date', accessor: 'dateFormatted' },
{ key: 'account_name', accessor: 'account.name' },
{ key: 'reference_type', accessor: 'referenceTypeFormatted' },
{ key: 'reference_number', accessor: 'transactionNumber' },
{ key: 'description', accessor: 'note' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'amount', accessor: 'formattedAmount' },
{ key: 'running_balance', accessor: 'formattedRunningBalance' },
];
}
/**
* Retrieves the opening row column accessors.
* @returns {ITableRowIColumnMapperMeta[]}
*/
private openingBalanceColumnsAccessors(): IColumnMapperMeta[] {
return [
{ key: 'date', value: this.meta.fromDate },
{ key: 'account_name', value: 'Opening Balance' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'openingBalance.formattedAmount' },
{ key: 'running_balance', accessor: 'openingBalance.formattedAmount' },
];
}
/**
* Closing balance row column accessors.
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceColumnAccessors(): IColumnMapperMeta[] {
return [
{ key: 'date', value: this.meta.toDate },
{ key: 'account_name', value: 'Closing Balance' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'closingBalance.formattedAmount' },
{ key: 'running_balance', accessor: 'closingBalance.formattedAmount' },
];
}
/**
* Retrieves the common table columns.
* @returns {ITableColumn[]}
*/
private commonColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'account_name', label: 'Account Name' },
{ key: 'reference_type', label: 'Transaction Type' },
{ key: 'reference_number', label: 'Transaction #' },
{ key: 'description', label: 'Description' },
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
{ key: 'amount', label: 'Amount' },
{ key: 'running_balance', label: 'Running Balance' },
];
}
/**
* Maps the given transaction node to table row.
* @param {IGeneralLedgerSheetAccountTransaction} transaction
* @returns {ITableRow}
*/
private transactionMapper = R.curry(
(
account: IGeneralLedgerSheetAccount,
transaction: IGeneralLedgerSheetAccountTransaction
): ITableRow => {
const columns = this.transactionColumnAccessors();
const data = { ...transaction, account };
const meta = {
rowTypes: [ROW_TYPE.TRANSACTION],
};
return tableRowMapper(data, columns, meta);
}
);
/**
* Maps the given transactions nodes to table rows.
* @param {IGeneralLedgerSheetAccountTransaction[]} transactions
* @returns {ITableRow[]}
*/
private transactionsMapper = (
account: IGeneralLedgerSheetAccount
): ITableRow[] => {
const transactionMapper = this.transactionMapper(account);
return R.map(transactionMapper)(account.transactions);
};
/**
* Maps the given account node to opening balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private openingBalanceMapper = (
account: IGeneralLedgerSheetAccount
): ITableRow => {
const columns = this.openingBalanceColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.OPENING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to closing balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => {
const columns = this.closingBalanceColumnAccessors();
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to transactions table rows.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow[]}
*/
private transactionsNode = (
account: IGeneralLedgerSheetAccount
): ITableRow[] => {
const openingBalance = this.openingBalanceMapper(account);
const transactions = this.transactionsMapper(account);
const closingBalance = this.closingBalanceMapper(account);
return R.when(
R.always(R.not(R.isEmpty(transactions))),
R.prepend(openingBalance)
)([...transactions, closingBalance]) as ITableRow[];
};
/**
* Maps the given account node to the table rows.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private accountMapper = (account: IGeneralLedgerSheetAccount): ITableRow => {
const columns = this.accountColumnsAccessors();
const transactions = this.transactionsNode(account);
const meta = {
rowTypes: [ROW_TYPE.ACCOUNT],
};
const row = tableRowMapper(account, columns, meta);
return R.assoc('children', transactions)(row);
};
/**
* Maps the given account node to table rows.
* @param {IGeneralLedgerSheetAccount[]} accounts
* @returns {ITableRow[]}
*/
private accountsMapper = (
accounts: IGeneralLedgerSheetAccount[]
): ITableRow[] => {
return this.mapNodesDeep(accounts, this.accountMapper);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
return R.compose(this.accountsMapper)(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,45 @@
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { GeneralLedgerService } from './GeneralLedgerService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GeneralLedgerTable } from './GeneralLedgerTable';
@Service()
export class GeneralLedgerTableInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private GLSheet: GeneralLedgerService;
/**
* Retrieves the G/L table.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public async table(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<IGeneralLedgerTableData> {
const {
data: sheetData,
query: sheetQuery,
meta: sheetMeta,
} = await this.GLSheet.generalLedger(tenantId, query);
const table = new GeneralLedgerTable(sheetData, sheetQuery, sheetMeta);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query: sheetQuery,
meta: sheetMeta,
};
}
}

View File

@@ -0,0 +1,6 @@
export enum ROW_TYPE {
ACCOUNT = 'ACCOUNT',
OPENING_BALANCE = 'OPENING_BALANCE',
TRANSACTION = 'TRANSACTION',
CLOSING_BALANCE = 'CLOSING_BALANCE',
}

View File

@@ -6,8 +6,10 @@ import {
IJournalReportQuery, IJournalReportQuery,
IJournalReport, IJournalReport,
IContact, IContact,
IJournalTableData,
} from '@/interfaces'; } from '@/interfaces';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import moment from 'moment';
export default class JournalSheet extends FinancialSheet { export default class JournalSheet extends FinancialSheet {
readonly tenantId: number; readonly tenantId: number;
@@ -96,6 +98,8 @@ export default class JournalSheet extends FinancialSheet {
return { return {
date: groupEntry.date, date: groupEntry.date,
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
referenceType: groupEntry.referenceType, referenceType: groupEntry.referenceType,
referenceId: groupEntry.referenceId, referenceId: groupEntry.referenceId,
referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted), referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted),
@@ -131,7 +135,7 @@ export default class JournalSheet extends FinancialSheet {
* Retrieve journal report. * Retrieve journal report.
* @return {IJournalReport} * @return {IJournalReport}
*/ */
reportData(): IJournalReport { reportData(): IJournalTableData {
return this.entriesWalker(this.journal.entries); return this.entriesWalker(this.journal.entries);
} }
} }

View File

@@ -0,0 +1,59 @@
import { Inject } from 'typedi';
import { JournalSheetService } from './JournalSheetService';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
import { IJournalReportQuery, IJournalTable } from '@/interfaces';
import { JournalSheetExportInjectable } from './JournalSheetExport';
export class JournalSheetApplication {
@Inject()
private journalSheetTable: JournalSheetTableInjectable;
@Inject()
private journalSheet: JournalSheetService;
@Inject()
private journalExport: JournalSheetExportInjectable;
/**
* Retrieves the journal sheet.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {}
*/
public sheet(tenantId: number, query: IJournalReportQuery) {
return this.journalSheet.journalSheet(tenantId, query);
}
/**
* Retrieves the journal sheet in table format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalTable>}
*/
public table(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalTable> {
return this.journalSheetTable.table(tenantId, query);
}
/**
* Retrieves the journal sheet in xlsx format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns
*/
public xlsx(tenantId: number, query: IJournalReportQuery) {
return this.journalExport.xlsx(tenantId, query);
}
/**
* Retrieves the journal sheet in csv format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns
*/
public csv(tenantId: number, query: IJournalReportQuery) {
return this.journalExport.csv(tenantId, query);
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { IJournalReportQuery } from '@/interfaces';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
@Service()
export class JournalSheetExportInjectable {
@Inject()
private journalSheetTable: JournalSheetTableInjectable;
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(tenantId: number, query: IJournalReportQuery) {
const table = await this.journalSheetTable.table(tenantId, 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 {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IJournalReportQuery
): Promise<string> {
const table = await this.journalSheetTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -1,24 +1,25 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces'; import {
IJournalReportQuery,
IJournalSheet,
IJournalSheetMeta,
IJournalTableData,
} from '@/interfaces';
import JournalSheet from './JournalSheet'; import JournalSheet from './JournalSheet';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster'; import Journal from '@/services/Accounting/JournalPoster';
import InventoryService from '@/services/Inventory/Inventory'; import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean, transformToMap } from 'utils';
import { Tenant } from '@/system/models'; import { Tenant } from '@/system/models';
import { parseBoolean, transformToMap } from 'utils';
@Service() @Service()
export default class JournalSheetService { export class JournalSheetService {
@Inject() @Inject()
tenancy: TenancyService; private tenancy: TenancyService;
@Inject() @Inject()
inventoryService: InventoryService; private inventoryService: InventoryService;
@Inject('logger')
logger: any;
/** /**
* Default journal sheet filter queyr. * Default journal sheet filter queyr.
@@ -67,9 +68,13 @@ export default class JournalSheetService {
/** /**
* Journal sheet. * Journal sheet.
* @param {number} tenantId * @param {number} tenantId
* @param {IJournalSheetFilterQuery} query * @param {IJournalReportQuery} query
* @returns {Promise<IJournalSheet>}
*/ */
async journalSheet(tenantId: number, query: IJournalReportQuery) { async journalSheet(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalSheet> {
const i18n = this.tenancy.i18n(tenantId); const i18n = this.tenancy.i18n(tenantId);
const { accountRepository, transactionsRepository, contactRepository } = const { accountRepository, transactionsRepository, contactRepository } =
this.tenancy.repositories(tenantId); this.tenancy.repositories(tenantId);
@@ -80,11 +85,6 @@ export default class JournalSheetService {
...this.defaultQuery, ...this.defaultQuery,
...query, ...query,
}; };
this.logger.info('[journal] trying to calculate the report.', {
tenantId,
filter,
});
const tenant = await Tenant.query() const tenant = await Tenant.query()
.findById(tenantId) .findById(tenantId)
.withGraphFetched('metadata'); .withGraphFetched('metadata');

View File

@@ -0,0 +1,232 @@
import * as R from 'ramda';
import { first } from 'lodash';
import {
IColumnMapperMeta,
IJournalEntry,
IJournalReportEntriesGroup,
IJournalReportQuery,
IJournalTableData,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import { FinancialTable } from '../FinancialTable';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import FinancialSheet from '../FinancialSheet';
import { ROW_TYPE } from './types';
export class JournalSheetTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private data: IJournalTableData;
private query: IJournalReportQuery;
private i18n: any;
/**
* Constructor method.
* @param {IJournalTableData} data
* @param {IJournalReportQuery} query
* @param i18n
*/
constructor(data: IJournalTableData, query: IJournalReportQuery, i18n: any) {
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: 'credit', accessor: 'entry.formattedCredit' },
{ key: 'debit', accessor: 'entry.formattedDebit' },
];
};
/**
* 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: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
];
};
/**
* 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: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
];
};
/**
* 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: 'credit', value: '' },
{ key: 'debit', 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: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
];
}
/**
* 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: IJournalEntry): 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 IJournalEntry, 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,39 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject } from 'typedi';
import { JournalSheetService } from './JournalSheetService';
import { IJournalReportQuery, IJournalTable } from '@/interfaces';
import { JournalSheetTable } from './JournalSheetTable';
export class JournalSheetTableInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private journalSheetService: JournalSheetService;
/**
* Retrieves the journal sheet in table format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalTable>}
*/
public async table(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalTable> {
const journal = await this.journalSheetService.journalSheet(
tenantId,
query
);
const table = new JournalSheetTable(journal.data, journal.query, {});
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
query: journal.query,
meta: journal.meta,
};
}
}

View File

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

View File

@@ -7,6 +7,7 @@ import {
DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, DEFAULT_INVOICE_REMINDER_MAIL_CONTENT,
DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
} from './constants'; } from './constants';
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
@Service() @Service()
export class SendInvoiceMailReminder { export class SendInvoiceMailReminder {
@@ -66,10 +67,10 @@ export class SendInvoiceMailReminder {
) { ) {
const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId); const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId);
const messageOpts = { const messageOpts = parseAndValidateMailOptions(
...localMessageOpts, localMessageOpts,
...messageOptions, messageOptions
}; );
const mail = new Mail() const mail = new Mail()
.setSubject(messageOpts.subject) .setSubject(messageOpts.subject)
.setTo(messageOpts.to) .setTo(messageOpts.to)

View File

@@ -1,5 +1,4 @@
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import events from '@/subscribers/events';
import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification'; import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
@Service() @Service()
@@ -22,8 +21,6 @@ export class PaymentReceiveMailNotificationJob {
const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data; const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data;
const paymentMail = Container.get(SendPaymentReceiveMailNotification); const paymentMail = Container.get(SendPaymentReceiveMailNotification);
console.log(tenantId, paymentReceiveId, messageDTO);
try { try {
await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO); await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO);
done(); done();

View File

@@ -177,6 +177,7 @@ export class SaleReceiptApplication {
* Sends the receipt mail of the given sale receipt. * Sends the receipt mail of the given sale receipt.
* @param {number} tenantId * @param {number} tenantId
* @param {number} saleReceiptId * @param {number} saleReceiptId
* @param {SaleReceiptMailOptsDTO} messageOpts
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public sendSaleReceiptMail( public sendSaleReceiptMail(

View File

@@ -51,8 +51,8 @@ export default function Dashboard() {
</Switch> </Switch>
<DashboardUniversalSearch /> <DashboardUniversalSearch />
<DialogsContainer />
<GlobalHotkeys /> <GlobalHotkeys />
<DialogsContainer />
<DrawersContainer /> <DrawersContainer />
<AlertsContainer /> <AlertsContainer />
</DashboardProvider> </DashboardProvider>

View File

@@ -32,7 +32,7 @@ function AccountsDataTable({
// #withAlertsDialog // #withAlertsDialog
openAlert, openAlert,
// #withDial // #withDialog
openDialog, openDialog,
// #withDrawerActions // #withDrawerActions

View File

@@ -8,9 +8,9 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group';
import authenticationRoutes from '@/routes/authentication'; import authenticationRoutes from '@/routes/authentication';
import { Icon, FormattedMessage as T } from '@/components'; import { Icon, FormattedMessage as T } from '@/components';
import { useIsAuthenticated } from '@/hooks/state'; import { useIsAuthenticated } from '@/hooks/state';
import { AuthMetaBootProvider } from './AuthMetaBoot';
import '@/style/pages/Authentication/Auth.scss'; import '@/style/pages/Authentication/Auth.scss';
import { AuthMetaBootProvider } from './AuthMetaBoot';
export function Authentication() { export function Authentication() {
const to = { pathname: '/' }; const to = { pathname: '/' };

View File

@@ -12,6 +12,7 @@ import {
import classNames from 'classnames'; import classNames from 'classnames';
import { DashboardActionsBar, FormattedMessage as T, Icon } from '@/components'; import { DashboardActionsBar, FormattedMessage as T, Icon } from '@/components';
import { GeneralLedgerSheetExportMenu } from './components';
import { useGeneralLedgerContext } from './GeneralLedgerProvider'; import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import { compose } from '@/utils'; import { compose } from '@/utils';
@@ -84,11 +85,18 @@ function GeneralLedgerActionsBar({
icon={<Icon icon="print-16" iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
/> />
<Button <Popover
className={Classes.MINIMAL} content={<GeneralLedgerSheetExportMenu />}
icon={<Icon icon="file-export-16" iconSize={16} />} interactionKind={PopoverInteractionKind.CLICK}
text={<T id={'export'} />} placement="bottom-start"
/> minimal
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</Popover>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>
); );

View File

@@ -10,7 +10,7 @@ const GeneralLedgerHeaderDimensionsPanelContext = React.createContext();
/** /**
* General Ledger Header Dimensions Panel provider. * General Ledger Header Dimensions Panel provider.
* @returns * @returns {JSX.Element}
*/ */
function GeneralLedgerHeaderDimensionsPanelProvider({ query, ...props }) { function GeneralLedgerHeaderDimensionsPanelProvider({ query, ...props }) {
// Features guard. // Features guard.

View File

@@ -29,6 +29,7 @@ function GeneralLedgerProvider({ query, ...props }) {
sheetRefresh: refetch, sheetRefresh: refetch,
isFetching, isFetching,
isLoading, isLoading,
httpRequest: requestQuery
}; };
return ( return (
<FinancialReportPage name={'general-ledger-sheet'}> <FinancialReportPage name={'general-ledger-sheet'}>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React, { useMemo } from 'react'; import { useMemo } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -13,7 +13,7 @@ import {
} from '@/components'; } from '@/components';
import { useGeneralLedgerContext } from './GeneralLedgerProvider'; import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import { useGeneralLedgerTableColumns } from './components'; import { useGeneralLedgerTableColumns } from './dynamicColumns';
/** /**
* General ledger table. * General ledger table.
@@ -21,7 +21,7 @@ import { useGeneralLedgerTableColumns } from './components';
export default function GeneralLedgerTable({ companyName }) { export default function GeneralLedgerTable({ companyName }) {
// General ledger context. // General ledger context.
const { const {
generalLedger: { tableRows, query }, generalLedger: { query, table },
isLoading, isLoading,
} = useGeneralLedgerContext(); } = useGeneralLedgerContext();
@@ -30,8 +30,8 @@ export default function GeneralLedgerTable({ companyName }) {
// Default expanded rows of general ledger table. // Default expanded rows of general ledger table.
const expandedRows = useMemo( const expandedRows = useMemo(
() => defaultExpanderReducer(tableRows, 1), () => defaultExpanderReducer(table.rows, 1),
[tableRows], [table.rows],
); );
return ( return (
@@ -48,7 +48,7 @@ export default function GeneralLedgerTable({ companyName }) {
'this_report_does_not_contain_any_data_between_date_period', 'this_report_does_not_contain_any_data_between_date_period',
)} )}
columns={columns} columns={columns}
data={tableRows} data={table.rows}
rowClassNames={tableRowTypesToClassnames} rowClassNames={tableRowTypesToClassnames}
expanded={expandedRows} expanded={expandedRows}
virtualizedRows={true} virtualizedRows={true}
@@ -79,23 +79,20 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
opacity: 0; opacity: 0;
} }
} }
.tr:not(.no-results) .td:not(:first-of-type) { .tr:not(.no-results) .td:not(:first-of-type) {
border-left: 1px solid #ececec; border-left: 1px solid #ececec;
} }
.tr:last-child .td { .tr:last-child .td {
border-bottom: 1px solid #ececec; border-bottom: 1px solid #ececec;
} }
.tr.row_type { .tr.row_type {
&--ACCOUNT_ROW { &--ACCOUNT {
.td { .td {
&.date { &.date {
font-weight: 500; font-weight: 500;
.cell-inner { .cell-inner {
white-space: nowrap; position: absolute;
position: relative;
} }
} }
} }
@@ -103,7 +100,6 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }
} }
&--OPENING_BALANCE, &--OPENING_BALANCE,
&--CLOSING_BALANCE { &--CLOSING_BALANCE {
.amount { .amount {

View File

@@ -1,107 +1,31 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useRef } from 'react';
import intl from 'react-intl-universal'; import classNames from 'classnames';
import { Button } from '@blueprintjs/core'; import {
import { FormattedMessage as T, Icon, If } from '@/components'; Button,
Classes,
Intent,
Menu,
MenuItem,
ProgressBar,
Text,
} from '@blueprintjs/core';
import {
FormattedMessage as T,
Icon,
If,
Stack,
AppToaster,
} from '@/components';
import { getColumnWidth } from '@/utils';
import { useGeneralLedgerContext } from './GeneralLedgerProvider'; import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import FinancialLoadingBar from '../FinancialLoadingBar'; import FinancialLoadingBar from '../FinancialLoadingBar';
import { FinancialComputeAlert } from '../FinancialReportPage'; import { FinancialComputeAlert } from '../FinancialReportPage';
import { Align } from '@/constants'; import {
useGeneralLedgerSheetCsvExport,
/** useGeneralLedgerSheetXlsxExport,
* Retrieve the general ledger table columns. } from '@/hooks/query';
*/
export function useGeneralLedgerTableColumns() {
// General ledger context.
const {
generalLedger: { tableRows },
} = useGeneralLedgerContext();
return React.useMemo(
() => [
{
Header: intl.get('date'),
accessor: 'date',
className: 'date',
width: 120,
},
{
Header: intl.get('account_name'),
accessor: 'name',
className: 'name',
textOverview: true,
},
{
Header: intl.get('transaction_type'),
accessor: 'reference_type_formatted',
className: 'transaction_type',
width: 125,
textOverview: true,
},
{
Header: intl.get('transaction_number'),
accessor: 'reference_id',
className: 'transaction_number',
width: 100,
textOverview: true,
},
{
Header: intl.get('description'),
accessor: 'note',
className: 'description',
textOverview: true,
},
{
Header: intl.get('credit'),
accessor: 'formatted_credit',
className: 'credit',
width: getColumnWidth(tableRows, 'formatted_credit', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('debit'),
accessor: 'formatted_debit',
className: 'debit',
width: getColumnWidth(tableRows, 'formatted_debit', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('amount'),
accessor: 'formatted_amount',
className: 'amount',
width: getColumnWidth(tableRows, 'formatted_amount', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('running_balance'),
accessor: 'formatted_running_balance',
className: 'running_balance',
width: getColumnWidth(tableRows, 'formatted_running_balance', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
],
[tableRows],
);
}
/** /**
* General ledger sheet alerts. * General ledger sheet alerts.
@@ -144,3 +68,93 @@ export function GeneralLedgerSheetLoadingBar() {
</If> </If>
); );
} }
/**
* Renders the G/L sheet export menu.
* @returns {JSX.Element}
*/
export const GeneralLedgerSheetExportMenu = () => {
const toastKey = useRef(null);
const commonToastConfig = {
isCloseButtonShown: true,
timeout: 2000,
};
const { httpRequest } = useGeneralLedgerContext();
const openProgressToast = (amount: number) => {
return (
<Stack spacing={8}>
<Text>The report has been exported successfully.</Text>
<ProgressBar
className={classNames('toast-progress', {
[Classes.PROGRESS_NO_STRIPES]: amount >= 100,
})}
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
value={amount / 100}
/>
</Stack>
);
};
// Export the report to xlsx.
const { mutateAsync: xlsxExport } = useGeneralLedgerSheetXlsxExport(
httpRequest,
{
onDownloadProgress: (xlsxExportProgress: number) => {
if (!toastKey.current) {
toastKey.current = AppToaster.show({
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
});
} else {
AppToaster.show(
{
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
},
toastKey.current,
);
}
},
},
);
// Export the report to csv.
const { mutateAsync: csvExport } = useGeneralLedgerSheetCsvExport(
httpRequest,
{
onDownloadProgress: (xlsxExportProgress: number) => {
if (!toastKey.current) {
toastKey.current = AppToaster.show({
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
});
} else {
AppToaster.show(
{
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
},
toastKey.current,
);
}
},
},
);
// Handle csv export button click.
const handleCsvExportBtnClick = () => {
csvExport();
};
// Handle xlsx export button click.
const handleXlsxExportBtnClick = () => {
xlsxExport();
};
return (
<Menu>
<MenuItem
text={'XLSX (Microsoft Excel)'}
onClick={handleXlsxExportBtnClick}
/>
<MenuItem text={'CSV'} onClick={handleCsvExportBtnClick} />
</Menu>
);
};

View File

@@ -0,0 +1,117 @@
// @ts-nocheck
import { getColumnWidth } from '@/utils';
import * as R from 'ramda';
import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import { Align } from '@/constants';
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const getReportColWidth = (data, accessor, headerText) => {
return getColumnWidth(
data,
accessor,
{ magicSpacing: 10, minWidth: 100 },
headerText,
);
};
/**
* Account name column mapper.
*/
const commonColumnMapper = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
key: column.key,
Header: column.label,
accessor,
className: column.key,
textOverview: true,
};
});
/**
* Numeric columns accessor.
*/
const numericColumnAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
const width = getReportColWidth(data, accessor, column.label);
return {
...column,
align: Align.Right,
width,
};
});
/**
* Date column accessor.
*/
const dateColumnAccessor = R.curry((column) => {
return {
...column,
width: 120,
};
});
/**
* Transaction type column accessor.
*/
const transactionTypeColumnAccessor = (column) => {
return {
...column,
width: 125,
};
};
/**
* Transaction number column accessor.
*/
const transactionIdColumnAccessor = (column) => {
return {
...column,
width: 80,
};
};
const dynamiColumnMapper = R.curry((data, column) => {
const _numericColumnAccessor = numericColumnAccessor(data);
return R.compose(
R.when(R.pathEq(['key'], 'date'), dateColumnAccessor),
R.when(
R.pathEq(['key'], 'reference_type'),
transactionTypeColumnAccessor,
),
R.when(
R.pathEq(['key'], 'reference_number'),
transactionIdColumnAccessor,
),
R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'amount'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'running_balance'), _numericColumnAccessor),
commonColumnMapper(data),
)(column);
});
/**
* Composes the dynamic columns that fetched from request to columns to table component.
*/
export const dynamicColumns = R.curry((data, columns) => {
return R.map(dynamiColumnMapper(data), columns);
});
/**
* Retrieves the G/L sheet table columns for table component.
*/
export const useGeneralLedgerTableColumns = () => {
const { generalLedger } = useGeneralLedgerContext();
if (!generalLedger) {
throw new Error('asdfadsf');
}
const { table } = generalLedger;
return dynamicColumns(table.rows, table.columns);
};

View File

@@ -18,6 +18,7 @@ import withJournal from './withJournal';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { useJournalSheetContext } from './JournalProvider'; import { useJournalSheetContext } from './JournalProvider';
import { JournalSheetExportMenu } from './components';
/** /**
* Journal sheeet - Actions bar. * Journal sheeet - Actions bar.
@@ -85,11 +86,18 @@ function JournalActionsBar({
icon={<Icon icon="print-16" iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
/> />
<Button <Popover
className={Classes.MINIMAL} content={<JournalSheetExportMenu />}
icon={<Icon icon="file-export-16" iconSize={16} />} interactionKind={PopoverInteractionKind.CLICK}
text={<T id={'export'} />} placement="bottom-start"
/> minimal
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</Popover>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>
); );

View File

@@ -28,6 +28,7 @@ function JournalSheetProvider({ query, ...props }) {
isLoading, isLoading,
isFetching, isFetching,
refetchSheet: refetch, refetchSheet: refetch,
httpQuery: requestQuery
}; };
return ( return (

View File

@@ -11,10 +11,10 @@ import {
TableVirtualizedListRows, TableVirtualizedListRows,
} from '@/components'; } from '@/components';
import { useJournalTableColumns } from './components';
import { useJournalSheetContext } from './JournalProvider'; import { useJournalSheetContext } from './JournalProvider';
import { defaultExpanderReducer, tableRowTypesToClassnames } from '@/utils'; import { defaultExpanderReducer, tableRowTypesToClassnames } from '@/utils';
import { useJournalSheetColumns } from './dynamicColumns';
/** /**
* Journal sheet table. * Journal sheet table.
@@ -23,12 +23,12 @@ import { defaultExpanderReducer, tableRowTypesToClassnames } from '@/utils';
export function JournalTable({ companyName }) { export function JournalTable({ companyName }) {
// Journal sheet context. // Journal sheet context.
const { const {
journalSheet: { tableRows, query }, journalSheet: { table, query },
isLoading, isLoading,
} = useJournalSheetContext(); } = useJournalSheetContext();
// Retreive the journal table columns. // Retrieves the journal table columns.
const columns = useJournalTableColumns(); const columns = useJournalSheetColumns();
// Default expanded rows of general journal table. // Default expanded rows of general journal table.
const expandedRows = useMemo(() => defaultExpanderReducer([], 1), []); const expandedRows = useMemo(() => defaultExpanderReducer([], 1), []);
@@ -39,13 +39,13 @@ export function JournalTable({ companyName }) {
sheetType={intl.get('journal_sheet')} sheetType={intl.get('journal_sheet')}
fromDate={query.from_date} fromDate={query.from_date}
toDate={query.to_date} toDate={query.to_date}
name="journal"
loading={isLoading} loading={isLoading}
fullWidth={true} fullWidth={true}
name="journal"
> >
<JournalDataTable <JournalDataTable
columns={columns} columns={columns}
data={tableRows} data={table.rows}
rowClassNames={tableRowTypesToClassnames} rowClassNames={tableRowTypesToClassnames}
noResults={intl.get( noResults={intl.get(
'this_report_does_not_contain_any_data_between_date_period', 'this_report_does_not_contain_any_data_between_date_period',
@@ -83,10 +83,9 @@ const JournalDataTable = styled(ReportDataTable)`
border-bottom: 1px solid #dbdbdb; border-bottom: 1px solid #dbdbdb;
} }
} }
.tr.row_type--TOTAL_ENTRIES { .tr.row_type--TOTAL{
font-weight: 600; font-weight: 600;
} }
.tr:not(.no-results) { .tr:not(.no-results) {
height: 28px; height: 28px;
} }

View File

@@ -1,77 +1,31 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useRef } from 'react';
import intl from 'react-intl-universal'; import classNames from 'classnames';
import moment from 'moment'; import {
import { Button } from '@blueprintjs/core'; Button,
Classes,
Menu,
MenuItem,
ProgressBar,
Text,
Intent,
} from '@blueprintjs/core';
import { Icon, If, FormattedMessage as T } from '@/components'; import {
AppToaster,
Icon,
If,
Stack,
FormattedMessage as T,
} from '@/components';
import { useJournalSheetContext } from './JournalProvider'; import { useJournalSheetContext } from './JournalProvider';
import FinancialLoadingBar from '../FinancialLoadingBar'; import FinancialLoadingBar from '../FinancialLoadingBar';
import { FinancialComputeAlert } from '../FinancialReportPage'; import { FinancialComputeAlert } from '../FinancialReportPage';
import { Align } from '@/constants'; import {
useJournalSheetCsvExport,
/** useJournalSheetXlsxExport,
* Retrieve the journal table columns. } from '@/hooks/query';
*/
export const useJournalTableColumns = () => {
return React.useMemo(
() => [
{
Header: intl.get('date'),
accessor: (row) =>
row.date ? moment(row.date).format('YYYY MMM DD') : '',
className: 'date',
width: 100,
textOverview: true,
},
{
Header: intl.get('transaction_type'),
accessor: 'reference_type_formatted',
className: 'reference_type_formatted',
width: 120,
textOverview: true,
},
{
Header: intl.get('num'),
accessor: 'transaction_number',
className: 'reference_id',
width: 70,
textOverview: true,
},
{
Header: intl.get('description'),
accessor: 'note',
className: 'note',
textOverview: true,
},
{
Header: intl.get('acc_code'),
accessor: 'account_code',
width: 95,
className: 'account_code',
textOverview: true,
},
{
Header: intl.get('account'),
accessor: 'account_name',
className: 'account_name',
textOverview: true,
},
{
Header: intl.get('credit'),
accessor: 'formatted_credit',
align: Align.Right,
},
{
Header: intl.get('debit'),
accessor: 'formatted_debit',
align: Align.Right,
},
],
[],
);
};
/** /**
* Journal sheet loading bar. * Journal sheet loading bar.
@@ -115,3 +69,87 @@ export function JournalSheetAlerts() {
</FinancialComputeAlert> </FinancialComputeAlert>
); );
} }
/**
* Retrieves the journal sheet export menu.
* @returns {JSX.Element}
*/
export const JournalSheetExportMenu = () => {
const toastKey = useRef(null);
const commonToastConfig = {
isCloseButtonShown: true,
timeout: 2000,
};
const { httpQuery } = useJournalSheetContext();
const openProgressToast = (amount: number) => {
return (
<Stack spacing={8}>
<Text>The report has been exported successfully.</Text>
<ProgressBar
className={classNames('toast-progress', {
[Classes.PROGRESS_NO_STRIPES]: amount >= 100,
})}
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
value={amount / 100}
/>
</Stack>
);
};
// Exports the report to xlsx.
const { mutateAsync: xlsxExport } = useJournalSheetXlsxExport(httpQuery, {
onDownloadProgress: (xlsxExportProgress: number) => {
if (!toastKey.current) {
toastKey.current = AppToaster.show({
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
});
} else {
AppToaster.show(
{
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
},
toastKey.current,
);
}
},
});
// Exports the report to csv.
const { mutateAsync: csvExport } = useJournalSheetCsvExport(httpQuery, {
onDownloadProgress: (xlsxExportProgress: number) => {
if (!toastKey.current) {
toastKey.current = AppToaster.show({
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
});
} else {
AppToaster.show(
{
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
},
toastKey.current,
);
}
},
});
// Handle csv export button click.
const handleCsvExportBtnClick = () => {
csvExport();
};
// Handle xlsx export button click.
const handleXlsxExportBtnClick = () => {
xlsxExport();
};
return (
<Menu>
<MenuItem
text={'XLSX (Microsoft Excel)'}
onClick={handleXlsxExportBtnClick}
/>
<MenuItem text={'CSV'} onClick={handleCsvExportBtnClick} />
</Menu>
);
};

View File

@@ -0,0 +1,134 @@
// @ts-nocheck
import { Align } from '@/constants';
import { getColumnWidth } from '@/utils';
import * as R from 'ramda';
import { useJournalSheetContext } from './JournalProvider';
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const getReportColWidth = (data, accessor, headerText) => {
return getColumnWidth(
data,
accessor,
{ magicSpacing: 10, minWidth: 100 },
headerText,
);
};
/**
* Common column mapper.
*/
const commonAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
key: column.key,
Header: column.label,
accessor,
className: column.key,
textOverview: true,
align: Align.Left,
};
});
/**
* Numeric columns accessor.
*/
const numericColumnAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
const width = getReportColWidth(data, accessor, column.label);
return {
...column,
align: Align.Right,
width,
};
});
/**
* Date column accessor.
*/
const dateColumnAccessor = (column) => {
return {
...column,
width: 100,
};
};
/**
* Transaction type column accessor.
*/
const transactionTypeColumnAccessor = (column) => {
return {
...column,
width: 120,
};
};
/**
* Transaction number column accessor.
*/
const transactionNumberColumnAccessor = (column) => {
return {
...column,
width: 70,
};
};
/**
* Account code column accessor.
*/
const accountCodeColumnAccessor = (column) => {
return {
...column,
width: 70,
};
};
/**
* Dynamic column mapper.
* @param {} data -
* @param {} column -
*/
const dynamicColumnMapper = R.curry((data, column) => {
const _commonAccessor = commonAccessor(data);
const _numericColumnAccessor = numericColumnAccessor(data);
return R.compose(
R.when(R.pathEq(['key'], 'date'), dateColumnAccessor),
R.when(
R.pathEq(['key'], 'transaction_type'),
transactionTypeColumnAccessor,
),
R.when(
R.pathEq(['key'], 'transaction_number'),
transactionNumberColumnAccessor,
),
R.when(R.pathEq(['key'], 'account_code'), accountCodeColumnAccessor),
R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor),
_commonAccessor,
)(column);
});
/**
* Composes the fetched dynamic columns from the server to the columns to pass it
* to the table component.
*/
export const dynamicColumns = (columns, data) => {
return R.map(dynamicColumnMapper(data), columns);
};
/**
* Retrieves the table columns of journal sheet.
*/
export const useJournalSheetColumns = () => {
const { journalSheet } = useJournalSheetContext();
if (!journalSheet) {
throw new Error('The journal sheet is not loaded');
}
const { table } = journalSheet;
return dynamicColumns(table.columns, table.rows);
};

View File

@@ -1,6 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { castArray } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { useAppQueryString } from '@/hooks'; import { useAppQueryString } from '@/hooks';

View File

@@ -1,8 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { useRequestQuery } from '../useQueryRequest'; import { useRequestQuery } from '../useQueryRequest';
import { import {
generalLedgerTableRowsReducer,
journalTableRowsReducer,
inventoryValuationReducer, inventoryValuationReducer,
purchasesByItemsReducer, purchasesByItemsReducer,
salesByItemsReducer, salesByItemsReducer,
@@ -167,21 +165,44 @@ export function useGeneralLedgerSheet(query, props) {
method: 'get', method: 'get',
url: '/financial_statements/general_ledger', url: '/financial_statements/general_ledger',
params: query, params: query,
headers: {
Accept: 'application/json+table',
},
}, },
{ {
select: (res) => ({ select: (res) => res.data,
tableRows: generalLedgerTableRowsReducer(res.data.data),
...res.data,
}),
defaultData: {
tableRows: [],
data: {},
query: {},
},
...props, ...props,
}, },
); );
} }
export const useGeneralLedgerSheetXlsxExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/general_ledger',
config: {
headers: {
accept: 'application/xlsx',
},
params: query,
},
filename: 'general_ledger.xlsx',
...args,
});
};
export const useGeneralLedgerSheetCsvExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/general_ledger',
config: {
headers: {
accept: 'application/csv',
},
params: query,
},
filename: 'general_ledger.csv',
...args,
});
};
/** /**
* Retrieve journal sheet. * Retrieve journal sheet.
@@ -189,22 +210,49 @@ export function useGeneralLedgerSheet(query, props) {
export function useJournalSheet(query, props) { export function useJournalSheet(query, props) {
return useRequestQuery( return useRequestQuery(
[t.FINANCIAL_REPORT, t.JOURNAL, query], [t.FINANCIAL_REPORT, t.JOURNAL, query],
{ method: 'get', url: '/financial_statements/journal', params: query },
{ {
select: (res) => ({ method: 'get',
tableRows: journalTableRowsReducer(res.data.data), url: '/financial_statements/journal',
...res.data, params: query,
}), headers: {
defaultData: { Accept: 'application/json+table',
data: {},
tableRows: [],
query: {},
}, },
},
{
select: (res) => res.data,
...props, ...props,
}, },
); );
} }
export const useJournalSheetXlsxExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/journal',
config: {
headers: {
accept: 'application/xlsx',
},
params: query,
},
filename: 'journal.xlsx',
...args,
});
};
export const useJournalSheetCsvExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/journal',
config: {
headers: {
accept: 'application/csv',
},
params: query,
},
filename: 'journal.csv',
...args,
});
};
/** /**
* Retrieve A/R aging summary report. * Retrieve A/R aging summary report.
*/ */