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