refator: reports to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-20 15:44:06 +02:00
parent 9eec60ea22
commit 8e36aab529
98 changed files with 5075 additions and 97 deletions

View File

@@ -3,11 +3,15 @@ import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByIt
import { CustomerBalanceSummaryModule } from './modules/CustomerBalanceSummary/CustomerBalanceSummary.module';
import { SalesByItemsModule } from './modules/SalesByItems/SalesByItems.module';
import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.module';
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
import { TrialBalanceSheetModule } from './modules/TrialBalanceSheet/TrialBalanceSheet.module';
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module';
import { TransactionsByVendorModule } from './modules/TransactionsByVendor/TransactionsByVendor.module';
//
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 { InventoryItemDetailsModule } from './modules/InventoryItemDetails/InventoryItemDetails.module';
import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet/InventoryValuationSheet.module';
@Module({
providers: [],
imports: [
@@ -16,12 +20,13 @@ import { TransactionsByVendorModule } from './modules/TransactionsByVendor/Trans
SalesByItemsModule,
GeneralLedgerModule,
TrialBalanceSheetModule,
TransactionsByCustomerModule,
TransactionsByVendorModule,
// TransactionsByReferenceModule,
// TransactionsByVendorModule,
// TransactionsByContactModule,
TransactionsByCustomerModule,
TransactionsByReferenceModule,
ARAgingSummaryModule,
APAgingSummaryModule,
InventoryItemDetailsModule,
InventoryValuationSheetModule,
],
})
export class FinancialStatementsModule {}

View File

