mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-24 08:39:49 +00:00
refator: reports to nestjs
This commit is contained in:
@@ -1,3 +1,2 @@
|
|||||||
|
|
||||||
export type Constructor = new (...args: any[]) => {};
|
export type Constructor = new (...args: any[]) => {};
|
||||||
export type GConstructor<T> = new (...args: any[]) => T;
|
export type GConstructor<T> = new (...args: any[]) => T;
|
||||||
3
packages/server-nest/src/common/types/Date.ts
Normal file
3
packages/server-nest/src/common/types/Date.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
export type DateInput = moment.MomentInput;
|
||||||
@@ -3,11 +3,15 @@ import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByIt
|
|||||||
import { CustomerBalanceSummaryModule } from './modules/CustomerBalanceSummary/CustomerBalanceSummary.module';
|
import { CustomerBalanceSummaryModule } from './modules/CustomerBalanceSummary/CustomerBalanceSummary.module';
|
||||||
import { SalesByItemsModule } from './modules/SalesByItems/SalesByItems.module';
|
import { SalesByItemsModule } from './modules/SalesByItems/SalesByItems.module';
|
||||||
import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.module';
|
import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.module';
|
||||||
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
|
|
||||||
import { TrialBalanceSheetModule } from './modules/TrialBalanceSheet/TrialBalanceSheet.module';
|
import { TrialBalanceSheetModule } from './modules/TrialBalanceSheet/TrialBalanceSheet.module';
|
||||||
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module';
|
|
||||||
import { TransactionsByVendorModule } from './modules/TransactionsByVendor/TransactionsByVendor.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({
|
@Module({
|
||||||
providers: [],
|
providers: [],
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,12 +20,13 @@ import { TransactionsByVendorModule } from './modules/TransactionsByVendor/Trans
|
|||||||
SalesByItemsModule,
|
SalesByItemsModule,
|
||||||
GeneralLedgerModule,
|
GeneralLedgerModule,
|
||||||
TrialBalanceSheetModule,
|
TrialBalanceSheetModule,
|
||||||
TransactionsByCustomerModule,
|
|
||||||
TransactionsByVendorModule,
|
TransactionsByVendorModule,
|
||||||
// TransactionsByReferenceModule,
|
TransactionsByCustomerModule,
|
||||||
// TransactionsByVendorModule,
|
TransactionsByReferenceModule,
|
||||||
// TransactionsByContactModule,
|
ARAgingSummaryModule,
|
||||||
|
APAgingSummaryModule,
|
||||||
|
InventoryItemDetailsModule,
|
||||||
|
InventoryValuationSheetModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class FinancialStatementsModule {}
|
export class FinancialStatementsModule {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import {
|
import {
|
||||||
IFormatNumberSettings,
|
IFormatNumberSettings,
|
||||||
INumberFormatQuery,
|
INumberFormatQuery,
|
||||||
|
|
||||||
} from '../types/Report.types';
|
} from '../types/Report.types';
|
||||||
import { formatNumber } from '@/utils/format-number';
|
import { formatNumber } from '@/utils/format-number';
|
||||||
import { IFinancialTableTotal } from '../types/Table.types';
|
import { IFinancialTableTotal } from '../types/Table.types';
|
||||||
@@ -41,7 +40,7 @@ export class FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
protected formatNumber(
|
protected formatNumber(
|
||||||
number,
|
number,
|
||||||
overrideSettings: IFormatNumberSettings = {}
|
overrideSettings: IFormatNumberSettings = {},
|
||||||
): string {
|
): string {
|
||||||
const settings = {
|
const settings = {
|
||||||
...this.transfromFormatQueryToSettings(),
|
...this.transfromFormatQueryToSettings(),
|
||||||
@@ -57,7 +56,7 @@ export class FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
protected formatTotalNumber = (
|
protected formatTotalNumber = (
|
||||||
amount: number,
|
amount: number,
|
||||||
settings: IFormatNumberSettings = {}
|
settings: IFormatNumberSettings = {},
|
||||||
): string => {
|
): string => {
|
||||||
const { numberFormat } = this;
|
const { numberFormat } = this;
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ export class FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
protected formatPercentage = (
|
protected formatPercentage = (
|
||||||
amount: number,
|
amount: number,
|
||||||
overrideSettings: IFormatNumberSettings = {}
|
overrideSettings: IFormatNumberSettings = {},
|
||||||
): string => {
|
): string => {
|
||||||
const percentage = amount * 100;
|
const percentage = amount * 100;
|
||||||
const settings = {
|
const settings = {
|
||||||
@@ -94,7 +93,7 @@ export class FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
protected formatTotalPercentage = (
|
protected formatTotalPercentage = (
|
||||||
amount: number,
|
amount: number,
|
||||||
settings: IFormatNumberSettings = {}
|
settings: IFormatNumberSettings = {},
|
||||||
): string => {
|
): string => {
|
||||||
return this.formatPercentage(amount, {
|
return this.formatPercentage(amount, {
|
||||||
...settings,
|
...settings,
|
||||||
@@ -109,7 +108,7 @@ export class FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
protected getAmountMeta(
|
protected getAmountMeta(
|
||||||
amount: number,
|
amount: number,
|
||||||
overrideSettings?: IFormatNumberSettings
|
overrideSettings?: IFormatNumberSettings,
|
||||||
): IFinancialTableTotal {
|
): IFinancialTableTotal {
|
||||||
return {
|
return {
|
||||||
amount,
|
amount,
|
||||||
@@ -125,7 +124,7 @@ export class FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
protected getTotalAmountMeta(
|
protected getTotalAmountMeta(
|
||||||
amount: number,
|
amount: number,
|
||||||
title?: string
|
title?: string,
|
||||||
): IFinancialTableTotal {
|
): IFinancialTableTotal {
|
||||||
return {
|
return {
|
||||||
...(title ? { title } : {}),
|
...(title ? { title } : {}),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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' };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AgingSummaryMeta } from './AgingSummaryMeta';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [AgingSummaryMeta],
|
||||||
|
providers: [AgingSummaryMeta],
|
||||||
|
})
|
||||||
|
export class AgingSummaryModule {}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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') },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,9 +11,9 @@ import { FinancialSheet } from '../../common/FinancialSheet';
|
|||||||
import { ITableColumn, ITableRow } from '../../types/Table.types';
|
import { ITableColumn, ITableRow } from '../../types/Table.types';
|
||||||
import { tableRowMapper } from '../../utils/Table.utils';
|
import { tableRowMapper } from '../../utils/Table.utils';
|
||||||
|
|
||||||
export class SalesByItemsTable extends R.compose(
|
export class SalesByItemsTable extends R.pipe(
|
||||||
FinancialTable,
|
FinancialTable,
|
||||||
FinancialSheetStructure
|
FinancialSheetStructure,
|
||||||
)(FinancialSheet) {
|
)(FinancialSheet) {
|
||||||
private readonly data: ISalesByItemsSheetData;
|
private readonly data: ISalesByItemsSheetData;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
accountName: account.name,
|
accountName: account.name,
|
||||||
currencyCode: this.baseCurrency,
|
currencyCode: this.baseCurrency,
|
||||||
transactionNumber: entry.transactionNumber,
|
transactionNumber: entry.transactionNumber,
|
||||||
transactionType: this.i18n.t(entry.referenceTypeFormatted),
|
|
||||||
|
// @ts-ignore
|
||||||
|
// transactionType: this.i18n.t(entry.referenceTypeFormatted),
|
||||||
|
transactionType: '',
|
||||||
date: entry.date,
|
date: entry.date,
|
||||||
createdAt: entry.createdAt,
|
createdAt: entry.createdAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ export class TransactionsByContactRepository {
|
|||||||
*/
|
*/
|
||||||
public accountsGraph: any;
|
public accountsGraph: any;
|
||||||
|
|
||||||
/**
|
|
||||||
* Report data.
|
|
||||||
* @param {Ledger} ledger
|
|
||||||
*/
|
|
||||||
public ledger: Ledger;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opening balance entries.
|
* Opening balance entries.
|
||||||
* @param {ILedgerEntry[]} openingBalanceEntries
|
* @param {ILedgerEntry[]} openingBalanceEntries
|
||||||
*/
|
*/
|
||||||
public openingBalanceEntries: ILedgerEntry[];
|
public openingBalanceEntries: ILedgerEntry[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ledger.
|
||||||
|
* @param {Ledger} ledger
|
||||||
|
*/
|
||||||
|
public ledger: Ledger;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,18 +8,21 @@ import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
|
|||||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||||
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
|
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
|
||||||
|
import { TransactionsByCustomerController } from './TransactionsByCustomer.controller';
|
||||||
|
import { TransactionsByCustomerApplication } from './TransactionsByCustomersApplication';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [FinancialSheetCommonModule, AccountsModule],
|
imports: [FinancialSheetCommonModule, AccountsModule],
|
||||||
providers: [
|
providers: [
|
||||||
|
TransactionsByCustomerApplication,
|
||||||
TransactionsByCustomersRepository,
|
TransactionsByCustomersRepository,
|
||||||
TransactionsByCustomersTableInjectable,
|
TransactionsByCustomersTableInjectable,
|
||||||
TransactionsByCustomersExportInjectable,
|
TransactionsByCustomersExportInjectable,
|
||||||
TransactionsByCustomersSheet,
|
TransactionsByCustomersSheet,
|
||||||
TransactionsByCustomersPdf,
|
TransactionsByCustomersPdf,
|
||||||
TransactionsByCustomersMeta,
|
TransactionsByCustomersMeta,
|
||||||
TenancyContext
|
TenancyContext,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [TransactionsByCustomerController],
|
||||||
})
|
})
|
||||||
export class TransactionsByCustomerModule {}
|
export class TransactionsByCustomerModule {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import * as moment from 'moment';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { ACCOUNT_TYPE } from '@/constants/accounts';
|
import { ACCOUNT_TYPE } from '@/constants/accounts';
|
||||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
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 { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
|
||||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||||
import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types';
|
import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types';
|
||||||
import { ModelObject } from 'objection';
|
|
||||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
import { TransactionsByContactRepository } from '../TransactionsByContact/TransactionsByContactRepository';
|
import { TransactionsByContactRepository } from '../TransactionsByContact/TransactionsByContactRepository';
|
||||||
|
import { DateInput } from '@/common/types/Date';
|
||||||
|
|
||||||
@Injectable({ scope: Scope.TRANSIENT })
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
export class TransactionsByCustomersRepository extends TransactionsByContactRepository {
|
export class TransactionsByCustomersRepository extends TransactionsByContactRepository {
|
||||||
@@ -50,11 +52,8 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the report data.
|
* Initialize the report data.
|
||||||
* @param {ITransactionsByCustomersFilter} filter
|
|
||||||
*/
|
*/
|
||||||
public async asyncInit(filter: ITransactionsByCustomersFilter) {
|
public async asyncInit() {
|
||||||
this.filter = filter;
|
|
||||||
|
|
||||||
await this.initAccountsGraph();
|
await this.initAccountsGraph();
|
||||||
await this.initCustomers();
|
await this.initCustomers();
|
||||||
await this.initOpeningBalanceEntries();
|
await this.initOpeningBalanceEntries();
|
||||||
@@ -63,6 +62,14 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
await this.initBaseCurrency();
|
await this.initBaseCurrency();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the filter.
|
||||||
|
* @param {ITransactionsByCustomersFilter} filter
|
||||||
|
*/
|
||||||
|
public setFilter(filter: ITransactionsByCustomersFilter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the accounts graph.
|
* Initialize the accounts graph.
|
||||||
*/
|
*/
|
||||||
@@ -119,6 +126,9 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
this.ledger = new Ledger(journalTransactions);
|
this.ledger = new Ledger(journalTransactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the base currency.
|
||||||
|
*/
|
||||||
async initBaseCurrency() {
|
async initBaseCurrency() {
|
||||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
|
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
|
||||||
this.baseCurrency = tenantMetadata.baseCurrency;
|
this.baseCurrency = tenantMetadata.baseCurrency;
|
||||||
@@ -140,6 +150,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
openingDate,
|
openingDate,
|
||||||
customersIds,
|
customersIds,
|
||||||
);
|
);
|
||||||
|
// @ts-ignore
|
||||||
return R.compose(
|
return R.compose(
|
||||||
R.map(R.assoc('date', openingDate)),
|
R.map(R.assoc('date', openingDate)),
|
||||||
R.map(R.assoc('accountNormal', 'debit')),
|
R.map(R.assoc('accountNormal', 'debit')),
|
||||||
@@ -154,25 +165,29 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
* @returns {Promise<ILedgerEntry[]>}
|
* @returns {Promise<ILedgerEntry[]>}
|
||||||
*/
|
*/
|
||||||
private async getCustomersPeriodsEntries(
|
private async getCustomersPeriodsEntries(
|
||||||
fromDate: Date | string,
|
fromDate: DateInput,
|
||||||
toDate: Date | string,
|
toDate: DateInput,
|
||||||
): Promise<ILedgerEntry[]> {
|
): Promise<ILedgerEntry[]> {
|
||||||
const transactions =
|
const transactions =
|
||||||
await this.getCustomersPeriodTransactions(
|
await this.getCustomersPeriodTransactions(
|
||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
toDate,
|
||||||
);
|
);
|
||||||
return R.compose(
|
|
||||||
|
// @ts-ignore
|
||||||
|
return R.pipe(
|
||||||
R.map(R.assoc('accountNormal', 'debit')),
|
R.map(R.assoc('accountNormal', 'debit')),
|
||||||
R.map((trans) => ({
|
R.map((trans) => ({
|
||||||
...trans,
|
...trans,
|
||||||
referenceTypeFormatted: trans.referenceTypeFormatted,
|
// @ts-ignore
|
||||||
|
referenceTypeFormatted: '',
|
||||||
|
// referenceTypeFormatted: trans.referenceTypeFormatted,
|
||||||
})),
|
})),
|
||||||
)(transactions);
|
)(transactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the report customers.
|
* Retrieves the report customers.
|
||||||
* @param {number[]} customersIds - Customers ids.
|
* @param {number[]} customersIds - Customers ids.
|
||||||
* @returns {Promise<ICustomer[]>}
|
* @returns {Promise<ICustomer[]>}
|
||||||
*/
|
*/
|
||||||
@@ -187,7 +202,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the accounts receivable.
|
* Retrieves the accounts receivable.
|
||||||
* @returns {Promise<IAccount[]>}
|
* @returns {Promise<IAccount[]>}
|
||||||
*/
|
*/
|
||||||
public async getReceivableAccounts(): Promise<Account[]> {
|
public async getReceivableAccounts(): Promise<Account[]> {
|
||||||
@@ -204,7 +219,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
* @returns {Promise<IAccountTransaction[]>}
|
* @returns {Promise<IAccountTransaction[]>}
|
||||||
*/
|
*/
|
||||||
public async getCustomersOpeningBalanceTransactions(
|
public async getCustomersOpeningBalanceTransactions(
|
||||||
openingDate: Date,
|
openingDate: DateInput,
|
||||||
customersIds?: number[],
|
customersIds?: number[],
|
||||||
): Promise<AccountTransaction[]> {
|
): Promise<AccountTransaction[]> {
|
||||||
const receivableAccounts = await this.getReceivableAccounts();
|
const receivableAccounts = await this.getReceivableAccounts();
|
||||||
@@ -222,14 +237,14 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the customers periods transactions.
|
* Retrieves the customers periods transactions.
|
||||||
* @param {Date} fromDate - From date.
|
* @param {DateInput} fromDate - From date.
|
||||||
* @param {Date} toDate - To date.
|
* @param {DateInput} toDate - To date.
|
||||||
* @return {Promise<IAccountTransaction[]>}
|
* @return {Promise<IAccountTransaction[]>}
|
||||||
*/
|
*/
|
||||||
public async getCustomersPeriodTransactions(
|
public async getCustomersPeriodTransactions(
|
||||||
fromDate: Date,
|
fromDate: DateInput,
|
||||||
toDate: Date,
|
toDate: DateInput,
|
||||||
): Promise<AccountTransaction[]> {
|
): Promise<AccountTransaction[]> {
|
||||||
const receivableAccounts = await this.getReceivableAccounts();
|
const receivableAccounts = await this.getReceivableAccounts();
|
||||||
const receivableAccountsIds = map(receivableAccounts, 'id');
|
const receivableAccountsIds = map(receivableAccounts, 'id');
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ITransactionsByCustomersFilter,
|
ITransactionsByCustomersFilter,
|
||||||
ITransactionsByCustomersStatement,
|
ITransactionsByCustomersStatement,
|
||||||
@@ -5,11 +8,8 @@ import {
|
|||||||
import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository';
|
import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository';
|
||||||
import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
|
import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
|
||||||
import { getTransactionsByCustomerDefaultQuery } from './utils';
|
import { getTransactionsByCustomerDefaultQuery } from './utils';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { TransactionsByCustomers } from './TransactionsByCustomers';
|
import { TransactionsByCustomers } from './TransactionsByCustomers';
|
||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TransactionsByCustomersSheet {
|
export class TransactionsByCustomersSheet {
|
||||||
@@ -17,7 +17,7 @@ export class TransactionsByCustomersSheet {
|
|||||||
private readonly transactionsByCustomersMeta: TransactionsByCustomersMeta,
|
private readonly transactionsByCustomersMeta: TransactionsByCustomersMeta,
|
||||||
private readonly transactionsByCustomersRepository: TransactionsByCustomersRepository,
|
private readonly transactionsByCustomersRepository: TransactionsByCustomersRepository,
|
||||||
private readonly eventPublisher: EventEmitter2,
|
private readonly eventPublisher: EventEmitter2,
|
||||||
private readonly tenancyContext: TenancyContext,
|
private readonly i18n: I18nService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,13 +29,12 @@ export class TransactionsByCustomersSheet {
|
|||||||
public async transactionsByCustomers(
|
public async transactionsByCustomers(
|
||||||
query: ITransactionsByCustomersFilter,
|
query: ITransactionsByCustomersFilter,
|
||||||
): Promise<ITransactionsByCustomersStatement> {
|
): Promise<ITransactionsByCustomersStatement> {
|
||||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
|
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
...getTransactionsByCustomerDefaultQuery(),
|
...getTransactionsByCustomerDefaultQuery(),
|
||||||
...query,
|
...query,
|
||||||
};
|
};
|
||||||
await this.transactionsByCustomersRepository.asyncInit(filter);
|
this.transactionsByCustomersRepository.setFilter(filter);
|
||||||
|
await this.transactionsByCustomersRepository.asyncInit();
|
||||||
|
|
||||||
// Transactions by customers data mapper.
|
// Transactions by customers data mapper.
|
||||||
const reportInstance = new TransactionsByCustomers(
|
const reportInstance = new TransactionsByCustomers(
|
||||||
@@ -43,15 +42,12 @@ export class TransactionsByCustomersSheet {
|
|||||||
this.transactionsByCustomersRepository,
|
this.transactionsByCustomersRepository,
|
||||||
this.i18n,
|
this.i18n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const meta = await this.transactionsByCustomersMeta.meta(filter);
|
const meta = await this.transactionsByCustomersMeta.meta(filter);
|
||||||
|
|
||||||
// Triggers `onCustomerTransactionsViewed` event.
|
// Triggers `onCustomerTransactionsViewed` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.reports.onCustomerTransactionsViewed,
|
events.reports.onCustomerTransactionsViewed,
|
||||||
{
|
{ query },
|
||||||
query,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
import { ITransactionsByCustomersCustomer } from './TransactionsByCustomer.types';
|
import { ITransactionsByCustomersCustomer } from './TransactionsByCustomer.types';
|
||||||
import { ITableRow, ITableColumn } from '../../types/Table.types';
|
import { ITableRow, ITableColumn } from '../../types/Table.types';
|
||||||
import { TransactionsByContactsTableRows } from '../TransactionsByContact/TransactionsByContactTableRows';
|
import { TransactionsByContactsTableRows } from '../TransactionsByContact/TransactionsByContactTableRows';
|
||||||
@@ -18,7 +19,10 @@ export class TransactionsByCustomersTable extends TransactionsByContactsTableRow
|
|||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {ITransactionsByCustomersCustomer[]} customersTransactions - Customers transactions.
|
* @param {ITransactionsByCustomersCustomer[]} customersTransactions - Customers transactions.
|
||||||
*/
|
*/
|
||||||
constructor(customersTransactions: ITransactionsByCustomersCustomer[], i18n) {
|
constructor(
|
||||||
|
customersTransactions: ITransactionsByCustomersCustomer[],
|
||||||
|
i18n: I18nService,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.customersTransactions = customersTransactions;
|
this.customersTransactions = customersTransactions;
|
||||||
this.i18n = i18n;
|
this.i18n = i18n;
|
||||||
@@ -29,7 +33,9 @@ export class TransactionsByCustomersTable extends TransactionsByContactsTableRow
|
|||||||
* @param {ITransactionsByCustomersCustomer} customer -
|
* @param {ITransactionsByCustomersCustomer} customer -
|
||||||
* @returns {ITableRow[]}
|
* @returns {ITableRow[]}
|
||||||
*/
|
*/
|
||||||
private customerDetails = (customer: ITransactionsByCustomersCustomer) => {
|
private customerDetails = (
|
||||||
|
customer: ITransactionsByCustomersCustomer,
|
||||||
|
): ITableRow => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'customerName', accessor: 'customerName' },
|
{ key: 'customerName', accessor: 'customerName' },
|
||||||
...R.repeat({ key: 'empty', value: '' }, 5),
|
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||||
@@ -56,22 +62,22 @@ export class TransactionsByCustomersTable extends TransactionsByContactsTableRow
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the table rows of the customer section.
|
* Retrieve the table rows of the customer section.
|
||||||
* @param {ITransactionsByCustomersCustomer} customer
|
* @param {ITransactionsByCustomersCustomer} customer - Customer object.
|
||||||
* @returns {ITableRow[]}
|
* @returns {ITableRow[]} - Table rows.
|
||||||
*/
|
*/
|
||||||
private customerRowsMapper = (customer: ITransactionsByCustomersCustomer) => {
|
private customerRowsMapper = (
|
||||||
|
customer: ITransactionsByCustomersCustomer,
|
||||||
|
): ITableRow => {
|
||||||
return R.pipe(this.customerDetails)(customer);
|
return R.pipe(this.customerDetails)(customer);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the table rows of transactions by customers report.
|
* Retrieve the table rows of transactions by customers report.
|
||||||
* @param {ITransactionsByCustomersCustomer[]} customers
|
* @param {ITransactionsByCustomersCustomer[]} customers - Customer objects.
|
||||||
* @returns {ITableRow[]}
|
* @returns {ITableRow[]} - Table rows.
|
||||||
*/
|
*/
|
||||||
public tableRows = (): ITableRow[] => {
|
public tableRows = (): ITableRow[] => {
|
||||||
return R.map(this.customerRowsMapper.bind(this))(
|
return R.map(this.customerRowsMapper)(this.customersTransactions);
|
||||||
this.customersTransactions,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
import {
|
import {
|
||||||
ITransactionsByCustomersFilter,
|
ITransactionsByCustomersFilter,
|
||||||
ITransactionsByCustomersTable,
|
ITransactionsByCustomersTable,
|
||||||
} from './TransactionsByCustomer.types';
|
} from './TransactionsByCustomer.types';
|
||||||
import { TransactionsByCustomersSheet } from './TransactionsByCustomersService';
|
import { TransactionsByCustomersSheet } from './TransactionsByCustomersService';
|
||||||
import { TransactionsByCustomersTable } from './TransactionsByCustomersTable';
|
import { TransactionsByCustomersTable } from './TransactionsByCustomersTable';
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { I18nService } from 'nestjs-i18n';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TransactionsByCustomersTableInjectable {
|
export class TransactionsByCustomersTableInjectable {
|
||||||
@@ -16,9 +16,8 @@ export class TransactionsByCustomersTableInjectable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the transactions by customers sheet in table format.
|
* Retrieves the transactions by customers sheet in table format.
|
||||||
* @param {number} tenantId
|
* @param {ITransactionsByCustomersFilter} filter - Filter object.
|
||||||
* @param {ITransactionsByCustomersFilter} filter
|
* @returns {Promise<ITransactionsByCustomersFilter>} - Transactions by customers table.
|
||||||
* @returns {Promise<ITransactionsByCustomersFilter>}
|
|
||||||
*/
|
*/
|
||||||
public async table(
|
public async table(
|
||||||
filter: ITransactionsByCustomersFilter,
|
filter: ITransactionsByCustomersFilter,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
|
||||||
export const getTransactionsByCustomerDefaultQuery = () => {
|
export const getTransactionsByCustomerDefaultQuery = () => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { TransactionsByReferenceApplication } from './TransactionsByReferenceApp
|
|||||||
import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepository';
|
import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepository';
|
||||||
import { TransactionsByReferenceService } from './TransactionsByReference.service';
|
import { TransactionsByReferenceService } from './TransactionsByReference.service';
|
||||||
import { TransactionsByReferenceController } from './TransactionsByReference.controller';
|
import { TransactionsByReferenceController } from './TransactionsByReference.controller';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
TransactionsByReferenceRepository,
|
TransactionsByReferenceRepository,
|
||||||
TransactionsByReferenceApplication,
|
TransactionsByReferenceApplication,
|
||||||
TransactionsByReferenceService,
|
TransactionsByReferenceService,
|
||||||
|
TenancyContext
|
||||||
],
|
],
|
||||||
controllers: [TransactionsByReferenceController],
|
controllers: [TransactionsByReferenceController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication';
|
import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication';
|
||||||
import { ITransactionsByReferenceQuery } from './TransactionsByReference.types';
|
import { ITransactionsByReferenceQuery } from './TransactionsByReference.types';
|
||||||
|
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
|
||||||
|
|
||||||
@Controller('reports/transactions-by-reference')
|
@Controller('reports/transactions-by-reference')
|
||||||
|
@PublicRoute()
|
||||||
export class TransactionsByReferenceController {
|
export class TransactionsByReferenceController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly transactionsByReferenceApp: TransactionsByReferenceApplication,
|
private readonly transactionsByReferenceApp: TransactionsByReferenceApplication,
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ export interface ITransactionsByReferenceAmount {
|
|||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ITransactionsByReferenceDate {
|
||||||
|
formattedDate: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITransactionsByReferenceTransaction {
|
export interface ITransactionsByReferenceTransaction {
|
||||||
|
date: ITransactionsByReferenceDate;
|
||||||
|
|
||||||
credit: ITransactionsByReferenceAmount;
|
credit: ITransactionsByReferenceAmount;
|
||||||
debit: ITransactionsByReferenceAmount;
|
debit: ITransactionsByReferenceAmount;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class TransactionsByReference extends FinancialSheet {
|
|||||||
this.transactions = transactions;
|
this.transactions = transactions;
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.baseCurrency = baseCurrency;
|
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 }),
|
credit: this.getAmountMeta(transaction.credit, { money: false }),
|
||||||
debit: this.getAmountMeta(transaction.debit, { money: false }),
|
debit: this.getAmountMeta(transaction.debit, { money: false }),
|
||||||
|
|
||||||
referenceTypeFormatted: transaction.referenceTypeFormatted,
|
// @ts-ignore
|
||||||
|
// referenceTypeFormatted: transaction.referenceTypeFormatted,
|
||||||
|
referenceTypeFormatted: '',
|
||||||
|
|
||||||
referenceType: transaction.referenceType,
|
referenceType: transaction.referenceType,
|
||||||
referenceId: transaction.referenceId,
|
referenceId: transaction.referenceId,
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ export class TransactionsByVendor extends TransactionsByContact {
|
|||||||
* @param {number} openingBalance - Opening balance amount.
|
* @param {number} openingBalance - Opening balance amount.
|
||||||
* @returns {ITransactionsByVendorsTransaction[]}
|
* @returns {ITransactionsByVendorsTransaction[]}
|
||||||
*/
|
*/
|
||||||
private vendorTransactions(
|
public vendorTransactions(
|
||||||
vendorId: number,
|
vendorId: number,
|
||||||
openingBalance: number,
|
openingBalance: number,
|
||||||
): ITransactionsByVendorsTransaction[] {
|
): ITransactionsByVendorsTransaction[] {
|
||||||
const openingBalanceLedger = this.repository.journal
|
const openingBalanceLedger = this.repository.ledger
|
||||||
.whereContactId(vendorId)
|
.whereContactId(vendorId)
|
||||||
.whereFromDate(this.filter.fromDate)
|
.whereFromDate(this.filter.fromDate)
|
||||||
.whereToDate(this.filter.toDate);
|
.whereToDate(this.filter.toDate);
|
||||||
@@ -68,7 +68,7 @@ export class TransactionsByVendor extends TransactionsByContact {
|
|||||||
* @param {IVendor} vendor
|
* @param {IVendor} vendor
|
||||||
* @returns {ITransactionsByVendorsVendor}
|
* @returns {ITransactionsByVendorsVendor}
|
||||||
*/
|
*/
|
||||||
private vendorMapper(
|
public vendorMapper(
|
||||||
vendor: ModelObject<Vendor>,
|
vendor: ModelObject<Vendor>,
|
||||||
): ITransactionsByVendorsVendor {
|
): ITransactionsByVendorsVendor {
|
||||||
const openingBalance = this.getContactOpeningBalance(vendor.id);
|
const openingBalance = this.getContactOpeningBalance(vendor.id);
|
||||||
@@ -93,7 +93,7 @@ export class TransactionsByVendor extends TransactionsByContact {
|
|||||||
* @param {number} openingBalance
|
* @param {number} openingBalance
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
private getVendorClosingBalance(
|
public getVendorClosingBalance(
|
||||||
vendorTransactions: ITransactionsByVendorsTransaction[],
|
vendorTransactions: ITransactionsByVendorsTransaction[],
|
||||||
openingBalance: number,
|
openingBalance: number,
|
||||||
) {
|
) {
|
||||||
@@ -108,7 +108,7 @@ export class TransactionsByVendor extends TransactionsByContact {
|
|||||||
* Detarmines whether the vendors post filter is active.
|
* Detarmines whether the vendors post filter is active.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
private isVendorsPostFilter = (): boolean => {
|
public isVendorsPostFilter = (): boolean => {
|
||||||
return isEmpty(this.filter.vendorsIds);
|
return isEmpty(this.filter.vendorsIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ export class TransactionsByVendor extends TransactionsByContact {
|
|||||||
* @param {IVendor[]} vendors
|
* @param {IVendor[]} vendors
|
||||||
* @returns {ITransactionsByVendorsVendor[]}
|
* @returns {ITransactionsByVendorsVendor[]}
|
||||||
*/
|
*/
|
||||||
private vendorsMapper(
|
public vendorsMapper(
|
||||||
vendors: ModelObject<Vendor>[],
|
vendors: ModelObject<Vendor>[],
|
||||||
): ITransactionsByVendorsVendor[] {
|
): ITransactionsByVendorsVendor[] {
|
||||||
return R.compose(
|
return R.compose(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ITransactionsByVendorTable,
|
ITransactionsByVendorTable,
|
||||||
ITransactionsByVendorsFilter,
|
ITransactionsByVendorsFilter,
|
||||||
@@ -7,7 +8,6 @@ import { TransactionsByVendorExportInjectable } from './TransactionsByVendorExpo
|
|||||||
import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
|
import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
|
||||||
import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable';
|
import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable';
|
||||||
import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf';
|
import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf';
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TransactionsByVendorApplication {
|
export class TransactionsByVendorApplication {
|
||||||
@@ -55,7 +55,6 @@ export class TransactionsByVendorApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the transactions by vendor in XLSX format.
|
* Retrieves the transactions by vendor in XLSX format.
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {ITransactionsByVendorsFilter} query
|
* @param {ITransactionsByVendorsFilter} query
|
||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
@@ -67,7 +66,6 @@ export class TransactionsByVendorApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the transactions by vendor in PDF format.
|
* Retrieves the transactions by vendor in PDF format.
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {ITransactionsByVendorsFilter} query
|
* @param {ITransactionsByVendorsFilter} query
|
||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ export class TransactionsByVendorsInjectable {
|
|||||||
): Promise<ITransactionsByVendorsStatement> {
|
): Promise<ITransactionsByVendorsStatement> {
|
||||||
const filter = { ...getTransactionsByVendorDefaultQuery(), ...query };
|
const filter = { ...getTransactionsByVendorDefaultQuery(), ...query };
|
||||||
|
|
||||||
|
// Set filter.
|
||||||
|
this.transactionsByVendorRepository.setFilter(filter);
|
||||||
|
|
||||||
|
// Initialize the repository.
|
||||||
|
await this.transactionsByVendorRepository.asyncInit();
|
||||||
|
|
||||||
// Transactions by customers data mapper.
|
// Transactions by customers data mapper.
|
||||||
const reportInstance = new TransactionsByVendor(
|
const reportInstance = new TransactionsByVendor(
|
||||||
this.transactionsByVendorRepository,
|
this.transactionsByVendorRepository,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
import * as moment from 'moment';
|
||||||
import { isEmpty, map } from 'lodash';
|
import { isEmpty, map } from 'lodash';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
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 { Ledger } from '@/modules/Ledger/Ledger';
|
||||||
import { ModelObject } from 'objection';
|
import { ModelObject } from 'objection';
|
||||||
import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types';
|
import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types';
|
||||||
|
import { DateInput } from '@/common/types/Date';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TransactionsByVendorRepository extends TransactionsByContactRepository {
|
export class TransactionsByVendorRepository extends TransactionsByContactRepository {
|
||||||
@@ -67,11 +69,17 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
|
|||||||
public reportEntries: ILedgerEntry[];
|
public reportEntries: ILedgerEntry[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Journal.
|
* Set filter.
|
||||||
* @param {Ledger} journal
|
* @param {ITransactionsByVendorsFilter} filter
|
||||||
*/
|
*/
|
||||||
public journal: Ledger;
|
public setFilter(filter: ITransactionsByVendorsFilter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the repository.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async asyncInit() {
|
async asyncInit() {
|
||||||
await this.initBaseCurrency();
|
await this.initBaseCurrency();
|
||||||
await this.initVendors();
|
await this.initVendors();
|
||||||
@@ -114,7 +122,7 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initLedger() {
|
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
|
* @param {number[]} customersIds
|
||||||
*/
|
*/
|
||||||
public async getVendorsPeriodEntries(
|
public async getVendorsPeriodEntries(
|
||||||
fromDate: moment.MomentInput,
|
fromDate: DateInput,
|
||||||
toDate: moment.MomentInput,
|
toDate: DateInput,
|
||||||
): Promise<ILedgerEntry[]> {
|
): Promise<ILedgerEntry[]> {
|
||||||
const transactions = await this.getVendorsPeriodTransactions(
|
const transactions = await this.getVendorsPeriodTransactions(
|
||||||
fromDate,
|
fromDate,
|
||||||
@@ -172,8 +180,8 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
|
|||||||
* @returns {Promise<ILedgerEntry[]>}
|
* @returns {Promise<ILedgerEntry[]>}
|
||||||
*/
|
*/
|
||||||
public async getReportEntries(
|
public async getReportEntries(
|
||||||
fromDate: moment.MomentInput,
|
fromDate: DateInput,
|
||||||
toDate: moment.MomentInput,
|
toDate: DateInput,
|
||||||
): Promise<ILedgerEntry[]> {
|
): Promise<ILedgerEntry[]> {
|
||||||
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
|
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
|
||||||
|
|
||||||
@@ -240,8 +248,8 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
|
|||||||
* @returns {Promise<AccountTransaction[]>}
|
* @returns {Promise<AccountTransaction[]>}
|
||||||
*/
|
*/
|
||||||
public async getVendorsPeriodTransactions(
|
public async getVendorsPeriodTransactions(
|
||||||
fromDate: moment.MomentInput,
|
fromDate: DateInput,
|
||||||
toDate: moment.MomentInput,
|
toDate: DateInput,
|
||||||
): Promise<AccountTransaction[]> {
|
): Promise<AccountTransaction[]> {
|
||||||
const receivableAccounts = await this.getPayableAccounts();
|
const receivableAccounts = await this.getPayableAccounts();
|
||||||
const receivableAccountsIds = map(receivableAccounts, 'id');
|
const receivableAccountsIds = map(receivableAccounts, 'id');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
)([]);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export type ITableRow = {
|
|||||||
cells: ITableCell[];
|
cells: ITableCell[];
|
||||||
rowTypes?: Array<any>;
|
rowTypes?: Array<any>;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
children?: ITableRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ITableColumn {
|
export interface ITableColumn {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { defaultTo, sumBy, uniqBy } from 'lodash';
|
import { defaultTo, sumBy, uniqBy } from 'lodash';
|
||||||
import { ILedger } from './types/Ledger.types';
|
import { ILedger } from './types/Ledger.types';
|
||||||
import { ILedgerEntry } from './types/Ledger.types';
|
import { ILedgerEntry } from './types/Ledger.types';
|
||||||
|
|||||||
Reference in New Issue
Block a user