@@ -1,8 +1,7 @@
import moment from 'moment';
import * as moment from 'moment';
import {
IFormatNumberSettings,
INumberFormatQuery,
} from '../types/Report.types';
import { formatNumber } from '@/utils/format-number';
import { IFinancialTableTotal } from '../types/Table.types';
@@ -41,7 +40,7 @@ export class FinancialSheet {
*/
protected formatNumber(
number,
overrideSettings: IFormatNumberSettings = {}
overrideSettings: IFormatNumberSettings = {},
): string {
const settings = {
...this.transfromFormatQueryToSettings(),
@@ -57,7 +56,7 @@ export class FinancialSheet {
*/
protected formatTotalNumber = (
amount: number,
settings: IFormatNumberSettings = {}
settings: IFormatNumberSettings = {},
): string => {
const { numberFormat } = this;
@@ -75,7 +74,7 @@ export class FinancialSheet {
*/
protected formatPercentage = (
amount: number,
overrideSettings: IFormatNumberSettings = {}
overrideSettings: IFormatNumberSettings = {},
): string => {
const percentage = amount * 100;
const settings = {
@@ -94,7 +93,7 @@ export class FinancialSheet {
*/
protected formatTotalPercentage = (
amount: number,
settings: IFormatNumberSettings = {}
settings: IFormatNumberSettings = {},
): string => {
return this.formatPercentage(amount, {
...settings,
@@ -109,7 +108,7 @@ export class FinancialSheet {
*/
protected getAmountMeta(
amount: number,
overrideSettings?: IFormatNumberSettings
overrideSettings?: IFormatNumberSettings,
): IFinancialTableTotal {
return {
amount,
@@ -125,7 +124,7 @@ export class FinancialSheet {
*/
protected getTotalAmountMeta(
amount: number,
title?: string
title?: string,
): IFinancialTableTotal {
return {
...(title ? { title } : {}),

View File

@@ -0,0 +1,56 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
@Controller('reports/payable-aging-summary')
export class APAgingSummaryController {
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) {}
@Get()
public async get(
@Query() filter: IAPAgingSummaryQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.APAgingSummaryApp.table(filter);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const csv = await this.APAgingSummaryApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(csv);
// Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.APAgingSummaryApp.xlsx(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 pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.APAgingSummaryApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.APAgingSummaryApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { APAgingSummaryService } from './APAgingSummaryService';
import { AgingSummaryModule } from '../AgingSummary/AgingSummary.module';
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
import { APAgingSummaryExportInjectable } from './APAgingSummaryExportInjectable';
import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
import { APAgingSummaryController } from './APAgingSummary.controller';
@Module({
imports: [AgingSummaryModule],
providers: [
APAgingSummaryService,
APAgingSummaryTableInjectable,
APAgingSummaryExportInjectable,
APAgingSummaryPdfInjectable,
APAgingSummaryRepository,
APAgingSummaryApplication
],
controllers: [APAgingSummaryController],
})
export class APAgingSummaryModule {}

View File

@@ -0,0 +1,45 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
import {
IAgingPeriod,
IAgingSummaryQuery,
IAgingSummaryTotal,
IAgingSummaryContact,
IAgingSummaryData,
} from '../AgingSummary/AgingSummary.types';
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
vendorsIds: number[];
}
export interface IAPAgingSummaryVendor extends IAgingSummaryContact {
vendorName: string;
}
export interface IAPAgingSummaryTotal extends IAgingSummaryTotal {}
export interface IAPAgingSummaryData extends IAgingSummaryData {
vendors: IAPAgingSummaryVendor[];
}
export type IAPAgingSummaryColumns = IAgingPeriod[];
export interface IARAgingSummaryMeta extends IFinancialSheetCommonMeta {
formattedAsDate: string;
}
export interface IAPAgingSummaryMeta extends IFinancialSheetCommonMeta {
formattedAsDate: string;
}
export interface IAPAgingSummaryTable extends IFinancialTable {
query: IAPAgingSummaryQuery;
meta: IAPAgingSummaryMeta;
}
export interface IAPAgingSummarySheet {
data: IAPAgingSummaryData;
meta: IAPAgingSummaryMeta;
query: IAPAgingSummaryQuery;
columns: any;
}

View File

@@ -0,0 +1,57 @@
import { APAgingSummaryExportInjectable } from './APAgingSummaryExportInjectable';
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { APAgingSummaryService } from './APAgingSummaryService';
import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class APAgingSummaryApplication {
constructor(
private readonly APAgingSummaryTable: APAgingSummaryTableInjectable,
private readonly APAgingSummaryExport: APAgingSummaryExportInjectable,
private readonly APAgingSummarySheet: APAgingSummaryService,
private readonly APAgingSumaryPdf: APAgingSummaryPdfInjectable,
) {}
/**
* Retrieve the A/P aging summary in sheet format.
* @param {IAPAgingSummaryQuery} query
*/
public sheet(query: IAPAgingSummaryQuery) {
return this.APAgingSummarySheet.APAgingSummary(query);
}
/**
* Retrieve the A/P aging summary in table format.
* @param {IAPAgingSummaryQuery} query
*/
public table(query: IAPAgingSummaryQuery) {
return this.APAgingSummaryTable.table(query);
}
/**
* Retrieve the A/P aging summary in CSV format.
* @param {IAPAgingSummaryQuery} query
*/
public csv(query: IAPAgingSummaryQuery) {
return this.APAgingSummaryExport.csv(query);
}
/**
* Retrieve the A/P aging summary in XLSX format.
* @param {IAPAgingSummaryQuery} query
*/
public xlsx(query: IAPAgingSummaryQuery) {
return this.APAgingSummaryExport.xlsx(query);
}
/**
* Retrieves the A/P aging summary in pdf format.
* @param {IAPAgingSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: IAPAgingSummaryQuery) {
return this.APAgingSumaryPdf.pdf(query);
}
}

View File

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

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import {
IAgingSummaryMeta,
IAgingSummaryQuery,
} from '../AgingSummary/AgingSummary.types';
import { AgingSummaryMeta } from '../AgingSummary/AgingSummaryMeta';
@Injectable()
export class APAgingSummaryMeta {
constructor(private readonly agingSummaryMeta: AgingSummaryMeta) {}
/**
* Retrieve the aging summary meta.
* @returns {IBalanceSheetMeta}
*/
public async meta(query: IAgingSummaryQuery): Promise<IAgingSummaryMeta> {
const commonMeta = await this.agingSummaryMeta.meta(query);
return {
...commonMeta,
sheetName: 'A/P Aging Summary',
};
}
}

View File

@@ -0,0 +1,29 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { HtmlTableCss } from '../AgingSummary/_constants';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class APAgingSummaryPdfInjectable {
constructor(
private readonly APAgingSummaryTable: APAgingSummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given A/P aging summary sheet table to pdf.
* @param {IAPAgingSummaryQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: IAPAgingSummaryQuery): Promise<Buffer> {
const table = await this.APAgingSummaryTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedAsDate,
HtmlTableCss,
);
}
}

View File

@@ -0,0 +1,35 @@
export class APAgingSummaryRepository {
asyncInit() {
// Settings tenant service.
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
// Retrieve all vendors from the storage.
const vendors =
filter.vendorsIds.length > 0
? await vendorRepository.findWhereIn('id', filter.vendorsIds)
: await vendorRepository.all();
// Common query.
const commonQuery = (query) => {
if (!isEmpty(filter.branchesIds)) {
query.modify('filterByBranches', filter.branchesIds);
}
};
// Retrieve all overdue vendors bills.
const overdueBills = await Bill.query()
.modify('overdueBillsFromDate', filter.asDate)
.onBuild(commonQuery);
// Retrieve all due vendors bills.
const dueBills = await Bill.query()
.modify('dueBillsFromDate', filter.asDate)
.onBuild(commonQuery);
}
}

View File

@@ -0,0 +1,60 @@
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import {
IAPAgingSummaryQuery,
IAPAgingSummarySheet,
} from './APAgingSummary.types';
import { APAgingSummarySheet } from './APAgingSummarySheet';
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
import { getAPAgingSummaryDefaultQuery } from './utils';
import { events } from '@/common/events/events';
@Injectable()
export class APAgingSummaryService {
constructor(
private readonly APAgingSummaryMeta: APAgingSummaryMeta,
private readonly eventPublisher: EventEmitter2,
) {}
/**
* Retrieve A/P aging summary report.
* @param {number} tenantId -
* @param {IAPAgingSummaryQuery} query -
* @returns {Promise<IAPAgingSummarySheet>}
*/
public async APAgingSummary(
query: IAPAgingSummaryQuery,
): Promise<IAPAgingSummarySheet> {
const filter = {
...getAPAgingSummaryDefaultQuery(),
...query,
};
// A/P aging summary report instance.
const APAgingSummaryReport = new APAgingSummarySheet(
filter,
vendors,
overdueBills,
dueBills,
tenant.metadata.baseCurrency,
);
// A/P aging summary report data and columns.
const data = APAgingSummaryReport.reportData();
const columns = APAgingSummaryReport.reportColumns();
// Retrieve the aging summary report meta.
const meta = await this.APAgingSummaryMeta.meta(filter);
// Triggers `onPayableAgingViewed` event.
await this.eventPublisher.emitAsync(events.reports.onPayableAgingViewed, {
query,
});
return {
data,
columns,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,188 @@
import { groupBy, sum, isEmpty } from 'lodash';
import * as R from 'ramda';
import {
IAPAgingSummaryQuery,
IAPAgingSummaryData,
IAPAgingSummaryVendor,
IAPAgingSummaryColumns,
IAPAgingSummaryTotal,
} from './APAgingSummary.types';
import { AgingSummaryReport } from '../AgingSummary/AgingSummary';
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
import { ModelObject } from 'objection';
import { Bill } from '@/modules/Bills/models/Bill';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
export class APAgingSummarySheet extends AgingSummaryReport {
readonly tenantId: number;
readonly query: IAPAgingSummaryQuery;
readonly contacts: ModelObject<Vendor>[];
readonly unpaidBills: ModelObject<Bill>[];
readonly baseCurrency: string;
readonly overdueInvoicesByContactId: Record<number, Array<ModelObject<Bill>>>;
readonly currentInvoicesByContactId: Record<number, Array<ModelObject<Bill>>>;
readonly agingPeriods: IAgingPeriod[];
/**
* Constructor method.
* @param {number} tenantId - Tenant id.
* @param {IAPAgingSummaryQuery} query - Report query.
* @param {ModelObject<Vendor>[]} vendors - Unpaid bills.
* @param {string} baseCurrency - Base currency of the organization.
*/
constructor(
tenantId: number,
query: IAPAgingSummaryQuery,
vendors: ModelObject<Vendor>[],
overdueBills: ModelObject<Bill>[],
unpaidBills: ModelObject<Bill>[],
baseCurrency: string,
) {
super();
this.tenantId = tenantId;
this.query = query;
this.numberFormat = this.query.numberFormat;
this.contacts = vendors;
this.baseCurrency = baseCurrency;
this.overdueInvoicesByContactId = groupBy(overdueBills, 'vendorId');
this.currentInvoicesByContactId = groupBy(unpaidBills, 'vendorId');
// Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods(
this.query.asDate,
this.query.agingDaysBefore,
this.query.agingPeriods,
);
}
/**
* Retrieve the vendors aging and current total.
* @param {IAPAgingSummaryTotal} vendorsAgingPeriods
* @return {IAPAgingSummaryTotal}
*/
private getVendorsTotal = (vendorsAgingPeriods): IAPAgingSummaryTotal => {
const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods);
const totalCurrent = this.getTotalCurrent(vendorsAgingPeriods);
const totalVendorsTotal = this.getTotalContactsTotals(vendorsAgingPeriods);
return {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
total: this.formatTotalAmount(totalVendorsTotal),
};
};
/**
* Retrieve the vendor section data.
* @param {ModelObject<Vendor>} vendor
* @return {IAPAgingSummaryVendor}
*/
private vendorTransformer = (
vendor: ModelObject<Vendor>,
): IAPAgingSummaryVendor => {
const agingPeriods = this.getContactAgingPeriods(vendor.id);
const currentTotal = this.getContactCurrentTotal(vendor.id);
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
const amount = sum([agingPeriodsTotal, currentTotal]);
return {
vendorName: vendor.displayName,
current: this.formatAmount(currentTotal),
aging: agingPeriods,
total: this.formatTotalAmount(amount),
};
};
/**
* Mappes the given vendor objects to vendor report node.
* @param {ModelObject<Vendor>[]} vendors
* @returns {IAPAgingSummaryVendor[]}
*/
private vendorsMapper = (
vendors: ModelObject<Vendor>[],
): IAPAgingSummaryVendor[] => {
return vendors.map(this.vendorTransformer);
};
/**
* Detarmines whether the given vendor node is none zero.
* @param {IAPAgingSummaryVendor} vendorNode
* @returns {boolean}
*/
private filterNoneZeroVendorNode = (
vendorNode: IAPAgingSummaryVendor,
): boolean => {
return vendorNode.total.amount !== 0;
};
/**
* Filters vendors report nodes based on the given report query.
* @param {IAPAgingSummaryVendor} vendorNode
* @returns {boolean}
*/
private vendorNodeFilter = (vendorNode: IAPAgingSummaryVendor): boolean => {
const { noneZero } = this.query;
const conditions = [[noneZero, this.filterNoneZeroVendorNode]];
return allPassedConditionsPass(conditions)(vendorNode);
};
/**
* Filtesr the given report vendors nodes.
* @param {IAPAgingSummaryVendor[]} vendorNodes
* @returns {IAPAgingSummaryVendor[]}
*/
private vendorsFilter = (
vendorNodes: IAPAgingSummaryVendor[],
): IAPAgingSummaryVendor[] => {
return vendorNodes.filter(this.vendorNodeFilter);
};
/**
* Detarmines whether vendors nodes filter enabled.
* @returns {boolean}
*/
private isVendorNodesFilter = (): boolean => {
return isEmpty(this.query.vendorsIds);
};
/**
* Retrieve vendors aging periods.
* @return {IAPAgingSummaryVendor[]}
*/
private vendorsSection = (
vendors: ModelObject<Vendor>[],
): IAPAgingSummaryVendor[] => {
return R.compose(
R.when(this.isVendorNodesFilter, this.vendorsFilter),
this.vendorsMapper,
)(vendors);
};
/**
* Retrieve the A/P aging summary report data.
* @return {IAPAgingSummaryData}
*/
public reportData = (): IAPAgingSummaryData => {
const vendorsAgingPeriods = this.vendorsSection(this.contacts);
const vendorsTotal = this.getVendorsTotal(vendorsAgingPeriods);
return {
vendors: vendorsAgingPeriods,
total: vendorsTotal,
};
};
/**
* Retrieve the A/P aging summary report columns.
*/
public reportColumns = (): IAPAgingSummaryColumns => {
return this.agingPeriods;
};
}

View File

@@ -0,0 +1,49 @@
import { I18nService } from 'nestjs-i18n';
import { IAPAgingSummaryData } from './APAgingSummary.types';
import { AgingSummaryTable } from '../AgingSummary/AgingSummaryTable';
import { ITableColumnAccessor } from '../../types/Table.types';
import { IAgingSummaryQuery } from '../AgingSummary/AgingSummary.types';
import { ITableColumn } from '../../types/Table.types';
import { ITableRow } from '../../types/Table.types';
export class APAgingSummaryTable extends AgingSummaryTable {
readonly report: IAPAgingSummaryData;
/**
* Constructor method.
* @param {IARAgingSummaryData} data
* @param {IAgingSummaryQuery} query
* @param {any} i18n
*/
constructor(
data: IAPAgingSummaryData,
query: IAgingSummaryQuery,
i18n: I18nService,
) {
super(data, query, i18n);
}
/**
* Retrieves the contacts table rows.
* @returns {ITableRow[]}
*/
get contactsRows(): ITableRow[] {
return this.contactsNodes(this.report.vendors);
}
/**
* Contact name node accessor.
* @returns {ITableColumnAccessor}
*/
get contactNameNodeAccessor(): ITableColumnAccessor {
return { key: 'vendor_name', accessor: 'vendorName' };
}
/**
* Retrieves the contact name table column.
* @returns {ITableColumn}
*/
contactNameTableColumn = (): ITableColumn => {
return { label: 'Vendor name', key: 'vendor_name' };
};
}

View File

@@ -0,0 +1,33 @@
import {
IAPAgingSummaryQuery,
IAPAgingSummaryTable,
} from './APAgingSummary.types';
import { APAgingSummaryService } from './APAgingSummaryService';
import { APAgingSummaryTable } from './APAgingSummaryTable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class APAgingSummaryTableInjectable {
constructor(private readonly APAgingSummarySheet: APAgingSummaryService) {}
/**
* Retrieves A/P aging summary in table format.
* @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, {});
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
meta: report.meta,
query: report.query,
};
}
}

View File

@@ -0,0 +1,17 @@
export const getAPAgingSummaryDefaultQuery = () => {
return {
asDate: moment().format('YYYY-MM-DD'),
agingDaysBefore: 30,
agingPeriods: 3,
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
vendorsIds: [],
branchesIds: [],
noneZero: false,
};
};

View File

@@ -0,0 +1,56 @@
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { Get, Headers } from '@nestjs/common';
import { Query, Res } from '@nestjs/common';
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
export class ARAgingSummaryController {
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
@Get()
public async get(
@Query() filter: IARAgingSummaryQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the xlsx format.
if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.ARAgingSummaryApp.xlsx(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 table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.ARAgingSummaryApp.table(filter);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.ARAgingSummaryApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.ARAgingSummaryApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.ARAgingSummaryApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
import { ARAgingSummaryExportInjectable } from './ARAgingSummaryExportInjectable';
import { ARAgingSummaryService } from './ARAgingSummaryService';
import { ARAgingSummaryPdfInjectable } from './ARAgingSummaryPdfInjectable';
import { AgingSummaryModule } from '../AgingSummary/AgingSummary.module';
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
import { ARAgingSummaryController } from './ARAgingSummary.controller';
@Module({
imports: [AgingSummaryModule],
controllers: [ARAgingSummaryController],
providers: [
ARAgingSummaryTableInjectable,
ARAgingSummaryExportInjectable,
ARAgingSummaryService,
ARAgingSummaryPdfInjectable,
ARAgingSummaryRepository,
ARAgingSummaryApplication,
],
})
export class ARAgingSummaryModule {}

View File

@@ -0,0 +1,43 @@
import {
IAgingPeriod,
IAgingSummaryQuery,
IAgingSummaryTotal,
IAgingSummaryContact,
IAgingSummaryData,
IAgingSummaryMeta,
} from '../AgingSummary/AgingSummary.types';
import { IFinancialTable } from '../../types/Table.types';
export interface IARAgingSummaryQuery extends IAgingSummaryQuery {
customersIds: number[];
}
export interface IARAgingSummaryCustomer extends IAgingSummaryContact {
customerName: string;
}
export interface IARAgingSummaryTotal extends IAgingSummaryTotal {}
export interface IARAgingSummaryData extends IAgingSummaryData {
customers: IARAgingSummaryCustomer[];
}
export type IARAgingSummaryColumns = IAgingPeriod[];
export interface IARAgingSummaryMeta extends IAgingSummaryMeta {
organizationName: string;
baseCurrency: string;
}
export interface IARAgingSummaryTable extends IFinancialTable {
meta: IARAgingSummaryMeta;
query: IARAgingSummaryQuery;
}
export interface IARAgingSummarySheet {
data: IARAgingSummaryData;
meta: IARAgingSummaryMeta;
query: IARAgingSummaryQuery;
columns: IARAgingSummaryColumns;
}

View File

@@ -0,0 +1,60 @@
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
import { ARAgingSummaryExportInjectable } from './ARAgingSummaryExportInjectable';
import { ARAgingSummaryService } from './ARAgingSummaryService';
import { ARAgingSummaryPdfInjectable } from './ARAgingSummaryPdfInjectable';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ARAgingSummaryApplication {
constructor(
private readonly ARAgingSummaryTable: ARAgingSummaryTableInjectable,
private readonly ARAgingSummaryExport: ARAgingSummaryExportInjectable,
private readonly ARAgingSummarySheet: ARAgingSummaryService,
private readonly ARAgingSummaryPdf: ARAgingSummaryPdfInjectable,
) {}
/**
* Retrieve the A/R aging summary sheet.
* @param {IAPAgingSummaryQuery} query
*/
public sheet(query: IARAgingSummaryQuery) {
return this.ARAgingSummarySheet.ARAgingSummary(query);
}
/**
* Retrieve the A/R aging summary in table format.
* @param {number} tenantId
* @param {IAPAgingSummaryQuery} query
*/
public table(query: IARAgingSummaryQuery) {
return this.ARAgingSummaryTable.table(query);
}
/**
* Retrieve the A/R aging summary in XLSX format.
* @param {number} tenantId
* @param {IAPAgingSummaryQuery} query
*/
public xlsx(query: IARAgingSummaryQuery) {
return this.ARAgingSummaryExport.xlsx(query);
}
/**
* Retrieve the A/R aging summary in CSV format.
* @param {number} tenantId
* @param {IAPAgingSummaryQuery} query
*/
public csv(query: IARAgingSummaryQuery) {
return this.ARAgingSummaryExport.csv(query);
}
/**
* Retrieves the A/R aging summary in pdf format.
* @param {IARAgingSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: IARAgingSummaryQuery) {
return this.ARAgingSummaryPdf.pdf(query);
}
}

View File

@@ -0,0 +1,43 @@
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { TableSheet } from '../../common/TableSheet';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ARAgingSummaryExportInjectable {
constructor(
private readonly ARAgingSummaryTable: ARAgingSummaryTableInjectable,
) {}
/**
* Retrieves the A/R aging summary sheet in XLSX format.
* @param {IARAgingSummaryQuery} query - A/R aging summary query.
* @returns {Promise<Buffer>}
*/
public async xlsx(
query: IARAgingSummaryQuery
): Promise<Buffer> {
const table = await this.ARAgingSummaryTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the A/R aging summary sheet in CSV format.
* @param {IARAgingSummaryQuery} query - A/R aging summary query.
* @returns {Promise<string>}
*/
public async csv(
query: IARAgingSummaryQuery
): Promise<string> {
const table = await this.ARAgingSummaryTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { AgingSummaryMeta } from '../AgingSummary/AgingSummaryMeta';
import { IAgingSummaryMeta, IAgingSummaryQuery } from '../AgingSummary/AgingSummary.types';
@Injectable()
export class ARAgingSummaryMeta {
constructor(
private readonly agingSummaryMeta: AgingSummaryMeta,
) {}
/**
* Retrieve the aging summary meta.
* @param {IAgingSummaryQuery} query - Aging summary query.
* @returns {IAgingSummaryMeta}
*/
public async meta(
query: IAgingSummaryQuery
): Promise<IAgingSummaryMeta> {
const commonMeta = await this.agingSummaryMeta.meta(query);
return {
...commonMeta,
sheetName: 'A/R Aging Summary',
};
}
}

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { HtmlTableCss } from '../AgingSummary/_constants';
@Injectable()
export class ARAgingSummaryPdfInjectable {
constructor(
private readonly ARAgingSummaryTable: ARAgingSummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given balance sheet table to pdf.
* @param {IBalanceSheetQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: IARAgingSummaryQuery): Promise<Buffer> {
const table = await this.ARAgingSummaryTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCss,
);
}
}

View File

@@ -0,0 +1,38 @@
export class ARAgingSummaryRepository {
init(){
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
// Retrieve all customers from the storage.
const customers =
filter.customersIds.length > 0
? await customerRepository.findWhereIn('id', filter.customersIds)
: await customerRepository.all();
// Common query.
const commonQuery = (query) => {
if (!isEmpty(filter.branchesIds)) {
query.modify('filterByBranches', filter.branchesIds);
}
};
// Retrieve all overdue sale invoices.
const overdueSaleInvoices = await SaleInvoice.query()
.modify('overdueInvoicesFromDate', filter.asDate)
.onBuild(commonQuery);
// Retrieve all due sale invoices.
const currentInvoices = await SaleInvoice.query()
.modify('dueInvoicesFromDate', filter.asDate)
.onBuild(commonQuery);
}
}

View File

@@ -0,0 +1,55 @@
import { ARAgingSummarySheet } from './ARAgingSummarySheet';
import { ARAgingSummaryMeta } from './ARAgingSummaryMeta';
import { getARAgingSummaryDefaultQuery } from './utils';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { events } from '@/common/events/events';
@Injectable()
export class ARAgingSummaryService {
constructor(
private readonly ARAgingSummaryMeta: ARAgingSummaryMeta,
private readonly eventPublisher: EventEmitter2,
) {}
/**
* Retrieve A/R aging summary report.
* @param {IARAgingSummaryQuery} query -
*/
async ARAgingSummary(query: IARAgingSummaryQuery) {
const filter = {
...getARAgingSummaryDefaultQuery(),
...query,
};
// AR aging summary report instance.
const ARAgingSummaryReport = new ARAgingSummarySheet(
filter,
customers,
overdueSaleInvoices,
currentInvoices,
tenant.metadata.baseCurrency,
);
// AR aging summary report data and columns.
const data = ARAgingSummaryReport.reportData();
const columns = ARAgingSummaryReport.reportColumns();
// Retrieve the aging summary report meta.
const meta = await this.ARAgingSummaryMeta.meta(filter);
// Triggers `onReceivableAgingViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onReceivableAgingViewed,
{
query,
},
);
return {
data,
columns,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,198 @@
import * as R from 'ramda';
import { Dictionary, groupBy, isEmpty, sum } from 'lodash';
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
import {
IARAgingSummaryQuery,
IARAgingSummaryCustomer,
IARAgingSummaryData,
IARAgingSummaryColumns,
IARAgingSummaryTotal,
} from './ARAgingSummary.types';
import { AgingSummaryReport } from '../AgingSummary/AgingSummary';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { ModelObject } from 'objection';
import { Customer } from '@/modules/Customers/models/Customer';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
export class ARAgingSummarySheet extends AgingSummaryReport {
readonly tenantId: number;
readonly query: IARAgingSummaryQuery;
readonly contacts: ModelObject<Customer>[];
readonly agingPeriods: IAgingPeriod[];
readonly baseCurrency: string;
readonly overdueInvoicesByContactId: Dictionary<ModelObject<SaleInvoice>[]>;
readonly currentInvoicesByContactId: Dictionary<ModelObject<SaleInvoice>[]>;
/**
* Constructor method.
* @param {number} tenantId
* @param {IARAgingSummaryQuery} query
* @param {ICustomer[]} customers
* @param {IJournalPoster} journal
*/
constructor(
tenantId: number,
query: IARAgingSummaryQuery,
customers: ModelObject<Customer>[],
overdueSaleInvoices: ModelObject<SaleInvoice>[],
currentSaleInvoices: ModelObject<SaleInvoice>[],
baseCurrency: string,
) {
super();
this.tenantId = tenantId;
this.contacts = customers;
this.query = query;
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat;
this.overdueInvoicesByContactId = groupBy(
overdueSaleInvoices,
'customerId',
);
this.currentInvoicesByContactId = groupBy(
currentSaleInvoices,
'customerId',
);
// Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods(
this.query.asDate,
this.query.agingDaysBefore,
this.query.agingPeriods,
);
}
/**
* Mapping aging customer.
* @param {ICustomer} customer -
* @return {IARAgingSummaryCustomer[]}
*/
private customerTransformer = (
customer: ModelObject<Customer>,
): IARAgingSummaryCustomer => {
const agingPeriods = this.getContactAgingPeriods(customer.id);
const currentTotal = this.getContactCurrentTotal(customer.id);
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
const amount = sum([agingPeriodsTotal, currentTotal]);
return {
customerName: customer.displayName,
current: this.formatAmount(currentTotal),
aging: agingPeriods,
total: this.formatTotalAmount(amount),
};
};
/**
* Mappes the customers objects to report accounts nodes.
* @param {ICustomer[]} customers
* @returns {IARAgingSummaryCustomer[]}
*/
private customersMapper = (
customers: ModelObject<Customer>[],
): IARAgingSummaryCustomer[] => {
return customers.map(this.customerTransformer);
};
/**
* Filters the none-zero account report node.
* @param {IARAgingSummaryCustomer} node
* @returns {boolean}
*/
private filterNoneZeroAccountNode = (
node: IARAgingSummaryCustomer,
): boolean => {
return node.total.amount !== 0;
};
/**
* Filters customer report node based on the given report query.
* @param {IARAgingSummaryCustomer} customerNode
* @returns {boolean}
*/
private customerNodeFilter = (
customerNode: IARAgingSummaryCustomer,
): boolean => {
const { noneZero } = this.query;
const conditions = [[noneZero, this.filterNoneZeroAccountNode]];
return allPassedConditionsPass(conditions)(customerNode);
};
/**
* Filters customers report nodes.
* @param {IARAgingSummaryCustomer[]} customers
* @returns {IARAgingSummaryCustomer[]}
*/
private customersFilter = (
customers: IARAgingSummaryCustomer[],
): IARAgingSummaryCustomer[] => {
return customers.filter(this.customerNodeFilter);
};
/**
* Detarmines the customers nodes filter is enabled.
* @returns {boolean}
*/
private isCustomersFilterEnabled = (): boolean => {
return isEmpty(this.query.customersIds);
};
/**
* Retrieve customers report.
* @param {ICustomer[]} customers
* @return {IARAgingSummaryCustomer[]}
*/
private customersWalker = (
customers: ModelObject<Customer>[],
): IARAgingSummaryCustomer[] => {
return R.compose(
R.when(this.isCustomersFilterEnabled, this.customersFilter),
this.customersMapper,
)(customers);
};
/**
* Retrieve the customers aging and current total.
* @param {IARAgingSummaryCustomer} customersAgingPeriods
*/
private getCustomersTotal = (
customersAgingPeriods: IARAgingSummaryCustomer[],
): IARAgingSummaryTotal => {
const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods);
const totalCurrent = this.getTotalCurrent(customersAgingPeriods);
const totalCustomersTotal = this.getTotalContactsTotals(
customersAgingPeriods,
);
return {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
total: this.formatTotalAmount(totalCustomersTotal),
};
};
/**
* Retrieve A/R aging summary report data.
* @return {IARAgingSummaryData}
*/
public reportData = (): IARAgingSummaryData => {
const customersAgingPeriods = this.customersWalker(this.contacts);
const customersTotal = this.getCustomersTotal(customersAgingPeriods);
return {
customers: customersAgingPeriods,
total: customersTotal,
};
};
/**
* Retrieve A/R aging summary report columns.
* @return {IARAgingSummaryColumns}
*/
public reportColumns(): IARAgingSummaryColumns {
return this.agingPeriods;
}
}

View File

@@ -0,0 +1,34 @@
import { IARAgingSummaryData } from './ARAgingSummary.types';
import { AgingSummaryTable } from '../AgingSummary/AgingSummaryTable';
import { IAgingSummaryQuery } from '../AgingSummary/AgingSummary.types';
import { ITableColumnAccessor, ITableRow } from '../../types/Table.types';
export class ARAgingSummaryTable extends AgingSummaryTable {
readonly report: IARAgingSummaryData;
/**
* Constructor method.
* @param {IARAgingSummaryData} data
* @param {IAgingSummaryQuery} query
* @param {any} i18n
*/
constructor(data: IARAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
super(data, query, i18n);
}
/**
* Retrieves the contacts table rows.
* @returns {ITableRow[]}
*/
get contactsRows(): ITableRow[] {
return this.contactsNodes(this.report.customers);
}
/**
* Contact name node accessor.
* @returns {ITableColumnAccessor}
*/
get contactNameNodeAccessor(): ITableColumnAccessor {
return { key: 'customer_name', accessor: 'customerName' };
}
}

View File

@@ -0,0 +1,33 @@
import { ARAgingSummaryTable } from './ARAgingSummaryTable';
import { ARAgingSummaryService } from './ARAgingSummaryService';
import { Injectable } from '@nestjs/common';
import {
IARAgingSummaryQuery,
IARAgingSummaryTable,
} from './ARAgingSummary.types';
@Injectable()
export class ARAgingSummaryTableInjectable {
constructor(private readonly ARAgingSummarySheet: ARAgingSummaryService) {}
/**
* Retrieves A/R aging summary in table format.
* @param {IARAgingSummaryQuery} query - Aging summary query.
* @returns {Promise<IARAgingSummaryTable>}
*/
public async table(
query: IARAgingSummaryQuery,
): Promise<IARAgingSummaryTable> {
const report = await this.ARAgingSummarySheet.ARAgingSummary(query);
const table = new ARAgingSummaryTable(report.data, query, {});
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
meta: report.meta,
query,
};
}
}

View File

@@ -0,0 +1,20 @@
export const getARAgingSummaryDefaultQuery = () => {
return {
asDate: moment().format('YYYY-MM-DD'),
agingDaysBefore: 30,
agingPeriods: 3,
numberFormat: {
divideOn1000: false,
negativeFormat: 'mines',
showZero: false,
formatMoney: 'total',
precision: 2,
},
customersIds: [],
branchesIds: [],
noneZero: false,
};
};

View File

@@ -0,0 +1,51 @@
import * as moment from 'moment';
import { IAgingPeriod } from './AgingSummary.types';
import { FinancialSheet } from '../../common/FinancialSheet';
export abstract class AgingReport extends FinancialSheet {
/**
* Retrieve the aging periods range.
* @param {string} asDay
* @param {number} agingDaysBefore
* @param {number} agingPeriodsFreq
*/
public agingRangePeriods(
asDay: Date | string,
agingDaysBefore: number,
agingPeriodsFreq: number,
): IAgingPeriod[] {
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
const startAging = moment(asDay).startOf('day');
const endAging = startAging
.clone()
.subtract(totalAgingDays, 'days')
.endOf('day');
const agingPeriods: IAgingPeriod[] = [];
const startingAging = startAging.clone();
let beforeDays = 1;
let toDays = 0;
while (startingAging > endAging) {
const currentAging = startingAging.clone();
startingAging.subtract(agingDaysBefore, 'days').endOf('day');
toDays += agingDaysBefore;
agingPeriods.push({
fromPeriod: moment(currentAging).format('YYYY-MM-DD'),
toPeriod: moment(startingAging).format('YYYY-MM-DD'),
beforeDays: beforeDays === 1 ? 0 : beforeDays,
toDays: toDays,
...(startingAging.valueOf() === endAging.valueOf()
? {
toPeriod: null,
toDays: null,
}
: {}),
});
beforeDays += agingDaysBefore;
}
return agingPeriods;
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { AgingSummaryMeta } from './AgingSummaryMeta';
@Module({
exports: [AgingSummaryMeta],
providers: [AgingSummaryMeta],
})
export class AgingSummaryModule {}

View File

@@ -0,0 +1,233 @@
import { ModelObject } from 'objection';
import { defaultTo, sumBy, get } from 'lodash';
import {
IAgingPeriod,
IAgingPeriodTotal,
IAgingAmount,
IAgingSummaryContact,
IAgingSummaryQuery,
} from './AgingSummary.types';
import { AgingReport } from './AgingReport';
import { Customer } from '@/modules/Customers/models/Customer';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { Bill } from '@/modules/Bills/models/Bill';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { IFormatNumberSettings } from '@/utils/format-number';
import { IARAgingSummaryCustomer } from '../ARAgingSummary/ARAgingSummary.types';
export abstract class AgingSummaryReport extends AgingReport {
readonly contacts: ModelObject<Customer | Vendor>[];
readonly agingPeriods: IAgingPeriod[] = [];
readonly baseCurrency: string;
readonly query: IAgingSummaryQuery;
readonly overdueInvoicesByContactId: Record<
number,
Array<ModelObject<Bill | SaleInvoice>>
>;
readonly currentInvoicesByContactId: Record<
number,
Array<ModelObject<Bill | SaleInvoice>>
>;
/**
* Setes initial aging periods to the contact.
* @return {IAgingPeriodTotal[]}
*/
protected getInitialAgingPeriodsTotal(): IAgingPeriodTotal[] {
return this.agingPeriods.map((agingPeriod) => ({
...agingPeriod,
total: this.formatAmount(0),
}));
}
/**
* Calculates the given contact aging periods.
* @param {number} contactId - Contact id.
* @return {IAgingPeriodTotal[]}
*/
protected getContactAgingPeriods(contactId: number): IAgingPeriodTotal[] {
const unpaidInvoices = this.getUnpaidInvoicesByContactId(contactId);
const initialAgingPeriods = this.getInitialAgingPeriodsTotal();
return unpaidInvoices.reduce(
(agingPeriods: IAgingPeriodTotal[], unpaidInvoice) => {
const newAgingPeriods = this.getContactAgingDueAmount(
agingPeriods,
unpaidInvoice.dueAmount,
unpaidInvoice.overdueDays,
);
return newAgingPeriods;
},
initialAgingPeriods,
);
}
/**
* Sets the contact aging due amount to the table.
* @param {IAgingPeriodTotal} agingPeriods - Aging periods.
* @param {number} dueAmount - Due amount.
* @param {number} overdueDays - Overdue days.
* @return {IAgingPeriodTotal[]}
*/
protected getContactAgingDueAmount(
agingPeriods: IAgingPeriodTotal[],
dueAmount: number,
overdueDays: number,
): IAgingPeriodTotal[] {
const newAgingPeriods = agingPeriods.map((agingPeriod) => {
const isInAgingPeriod =
agingPeriod.beforeDays <= overdueDays &&
(agingPeriod.toDays > overdueDays || !agingPeriod.toDays);
const total: number = isInAgingPeriod
? agingPeriod.total.amount + dueAmount
: agingPeriod.total.amount;
return {
...agingPeriod,
total: this.formatAmount(total),
};
});
return newAgingPeriods;
}
/**
* Retrieve the aging period total object.
* @param {number} amount
* @param {IFormatNumberSettings} settings - Override the format number settings.
* @return {IAgingAmount}
*/
protected formatAmount(
amount: number,
settings: IFormatNumberSettings = {},
): IAgingAmount {
return {
amount,
// @ts-ignore
formattedAmount: this.formatNumber(amount, settings),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the aging period total object.
* @param {number} amount
* @param {IFormatNumberSettings} settings - Override the format number settings.
* @return {IAgingPeriodTotal}
*/
protected formatTotalAmount(
amount: number,
settings: IFormatNumberSettings = {},
): IAgingAmount {
return this.formatAmount(amount, {
money: true,
excerptZero: false,
...settings,
});
}
/**
* Calculates the total of the aging period by the given index.
* @param {number} index
* @return {number}
*/
protected getTotalAgingPeriodByIndex(
contactsAgingPeriods: any,
index: number,
): number {
return this.contacts.reduce((acc, contact) => {
const totalPeriod = contactsAgingPeriods[index]
? contactsAgingPeriods[index].total
: 0;
return acc + totalPeriod;
}, 0);
}
/**
* Retrieve the due invoices by the given contact id.
* @param {number} contactId -
* @return {(ISaleInvoice | IBill)[]}
*/
protected getUnpaidInvoicesByContactId(
contactId: number,
): (ModelObject<SaleInvoice> | ModelObject<Bill>)[] {
return defaultTo(this.overdueInvoicesByContactId[contactId], []);
}
/**
* Retrieve total aging periods of the report.
* @return {(IAgingPeriodTotal & IAgingPeriod)[]}
*/
protected getTotalAgingPeriods(
contactsAgingPeriods: IARAgingSummaryCustomer[],
): IAgingPeriodTotal[] {
return this.agingPeriods.map((agingPeriod, index) => {
const total = sumBy(
contactsAgingPeriods,
(summary: IARAgingSummaryCustomer) => {
const aging = summary.aging[index];
if (!aging) {
return 0;
}
return aging.total.amount;
},
);
return {
...agingPeriod,
total: this.formatTotalAmount(total),
};
});
}
/**
* Retrieve the current invoices by the given contact id.
* @param {number} contactId - Specific contact id.
* @return {(ISaleInvoice | IBill)[]}
*/
protected getCurrentInvoicesByContactId(
contactId: number,
): (ModelObject<SaleInvoice> | ModelObject<Bill>)[] {
return get(this.currentInvoicesByContactId, contactId, []);
}
/**
* Retrieve the contact total due amount.
* @param {number} contactId - Specific contact id.
* @return {number}
*/
protected getContactCurrentTotal(contactId: number): number {
const currentInvoices = this.getCurrentInvoicesByContactId(contactId);
return sumBy(currentInvoices, (invoice) => invoice.dueAmount);
}
/**
* Retrieve to total sumation of the given contacts summeries sections.
* @param {IARAgingSummaryCustomer[]} contactsSections -
* @return {number}
*/
protected getTotalCurrent(contactsSummaries: IAgingSummaryContact[]): number {
return sumBy(contactsSummaries, (summary) => summary.current.amount);
}
/**
* Retrieve the total of the given aging periods.
* @param {IAgingPeriodTotal[]} agingPeriods
* @return {number}
*/
protected getAgingPeriodsTotal(agingPeriods: IAgingPeriodTotal[]): number {
return sumBy(agingPeriods, (period) => period.total.amount);
}
/**
* Retrieve total of contacts totals.
* @param {IAgingSummaryContact[]} contactsSummaries
*/
protected getTotalContactsTotals(
contactsSummaries: IAgingSummaryContact[],
): number {
return sumBy(contactsSummaries, (summary) => summary.total.amount);
}
}

View File

@@ -0,0 +1,49 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { INumberFormatQuery } from '../../types/Report.types';
export interface IAgingPeriodTotal extends IAgingPeriod {
total: IAgingAmount;
}
export interface IAgingAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IAgingPeriod {
fromPeriod: Date | string;
toPeriod: Date | string;
beforeDays: number;
toDays: number;
}
export interface IAgingSummaryContact {
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
}
export interface IAgingSummaryQuery {
asDate: Date | string;
agingDaysBefore: number;
agingPeriods: number;
numberFormat: INumberFormatQuery;
branchesIds: number[];
noneZero: boolean;
}
export interface IAgingSummaryTotal {
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
}
export interface IAgingSummaryData {
total: IAgingSummaryTotal;
}
export interface IAgingSummaryMeta extends IFinancialSheetCommonMeta {
formattedAsDate: string;
formattedDateRange: string;
}

View File

@@ -0,0 +1,26 @@
import moment from 'moment';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import { IAgingSummaryMeta, IAgingSummaryQuery } from './AgingSummary.types';
@Injectable()
export class AgingSummaryMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieve the aging summary meta.
* @returns {IBalanceSheetMeta}
*/
public async meta(query: IAgingSummaryQuery): Promise<IAgingSummaryMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
const formattedDateRange = `As ${formattedAsDate}`;
return {
...commonMeta,
sheetName: 'A/P Aging Summary',
formattedAsDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,219 @@
import { I18nService } from 'nestjs-i18n';
import * as R from 'ramda';
import {
IAgingPeriod,
IAgingSummaryContact,
IAgingSummaryData,
IAgingSummaryQuery,
IAgingSummaryTotal,
} from './AgingSummary.types';
import { AgingReport } from './AgingReport';
import { AgingSummaryRowType } from './_constants';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialTable } from '../../common/FinancialTable';
import {
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
export abstract class AgingSummaryTable extends R.pipe(
FinancialSheetStructure,
FinancialTable,
)(AgingReport) {
readonly report: IAgingSummaryData;
readonly query: IAgingSummaryQuery;
readonly agingPeriods: IAgingPeriod[];
readonly i18n: I18nService;
/**
* Constructor method.
* @param {IARAgingSummaryData} data - Aging summary data.
* @param {IAgingSummaryQuery} query - Aging summary query.
* @param {I18nService} i18n - Internationalization service.
*/
constructor(
data: IAgingSummaryData,
query: IAgingSummaryQuery,
i18n: I18nService,
) {
super();
this.report = data;
this.i18n = i18n;
this.query = query;
this.agingPeriods = this.agingRangePeriods(
this.query.asDate,
this.query.agingDaysBefore,
this.query.agingPeriods,
);
}
// -------------------------
// # Accessors.
// -------------------------
/**
* Aging accessors of contact and total nodes.
* @param {IAgingSummaryContact | IAgingSummaryTotal} node
* @returns {ITableColumnAccessor[]}
*/
protected agingNodeAccessors = (
node: IAgingSummaryContact | IAgingSummaryTotal,
): ITableColumnAccessor[] => {
return node.aging.map((aging, index) => ({
key: 'aging_period',
accessor: `aging[${index}].total.formattedAmount`,
}));
};
/**
* Contact name node accessor.
* @returns {ITableColumnAccessor}
*/
protected get contactNameNodeAccessor(): ITableColumnAccessor {
return { key: 'customer_name', accessor: 'customerName' };
}
/**
* Retrieves the common columns for all report nodes.
* @param {IAgingSummaryContact}
* @returns {ITableColumnAccessor[]}
*/
protected contactNodeAccessors = (
node: IAgingSummaryContact,
): ITableColumnAccessor[] => {
return R.compose(
R.concat([
this.contactNameNodeAccessor,
{ key: 'current', accessor: 'current.formattedAmount' },
...this.agingNodeAccessors(node),
{ key: 'total', accessor: 'total.formattedAmount' },
]),
)([]);
};
/**
* Retrieves the contact name table row.
* @param {IAgingSummaryContact} node -
* @return {ITableRow}
*/
protected contactNameNode = (node: IAgingSummaryContact): ITableRow => {
const columns = this.contactNodeAccessors(node);
const meta = {
rowTypes: [AgingSummaryRowType.Contact],
};
return tableRowMapper(node, columns, meta);
};
/**
* Maps the customers nodes to table rows.
* @param {IAgingSummaryContact[]} nodes
* @returns {ITableRow[]}
*/
protected contactsNodes = (nodes: IAgingSummaryContact[]): ITableRow[] => {
return nodes.map(this.contactNameNode);
};
/**
* Retrieves the common columns for all report nodes.
* @param {IAgingSummaryTotal}
* @returns {ITableColumnAccessor[]}
*/
protected totalNodeAccessors = (
node: IAgingSummaryTotal,
): ITableColumnAccessor[] => {
// @ts-ignore
return R.compose(
R.concat([
{ key: 'blank', value: '' },
{ key: 'current', accessor: 'current.formattedAmount' },
...this.agingNodeAccessors(node),
{ key: 'total', accessor: 'total.formattedAmount' },
]),
)([]);
};
/**
* Retrieves the total row of the given report total node.
* @param {IAgingSummaryTotal} node
* @returns {ITableRow}
*/
protected totalNode = (node: IAgingSummaryTotal): ITableRow => {
const columns = this.totalNodeAccessors(node);
const meta = {
rowTypes: [AgingSummaryRowType.Total],
};
return tableRowMapper(node, columns, meta);
};
// -------------------------
// # Computed Rows.
// -------------------------
/**
* Retrieves the contacts table rows.
* @returns {ITableRow[]}
*/
protected get contactsRows(): ITableRow[] {
return [];
}
/**
* Table total row.
* @returns {ITableRow}
*/
protected get totalRow(): ITableRow {
return this.totalNode(this.report.total);
}
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.compose(
R.unless(R.isEmpty, R.append(this.totalRow)),
R.concat(this.contactsRows),
)([]);
};
// -------------------------
// # Columns.
// -------------------------
/**
* Retrieves the aging table columns.
* @returns {ITableColumn[]}
*/
protected agingTableColumns = (): ITableColumn[] => {
return this.agingPeriods.map((agingPeriod) => {
return {
label: `${agingPeriod.beforeDays} - ${
agingPeriod.toDays || 'And Over'
}`,
key: 'aging_period',
};
});
};
/**
* Retrieves the contact name table column.
* @returns {ITableColumn}
*/
protected contactNameTableColumn = (): ITableColumn => {
return { label: 'Customer name', key: 'customer_name' };
};
/**
* Retrieves the report columns.
* @returns {ITableColumn}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(this.tableColumnsCellIndexing)([
this.contactNameTableColumn(),
{ label: 'Current', key: 'current' },
...this.agingTableColumns(),
{ label: 'Total', key: 'total' },
]);
};
}

View File

@@ -0,0 +1,21 @@
export enum AgingSummaryRowType {
Contact = 'contact',
Total = 'total',
}
export const HtmlTableCss = `
table tr.row-type--total td{
font-weight: 600;
border-top: 1px solid #bbb;
border-bottom: 3px double #333;
}
table .column--current,
table .column--aging_period,
table .column--total,
table .cell--current,
table .cell--aging_period,
table .cell--total {
text-align: right;
}
`;

View File

@@ -0,0 +1,54 @@
import { Controller, Headers, Query, Res } from '@nestjs/common';
import { InventortyDetailsApplication } from './InventoryItemDetailsApplication';
import { IInventoryDetailsQuery } from './InventoryItemDetails.types';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
@Controller('reports/inventory-item-details')
export class InventoryItemDetailsController {
constructor(
private readonly inventoryItemDetailsApp: InventortyDetailsApplication,
) {}
async inventoryItemDetails(
@Query() query: IInventoryDetailsQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.inventoryItemDetailsApp.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.inventoryItemDetailsApp.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 table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.inventoryItemDetailsApp.table(query);
return res.status(200).send(table);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const buffer = await this.inventoryItemDetailsApp.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': buffer.length,
});
return res.send(buffer);
} else {
const sheet = await this.inventoryItemDetailsApp.sheet(query);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { InventoryItemDetailsController } from './InventoryItemDetails.controller';
import { InventoryDetailsTablePdf } from './InventoryItemDetailsTablePdf';
import { InventoryDetailsService } from './InventoryItemDetailsService';
import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable';
import { InventoryItemDetailsExportInjectable } from './InventoryItemDetailsExportInjectable';
import { InventoryItemDetailsApplication } from './InventoryItemDetailsApplication';
@Module({
providers: [
InventoryItemDetailsApplication,
InventoryItemDetailsExportInjectable,
InventoryDetailsTableInjectable,
InventoryDetailsService,
InventoryDetailsTablePdf,
],
controllers: [InventoryItemDetailsController],
})
export class InventoryItemDetailsModule {}

View File

@@ -0,0 +1,428 @@
import * as R from 'ramda';
import { defaultTo, sumBy, get } from 'lodash';
import moment from 'moment';
import {
IInventoryDetailsQuery,
IInventoryDetailsNumber,
IInventoryDetailsDate,
IInventoryDetailsData,
IInventoryDetailsItem,
IInventoryDetailsClosing,
IInventoryDetailsOpening,
IInventoryDetailsItemTransaction,
} from './InventoryItemDetails.types';
import FinancialSheet from '../FinancialSheet';
import { transformToMapBy, transformToMapKeyValue } from 'utils';
import { filterDeep } from 'utils/deepdash';
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
enum INodeTypes {
ITEM = 'item',
TRANSACTION = 'transaction',
OPENING_ENTRY = 'OPENING_ENTRY',
CLOSING_ENTRY = 'CLOSING_ENTRY',
}
export class InventoryDetails extends FinancialSheet {
readonly inventoryTransactionsByItemId: Map<number, IInventoryTransaction[]>;
readonly openingBalanceTransactions: Map<number, IInventoryTransaction>;
readonly query: IInventoryDetailsQuery;
readonly numberFormat: INumberFormatQuery;
readonly baseCurrency: string;
readonly items: IItem[];
/**
* Constructor method.
* @param {IItem[]} items - Items.
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions.
* @param {IInventoryDetailsQuery} query - Report query.
* @param {string} baseCurrency - The base currency.
*/
constructor(
items: IItem[],
openingBalanceTransactions: IInventoryTransaction[],
inventoryTransactions: IInventoryTransaction[],
query: IInventoryDetailsQuery,
baseCurrency: string,
i18n: any
) {
super();
this.inventoryTransactionsByItemId = transformToMapBy(
inventoryTransactions,
'itemId'
);
this.openingBalanceTransactions = transformToMapKeyValue(
openingBalanceTransactions,
'itemId'
);
this.query = query;
this.numberFormat = this.query.numberFormat;
this.items = items;
this.baseCurrency = baseCurrency;
this.i18n = i18n;
}
/**
* Retrieve the number meta.
* @param {number} number
* @returns
*/
private getNumberMeta(
number: number,
settings?: IFormatNumberSettings
): IInventoryDetailsNumber {
return {
formattedNumber: this.formatNumber(number, {
excerptZero: true,
money: false,
...settings,
}),
number: number,
};
}
/**
* Retrieve the total number meta.
* @param {number} number -
* @param {IFormatNumberSettings} settings -
* @retrun {IInventoryDetailsNumber}
*/
private getTotalNumberMeta(
number: number,
settings?: IFormatNumberSettings
): IInventoryDetailsNumber {
return this.getNumberMeta(number, { excerptZero: false, ...settings });
}
/**
* Retrieve the date meta.
* @param {Date|string} date
* @returns {IInventoryDetailsDate}
*/
private getDateMeta(date: Date | string): IInventoryDetailsDate {
return {
formattedDate: moment(date).format('YYYY-MM-DD'),
date: moment(date).toDate(),
};
}
/**
* Adjusts the movement amount.
* @param {number} amount
* @param {TInventoryTransactionDirection} direction
* @returns {number}
*/
private adjustAmountMovement = R.curry(
(direction: TInventoryTransactionDirection, amount: number): number => {
return direction === 'OUT' ? amount * -1 : amount;
}
);
/**
* Accumulate and mapping running quantity on transactions.
* @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction[]}
*/
private mapAccumTransactionsRunningQuantity(
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0);
const mapAccumAppender = (a, b) => {
const total = a.runningQuantity.number + b.quantityMovement.number;
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
const accum = { ...b, runningQuantity: totalMeta };
return [accum, accum];
};
return R.mapAccum(
mapAccumAppender,
{ runningQuantity: initial },
transactions
)[1];
}
/**
* Accumulate and mapping running valuation on transactions.
* @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction}
*/
private mapAccumTransactionsRunningValuation(
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0);
const mapAccumAppender = (a, b) => {
const adjustment = b.direction === 'OUT' ? -1 : 1;
const total = a.runningValuation.number + b.cost.number * adjustment;
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
const accum = { ...b, runningValuation: totalMeta };
return [accum, accum];
};
return R.mapAccum(
mapAccumAppender,
{ runningValuation: initial },
transactions
)[1];
}
/**
* Retrieve the inventory transaction total.
* @param {IInventoryTransaction} transaction
* @returns {number}
*/
private getTransactionTotal = (transaction: IInventoryTransaction) => {
return transaction.quantity
? transaction.quantity * transaction.rate
: transaction.rate;
};
/**
* Mappes the item transaction to inventory item transaction node.
* @param {IItem} item
* @param {IInvetoryTransaction} transaction
* @returns {IInventoryDetailsItemTransaction}
*/
private itemTransactionMapper(
item: IItem,
transaction: IInventoryTransaction
): IInventoryDetailsItemTransaction {
const total = this.getTransactionTotal(transaction);
const amountMovement = this.adjustAmountMovement(transaction.direction);
// Quantity movement.
const quantityMovement = amountMovement(transaction.quantity);
const cost = get(transaction, 'costLotAggregated.cost', 0);
// Profit margin.
const profitMargin = total - cost;
// Value from computed cost in `OUT` or from total sell price in `IN` transaction.
const value = transaction.direction === 'OUT' ? cost : total;
// Value movement depends on transaction direction.
const valueMovement = amountMovement(value);
return {
nodeType: INodeTypes.TRANSACTION,
date: this.getDateMeta(transaction.date),
transactionType: this.i18n.__(transaction.transcationTypeFormatted),
transactionNumber: transaction?.meta?.transactionNumber,
direction: transaction.direction,
quantityMovement: this.getNumberMeta(quantityMovement),
valueMovement: this.getNumberMeta(valueMovement),
quantity: this.getNumberMeta(transaction.quantity),
total: this.getNumberMeta(total),
rate: this.getNumberMeta(transaction.rate),
cost: this.getNumberMeta(cost),
value: this.getNumberMeta(value),
profitMargin: this.getNumberMeta(profitMargin),
runningQuantity: this.getNumberMeta(0),
runningValuation: this.getNumberMeta(0),
};
}
/**
* Retrieve the inventory transcations by item id.
* @param {number} itemId
* @returns {IInventoryTransaction[]}
*/
private getInventoryTransactionsByItemId(
itemId: number
): IInventoryTransaction[] {
return defaultTo(this.inventoryTransactionsByItemId.get(itemId + ''), []);
}
/**
* Retrieve the item transaction node by the given item.
* @param {IItem} item
* @returns {IInventoryDetailsItemTransaction[]}
*/
private getItemTransactions(item: IItem): IInventoryDetailsItemTransaction[] {
const transactions = this.getInventoryTransactionsByItemId(item.id);
return R.compose(
this.mapAccumTransactionsRunningQuantity.bind(this),
this.mapAccumTransactionsRunningValuation.bind(this),
R.map(R.curry(this.itemTransactionMapper.bind(this))(item))
)(transactions);
}
/**
* Mappes the given item transactions.
* @param {IItem} item -
* @returns {(
* IInventoryDetailsItemTransaction
* | IInventoryDetailsOpening
* | IInventoryDetailsClosing
* )[]}
*/
private itemTransactionsMapper(
item: IItem
): (
| IInventoryDetailsItemTransaction
| IInventoryDetailsOpening
| IInventoryDetailsClosing
)[] {
const transactions = this.getItemTransactions(item);
const openingValuation = this.getItemOpeingValuation(item);
const closingValuation = this.getItemClosingValuation(
item,
transactions,
openingValuation
);
const hasTransactions = transactions.length > 0;
const isItemHasOpeningBalance = this.isItemHasOpeningBalance(item.id);
return R.pipe(
R.concat(transactions),
R.when(R.always(isItemHasOpeningBalance), R.prepend(openingValuation)),
R.when(R.always(hasTransactions), R.append(closingValuation))
)([]);
}
/**
* Detarmines the given item has opening balance transaction.
* @param {number} itemId - Item id.
* @return {boolean}
*/
private isItemHasOpeningBalance(itemId: number): boolean {
return !!this.openingBalanceTransactions.get(itemId);
}
/**
* Retrieve the given item opening valuation.
* @param {IItem} item -
* @returns {IInventoryDetailsOpening}
*/
private getItemOpeingValuation(item: IItem): IInventoryDetailsOpening {
const openingBalance = this.openingBalanceTransactions.get(item.id);
const quantity = defaultTo(get(openingBalance, 'quantity'), 0);
const value = defaultTo(get(openingBalance, 'value'), 0);
return {
nodeType: INodeTypes.OPENING_ENTRY,
date: this.getDateMeta(this.query.fromDate),
quantity: this.getTotalNumberMeta(quantity),
value: this.getTotalNumberMeta(value),
};
}
/**
* Retrieve the given item closing valuation.
* @param {IItem} item -
* @returns {IInventoryDetailsOpening}
*/
private getItemClosingValuation(
item: IItem,
transactions: IInventoryDetailsItemTransaction[],
openingValuation: IInventoryDetailsOpening
): IInventoryDetailsOpening {
const value = sumBy(transactions, 'valueMovement.number');
const quantity = sumBy(transactions, 'quantityMovement.number');
const profitMargin = sumBy(transactions, 'profitMargin.number');
const closingQuantity = quantity + openingValuation.quantity.number;
const closingValue = value + openingValuation.value.number;
return {
nodeType: INodeTypes.CLOSING_ENTRY,
date: this.getDateMeta(this.query.toDate),
quantity: this.getTotalNumberMeta(closingQuantity),
value: this.getTotalNumberMeta(closingValue),
profitMargin: this.getTotalNumberMeta(profitMargin),
};
}
/**
* Retrieve the item node of the report.
* @param {IItem} item
* @returns {IInventoryDetailsItem}
*/
private itemsNodeMapper(item: IItem): IInventoryDetailsItem {
return {
id: item.id,
name: item.name,
code: item.code,
nodeType: INodeTypes.ITEM,
children: this.itemTransactionsMapper(item),
};
}
/**
* Detarmines the given node equals the given type.
* @param {string} nodeType
* @param {IItem} node
* @returns {boolean}
*/
private isNodeTypeEquals(
nodeType: string,
node: IInventoryDetailsItem
): boolean {
return nodeType === node.nodeType;
}
/**
* Detarmines whether the given item node has transactions.
* @param {IInventoryDetailsItem} item
* @returns {boolean}
*/
private isItemNodeHasTransactions(item: IInventoryDetailsItem) {
return !!this.inventoryTransactionsByItemId.get(item.id);
}
/**
* Detarmines the filter
* @param {IInventoryDetailsItem} item
* @return {boolean}
*/
private isFilterNode(item: IInventoryDetailsItem): boolean {
return R.ifElse(
R.curry(this.isNodeTypeEquals)(INodeTypes.ITEM),
this.isItemNodeHasTransactions.bind(this),
R.always(true)
)(item);
}
/**
* Filters items nodes.
* @param {IInventoryDetailsItem[]} items -
* @returns {IInventoryDetailsItem[]}
*/
private filterItemsNodes(items: IInventoryDetailsItem[]) {
const filtered = filterDeep(
items,
this.isFilterNode.bind(this),
MAP_CONFIG
);
return defaultTo(filtered, []);
}
/**
* Retrieve the items nodes of the report.
* @param {IItem} items
* @returns {IInventoryDetailsItem[]}
*/
private itemsNodes(items: IItem[]): IInventoryDetailsItem[] {
return R.compose(
this.filterItemsNodes.bind(this),
R.map(this.itemsNodeMapper.bind(this))
)(items);
}
/**
* Retrieve the inventory item details report data.
* @returns {IInventoryDetailsData}
*/
public reportData(): IInventoryDetailsData {
return this.itemsNodes(this.items);
}
}

View File

@@ -0,0 +1,101 @@
import {
IFinancialSheetCommonMeta,
INumberFormatQuery,
} from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
export interface IInventoryDetailsQuery {
fromDate: Date | string;
toDate: Date | string;
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
itemsIds: number[];
warehousesIds?: number[];
branchesIds?: number[];
}
export interface IInventoryDetailsNumber {
number: number;
formattedNumber: string;
}
export interface IInventoryDetailsMoney {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IInventoryDetailsDate {
date: Date;
formattedDate: string;
}
export interface IInventoryDetailsOpening {
nodeType: 'OPENING_ENTRY';
date: IInventoryDetailsDate;
quantity: IInventoryDetailsNumber;
value: IInventoryDetailsNumber;
}
export interface IInventoryDetailsClosing
extends Omit<IInventoryDetailsOpening, 'nodeType'> {
nodeType: 'CLOSING_ENTRY';
}
export interface IInventoryDetailsItem {
id: number;
nodeType: string;
name: string;
code: string;
children: (
| IInventoryDetailsItemTransaction
| IInventoryDetailsOpening
| IInventoryDetailsClosing
)[];
}
export interface IInventoryDetailsItemTransaction {
nodeType: string;
date: IInventoryDetailsDate;
transactionType: string;
transactionNumber?: string;
quantityMovement: IInventoryDetailsNumber;
valueMovement: IInventoryDetailsNumber;
quantity: IInventoryDetailsNumber;
total: IInventoryDetailsNumber;
cost: IInventoryDetailsNumber;
value: IInventoryDetailsNumber;
profitMargin: IInventoryDetailsNumber;
rate: IInventoryDetailsNumber;
runningQuantity: IInventoryDetailsNumber;
runningValuation: IInventoryDetailsNumber;
direction: string;
}
export type IInventoryDetailsNode =
| IInventoryDetailsItem
| IInventoryDetailsItemTransaction;
export type IInventoryDetailsData = IInventoryDetailsItem[];
export interface IInventoryItemDetailMeta extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDay: string;
formattedDateRange: string;
}
export interface IInvetoryItemDetailDOO {
data: IInventoryDetailsData;
query: IInventoryDetailsQuery;
meta: IInventoryItemDetailMeta;
}
export interface IInvetoryItemDetailsTable extends IFinancialTable {
query: IInventoryDetailsQuery;
meta: IInventoryItemDetailMeta;
}

View File

@@ -0,0 +1,67 @@
import {
IInventoryDetailsQuery,
IInvetoryItemDetailsTable,
} from '@/interfaces';
import { InventoryItemDetailsExportInjectable } from './InventoryItemDetailsExportInjectable';
import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable';
import { InventoryDetailsService } from './InventoryItemDetailsService';
import { InventoryDetailsTablePdf } from './InventoryItemDetailsTablePdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class InventoryItemDetailsApplication {
constructor(
private readonly inventoryDetailsExport: InventoryItemDetailsExportInjectable,
private readonly inventoryDetailsTable: InventoryDetailsTableInjectable,
private readonly inventoryDetails: InventoryDetailsService,
private readonly inventoryDetailsPdf: InventoryDetailsTablePdf,
) {}
/**
* Retrieves the inventory details report in sheet format.
* @param {number} tenantId
* @param {IInventoryDetailsQuery} query
* @returns {Promise<IInvetoryItemDetailDOO>}
*/
public sheet(query: IInventoryDetailsQuery) {
return this.inventoryDetails.inventoryDetails(query);
}
/**
* Retrieve the inventory details report in table format.
* @param {IInventoryDetailsQuery} query - Inventory details query.
* @returns
*/
public table(
query: IInventoryDetailsQuery,
): Promise<IInvetoryItemDetailsTable> {
return this.inventoryDetailsTable.table(query);
}
/**
* Retrieves the inventory details report in XLSX format.
* @param {IInventoryDetailsQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: IInventoryDetailsQuery): Promise<Buffer> {
return this.inventoryDetailsExport.xlsx(query);
}
/**
* Retrieves the inventory details report in CSV format.
* @param {IInventoryDetailsQuery} query
* @returns {Promise<string>}
*/
public csv(query: IInventoryDetailsQuery): Promise<string> {
return this.inventoryDetailsExport.csv(query);
}
/**
* Retrieves the inventory details report in PDF format.
* @param {IInventoryDetailsQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: IInventoryDetailsQuery) {
return this.inventoryDetailsPdf.pdf(query);
}
}

View File

@@ -0,0 +1,39 @@
import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable';
import { Injectable } from '@nestjs/common';
import { IInventoryDetailsQuery } from './InventoryItemDetails.types';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class InventoryItemDetailsExportInjectable {
constructor(
private readonly inventoryDetailsTable: InventoryDetailsTableInjectable,
) {}
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {IInventoryDetailsQuery} query - Inventory details query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: IInventoryDetailsQuery) {
const table = await this.inventoryDetailsTable.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 {IInventoryDetailsQuery} query - Inventory details query.
* @returns {Promise<Buffer>}
*/
public async csv(query: IInventoryDetailsQuery): Promise<string> {
const table = await this.inventoryDetailsTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import * as moment from 'moment';
import {
IInventoryDetailsQuery,
IInventoryItemDetailMeta,
} from './InventoryItemDetails.types';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class InventoryDetailsMetaInjectable {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieve the inventoy details meta.
* @returns {IInventoryItemDetailMeta}
*/
public async meta(
query: IInventoryDetailsQuery,
): Promise<IInventoryItemDetailMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedToDay = moment(query.toDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDay}`;
const sheetName = 'Inventory Item Details';
return {
...commonMeta,
sheetName,
formattedFromDate,
formattedToDay,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,116 @@
import { ModelObject, raw } from 'objection';
import { isEmpty } from 'lodash';
import moment from 'moment';
import { IInventoryDetailsQuery } from './InventoryItemDetails.types';
import { Item } from '@/modules/Items/models/Item';
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class InventoryItemDetailsRepository {
/**
* Constructor method.
* @param {typeof Item} itemModel - Item model.
* @param {typeof InventoryTransaction} inventoryTransactionModel - Inventory transaction model.
*/
constructor(
private readonly itemModel: typeof Item,
private readonly inventoryTransactionModel: typeof InventoryTransaction,
) {}
/**
* Retrieve inventory items.
* @returns {Promise<ModelObject<Item>>}
*/
public async getInventoryItems(
itemsIds?: number[],
): Promise<ModelObject<Item>[]> {
return this.itemModel.query().onBuild((q) => {
q.where('type', 'inventory');
if (!isEmpty(itemsIds)) {
q.whereIn('id', itemsIds);
}
});
}
/**
* Retrieve the items opening balance transactions.
* @param {IInventoryDetailsQuery}
* @return {Promise<ModelObject<InventoryTransaction>>}
*/
public async openingBalanceTransactions(
filter: IInventoryDetailsQuery,
): Promise<ModelObject<InventoryTransaction>[]> {
const openingBalanceDate = moment(filter.fromDate)
.subtract(1, 'days')
.toDate();
// Opening inventory transactions.
const openingTransactions = this.inventoryTransactionModel
.query()
.select('*')
.select(raw("IF(`DIRECTION` = 'IN', `QUANTITY`, 0) as 'QUANTITY_IN'"))
.select(raw("IF(`DIRECTION` = 'OUT', `QUANTITY`, 0) as 'QUANTITY_OUT'"))
.select(
raw(
"IF(`DIRECTION` = 'IN', IF(`QUANTITY` IS NULL, `RATE`, `QUANTITY` * `RATE`), 0) as 'VALUE_IN'",
),
)
.select(
raw(
"IF(`DIRECTION` = 'OUT', IF(`QUANTITY` IS NULL, `RATE`, `QUANTITY` * `RATE`), 0) as 'VALUE_OUT'",
),
)
.modify('filterDateRange', null, openingBalanceDate)
.orderBy('date', 'ASC')
.as('inventory_transactions');
if (!isEmpty(filter.warehousesIds)) {
openingTransactions.modify('filterByWarehouses', filter.warehousesIds);
}
if (!isEmpty(filter.branchesIds)) {
openingTransactions.modify('filterByBranches', filter.branchesIds);
}
const openingBalanceTransactions = await this.inventoryTransactionModel
.query()
.from(openingTransactions)
.select('itemId')
.select(raw('SUM(`QUANTITY_IN` - `QUANTITY_OUT`) AS `QUANTITY`'))
.select(raw('SUM(`VALUE_IN` - `VALUE_OUT`) AS `VALUE`'))
.groupBy('itemId')
.sum('rate as rate')
.sum('quantityIn as quantityIn')
.sum('quantityOut as quantityOut')
.sum('valueIn as valueIn')
.sum('valueOut as valueOut')
.withGraphFetched('itemCostAggregated');
return openingBalanceTransactions;
}
/**
* Retrieve the items inventory tranasactions.
* @param {IInventoryDetailsQuery}
* @return {Promise<IInventoryTransaction>}
*/
public async itemInventoryTransactions(
filter: IInventoryDetailsQuery,
): Promise<ModelObject<InventoryTransaction>[]> {
const inventoryTransactions = this.inventoryTransactionModel
.query()
.modify('filterDateRange', filter.fromDate, filter.toDate)
.orderBy('date', 'ASC')
.withGraphFetched('meta')
.withGraphFetched('costLotAggregated');
if (!isEmpty(filter.branchesIds)) {
inventoryTransactions.modify('filterByBranches', filter.branchesIds);
}
if (!isEmpty(filter.warehousesIds)) {
inventoryTransactions.modify('filterByWarehouses', filter.warehousesIds);
}
return inventoryTransactions;
}
}

View File

@@ -0,0 +1,94 @@
import moment from 'moment';
import { Service, Inject } from 'typedi';
import { IInventoryDetailsQuery, IInvetoryItemDetailDOO } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import { InventoryDetails } from './InventoryItemDetails';
import FinancialSheet from '../FinancialSheet';
import InventoryDetailsRepository from './InventoryItemDetailsRepository';
import { Tenant } from '@/system/models';
import { InventoryDetailsMetaInjectable } from './InventoryItemDetailsMeta';
@Service()
export class InventoryDetailsService extends FinancialSheet {
@Inject()
private tenancy: TenancyService;
@Inject()
private reportRepo: InventoryDetailsRepository;
@Inject()
private inventoryDetailsMeta: InventoryDetailsMetaInjectable;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
private get defaultQuery(): IInventoryDetailsQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
itemsIds: [],
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneTransactions: false,
branchesIds: [],
warehousesIds: [],
};
}
/**
* Retrieve the inventory details report data.
* @param {number} tenantId -
* @param {IInventoryDetailsQuery} query -
* @return {Promise<IInvetoryItemDetailDOO>}
*/
public async inventoryDetails(
tenantId: number,
query: IInventoryDetailsQuery
): Promise<IInvetoryItemDetailDOO> {
const i18n = this.tenancy.i18n(tenantId);
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const filter = {
...this.defaultQuery,
...query,
};
// Retrieves the items.
const items = await this.reportRepo.getInventoryItems(
tenantId,
filter.itemsIds
);
// Opening balance transactions.
const openingBalanceTransactions =
await this.reportRepo.openingBalanceTransactions(tenantId, filter);
// Retrieves the inventory transaction.
const inventoryTransactions =
await this.reportRepo.itemInventoryTransactions(tenantId, filter);
// Inventory details report mapper.
const inventoryDetailsInstance = new InventoryDetails(
items,
openingBalanceTransactions,
inventoryTransactions,
filter,
tenant.metadata.baseCurrency,
i18n
);
const meta = await this.inventoryDetailsMeta.meta(tenantId, query);
return {
data: inventoryDetailsInstance.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,198 @@
import * as R from 'ramda';
import {
IInventoryDetailsItem,
IInventoryDetailsItemTransaction,
IInventoryDetailsClosing,
IInventoryDetailsNode,
IInventoryDetailsOpening,
} from './InventoryItemDetails.types';
import { I18nService } from 'nestjs-i18n';
import { IInventoryDetailsData } from './InventoryItemDetails.types';
import { tableRowMapper } from '../../utils/Table.utils';
import { ITableColumn, ITableRow } from '../../types/Table.types';
import mapValuesDeep from 'deepdash/es/mapValuesDeep';
enum IROW_TYPE {
ITEM = 'ITEM',
TRANSACTION = 'TRANSACTION',
CLOSING_ENTRY = 'CLOSING_ENTRY',
OPENING_ENTRY = 'OPENING_ENTRY',
}
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
export class InventoryItemDetailsTable {
i18n: I18nService;
report: any;
/**
* Constructor method.
* @param {ICashFlowStatement} report - Report statement.
*/
constructor(reportStatement: IInventoryDetailsData, i18n: I18nService) {
this.report = reportStatement;
this.i18n = i18n;
}
/**
* Mappes the item node to table rows.
* @param {IInventoryDetailsItem} item
* @returns {ITableRow}
*/
private itemNodeMapper = (item: IInventoryDetailsItem) => {
const columns = [{ key: 'item_name', accessor: 'name' }];
return tableRowMapper(item, columns, {
rowTypes: [IROW_TYPE.ITEM],
});
};
/**
* Mappes the item inventory transaction to table row.
* @param {IInventoryDetailsItemTransaction} transaction
* @returns {ITableRow}
*/
private itemTransactionNodeMapper = (
transaction: IInventoryDetailsItemTransaction
) => {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'transaction_type', accessor: 'transactionType' },
{ key: 'transaction_id', accessor: 'transactionNumber' },
{
key: 'quantity_movement',
accessor: 'quantityMovement.formattedNumber',
},
{ key: 'rate', accessor: 'rate.formattedNumber' },
{ key: 'total', accessor: 'total.formattedNumber' },
{ key: 'value', accessor: 'valueMovement.formattedNumber' },
{ key: 'profit_margin', accessor: 'profitMargin.formattedNumber' },
{ key: 'running_quantity', accessor: 'runningQuantity.formattedNumber' },
{
key: 'running_valuation',
accessor: 'runningValuation.formattedNumber',
},
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.TRANSACTION],
});
};
/**
* Opening balance transaction mapper to table row.
* @param {IInventoryDetailsOpening} transaction
* @returns {ITableRow}
*/
private openingNodeMapper = (
transaction: IInventoryDetailsOpening
): ITableRow => {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: this.i18n.t('Opening balance') },
{ key: 'empty' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty' },
{ key: 'empty' },
{ key: 'value', accessor: 'value.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.OPENING_ENTRY],
});
};
/**
* Closing balance transaction mapper to table raw.
* @param {IInventoryDetailsClosing} transaction
* @returns {ITableRow}
*/
private closingNodeMapper = (
transaction: IInventoryDetailsClosing
): ITableRow => {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: this.i18n.t('Closing balance') },
{ key: 'empty' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty' },
{ key: 'empty' },
{ key: 'value', accessor: 'value.formattedNumber' },
{ key: 'profitMargin', accessor: 'profitMargin.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.CLOSING_ENTRY],
});
};
/**
* Detarmines the ginve inventory details node type.
* @param {string} type
* @param {IInventoryDetailsNode} node
* @returns {boolean}
*/
private isNodeTypeEquals = (
type: string,
node: IInventoryDetailsNode
): boolean => {
return node.nodeType === type;
};
/**
* Mappes the given item or transactions node to table rows.
* @param {IInventoryDetailsNode} node -
* @return {ITableRow}
*/
private itemMapper = (node: IInventoryDetailsNode): ITableRow => {
return R.compose(
R.when(
R.curry(this.isNodeTypeEquals)('OPENING_ENTRY'),
this.openingNodeMapper
),
R.when(
R.curry(this.isNodeTypeEquals)('CLOSING_ENTRY'),
this.closingNodeMapper
),
R.when(R.curry(this.isNodeTypeEquals)('item'), this.itemNodeMapper),
R.when(
R.curry(this.isNodeTypeEquals)('transaction'),
this.itemTransactionNodeMapper
)
)(node);
};
/**
* Mappes the items nodes to table rows.
* @param {IInventoryDetailsItem[]} items
* @returns {ITableRow[]}
*/
private itemsMapper = (items: IInventoryDetailsItem[]): ITableRow[] => {
return mapValuesDeep(items, this.itemMapper, MAP_CONFIG);
};
/**
* Retrieve the table rows of the inventory item details.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return this.itemsMapper(this.report.data);
};
/**
* Retrieve the table columns of inventory details report.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return [
{ key: 'date', label: this.i18n.t('Date') },
{ key: 'transaction_type', label: this.i18n.t('Transaction type') },
{ key: 'transaction_id', label: this.i18n.t('Transaction #') },
{ key: 'quantity', label: this.i18n.t('Quantity') },
{ key: 'rate', label: this.i18n.t('Rate') },
{ key: 'total', label: this.i18n.t('Total') },
{ key: 'value', label: this.i18n.t('Value') },
{ key: 'profit_margin', label: this.i18n.t('Profit Margin') },
{ key: 'running_quantity', label: this.i18n.t('Running quantity') },
{ key: 'running_value', label: this.i18n.t('Running Value') },
];
};
}

View File

@@ -0,0 +1,38 @@
import { InventoryDetailsTable } from './InventoryItemDetailsTable';
import {
IInventoryDetailsQuery,
IInvetoryItemDetailsTable,
} from './InventoryItemDetails.types';
import { InventoryDetailsService } from './InventoryItemDetailsService';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class InventoryDetailsTableInjectable {
constructor(
private readonly inventoryDetails: InventoryDetailsService,
private readonly i18n: I18nService,
) {}
/**
* Retrieves the inventory item details in table format.
* @param {IInventoryDetailsQuery} query - Inventory details query.
* @returns {Promise<IInvetoryItemDetailsTable>}
*/
public async table(
query: IInventoryDetailsQuery,
): Promise<IInvetoryItemDetailsTable> {
const inventoryDetails =
await this.inventoryDetails.inventoryDetails(query);
const table = new InventoryDetailsTable(inventoryDetails, this.i18n);
return {
table: {
rows: table.tableRows(),
columns: table.tableColumns(),
},
query: inventoryDetails.query,
meta: inventoryDetails.meta,
};
}
}

View File

@@ -0,0 +1,34 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable';
import { IInventoryDetailsQuery } from './InventoryItemDetails.types';
import { HtmlTableCustomCss } from './constant';
import { Injectable } from '@nestjs/common';
@Injectable()
export class InventoryDetailsTablePdf {
/***
* Constructor method.
* @param {InventoryDetailsTableInjectable} inventoryDetailsTable - Inventory details table injectable.
* @param {TableSheetPdf} tableSheetPdf - Table sheet pdf.
*/
constructor(
private readonly inventoryDetailsTable: InventoryDetailsTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given inventory details sheet table to pdf.
* @param {IBalanceSheetQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: IInventoryDetailsQuery): Promise<Buffer> {
const table = await this.inventoryDetailsTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,7 @@
export const HtmlTableCustomCss = `
table tr.row-type--item td,
table tr.row-type--opening-entry td,
table tr.row-type--closing-entry td{
font-weight: 500;
}
`;

View File

@@ -0,0 +1,67 @@
import { Response } from 'express';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { InventoryValuationSheetApplication } from './InventoryValuationSheetApplication';
import { IInventoryValuationReportQuery } from './InventoryValuationSheet.types';
import { AcceptType } from '@/constants/accept-type';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('reports/inventory-valuation')
@PublicRoute()
@ApiTags('reports')
export class InventoryValuationController {
constructor(
private readonly inventoryValuationApp: InventoryValuationSheetApplication,
) {}
@Get()
@ApiOperation({ summary: 'Retrieves the inventory valuation sheet' })
@ApiResponse({
status: 200,
description: 'The inventory valuation sheet',
})
public async getInventoryValuationSheet(
@Query() query: IInventoryValuationReportQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.inventoryValuationApp.table(query);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.inventoryValuationApp.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xslx buffer format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.inventoryValuationApp.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 pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.inventoryValuationApp.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.status(200).send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.inventoryValuationApp.sheet(query);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { InventoryValuationSheetPdf } from './InventoryValuationSheetPdf';
import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
import { InventoryValuationMetaInjectable } from './InventoryValuationSheetMeta';
import { InventoryValuationController } from './InventoryValuation.controller';
import { InventoryValuationSheetService } from './InventoryValuationSheetService';
import { InventoryValuationSheetApplication } from './InventoryValuationSheetApplication';
@Module({
providers: [
InventoryValuationSheetPdf,
InventoryValuationSheetTableInjectable,
InventoryValuationMetaInjectable,
InventoryValuationSheetService,
InventoryValuationSheetApplication,
],
controllers: [InventoryValuationController],
exports: [InventoryValuationSheetApplication],
})
export class InventoryValuationSheetModule {}

View File

@@ -0,0 +1,265 @@
import { sumBy, get, isEmpty } from 'lodash';
import * as R from 'ramda';
import {
IInventoryValuationReportQuery,
IInventoryValuationItem,
IInventoryValuationStatement,
IInventoryValuationTotal,
} from './InventoryValuationSheet.types';
import { ModelObject } from 'objection';
import { Item } from '@/modules/Items/models/Item';
import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker';
import { FinancialSheet } from '../../common/FinancialSheet';
import { transformToMap } from '@/utils/transform-to-key';
export class InventoryValuationSheet extends FinancialSheet {
readonly query: IInventoryValuationReportQuery;
readonly items: ModelObject<Item>[];
readonly INInventoryCostLots: Map<number, InventoryCostLotTracker>;
readonly OUTInventoryCostLots: Map<number, InventoryCostLotTracker>;
readonly baseCurrency: string;
/**
* Constructor method.
* @param {IInventoryValuationReportQuery} query
* @param {ModelObject<Item>[]} items
* @param {Map<number, InventoryCostLotTracker[]>} INInventoryCostLots
* @param {Map<number, InventoryCostLotTracker[]>} OUTInventoryCostLots
* @param {string} baseCurrency
*/
constructor(
query: IInventoryValuationReportQuery,
items: ModelObject<Item>[],
INInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
OUTInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
baseCurrency: string,
) {
super();
this.query = query;
this.items = items;
this.INInventoryCostLots = transformToMap(INInventoryCostLots, 'itemId');
this.OUTInventoryCostLots = transformToMap(OUTInventoryCostLots, 'itemId');
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat;
}
/**
* Retrieve the item cost and quantity from the given transaction map.
* @param {Map<number, InventoryCostLotTracker[]>} transactionsMap
* @param {number} itemId
* @returns
*/
private getItemTransaction(
transactionsMap: Map<number, InventoryCostLotTracker[]>,
itemId: number,
): { cost: number; quantity: number } {
const meta = transactionsMap.get(itemId);
const cost = get(meta, 'cost', 0);
const quantity = get(meta, 'quantity', 0);
return { cost, quantity };
}
/**
* Retrieve the cost and quantity of the givne item from `IN` transactions.
* @param {number} itemId -
*/
private getItemINTransaction(itemId: number): {
cost: number;
quantity: number;
} {
return this.getItemTransaction(this.INInventoryCostLots, itemId);
}
/**
* Retrieve the cost and quantity of the given item from `OUT` transactions.
* @param {number} itemId -
*/
private getItemOUTTransaction(itemId: number): {
cost: number;
quantity: number;
} {
return this.getItemTransaction(this.OUTInventoryCostLots, itemId);
}
/**
* Retrieve the item closing valuation.
* @param {number} itemId - Item id.
*/
private getItemValuation(itemId: number): number {
const { cost: INValuation } = this.getItemINTransaction(itemId);
const { cost: OUTValuation } = this.getItemOUTTransaction(itemId);
return Math.max(INValuation - OUTValuation, 0);
}
/**
* Retrieve the item closing quantity.
* @param {number} itemId - Item id.
*/
private getItemQuantity(itemId: number): number {
const { quantity: INQuantity } = this.getItemINTransaction(itemId);
const { quantity: OUTQuantity } = this.getItemOUTTransaction(itemId);
return INQuantity - OUTQuantity;
}
/**
* Calculates the item weighted average cost from the given valuation and quantity.
* @param {number} valuation
* @param {number} quantity
* @returns {number}
*/
private calcAverage(valuation: number, quantity: number): number {
return quantity ? valuation / quantity : 0;
}
/**
* Mapping the item model object to inventory valuation item
* @param {IItem} item
* @returns {IInventoryValuationItem}
*/
private itemMapper(item: ModelObject<Item>): IInventoryValuationItem {
const valuation = this.getItemValuation(item.id);
const quantity = this.getItemQuantity(item.id);
const average = this.calcAverage(valuation, quantity);
return {
id: item.id,
name: item.name,
code: item.code,
valuation,
quantity,
average,
valuationFormatted: this.formatNumber(valuation),
quantityFormatted: this.formatNumber(quantity, { money: false }),
averageFormatted: this.formatNumber(average, { money: false }),
currencyCode: this.baseCurrency,
};
}
/**
* Filter none transactions items.
* @param {IInventoryValuationItem} valuationItem -
* @return {boolean}
*/
private filterNoneTransactions = (
valuationItem: IInventoryValuationItem,
): boolean => {
const transactionIN = this.INInventoryCostLots.get(valuationItem.id);
const transactionOUT = this.OUTInventoryCostLots.get(valuationItem.id);
return transactionOUT || transactionIN;
};
/**
* Filter active only items.
* @param {IInventoryValuationItem} valuationItem -
* @returns {boolean}
*/
private filterActiveOnly = (
valuationItem: IInventoryValuationItem,
): boolean => {
return (
valuationItem.average !== 0 ||
valuationItem.quantity !== 0 ||
valuationItem.valuation !== 0
);
};
/**
* Filter none-zero total valuation items.
* @param {IInventoryValuationItem} valuationItem
* @returns {boolean}
*/
private filterNoneZero = (valuationItem: IInventoryValuationItem) => {
return valuationItem.valuation !== 0;
};
/**
* Filters the inventory valuation items based on query.
* @param {IInventoryValuationItem} valuationItem
* @returns {boolean}
*/
private itemFilter = (valuationItem: IInventoryValuationItem): boolean => {
const { noneTransactions, noneZero, onlyActive } = this.query;
// Conditions pair filter detarminer.
const condsPairFilters = [
[noneTransactions, this.filterNoneTransactions],
[noneZero, this.filterNoneZero],
[onlyActive, this.filterActiveOnly],
];
return allPassedConditionsPass(condsPairFilters)(valuationItem);
};
/**
* Mappes the items to inventory valuation items nodes.
* @param {IItem[]} items
* @returns {IInventoryValuationItem[]}
*/
private itemsMapper = (items: IItem[]): IInventoryValuationItem[] => {
return this.items.map(this.itemMapper.bind(this));
};
/**
* Filters the inventory valuation items nodes.
* @param {IInventoryValuationItem[]} nodes -
* @returns {IInventoryValuationItem[]}
*/
private itemsFilter = (
nodes: IInventoryValuationItem[],
): IInventoryValuationItem[] => {
return nodes.filter(this.itemFilter);
};
/**
* Detarmines whether the items post filter is active.
*/
private isItemsPostFilter = (): boolean => {
return isEmpty(this.query.itemsIds);
};
/**
* Retrieve the inventory valuation items.
* @returns {IInventoryValuationItem[]}
*/
private itemsSection(): IInventoryValuationItem[] {
return R.compose(
R.when(this.isItemsPostFilter, this.itemsFilter),
this.itemsMapper,
)(this.items);
}
/**
* Retrieve the inventory valuation total.
* @param {IInventoryValuationItem[]} items
* @returns {IInventoryValuationTotal}
*/
private totalSection(
items: IInventoryValuationItem[],
): IInventoryValuationTotal {
const valuation = sumBy(items, (item) => item.valuation);
const quantity = sumBy(items, (item) => item.quantity);
return {
valuation,
quantity,
valuationFormatted: this.formatTotalNumber(valuation),
quantityFormatted: this.formatTotalNumber(quantity, { money: false }),
};
}
/**
* Retrieve the inventory valuation report data.
* @returns {IInventoryValuationStatement}
*/
public reportData(): IInventoryValuationStatement {
const items = this.itemsSection();
const total = this.totalSection(items);
return { items, total };
}
}

View File

@@ -0,0 +1,61 @@
import {
IFinancialSheetCommonMeta,
INumberFormatQuery,
} from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
export interface IInventoryValuationReportQuery {
asDate: Date | string;
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
noneZero: boolean;
onlyActive: boolean;
itemsIds: number[];
warehousesIds?: number[];
branchesIds?: number[];
}
export interface IInventoryValuationSheetMeta
extends IFinancialSheetCommonMeta {
formattedAsDate: string;
formattedDateRange: string;
}
export interface IInventoryValuationItem {
id: number;
name: string;
code: string;
valuation: number;
quantity: number;
average: number;
valuationFormatted: string;
quantityFormatted: string;
averageFormatted: string;
currencyCode: string;
}
export interface IInventoryValuationTotal {
valuation: number;
quantity: number;
valuationFormatted: string;
quantityFormatted: string;
}
export type IInventoryValuationStatement = {
items: IInventoryValuationItem[];
total: IInventoryValuationTotal;
};
export type IInventoryValuationSheetData = IInventoryValuationStatement;
export interface IInventoryValuationSheet {
data: IInventoryValuationStatement;
meta: IInventoryValuationSheetMeta;
query: IInventoryValuationReportQuery;
}
export interface IInventoryValuationTable extends IFinancialTable {
meta: IInventoryValuationSheetMeta;
query: IInventoryValuationReportQuery;
}

View File

@@ -0,0 +1,73 @@
import {
IInventoryValuationReportQuery,
IInventoryValuationSheet,
IInventoryValuationTable,
} from './InventoryValuationSheet.types';
import { InventoryValuationSheetService } from './InventoryValuationSheetService';
import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
import { InventoryValuationSheetExportable } from './InventoryValuationSheetExportable';
import { InventoryValuationSheetPdf } from './InventoryValuationSheetPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class InventoryValuationSheetApplication {
constructor(
private readonly inventoryValuationSheet: InventoryValuationSheetService,
private readonly inventoryValuationTable: InventoryValuationSheetTableInjectable,
private readonly inventoryValuationExport: InventoryValuationSheetExportable,
private readonly inventoryValuationPdf: InventoryValuationSheetPdf,
) {}
/**
* Retrieves the inventory valuation json format.
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public sheet(
query: IInventoryValuationReportQuery,
): Promise<IInventoryValuationSheet> {
return this.inventoryValuationSheet.inventoryValuationSheet(query);
}
/**
* Retrieves the inventory valuation json table format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<IInventoryValuationTable>}
*/
public table(
query: IInventoryValuationReportQuery,
): Promise<IInventoryValuationTable> {
return this.inventoryValuationTable.table(query);
}
/**
* Retrieves the inventory valuation xlsx format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public xlsx(query: IInventoryValuationReportQuery): Promise<Buffer> {
return this.inventoryValuationExport.xlsx(query);
}
/**
* Retrieves the inventory valuation csv format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public csv(query: IInventoryValuationReportQuery): Promise<string> {
return this.inventoryValuationExport.csv(query);
}
/**
* Retrieves the inventory valuation pdf format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: IInventoryValuationReportQuery): Promise<Buffer> {
return this.inventoryValuationPdf.pdf(query);
}
}

View File

@@ -0,0 +1,39 @@
import { TableSheet } from '../../common/TableSheet';
import { IInventoryValuationReportQuery } from './InventoryValuationSheet.types';
import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class InventoryValuationSheetExportable {
constructor(
private readonly inventoryValuationTable: InventoryValuationSheetTableInjectable,
) {}
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(query: IInventoryValuationReportQuery): Promise<Buffer> {
const table = await this.inventoryValuationTable.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 {IInventoryValuationReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(query: IInventoryValuationReportQuery): Promise<string> {
const table = await this.inventoryValuationTable.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 { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import {
IInventoryValuationSheetMeta,
IInventoryValuationReportQuery,
} from './InventoryValuationSheet.types';
import { Injectable } from '@nestjs/common';
@Injectable()
export class InventoryValuationMetaInjectable {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieve the balance sheet meta.
* @returns {Promise<IInventoryValuationSheetMeta>}
*/
public async meta(
query: IInventoryValuationReportQuery,
): Promise<IInventoryValuationSheetMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
const formattedDateRange = `As ${formattedAsDate}`;
return {
...commonMeta,
sheetName: 'Inventory Valuation Sheet',
formattedAsDate,
formattedDateRange,
};
}
}

View File

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

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class InventoryValuationSheetRepository {
asyncInit() {
const inventoryItems = await Item.query().onBuild((q) => {
q.where('type', 'inventory');
if (filter.itemsIds.length > 0) {
q.whereIn('id', filter.itemsIds);
}
});
const inventoryItemsIds = inventoryItems.map((item) => item.id);
const commonQuery = (builder) => {
builder.whereIn('item_id', inventoryItemsIds);
builder.sum('rate as rate');
builder.sum('quantity as quantity');
builder.sum('cost as cost');
builder.select('itemId');
builder.groupBy('itemId');
if (!isEmpty(query.branchesIds)) {
builder.modify('filterByBranches', query.branchesIds);
}
if (!isEmpty(query.warehousesIds)) {
builder.modify('filterByWarehouses', query.warehousesIds);
}
};
// Retrieve the inventory cost `IN` transactions.
const INTransactions = await InventoryCostLotTracker.query()
.onBuild(commonQuery)
.where('direction', 'IN');
// Retrieve the inventory cost `OUT` transactions.
const OUTTransactions = await InventoryCostLotTracker.query()
.onBuild(commonQuery)
.where('direction', 'OUT');
}
}

View File

@@ -0,0 +1,65 @@
import { Injectable, Inject } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import {
IInventoryValuationReportQuery,
IInventoryValuationSheet,
} from './InventoryValuationSheet.types';
import { InventoryValuationMetaInjectable } from './InventoryValuationSheetMeta';
import { getInventoryValuationDefaultQuery } from './_constants';
import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker';
import { Item } from '@/modules/Items/models/Item';
@Injectable()
export class InventoryValuationSheetService {
constructor(
private readonly inventoryService: InventoryService,
private readonly inventoryValuationMeta: InventoryValuationMetaInjectable,
private readonly eventPublisher: EventEmitter2,
@Inject(Item.name)
private readonly itemModel: typeof Item,
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: typeof InventoryCostLotTracker,
) {}
/**
* Inventory valuation sheet.
* @param {IInventoryValuationReportQuery} query - Valuation query.
*/
public async inventoryValuationSheet(
query: IInventoryValuationReportQuery,
): Promise<IInventoryValuationSheet> {
const filter = {
...getInventoryValuationDefaultQuery(),
...query,
};
const inventoryValuationInstance = new InventoryValuationSheet(
filter,
inventoryItems,
INTransactions,
OUTTransactions,
tenant.metadata.baseCurrency,
);
// Retrieve the inventory valuation report data.
const inventoryValuationData = inventoryValuationInstance.reportData();
// Retrieves the inventorty valuation meta.
const meta = await this.inventoryValuationMeta.meta(filter);
// Triggers `onInventoryValuationViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onInventoryValuationViewed,
{
query,
},
);
return {
data: inventoryValuationData,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,107 @@
import * as R from 'ramda';
import {
IInventoryValuationItem,
IInventoryValuationSheetData,
IInventoryValuationTotal,
} from './InventoryValuationSheet.types';
import { ROW_TYPE } from './_constants';
import { FinancialTable } from '../../common/FinancialTable';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialSheet } from '../../common/FinancialSheet';
import {
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
export class InventoryValuationSheetTable extends R.pipe(
FinancialTable,
FinancialSheetStructure,
)(FinancialSheet) {
private readonly data: IInventoryValuationSheetData;
/**
* Constructor method.
* @param {IInventoryValuationSheetData} data
*/
constructor(data: IInventoryValuationSheetData) {
super();
this.data = data;
}
/**
* Retrieves the common columns accessors.
* @returns {ITableColumnAccessor}
*/
private commonColumnsAccessors(): ITableColumnAccessor[] {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'quantity', accessor: 'quantityFormatted' },
{ key: 'valuation', accessor: 'valuationFormatted' },
{ key: 'average', accessor: 'averageFormatted' },
];
}
/**
* Maps the given total node to table row.
* @param {IInventoryValuationTotal} total
* @returns {ITableRow}
*/
private totalRowMapper = (total: IInventoryValuationTotal): ITableRow => {
const accessors = this.commonColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, accessors, meta);
};
/**
* Maps the given item node to table row.
* @param {IInventoryValuationItem} item
* @returns {ITableRow}
*/
private itemRowMapper = (item: IInventoryValuationItem): ITableRow => {
const accessors = this.commonColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.ITEM],
};
return tableRowMapper(item, accessors, meta);
};
/**
* Maps the given items nodes to table rowes.
* @param {IInventoryValuationItem[]} items
* @returns {ITableRow[]}
*/
private itemsRowsMapper = (items: IInventoryValuationItem[]): ITableRow[] => {
return R.map(this.itemRowMapper)(items);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
const itemsRows = this.itemsRowsMapper(this.data.items);
const totalRow = this.totalRowMapper(this.data.total);
return R.compose(
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow)),
)([...itemsRows]) as ITableRow[];
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = [
{ key: 'item_name', label: 'Item Name' },
{ key: 'quantity', label: 'Quantity' },
{ key: 'valuation', label: 'Valuation' },
{ key: 'average', label: 'Average' },
];
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,34 @@
import { InventoryValuationSheetService } from './InventoryValuationSheetService';
import {
IInventoryValuationReportQuery,
IInventoryValuationTable,
} from './InventoryValuationSheet.types';
import { InventoryValuationSheetTable } from './InventoryValuationSheetTable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class InventoryValuationSheetTableInjectable {
constructor(private readonly sheet: InventoryValuationSheetService) {}
/**
* Retrieves the inventory valuation json table format.
* @param {IInventoryValuationReportQuery} filter -
* @returns {Promise<IInventoryValuationTable>}
*/
public async table(
filter: IInventoryValuationReportQuery,
): Promise<IInventoryValuationTable> {
const { data, query, meta } =
await this.sheet.inventoryValuationSheet(filter);
const table = new InventoryValuationSheetTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query,
meta,
};
}
}

View File

@@ -0,0 +1,32 @@
export enum ROW_TYPE {
ITEM = 'ITEM',
TOTAL = 'TOTAL',
}
export const HtmlTableCustomCss = `
table tr.row-type--total td {
border-top: 1px solid #bbb;
font-weight: 600;
border-bottom: 3px double #000;
}
`;
export const getInventoryValuationDefaultQuery = () => {
return {
asDate: moment().format('YYYY-MM-DD'),
itemsIds: [],
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'always',
negativeFormat: 'mines',
},
noneTransactions: true,
noneZero: false,
onlyActive: false,
warehousesIds: [],
branchesIds: [],
};
};

View File

@@ -11,9 +11,9 @@ import { FinancialSheet } from '../../common/FinancialSheet';
import { ITableColumn, ITableRow } from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
export class SalesByItemsTable extends R.compose(
export class SalesByItemsTable extends R.pipe(
FinancialTable,
FinancialSheetStructure
FinancialSheetStructure,
)(FinancialSheet) {
private readonly data: ISalesByItemsSheetData;

View File

@@ -33,7 +33,10 @@ export class TransactionsByContact extends FinancialSheet {
accountName: account.name,
currencyCode: this.baseCurrency,
transactionNumber: entry.transactionNumber,
transactionType: this.i18n.t(entry.referenceTypeFormatted),
// @ts-ignore
// transactionType: this.i18n.t(entry.referenceTypeFormatted),
transactionType: '',
date: entry.date,
createdAt: entry.createdAt,
};

View File

@@ -13,15 +13,15 @@ export class TransactionsByContactRepository {
*/
public accountsGraph: any;
/**
* Report data.
* @param {Ledger} ledger
*/
public ledger: Ledger;
/**
* Opening balance entries.
* @param {ILedgerEntry[]} openingBalanceEntries
*/
public openingBalanceEntries: ILedgerEntry[];
/**
* Ledger.
* @param {Ledger} ledger
*/
public ledger: Ledger;
}

View File

@@ -0,0 +1,61 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types';
import { TransactionsByCustomerApplication } from './TransactionsByCustomersApplication';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('/reports/transactions-by-customers')
@PublicRoute()
export class TransactionsByCustomerController {
constructor(
private readonly transactionsByCustomersApp: TransactionsByCustomerApplication,
) {}
@Get()
@ApiOperation({ summary: 'Get transactions by customer' })
@ApiResponse({ status: 200, description: 'Transactions by customer' })
async transactionsByCustomer(
@Query() filter: ITransactionsByCustomersFilter,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.transactionsByCustomersApp.table(filter);
return res.status(200).send(table);
// Retrieve the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const csv = await this.transactionsByCustomersApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(csv);
// Retrieve the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.transactionsByCustomersApp.xlsx(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
return res.send(buffer);
// Retrieve the json format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.transactionsByCustomersApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
} else {
const sheet = await this.transactionsByCustomersApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -8,18 +8,21 @@ import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
import { TransactionsByCustomerController } from './TransactionsByCustomer.controller';
import { TransactionsByCustomerApplication } from './TransactionsByCustomersApplication';
@Module({
imports: [FinancialSheetCommonModule, AccountsModule],
providers: [
TransactionsByCustomerApplication,
TransactionsByCustomersRepository,
TransactionsByCustomersTableInjectable,
TransactionsByCustomersExportInjectable,
TransactionsByCustomersSheet,
TransactionsByCustomersPdf,
TransactionsByCustomersMeta,
TenancyContext
TenancyContext,
],
controllers: [],
controllers: [TransactionsByCustomerController],
})
export class TransactionsByCustomerModule {}

View File

@@ -1,4 +1,4 @@
import moment from 'moment';
import * as moment from 'moment';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import {

View File

@@ -1,3 +1,5 @@
import { ModelObject } from 'objection';
import * as moment from 'moment';
import * as R from 'ramda';
import { ACCOUNT_TYPE } from '@/constants/accounts';
import { Account } from '@/modules/Accounts/models/Account.model';
@@ -9,9 +11,9 @@ import { isEmpty, map } from 'lodash';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { Ledger } from '@/modules/Ledger/Ledger';
import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types';
import { ModelObject } from 'objection';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TransactionsByContactRepository } from '../TransactionsByContact/TransactionsByContactRepository';
import { DateInput } from '@/common/types/Date';
@Injectable({ scope: Scope.TRANSIENT })
export class TransactionsByCustomersRepository extends TransactionsByContactRepository {
@@ -50,11 +52,8 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
/**
* Initialize the report data.
* @param {ITransactionsByCustomersFilter} filter
*/
public async asyncInit(filter: ITransactionsByCustomersFilter) {
this.filter = filter;
public async asyncInit() {
await this.initAccountsGraph();
await this.initCustomers();
await this.initOpeningBalanceEntries();
@@ -63,6 +62,14 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
await this.initBaseCurrency();
}
/**
* Set the filter.
* @param {ITransactionsByCustomersFilter} filter
*/
public setFilter(filter: ITransactionsByCustomersFilter) {
this.filter = filter;
}
/**
* Initialize the accounts graph.
*/
@@ -119,6 +126,9 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
this.ledger = new Ledger(journalTransactions);
}
/**
* Initialize the base currency.
*/
async initBaseCurrency() {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = tenantMetadata.baseCurrency;
@@ -140,6 +150,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
openingDate,
customersIds,
);
// @ts-ignore
return R.compose(
R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'debit')),
@@ -154,25 +165,29 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
* @returns {Promise<ILedgerEntry[]>}
*/
private async getCustomersPeriodsEntries(
fromDate: Date | string,
toDate: Date | string,
fromDate: DateInput,
toDate: DateInput,
): Promise<ILedgerEntry[]> {
const transactions =
await this.getCustomersPeriodTransactions(
fromDate,
toDate,
);
return R.compose(
// @ts-ignore
return R.pipe(
R.map(R.assoc('accountNormal', 'debit')),
R.map((trans) => ({
...trans,
referenceTypeFormatted: trans.referenceTypeFormatted,
// @ts-ignore
referenceTypeFormatted: '',
// referenceTypeFormatted: trans.referenceTypeFormatted,
})),
)(transactions);
}
/**
* Retrieve the report customers.
* Retrieves the report customers.
* @param {number[]} customersIds - Customers ids.
* @returns {Promise<ICustomer[]>}
*/
@@ -187,7 +202,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
}
/**
* Retrieve the accounts receivable.
* Retrieves the accounts receivable.
* @returns {Promise<IAccount[]>}
*/
public async getReceivableAccounts(): Promise<Account[]> {
@@ -204,7 +219,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
* @returns {Promise<IAccountTransaction[]>}
*/
public async getCustomersOpeningBalanceTransactions(
openingDate: Date,
openingDate: DateInput,
customersIds?: number[],
): Promise<AccountTransaction[]> {
const receivableAccounts = await this.getReceivableAccounts();
@@ -222,14 +237,14 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
}
/**
* Retrieve the customers periods transactions.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* Retrieves the customers periods transactions.
* @param {DateInput} fromDate - From date.
* @param {DateInput} toDate - To date.
* @return {Promise<IAccountTransaction[]>}
*/
public async getCustomersPeriodTransactions(
fromDate: Date,
toDate: Date,
fromDate: DateInput,
toDate: DateInput,
): Promise<AccountTransaction[]> {
const receivableAccounts = await this.getReceivableAccounts();
const receivableAccountsIds = map(receivableAccounts, 'id');

View File

@@ -1,3 +1,6 @@
import { I18nService } from 'nestjs-i18n';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import {
ITransactionsByCustomersFilter,
ITransactionsByCustomersStatement,
@@ -5,11 +8,8 @@ import {
import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository';
import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
import { getTransactionsByCustomerDefaultQuery } from './utils';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TransactionsByCustomers } from './TransactionsByCustomers';
import { events } from '@/common/events/events';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TransactionsByCustomersSheet {
@@ -17,7 +17,7 @@ export class TransactionsByCustomersSheet {
private readonly transactionsByCustomersMeta: TransactionsByCustomersMeta,
private readonly transactionsByCustomersRepository: TransactionsByCustomersRepository,
private readonly eventPublisher: EventEmitter2,
private readonly tenancyContext: TenancyContext,
private readonly i18n: I18nService,
) {}
/**
@@ -29,13 +29,12 @@ export class TransactionsByCustomersSheet {
public async transactionsByCustomers(
query: ITransactionsByCustomersFilter,
): Promise<ITransactionsByCustomersStatement> {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
const filter = {
...getTransactionsByCustomerDefaultQuery(),
...query,
};
await this.transactionsByCustomersRepository.asyncInit(filter);
this.transactionsByCustomersRepository.setFilter(filter);
await this.transactionsByCustomersRepository.asyncInit();
// Transactions by customers data mapper.
const reportInstance = new TransactionsByCustomers(
@@ -43,15 +42,12 @@ export class TransactionsByCustomersSheet {
this.transactionsByCustomersRepository,
this.i18n,
);
const meta = await this.transactionsByCustomersMeta.meta(filter);
// Triggers `onCustomerTransactionsViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onCustomerTransactionsViewed,
{
query,
},
{ query },
);
return {

View File

@@ -1,4 +1,5 @@
import * as R from 'ramda';
import { I18nService } from 'nestjs-i18n';
import { ITransactionsByCustomersCustomer } from './TransactionsByCustomer.types';
import { ITableRow, ITableColumn } from '../../types/Table.types';
import { TransactionsByContactsTableRows } from '../TransactionsByContact/TransactionsByContactTableRows';
@@ -18,7 +19,10 @@ export class TransactionsByCustomersTable extends TransactionsByContactsTableRow
* Constructor method.
* @param {ITransactionsByCustomersCustomer[]} customersTransactions - Customers transactions.
*/
constructor(customersTransactions: ITransactionsByCustomersCustomer[], i18n) {
constructor(
customersTransactions: ITransactionsByCustomersCustomer[],
i18n: I18nService,
) {
super();
this.customersTransactions = customersTransactions;
this.i18n = i18n;
@@ -29,7 +33,9 @@ export class TransactionsByCustomersTable extends TransactionsByContactsTableRow
* @param {ITransactionsByCustomersCustomer} customer -
* @returns {ITableRow[]}
*/
private customerDetails = (customer: ITransactionsByCustomersCustomer) => {
private customerDetails = (
customer: ITransactionsByCustomersCustomer,
): ITableRow => {
const columns = [
{ key: 'customerName', accessor: 'customerName' },
...R.repeat({ key: 'empty', value: '' }, 5),
@@ -56,22 +62,22 @@ export class TransactionsByCustomersTable extends TransactionsByContactsTableRow
/**
* Retrieve the table rows of the customer section.
* @param {ITransactionsByCustomersCustomer} customer
* @returns {ITableRow[]}
* @param {ITransactionsByCustomersCustomer} customer - Customer object.
* @returns {ITableRow[]} - Table rows.
*/
private customerRowsMapper = (customer: ITransactionsByCustomersCustomer) => {
private customerRowsMapper = (
customer: ITransactionsByCustomersCustomer,
): ITableRow => {
return R.pipe(this.customerDetails)(customer);
};
/**
* Retrieve the table rows of transactions by customers report.
* @param {ITransactionsByCustomersCustomer[]} customers
* @returns {ITableRow[]}
* @param {ITransactionsByCustomersCustomer[]} customers - Customer objects.
* @returns {ITableRow[]} - Table rows.
*/
public tableRows = (): ITableRow[] => {
return R.map(this.customerRowsMapper.bind(this))(
this.customersTransactions,
);
return R.map(this.customerRowsMapper)(this.customersTransactions);
};
/**

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import {
ITransactionsByCustomersFilter,
ITransactionsByCustomersTable,
} from './TransactionsByCustomer.types';
import { TransactionsByCustomersSheet } from './TransactionsByCustomersService';
import { TransactionsByCustomersTable } from './TransactionsByCustomersTable';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class TransactionsByCustomersTableInjectable {
@@ -16,9 +16,8 @@ export class TransactionsByCustomersTableInjectable {
/**
* Retrieves the transactions by customers sheet in table format.
* @param {number} tenantId
* @param {ITransactionsByCustomersFilter} filter
* @returns {Promise<ITransactionsByCustomersFilter>}
* @param {ITransactionsByCustomersFilter} filter - Filter object.
* @returns {Promise<ITransactionsByCustomersFilter>} - Transactions by customers table.
*/
public async table(
filter: ITransactionsByCustomersFilter,

View File

@@ -1,5 +1,4 @@
import * as moment from 'moment';
export const getTransactionsByCustomerDefaultQuery = () => {
return {

View File

@@ -3,12 +3,14 @@ import { TransactionsByReferenceApplication } from './TransactionsByReferenceApp
import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepository';
import { TransactionsByReferenceService } from './TransactionsByReference.service';
import { TransactionsByReferenceController } from './TransactionsByReference.controller';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Module({
providers: [
TransactionsByReferenceRepository,
TransactionsByReferenceApplication,
TransactionsByReferenceService,
TenancyContext
],
controllers: [TransactionsByReferenceController],
})

View File

@@ -1,8 +1,10 @@
import { Controller, Get, Query } from '@nestjs/common';
import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication';
import { ITransactionsByReferenceQuery } from './TransactionsByReference.types';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('reports/transactions-by-reference')
@PublicRoute()
export class TransactionsByReferenceController {
constructor(
private readonly transactionsByReferenceApp: TransactionsByReferenceApplication,

View File

@@ -9,7 +9,14 @@ export interface ITransactionsByReferenceAmount {
currencyCode: string;
}
interface ITransactionsByReferenceDate {
formattedDate: string;
date: Date;
}
export interface ITransactionsByReferenceTransaction {
date: ITransactionsByReferenceDate;
credit: ITransactionsByReferenceAmount;
debit: ITransactionsByReferenceAmount;

View File

@@ -29,7 +29,7 @@ export class TransactionsByReference extends FinancialSheet {
this.transactions = transactions;
this.query = query;
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat;
// this.numberFormat = this.query.numberFormat;
}
/**
@@ -46,7 +46,10 @@ export class TransactionsByReference extends FinancialSheet {
credit: this.getAmountMeta(transaction.credit, { money: false }),
debit: this.getAmountMeta(transaction.debit, { money: false }),
referenceTypeFormatted: transaction.referenceTypeFormatted,
// @ts-ignore
// referenceTypeFormatted: transaction.referenceTypeFormatted,
referenceTypeFormatted: '',
referenceType: transaction.referenceType,
referenceId: transaction.referenceId,

View File

@@ -46,11 +46,11 @@ export class TransactionsByVendor extends TransactionsByContact {
* @param {number} openingBalance - Opening balance amount.
* @returns {ITransactionsByVendorsTransaction[]}
*/
private vendorTransactions(
public vendorTransactions(
vendorId: number,
openingBalance: number,
): ITransactionsByVendorsTransaction[] {
const openingBalanceLedger = this.repository.journal
const openingBalanceLedger = this.repository.ledger
.whereContactId(vendorId)
.whereFromDate(this.filter.fromDate)
.whereToDate(this.filter.toDate);
@@ -68,7 +68,7 @@ export class TransactionsByVendor extends TransactionsByContact {
* @param {IVendor} vendor
* @returns {ITransactionsByVendorsVendor}
*/
private vendorMapper(
public vendorMapper(
vendor: ModelObject<Vendor>,
): ITransactionsByVendorsVendor {
const openingBalance = this.getContactOpeningBalance(vendor.id);
@@ -93,7 +93,7 @@ export class TransactionsByVendor extends TransactionsByContact {
* @param {number} openingBalance
* @returns
*/
private getVendorClosingBalance(
public getVendorClosingBalance(
vendorTransactions: ITransactionsByVendorsTransaction[],
openingBalance: number,
) {
@@ -108,7 +108,7 @@ export class TransactionsByVendor extends TransactionsByContact {
* Detarmines whether the vendors post filter is active.
* @returns {boolean}
*/
private isVendorsPostFilter = (): boolean => {
public isVendorsPostFilter = (): boolean => {
return isEmpty(this.filter.vendorsIds);
};
@@ -117,7 +117,7 @@ export class TransactionsByVendor extends TransactionsByContact {
* @param {IVendor[]} vendors
* @returns {ITransactionsByVendorsVendor[]}
*/
private vendorsMapper(
public vendorsMapper(
vendors: ModelObject<Vendor>[],
): ITransactionsByVendorsVendor[] {
return R.compose(

View File

@@ -1,3 +1,4 @@
import { Injectable } from '@nestjs/common';
import {
ITransactionsByVendorTable,
ITransactionsByVendorsFilter,
@@ -7,7 +8,6 @@ import { TransactionsByVendorExportInjectable } from './TransactionsByVendorExpo
import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable';
import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TransactionsByVendorApplication {
@@ -55,7 +55,6 @@ export class TransactionsByVendorApplication {
/**
* Retrieves the transactions by vendor in XLSX format.
* @param {number} tenantId
* @param {ITransactionsByVendorsFilter} query
* @returns {Promise<Buffer>}
*/
@@ -67,7 +66,6 @@ export class TransactionsByVendorApplication {
/**
* Retrieves the transactions by vendor in PDF format.
* @param {number} tenantId
* @param {ITransactionsByVendorsFilter} query
* @returns {Promise<Buffer>}
*/

View File

@@ -30,6 +30,12 @@ export class TransactionsByVendorsInjectable {
): Promise<ITransactionsByVendorsStatement> {
const filter = { ...getTransactionsByVendorDefaultQuery(), ...query };
// Set filter.
this.transactionsByVendorRepository.setFilter(filter);
// Initialize the repository.
await this.transactionsByVendorRepository.asyncInit();
// Transactions by customers data mapper.
const reportInstance = new TransactionsByVendor(
this.transactionsByVendorRepository,

View File

@@ -1,4 +1,5 @@
import * as R from 'ramda';
import * as moment from 'moment';
import { isEmpty, map } from 'lodash';
import { Inject, Injectable } from '@nestjs/common';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
@@ -12,6 +13,7 @@ import { AccountRepository } from '@/modules/Accounts/repositories/Account.repos
import { Ledger } from '@/modules/Ledger/Ledger';
import { ModelObject } from 'objection';
import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types';
import { DateInput } from '@/common/types/Date';
@Injectable()
export class TransactionsByVendorRepository extends TransactionsByContactRepository {
@@ -67,11 +69,17 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
public reportEntries: ILedgerEntry[];
/**
* Journal.
* @param {Ledger} journal
* Set filter.
* @param {ITransactionsByVendorsFilter} filter
*/
public journal: Ledger;
public setFilter(filter: ITransactionsByVendorsFilter) {
this.filter = filter;
}
/**
* Initialize the repository.
* @returns {Promise<void>}
*/
async asyncInit() {
await this.initBaseCurrency();
await this.initVendors();
@@ -114,7 +122,7 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
}
async initLedger() {
this.journal = new Ledger(this.reportEntries);
this.ledger = new Ledger(this.reportEntries);
}
/**
@@ -145,8 +153,8 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
* @param {number[]} customersIds
*/
public async getVendorsPeriodEntries(
fromDate: moment.MomentInput,
toDate: moment.MomentInput,
fromDate: DateInput,
toDate: DateInput,
): Promise<ILedgerEntry[]> {
const transactions = await this.getVendorsPeriodTransactions(
fromDate,
@@ -172,8 +180,8 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
* @returns {Promise<ILedgerEntry[]>}
*/
public async getReportEntries(
fromDate: moment.MomentInput,
toDate: moment.MomentInput,
fromDate: DateInput,
toDate: DateInput,
): Promise<ILedgerEntry[]> {
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
@@ -240,8 +248,8 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
* @returns {Promise<AccountTransaction[]>}
*/
public async getVendorsPeriodTransactions(
fromDate: moment.MomentInput,
toDate: moment.MomentInput,
fromDate: DateInput,
toDate: DateInput,
): Promise<AccountTransaction[]> {
const receivableAccounts = await this.getPayableAccounts();
const receivableAccountsIds = map(receivableAccounts, 'id');

View File

@@ -0,0 +1,60 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { IVendorBalanceSummaryQuery } from './VendorBalanceSummary.types';
import { VendorBalanceSummaryApplication } from './VendorBalanceSummaryApplication';
import { Response } from 'express';
import { AcceptType } from '@/constants/accept-type';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('/reports/vendor-balance-summary')
@PublicRoute()
export class VendorBalanceSummaryController {
constructor(
private readonly vendorBalanceSummaryApp: VendorBalanceSummaryApplication,
) {}
@Get()
@ApiOperation({ summary: 'Get vendor balance summary' })
@ApiResponse({ status: 200, description: 'Vendor balance summary' })
async vendorBalanceSummary(
@Query() filter: IVendorBalanceSummaryQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the csv format.
if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.vendorBalanceSummaryApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.vendorBalanceSummaryApp.xlsx(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader('Content-Type', 'application/vnd.openxmlformats');
return res.send(buffer);
// Retrieves the json table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.vendorBalanceSummaryApp.table(filter);
return res.status(200).send(table);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.vendorBalanceSummaryApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.vendorBalanceSummaryApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { VendorBalanceSummaryController } from './VendorBalanceSummary.controller';
import { VendorBalanceSummaryService } from './VendorBalanceSummaryService';
import { VendorBalanceSummaryTableInjectable } from './VendorBalanceSummaryTableInjectable';
import { VendorBalanceSummaryExportInjectable } from './VendorBalanceSummaryExportInjectable';
import { VendorBalanceSummaryPdf } from './VendorBalanceSummaryPdf';
import { VendorBalanceSummaryApplication } from './VendorBalanceSummaryApplication';
@Module({
providers: [
VendorBalanceSummaryTableInjectable,
VendorBalanceSummaryExportInjectable,
VendorBalanceSummaryService,
VendorBalanceSummaryPdf,
VendorBalanceSummaryApplication,
],
controllers: [VendorBalanceSummaryController],
})
export class VendorBalanceSummaryModule {}

View File

@@ -0,0 +1,102 @@
import * as R from 'ramda';
import { isEmpty } from 'lodash';
import { ModelObject } from 'objection';
import {
IVendorBalanceSummaryVendor,
IVendorBalanceSummaryQuery,
IVendorBalanceSummaryData,
} from './VendorBalanceSummary.types';
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { INumberFormatQuery } from '../../types/Report.types';
import { VendorBalanceSummaryRepository } from './VendorBalanceSummaryRepository';
export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
readonly filter: IVendorBalanceSummaryQuery;
readonly numberFormat: INumberFormatQuery;
readonly repo: VendorBalanceSummaryRepository;
/**
* Constructor method.
* @param {IJournalPoster} receivableLedger
* @param {IVendor[]} vendors
* @param {IVendorBalanceSummaryQuery} filter
* @param {string} baseCurrency
*/
constructor(
repo: VendorBalanceSummaryRepository,
filter: IVendorBalanceSummaryQuery,
) {
super();
this.filter = filter;
this.numberFormat = this.filter.numberFormat;
}
/**
* Customer section mapper.
* @param {ModelObject<Vendor>} vendor
* @returns {IVendorBalanceSummaryVendor}
*/
private vendorMapper = (
vendor: ModelObject<Vendor>,
): IVendorBalanceSummaryVendor => {
const closingBalance = this.repo.ledger
.whereContactId(vendor.id)
.getClosingBalance();
return {
id: vendor.id,
vendorName: vendor.displayName,
total: this.getContactTotalFormat(closingBalance),
};
};
/**
* Mappes the vendor model object to vendor balance summary section.
* @param {ModelObject<Vendor>[]} vendors - Customers.
* @returns {IVendorBalanceSummaryVendor[]}
*/
private vendorsMapper = (
vendors: ModelObject<Vendor>[],
): IVendorBalanceSummaryVendor[] => {
return vendors.map(this.vendorMapper);
};
/**
* Detarmines whether the vendors post filter is active.
* @returns {boolean}
*/
private isVendorsPostFilter = (): boolean => {
return isEmpty(this.filter.vendorsIds);
};
/**
* Retrieve the vendors sections of the report.
* @param {ModelObject<Vendor>} vendors
* @returns {IVendorBalanceSummaryVendor[]}
*/
private getVendorsSection(
vendors: ModelObject<Vendor>[],
): IVendorBalanceSummaryVendor[] {
return R.compose(
R.when(this.isVendorsPostFilter, this.contactsFilter),
R.when(
R.always(this.filter.percentageColumn),
this.contactCamparsionPercentageOfColumn,
),
this.vendorsMapper,
)(vendors);
}
/**
* Retrieve the report statement data.
* @returns {IVendorBalanceSummaryData}
*/
public reportData(): IVendorBalanceSummaryData {
const vendors = this.getVendorsSection(this.repo.vendors);
const total = this.getContactsTotalSection(vendors);
return { vendors, total };
}
}

View File

@@ -0,0 +1,63 @@
import {
IFinancialSheetCommonMeta,
INumberFormatQuery,
} from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
export interface IVendorBalanceSummaryQuery {
asDate: Date;
vendorsIds: number[];
numberFormat: INumberFormatQuery;
percentageColumn: boolean;
noneTransactions: boolean;
noneZero: boolean;
}
export interface IVendorBalanceSummaryAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IVendorBalanceSummaryPercentage {
amount: number;
formattedAmount: string;
}
export interface IVendorBalanceSummaryVendor {
id: number;
vendorName: string;
total: IVendorBalanceSummaryAmount;
percentageOfColumn?: IVendorBalanceSummaryPercentage;
}
export interface IVendorBalanceSummaryTotal {
total: IVendorBalanceSummaryAmount;
percentageOfColumn?: IVendorBalanceSummaryPercentage;
}
export interface IVendorBalanceSummaryData {
vendors: IVendorBalanceSummaryVendor[];
total: IVendorBalanceSummaryTotal;
}
export interface IVendorBalanceSummaryStatement {
data: IVendorBalanceSummaryData;
query: IVendorBalanceSummaryQuery;
meta: IVendorBalanceSummaryMeta;
}
export interface IVendorBalanceSummaryService {
vendorBalanceSummary(
tenantId: number,
query: IVendorBalanceSummaryQuery,
): Promise<IVendorBalanceSummaryStatement>;
}
export interface IVendorBalanceSummaryTable extends IFinancialTable {
query: IVendorBalanceSummaryQuery;
meta: IVendorBalanceSummaryMeta;
}
export interface IVendorBalanceSummaryMeta extends IFinancialSheetCommonMeta {
formattedAsDate: string;
}

View File

@@ -0,0 +1,60 @@
import { VendorBalanceSummaryTableInjectable } from './VendorBalanceSummaryTableInjectable';
import { VendorBalanceSummaryExportInjectable } from './VendorBalanceSummaryExportInjectable';
import { VendorBalanceSummaryService } from './VendorBalanceSummaryService';
import { VendorBalanceSummaryPdf } from './VendorBalanceSummaryPdf';
import { Injectable } from '@nestjs/common';
import { IVendorBalanceSummaryQuery } from './VendorBalanceSummary.types';
@Injectable()
export class VendorBalanceSummaryApplication {
constructor(
private readonly vendorBalanceSummaryTable: VendorBalanceSummaryTableInjectable,
private readonly vendorBalanceSummarySheet: VendorBalanceSummaryService,
private readonly vendorBalanceSummaryExport: VendorBalanceSummaryExportInjectable,
private readonly vendorBalanceSummaryPdf: VendorBalanceSummaryPdf,
) {}
/**
* Retrieves the vendor balance summary sheet in sheet format.
* @param {IVendorBalanceSummaryQuery} query
*/
public sheet(query: IVendorBalanceSummaryQuery) {
return this.vendorBalanceSummarySheet.vendorBalanceSummary(query);
}
/**
* Retrieves the vendor balance summary sheet in table format.
* @param {IVendorBalanceSummaryQuery} query
* @returns {}
*/
public table(query: IVendorBalanceSummaryQuery) {
return this.vendorBalanceSummaryTable.table(query);
}
/**
* Retrieves the vendor balance summary sheet in xlsx format.
* @param {IVendorBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: IVendorBalanceSummaryQuery): Promise<Buffer> {
return this.vendorBalanceSummaryExport.xlsx(query);
}
/**
* Retrieves the vendor balance summary sheet in csv format.
* @param {IVendorBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public csv(query: IVendorBalanceSummaryQuery): Promise<string> {
return this.vendorBalanceSummaryExport.csv(query);
}
/**
* Retrieves the vendor balance summary sheet in pdf format.
* @param {IVendorBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: IVendorBalanceSummaryQuery) {
return this.vendorBalanceSummaryPdf.pdf(query);
}
}

View File

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

View File

@@ -0,0 +1,30 @@
import * as moment from 'moment';
import { Injectable } from '@nestjs/common';
import {
IVendorBalanceSummaryMeta,
IVendorBalanceSummaryQuery,
} from './VendorBalanceSummary.types';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class VendorBalanceSummaryMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieves the vendor balance summary meta.
* @param {IVendorBalanceSummaryQuery} query - Query.
* @returns {IBalanceSheetMeta}
*/
public async meta(
query: IVendorBalanceSummaryQuery,
): Promise<IVendorBalanceSummaryMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
return {
...commonMeta,
sheetName: 'Vendor Balance Summary',
formattedAsDate,
};
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { IVendorBalanceSummaryQuery } from './VendorBalanceSummary.types';
import { VendorBalanceSummaryTableInjectable } from './VendorBalanceSummaryTableInjectable';
import { HtmlTableCustomCss } from './constants';
@Injectable()
export class VendorBalanceSummaryPdf {
constructor(
private readonly vendorBalanceSummaryTable: VendorBalanceSummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Retrieves the sales by items sheet in pdf format.
* @param {IVendorBalanceSummaryQuery} query - Query.
* @returns {Promise<IBalanceSheetTable>}
*/
public async pdf(
query: IVendorBalanceSummaryQuery,
): Promise<Buffer> {
const table = await this.vendorBalanceSummaryTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedAsDate,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,161 @@
import * as R from 'ramda';
import { isEmpty, map } from 'lodash';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { Ledger } from '@/modules/Ledger/Ledger';
import { IVendorBalanceSummaryQuery } from './VendorBalanceSummary.types';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { ModelObject } from 'objection';
import { ACCOUNT_TYPE } from '@/constants/accounts';
@Injectable({ scope: Scope.TRANSIENT })
export class VendorBalanceSummaryRepository {
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction;
@Inject(Vendor.name)
private readonly vendorModel: typeof Vendor;
@Inject(Account.name)
private readonly accountModel: typeof Account;
/**
* Filter.
* @param {IVendorBalanceSummaryQuery} filter
*/
public filter: IVendorBalanceSummaryQuery;
/**
* Vendors entries.
* @param {Array<ILedgerEntry>} vendorEntries
*/
public vendorEntries: Array<ILedgerEntry>;
/**
* Vendors list.
* @param {Array<ModelObject<Vendor>>} vendors
*/
public vendors: Array<ModelObject<Vendor>>;
/**
* Ledger instance.
*/
public ledger: Ledger;
/**
* Base currency.
*/
public baseCurrency: string;
/**
* Set the filter.
* @param {IVendorBalanceSummaryQuery} filter
*/
public setFilter(filter: IVendorBalanceSummaryQuery) {
this.filter = filter;
}
/**
* Initialize the vendor balance summary repository.
*/
async asyncInit() {
this.initVendors();
this.initVendorsEntries();
this.initLedger();
}
/**
* Initialize the vendors.
*/
async initVendors() {
const vendors = await this.getVendors(this.filter.vendorsIds);
this.vendors = vendors;
}
/**
* Initialize the vendors entries.
*/
async initVendorsEntries() {
const vendorsEntries = await this.getReportVendorsEntries(
this.filter.asDate,
);
this.vendorEntries = vendorsEntries;
}
/**
* Initialize the ledger.
*/
async initLedger() {
this.ledger = new Ledger(this.vendorEntries);
}
/**
* Retrieve the report vendors.
* @param {number[]} vendorsIds - Vendors ids.
* @returns {IVendor[]}
*/
public async getVendors(
vendorsIds?: number[],
): Promise<ModelObject<Vendor>[]> {
const vendorQuery = this.vendorModel.query().orderBy('displayName');
if (!isEmpty(vendorsIds)) {
vendorQuery.whereIn('id', vendorsIds);
}
return vendorQuery;
}
/**
* Retrieve the payable accounts.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
public async getPayableAccounts(): Promise<ModelObject<Account>[]> {
return this.accountModel
.query()
.where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
}
/**
* Retrieve the vendors transactions.
* @param {number} tenantId
* @param {Date} asDate
* @returns
*/
public async getVendorsTransactions(asDate: Date | string) {
// Retrieve payable accounts .
const payableAccounts = await this.getPayableAccounts();
const payableAccountsIds = map(payableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await this.accountTransactionModel
.query()
.onBuild((query) => {
query.whereIn('accountId', payableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
});
return customersTranasctions;
}
/**
*
* Retrieve the vendors ledger entrjes.
* @param {number} tenantId -
* @param {Date|string} date -
* @returns {Promise<ILedgerEntry>}
*/
private async getReportVendorsEntries(
date: Date | string,
): Promise<ILedgerEntry[]> {
const transactions = await this.getVendorsTransactions(date);
const commonProps = { accountNormal: 'credit' };
return R.map(R.merge(commonProps))(transactions);
}
}

View File

@@ -0,0 +1,53 @@
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import {
IVendorBalanceSummaryQuery,
IVendorBalanceSummaryStatement,
} from './VendorBalanceSummary.types';
import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
import { VendorBalanceSummaryRepository } from './VendorBalanceSummaryRepository';
import { VendorBalanceSummaryMeta } from './VendorBalanceSummaryMeta';
import { getVendorBalanceSummaryDefaultQuery } from './utils';
import { events } from '@/common/events/events';
@Injectable()
export class VendorBalanceSummaryService {
constructor(
private readonly vendorBalanceSummaryRepository: VendorBalanceSummaryRepository,
private readonly vendorBalanceSummaryMeta: VendorBalanceSummaryMeta,
private readonly eventEmitter: EventEmitter2,
) {}
/**
* Retrieve the statment of customer balance summary report.
* @param {IVendorBalanceSummaryQuery} query -
* @return {Promise<IVendorBalanceSummaryStatement>}
*/
public async vendorBalanceSummary(
query: IVendorBalanceSummaryQuery,
): Promise<IVendorBalanceSummaryStatement> {
const filter = { ...getVendorBalanceSummaryDefaultQuery(), ...query };
this.vendorBalanceSummaryRepository.setFilter(filter);
this.vendorBalanceSummaryRepository.asyncInit();
// Report instance.
const reportInstance = new VendorBalanceSummaryReport(
this.vendorBalanceSummaryRepository,
filter,
);
// Retrieve the vendor balance summary meta.
const meta = await this.vendorBalanceSummaryMeta.meta(filter);
// Triggers `onVendorBalanceSummaryViewed` event.
await this.eventEmitter.emitAsync(
events.reports.onVendorBalanceSummaryViewed,
{ query },
);
return {
data: reportInstance.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import {
IVendorBalanceSummaryQuery,
IVendorBalanceSummaryTable,
} from './VendorBalanceSummary.types';
import { VendorBalanceSummaryTable } from './VendorBalanceSummaryTableRows';
import { VendorBalanceSummaryService } from './VendorBalanceSummaryService';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class VendorBalanceSummaryTableInjectable {
constructor(
private readonly vendorBalanceSummarySheet: VendorBalanceSummaryService,
private readonly i18n: I18nService
) {}
/**
* Retrieves the vendor balance summary sheet in table format.
* @param {IVendorBalanceSummaryQuery} query - Query.
* @returns {Promise<IVendorBalanceSummaryTable>}
*/
public async table(
query: IVendorBalanceSummaryQuery,
): Promise<IVendorBalanceSummaryTable> {
const { data, meta } =
await this.vendorBalanceSummarySheet.vendorBalanceSummary(
query,
);
const table = new VendorBalanceSummaryTable(data, query, this.i18n);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query,
meta,
};
}
}

View File

@@ -0,0 +1,154 @@
import * as R from 'ramda';
import { I18nService } from 'nestjs-i18n';
import {
IVendorBalanceSummaryData,
IVendorBalanceSummaryVendor,
IVendorBalanceSummaryTotal,
IVendorBalanceSummaryQuery,
} from './VendorBalanceSummary.types';
import {
ITableRow,
ITableColumn,
IColumnMapperMeta,
} from '../../types/Table.types';
import { tableMapper, tableRowMapper } from '../../utils/Table.utils';
enum TABLE_ROWS_TYPES {
VENDOR = 'VENDOR',
TOTAL = 'TOTAL',
}
export class VendorBalanceSummaryTable {
private readonly i18n: I18nService;
private readonly report: IVendorBalanceSummaryData;
private readonly query: IVendorBalanceSummaryQuery;
/**
* Constructor method.
* @param {IVendorBalanceSummaryData} report - Report.
* @param {IVendorBalanceSummaryQuery} query - Query.
* @param {I18nService} i18n - I18n service.
*/
constructor(
report: IVendorBalanceSummaryData,
query: IVendorBalanceSummaryQuery,
i18n: I18nService
) {
this.report = report;
this.query = query;
this.i18n = i18n;
}
/**
* Retrieve percentage columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getPercentageColumnsAccessor = (): IColumnMapperMeta[] => {
return [
{
key: 'percentageOfColumn',
accessor: 'percentageOfColumn.formattedAmount',
},
];
};
/**
* Retrieve vendor node columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getVendorColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
{ key: 'name', accessor: 'vendorName' },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
R.concat(columns),
R.when(
R.always(this.query.percentageColumn),
R.concat(this.getPercentageColumnsAccessor())
)
)([]);
};
/**
* Transformes the vendors to table rows.
* @param {IVendorBalanceSummaryVendor[]} vendors
* @returns {ITableRow[]}
*/
private vendorsTransformer = (
vendors: IVendorBalanceSummaryVendor[]
): ITableRow[] => {
const columns = this.getVendorColumnsAccessor();
return tableMapper(vendors, columns, {
rowTypes: [TABLE_ROWS_TYPES.VENDOR],
});
};
/**
* Retrieve total node columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
{ key: 'name', value: this.i18n.t('Total') },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
R.concat(columns),
R.when(
R.always(this.query.percentageColumn),
R.concat(this.getPercentageColumnsAccessor())
)
)([]);
};
/**
* Transformes the total to table row.
* @param {IVendorBalanceSummaryTotal} total
* @returns {ITableRow}
*/
private totalTransformer = (total: IVendorBalanceSummaryTotal): ITableRow => {
const columns = this.getTotalColumnsAccessor();
return tableRowMapper(total, columns, {
rowTypes: [TABLE_ROWS_TYPES.TOTAL],
});
};
/**
* Transformes the vendor balance summary to table rows.
* @param {IVendorBalanceSummaryData} vendorBalanceSummary
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
const vendors = this.vendorsTransformer(this.report.vendors);
const total = this.totalTransformer(this.report.total);
return vendors.length > 0 ? [...vendors, total] : [];
};
/**
* Retrieve the report statement columns
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
const columns = [
{
key: 'name',
label: this.i18n.t('contact_summary_balance.account_name'),
},
{ key: 'total', label: this.i18n.t('contact_summary_balance.total') },
];
return R.compose(
R.when(
R.always(this.query.percentageColumn),
R.append({
key: 'percentage_of_column',
label: this.i18n.t('contact_summary_balance.percentage_column'),
})
),
R.concat(columns)
)([]);
};
}

View File

@@ -0,0 +1,14 @@
export const HtmlTableCustomCss = `
table tr.row-type--total td {
font-weight: 600;
border-top: 1px solid #bbb;
border-bottom: 3px double #333;
}
table .column--name {
width: 65%;
}
table .column--total,
table .cell--total {
text-align: right;
}
`;

View File

@@ -0,0 +1,17 @@
export const getVendorBalanceSummaryDefaultQuery = () => {
return {
asDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
percentageColumn: false,
noneZero: false,
noneTransactions: true,
};
}

View File

@@ -13,6 +13,7 @@ export type ITableRow = {
cells: ITableCell[];
rowTypes?: Array<any>;
id?: string;
children?: ITableRow[];
};
export interface ITableColumn {