fix: AR/AP aging report

This commit is contained in:
Ahmed Bouhuolia
2025-06-21 20:15:42 +02:00
parent 4d52059dba
commit 91976842a7
23 changed files with 265 additions and 126 deletions

View File

@@ -1,25 +1,54 @@
import { Type } from "class-transformer"; import { Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsPositive } from "class-validator"; import {
IsBoolean,
IsEnum,
IsNumber,
IsOptional,
IsPositive,
} from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class NumberFormatQueryDto { export class NumberFormatQueryDto {
@ApiPropertyOptional({
description: 'Number of decimal places to display',
example: 2,
})
@Type(() => Number) @Type(() => Number)
@IsNumber() @IsNumber()
@IsPositive() @IsPositive()
@IsOptional() @IsOptional()
readonly precision: number; readonly precision: number;
@ApiPropertyOptional({
description: 'Whether to divide the number by 1000',
example: false,
})
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
readonly divideOn1000: boolean; readonly divideOn1000: boolean;
@ApiPropertyOptional({
description: 'Whether to show zero values',
example: true,
})
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
readonly showZero: boolean; readonly showZero: boolean;
@ApiPropertyOptional({
description: 'How to format money values',
example: 'total',
enum: ['total', 'always', 'none'],
})
@IsEnum(['total', 'always', 'none']) @IsEnum(['total', 'always', 'none'])
@IsOptional() @IsOptional()
readonly formatMoney: 'total' | 'always' | 'none'; readonly formatMoney: 'total' | 'always' | 'none';
@ApiPropertyOptional({
description: 'How to format negative numbers',
example: 'parentheses',
enum: ['parentheses', 'mines'],
})
@IsEnum(['parentheses', 'mines']) @IsEnum(['parentheses', 'mines'])
@IsOptional() @IsOptional()
readonly negativeFormat: 'parentheses' | 'mines'; readonly negativeFormat: 'parentheses' | 'mines';

View File

@@ -1,8 +1,7 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { APAgingSummaryApplication } from './APAgingSummaryApplication'; import { APAgingSummaryApplication } from './APAgingSummaryApplication';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto'; import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';

View File

@@ -7,10 +7,7 @@ import {
IAgingSummaryContact, IAgingSummaryContact,
IAgingSummaryData, IAgingSummaryData,
} from '../AgingSummary/AgingSummary.types'; } from '../AgingSummary/AgingSummary.types';
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
vendorsIds: number[];
}
export interface IAPAgingSummaryVendor extends IAgingSummaryContact { export interface IAPAgingSummaryVendor extends IAgingSummaryContact {
vendorName: string; vendorName: string;
@@ -33,13 +30,13 @@ export interface IAPAgingSummaryMeta extends IFinancialSheetCommonMeta {
} }
export interface IAPAgingSummaryTable extends IFinancialTable { export interface IAPAgingSummaryTable extends IFinancialTable {
query: IAPAgingSummaryQuery; query: APAgingSummaryQueryDto;
meta: IAPAgingSummaryMeta; meta: IAPAgingSummaryMeta;
} }
export interface IAPAgingSummarySheet { export interface IAPAgingSummarySheet {
data: IAPAgingSummaryData; data: IAPAgingSummaryData;
meta: IAPAgingSummaryMeta; meta: IAPAgingSummaryMeta;
query: IAPAgingSummaryQuery; query: APAgingSummaryQueryDto;
columns: any; columns: any;
} }

View File

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

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TableSheet } from '../../common/TableSheet'; import { TableSheet } from '../../common/TableSheet';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable'; import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
@Injectable() @Injectable()
export class APAgingSummaryExportInjectable { export class APAgingSummaryExportInjectable {
@@ -11,10 +11,10 @@ export class APAgingSummaryExportInjectable {
/** /**
* Retrieves the A/P aging summary sheet in XLSX format. * Retrieves the A/P aging summary sheet in XLSX format.
* @param {IAPAgingSummaryQuery} query * @param {APAgingSummaryQueryDto} query
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
public async xlsx(query: IAPAgingSummaryQuery) { public async xlsx(query: APAgingSummaryQueryDto) {
const table = await this.APAgingSummaryTable.table(query); const table = await this.APAgingSummaryTable.table(query);
const tableSheet = new TableSheet(table.table); const tableSheet = new TableSheet(table.table);
@@ -25,10 +25,10 @@ export class APAgingSummaryExportInjectable {
/** /**
* Retrieves the A/P aging summary sheet in CSV format. * Retrieves the A/P aging summary sheet in CSV format.
* @param {IAPAgingSummaryQuery} query * @param {APAgingSummaryQueryDto} query
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
public async csv(query: IAPAgingSummaryQuery): Promise<string> { public async csv(query: APAgingSummaryQueryDto): Promise<string> {
const table = await this.APAgingSummaryTable.table(query); const table = await this.APAgingSummaryTable.table(query);
const tableSheet = new TableSheet(table.table); const tableSheet = new TableSheet(table.table);

View File

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

View File

@@ -1,36 +1,74 @@
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { IsNumber, IsOptional, IsDateString, IsBoolean, ValidateNested, IsArray } from 'class-validator'; import {
IsNumber,
IsOptional,
IsDateString,
IsBoolean,
ValidateNested,
IsArray,
} from 'class-validator';
import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto'; import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto';
import { parseBoolean } from '@/utils/parse-boolean'; import { parseBoolean } from '@/utils/parse-boolean';
import { ToNumber } from '@/common/decorators/Validators'; import { ToNumber } from '@/common/decorators/Validators';
import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto'; import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class APAgingSummaryQueryDto extends FinancialSheetBranchesQueryDto { export class APAgingSummaryQueryDto extends FinancialSheetBranchesQueryDto {
@IsDateString() @IsDateString()
@IsOptional() @IsOptional()
@ApiPropertyOptional({
description: 'The date as of which the AP aging summary is calculated',
example: '2024-06-01',
})
asDate: Date | string; asDate: Date | string;
@ToNumber() @ToNumber()
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
@ApiPropertyOptional({
description: 'Number of days before the aging period starts',
example: 30,
})
agingDaysBefore: number; agingDaysBefore: number;
@ToNumber() @ToNumber()
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
@ApiPropertyOptional({
description: 'Number of aging periods to calculate',
example: 4,
})
agingPeriods: number; agingPeriods: number;
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@Type(() => NumberFormatQueryDto) @Type(() => NumberFormatQueryDto)
@ApiPropertyOptional({
description: 'Number format configuration',
example: {
precision: 2,
divideOn1000: false,
showZero: true,
formatMoney: 'total',
negativeFormat: 'parentheses',
},
})
numberFormat: NumberFormatQueryDto; numberFormat: NumberFormatQueryDto;
@Transform(({ value }) => parseBoolean(value, false)) @Transform(({ value }) => parseBoolean(value, false))
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
@ApiPropertyOptional({
description: 'Whether to exclude zero values',
example: false,
})
noneZero: boolean; noneZero: boolean;
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@ApiPropertyOptional({
description: 'Array of vendor IDs to include',
example: [1, 2, 3],
})
vendorsIds: number[]; vendorsIds: number[];
} }

View File

@@ -1,12 +1,13 @@
import { Inject } from '@nestjs/common'; import { Inject, Injectable, Scope } from '@nestjs/common';
import { isEmpty, groupBy } from 'lodash'; import { isEmpty, groupBy } from 'lodash';
import { ModelObject } from 'objection';
import { Bill } from '@/modules/Bills/models/Bill'; import { Bill } from '@/modules/Bills/models/Bill';
import { Vendor } from '@/modules/Vendors/models/Vendor'; import { Vendor } from '@/modules/Vendors/models/Vendor';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { ModelObject } from 'objection';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
@Injectable({ scope: Scope.REQUEST })
export class APAgingSummaryRepository { export class APAgingSummaryRepository {
@Inject(Vendor.name) @Inject(Vendor.name)
private readonly vendorModel: TenantModelProxy<typeof Vendor>; private readonly vendorModel: TenantModelProxy<typeof Vendor>;
@@ -18,10 +19,10 @@ export class APAgingSummaryRepository {
private readonly tenancyContext: TenancyContext; private readonly tenancyContext: TenancyContext;
/** /**
* Filter. * A/P aging filter.
* @param {IAPAgingSummaryQuery} filter * @param {APAgingSummaryQueryDto} filter
*/ */
filter: IAPAgingSummaryQuery; filter: APAgingSummaryQueryDto;
/** /**
* Due bills. * Due bills.
@@ -45,7 +46,7 @@ export class APAgingSummaryRepository {
* Overdue bills by vendor id. * Overdue bills by vendor id.
* @param {Record<string, Bill[]>} overdueBillsByVendorId - Overdue bills by vendor id. * @param {Record<string, Bill[]>} overdueBillsByVendorId - Overdue bills by vendor id.
*/ */
overdueBillsByVendorId: ModelObject<Bill>[]; overdueBillsByVendorId: Record<string, Array<ModelObject<Bill>>>;
/** /**
* Vendors. * Vendors.
@@ -61,9 +62,9 @@ export class APAgingSummaryRepository {
/** /**
* Set the filter. * Set the filter.
* @param {IAPAgingSummaryQuery} filter * @param {APAgingSummaryQueryDto} filter
*/ */
setFilter(filter: IAPAgingSummaryQuery) { setFilter(filter: APAgingSummaryQueryDto) {
this.filter = filter; this.filter = filter;
} }

View File

@@ -1,14 +1,12 @@
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { import { IAPAgingSummarySheet } from './APAgingSummary.types';
IAPAgingSummaryQuery,
IAPAgingSummarySheet,
} from './APAgingSummary.types';
import { APAgingSummarySheet } from './APAgingSummarySheet'; import { APAgingSummarySheet } from './APAgingSummarySheet';
import { APAgingSummaryMeta } from './APAgingSummaryMeta'; import { APAgingSummaryMeta } from './APAgingSummaryMeta';
import { getAPAgingSummaryDefaultQuery } from './utils'; import { getAPAgingSummaryDefaultQuery } from './utils';
import { APAgingSummaryRepository } from './APAgingSummaryRepository'; import { APAgingSummaryRepository } from './APAgingSummaryRepository';
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
@Injectable() @Injectable()
export class APAgingSummaryService { export class APAgingSummaryService {
@@ -20,13 +18,12 @@ export class APAgingSummaryService {
/** /**
* Retrieve A/P aging summary report. * Retrieve A/P aging summary report.
* @param {IAPAgingSummaryQuery} query - A/P aging summary query. * @param {APAgingSummaryQueryDto} query - A/P aging summary query.
* @returns {Promise<IAPAgingSummarySheet>} * @returns {Promise<IAPAgingSummarySheet>}
*/ */
public async APAgingSummary( public async APAgingSummary(
query: IAPAgingSummaryQuery, query: APAgingSummaryQueryDto,
): Promise<IAPAgingSummarySheet> { ): Promise<IAPAgingSummarySheet> {
const filter = { const filter = {
...getAPAgingSummaryDefaultQuery(), ...getAPAgingSummaryDefaultQuery(),
...query, ...query,

View File

@@ -1,7 +1,6 @@
import { sum, isEmpty } from 'lodash'; import { sum, isEmpty } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import { import {
IAPAgingSummaryQuery,
IAPAgingSummaryData, IAPAgingSummaryData,
IAPAgingSummaryVendor, IAPAgingSummaryVendor,
IAPAgingSummaryColumns, IAPAgingSummaryColumns,
@@ -13,21 +12,24 @@ import { ModelObject } from 'objection';
import { Vendor } from '@/modules/Vendors/models/Vendor'; import { Vendor } from '@/modules/Vendors/models/Vendor';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed'; import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { APAgingSummaryRepository } from './APAgingSummaryRepository'; import { APAgingSummaryRepository } from './APAgingSummaryRepository';
import { Bill } from '@/modules/Bills/models/Bill';
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
export class APAgingSummarySheet extends AgingSummaryReport { export class APAgingSummarySheet extends AgingSummaryReport {
readonly repository: APAgingSummaryRepository; readonly repository: APAgingSummaryRepository;
readonly query: IAPAgingSummaryQuery; readonly query: APAgingSummaryQueryDto;
readonly agingPeriods: IAgingPeriod[]; readonly agingPeriods: IAgingPeriod[];
readonly overdueInvoicesByContactId: Record<string, Array<ModelObject<Bill>>>;
readonly currentInvoicesByContactId: Record<number, Array<ModelObject<Bill>>>;
/** /**
* Constructor method. * Constructor method.
* @param {number} tenantId - Tenant id. * @param {APAgingSummaryQueryDto} query - Report query.
* @param {IAPAgingSummaryQuery} query - Report query. * @param {APAgingSummaryRepository} repository - Repository
* @param {ModelObject<Vendor>[]} vendors - Unpaid bills.
* @param {string} baseCurrency - Base currency of the organization.
*/ */
constructor( constructor(
query: IAPAgingSummaryQuery, query: APAgingSummaryQueryDto,
repository: APAgingSummaryRepository, repository: APAgingSummaryRepository,
) { ) {
super(); super();
@@ -36,6 +38,9 @@ export class APAgingSummarySheet extends AgingSummaryReport {
this.repository = repository; this.repository = repository;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.overdueInvoicesByContactId = this.repository.overdueBillsByVendorId;
this.currentInvoicesByContactId = this.repository.dueBillsByVendorId;
// Initializes the aging periods. // Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods( this.agingPeriods = this.agingRangePeriods(
this.query.asDate, this.query.asDate,

View File

@@ -1,11 +1,9 @@
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n'; import { I18nService } from 'nestjs-i18n';
import { import { IAPAgingSummaryTable } from './APAgingSummary.types';
IAPAgingSummaryQuery,
IAPAgingSummaryTable,
} from './APAgingSummary.types';
import { APAgingSummaryService } from './APAgingSummaryService'; import { APAgingSummaryService } from './APAgingSummaryService';
import { APAgingSummaryTable } from './APAgingSummaryTable'; import { APAgingSummaryTable } from './APAgingSummaryTable';
import { Injectable } from '@nestjs/common'; import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
@Injectable() @Injectable()
export class APAgingSummaryTableInjectable { export class APAgingSummaryTableInjectable {
@@ -16,11 +14,11 @@ export class APAgingSummaryTableInjectable {
/** /**
* Retrieves A/P aging summary in table format. * Retrieves A/P aging summary in table format.
* @param {IAPAgingSummaryQuery} query - * @param {APAgingSummaryQueryDto} query -
* @returns {Promise<IAPAgingSummaryTable>} * @returns {Promise<IAPAgingSummaryTable>}
*/ */
public async table( public async table(
query: IAPAgingSummaryQuery, query: APAgingSummaryQueryDto,
): Promise<IAPAgingSummaryTable> { ): Promise<IAPAgingSummaryTable> {
const report = await this.APAgingSummarySheet.APAgingSummary(query); const report = await this.APAgingSummarySheet.APAgingSummary(query);
const table = new APAgingSummaryTable(report.data, query, this.i18nService); const table = new APAgingSummaryTable(report.data, query, this.i18nService);

View File

@@ -1,3 +1,5 @@
import * as moment from 'moment';
export const getAPAgingSummaryDefaultQuery = () => { export const getAPAgingSummaryDefaultQuery = () => {
return { return {
asDate: moment().format('YYYY-MM-DD'), asDate: moment().format('YYYY-MM-DD'),

View File

@@ -1,10 +1,10 @@
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { Controller, Get, Headers } from '@nestjs/common'; import { Controller, Get, Headers } from '@nestjs/common';
import { Query, Res } from '@nestjs/common'; import { Query, Res } from '@nestjs/common';
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication'; import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express'; import { Response } from 'express';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
@Controller('reports/receivable-aging-summary') @Controller('reports/receivable-aging-summary')
@ApiTags('Reports') @ApiTags('Reports')
@@ -14,7 +14,7 @@ export class ARAgingSummaryController {
@Get() @Get()
@ApiOperation({ summary: 'Get receivable aging summary' }) @ApiOperation({ summary: 'Get receivable aging summary' })
public async get( public async get(
@Query() filter: IARAgingSummaryQuery, @Query() filter: ARAgingSummaryQueryDto,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Headers('accept') acceptHeader: string, @Headers('accept') acceptHeader: string,
) { ) {

View File

@@ -1,16 +1,12 @@
import { import {
IAgingPeriod, IAgingPeriod,
IAgingSummaryQuery,
IAgingSummaryTotal, IAgingSummaryTotal,
IAgingSummaryContact, IAgingSummaryContact,
IAgingSummaryData, IAgingSummaryData,
IAgingSummaryMeta, IAgingSummaryMeta,
} from '../AgingSummary/AgingSummary.types'; } from '../AgingSummary/AgingSummary.types';
import { IFinancialTable } from '../../types/Table.types'; import { IFinancialTable } from '../../types/Table.types';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
export interface IARAgingSummaryQuery extends IAgingSummaryQuery {
customersIds: number[];
}
export interface IARAgingSummaryCustomer extends IAgingSummaryContact { export interface IARAgingSummaryCustomer extends IAgingSummaryContact {
customerName: string; customerName: string;
@@ -31,13 +27,12 @@ export interface IARAgingSummaryMeta extends IAgingSummaryMeta {
export interface IARAgingSummaryTable extends IFinancialTable { export interface IARAgingSummaryTable extends IFinancialTable {
meta: IARAgingSummaryMeta; meta: IARAgingSummaryMeta;
query: IARAgingSummaryQuery; query: ARAgingSummaryQueryDto;
} }
export interface IARAgingSummarySheet { export interface IARAgingSummarySheet {
data: IARAgingSummaryData; data: IARAgingSummaryData;
meta: IARAgingSummaryMeta; meta: IARAgingSummaryMeta;
query: IARAgingSummaryQuery; query: ARAgingSummaryQueryDto;
columns: IARAgingSummaryColumns; columns: IARAgingSummaryColumns;
} }

View File

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

View File

@@ -1,7 +1,7 @@
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable'; import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { TableSheet } from '../../common/TableSheet'; import { TableSheet } from '../../common/TableSheet';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
@Injectable() @Injectable()
export class ARAgingSummaryExportInjectable { export class ARAgingSummaryExportInjectable {
@@ -11,12 +11,10 @@ export class ARAgingSummaryExportInjectable {
/** /**
* Retrieves the A/R aging summary sheet in XLSX format. * Retrieves the A/R aging summary sheet in XLSX format.
* @param {IARAgingSummaryQuery} query - A/R aging summary query. * @param {ARAgingSummaryQueryDto} query - A/R aging summary query.
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
public async xlsx( public async xlsx(query: ARAgingSummaryQueryDto): Promise<Buffer> {
query: IARAgingSummaryQuery
): Promise<Buffer> {
const table = await this.ARAgingSummaryTable.table(query); const table = await this.ARAgingSummaryTable.table(query);
const tableSheet = new TableSheet(table.table); const tableSheet = new TableSheet(table.table);
@@ -27,12 +25,10 @@ export class ARAgingSummaryExportInjectable {
/** /**
* Retrieves the A/R aging summary sheet in CSV format. * Retrieves the A/R aging summary sheet in CSV format.
* @param {IARAgingSummaryQuery} query - A/R aging summary query. * @param {ARAgingSummaryQueryDto} query - A/R aging summary query.
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
public async csv( public async csv(query: ARAgingSummaryQueryDto): Promise<string> {
query: IARAgingSummaryQuery
): Promise<string> {
const table = await this.ARAgingSummaryTable.table(query); const table = await this.ARAgingSummaryTable.table(query);
const tableSheet = new TableSheet(table.table); const tableSheet = new TableSheet(table.table);

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable'; import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { TableSheetPdf } from '../../common/TableSheetPdf'; import { TableSheetPdf } from '../../common/TableSheetPdf';
import { HtmlTableCss } from '../AgingSummary/_constants'; import { HtmlTableCss } from '../AgingSummary/_constants';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
@Injectable() @Injectable()
export class ARAgingSummaryPdfInjectable { export class ARAgingSummaryPdfInjectable {
@@ -16,7 +16,7 @@ export class ARAgingSummaryPdfInjectable {
* @param {IBalanceSheetQuery} query - Balance sheet query. * @param {IBalanceSheetQuery} query - Balance sheet query.
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
public async pdf(query: IARAgingSummaryQuery): Promise<Buffer> { public async pdf(query: ARAgingSummaryQueryDto): Promise<Buffer> {
const table = await this.ARAgingSummaryTable.table(query); const table = await this.ARAgingSummaryTable.table(query);
return this.tableSheetPdf.convertToPdf( return this.tableSheetPdf.convertToPdf(

View File

@@ -0,0 +1,74 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import {
IsNumber,
IsOptional,
IsDateString,
IsBoolean,
ValidateNested,
IsArray,
} from 'class-validator';
import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto';
import { ToNumber } from '@/common/decorators/Validators';
import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto';
import { parseBoolean } from '@/utils/parse-boolean';
export class ARAgingSummaryQueryDto extends FinancialSheetBranchesQueryDto {
@IsDateString()
@IsOptional()
@ApiPropertyOptional({
description: 'The date as of which the A/R aging summary is calculated',
example: '2024-06-01',
})
asDate: Date | string;
@ToNumber()
@IsNumber()
@IsOptional()
@ApiPropertyOptional({
description: 'Number of days before the aging period starts',
example: 30,
})
agingDaysBefore: number;
@ToNumber()
@IsNumber()
@IsOptional()
@ApiPropertyOptional({
description: 'Number of aging periods to calculate',
example: 4,
})
agingPeriods: number;
@IsOptional()
@ValidateNested()
@Type(() => NumberFormatQueryDto)
@ApiPropertyOptional({
description: 'Number format configuration',
example: {
precision: 2,
divideOn1000: false,
showZero: true,
formatMoney: 'total',
negativeFormat: 'parentheses',
},
})
numberFormat: NumberFormatQueryDto;
@Transform(({ value }) => parseBoolean(value, false))
@IsBoolean()
@IsOptional()
@ApiPropertyOptional({
description: 'Whether to exclude zero values',
example: false,
})
noneZero: boolean;
@IsArray()
@IsOptional()
@ApiPropertyOptional({
description: 'Array of customer IDs to include',
example: [1, 2, 3],
})
customersIds: number[];
}

View File

@@ -1,12 +1,13 @@
import { Inject } from '@nestjs/common'; import { Inject, Injectable, Scope } from '@nestjs/common';
import { isEmpty, groupBy } from 'lodash'; import { isEmpty, groupBy } from 'lodash';
import { Customer } from '@/modules/Customers/models/Customer'; import { Customer } from '@/modules/Customers/models/Customer';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
@Injectable({ scope: Scope.REQUEST })
export class ARAgingSummaryRepository { export class ARAgingSummaryRepository {
@Inject(TenancyContext) @Inject(TenancyContext)
private tenancyContext: TenancyContext; private tenancyContext: TenancyContext;
@@ -19,9 +20,9 @@ export class ARAgingSummaryRepository {
/** /**
* Filter. * Filter.
* @param {IARAgingSummaryQuery} filter * @param {ARAgingSummaryQueryDto} filter
*/ */
filter: IARAgingSummaryQuery; filter: ARAgingSummaryQueryDto;
/** /**
* Base currency. * Base currency.
@@ -61,9 +62,9 @@ export class ARAgingSummaryRepository {
/** /**
* Set the filter. * Set the filter.
* @param {IARAgingSummaryQuery} filter * @param {ARAgingSummaryQueryDto} filter
*/ */
setFilter(filter: IARAgingSummaryQuery) { setFilter(filter: ARAgingSummaryQueryDto) {
this.filter = filter; this.filter = filter;
} }
@@ -139,9 +140,6 @@ export class ARAgingSummaryRepository {
.onBuild(commonQuery); .onBuild(commonQuery);
this.currentInvoices = currentInvoices; this.currentInvoices = currentInvoices;
this.currentInvoicesByContactId = groupBy( this.currentInvoicesByContactId = groupBy(currentInvoices, 'customerId');
currentInvoices,
'customerId',
);
} }
} }

View File

@@ -3,9 +3,9 @@ import { ARAgingSummaryMeta } from './ARAgingSummaryMeta';
import { getARAgingSummaryDefaultQuery } from './utils'; import { getARAgingSummaryDefaultQuery } from './utils';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository'; import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
@Injectable() @Injectable()
export class ARAgingSummaryService { export class ARAgingSummaryService {
@@ -17,9 +17,9 @@ export class ARAgingSummaryService {
/** /**
* Retrieve A/R aging summary report. * Retrieve A/R aging summary report.
* @param {IARAgingSummaryQuery} query - * @param {ARAgingSummaryQueryDto} query -
*/ */
async ARAgingSummary(query: IARAgingSummaryQuery) { async ARAgingSummary(query: ARAgingSummaryQueryDto) {
const filter = { const filter = {
...getARAgingSummaryDefaultQuery(), ...getARAgingSummaryDefaultQuery(),
...query, ...query,
@@ -28,12 +28,12 @@ export class ARAgingSummaryService {
this.ARAgingSummaryRepository.setFilter(filter); this.ARAgingSummaryRepository.setFilter(filter);
await this.ARAgingSummaryRepository.load(); await this.ARAgingSummaryRepository.load();
// AR aging summary report instance. // A/R aging summary report instance.
const ARAgingSummaryReport = new ARAgingSummarySheet( const ARAgingSummaryReport = new ARAgingSummarySheet(
filter, filter,
this.ARAgingSummaryRepository, this.ARAgingSummaryRepository,
); );
// AR aging summary report data and columns. // A/R aging summary report data and columns.
const data = ARAgingSummaryReport.reportData(); const data = ARAgingSummaryReport.reportData();
const columns = ARAgingSummaryReport.reportColumns(); const columns = ARAgingSummaryReport.reportColumns();

View File

@@ -2,7 +2,6 @@ import * as R from 'ramda';
import { isEmpty, sum } from 'lodash'; import { isEmpty, sum } from 'lodash';
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types'; import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
import { import {
IARAgingSummaryQuery,
IARAgingSummaryCustomer, IARAgingSummaryCustomer,
IARAgingSummaryData, IARAgingSummaryData,
IARAgingSummaryColumns, IARAgingSummaryColumns,
@@ -11,23 +10,31 @@ import {
import { AgingSummaryReport } from '../AgingSummary/AgingSummary'; import { AgingSummaryReport } from '../AgingSummary/AgingSummary';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed'; import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { Customer } from '@/modules/Customers/models/Customer';
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository'; import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
import { Customer } from '@/modules/Customers/models/Customer';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
export class ARAgingSummarySheet extends AgingSummaryReport { export class ARAgingSummarySheet extends AgingSummaryReport {
readonly query: IARAgingSummaryQuery; readonly query: ARAgingSummaryQueryDto;
readonly agingPeriods: IAgingPeriod[]; readonly agingPeriods: IAgingPeriod[];
readonly repository: ARAgingSummaryRepository; readonly repository: ARAgingSummaryRepository;
readonly overdueInvoicesByContactId: Record<
string,
ModelObject<SaleInvoice>[]
>;
readonly currentInvoicesByContactId: Record<
number,
Array<ModelObject<SaleInvoice>>
>;
/** /**
* Constructor method. * Constructor method.
* @param {number} tenantId * @param {ARAgingSummaryQueryDto} query - Query
* @param {IARAgingSummaryQuery} query * @param {ARAgingSummaryRepository} repository - Repository.
* @param {ICustomer[]} customers
* @param {IJournalPoster} journal
*/ */
constructor( constructor(
query: IARAgingSummaryQuery, query: ARAgingSummaryQueryDto,
repository: ARAgingSummaryRepository, repository: ARAgingSummaryRepository,
) { ) {
super(); super();
@@ -36,6 +43,11 @@ export class ARAgingSummarySheet extends AgingSummaryReport {
this.repository = repository; this.repository = repository;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.overdueInvoicesByContactId =
this.repository.overdueInvoicesByContactId;
this.currentInvoicesByContactId =
this.repository.currentInvoicesByContactId;
// Initializes the aging periods. // Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods( this.agingPeriods = this.agingRangePeriods(
this.query.asDate, this.query.asDate,

View File

@@ -1,10 +1,8 @@
import { ARAgingSummaryTable } from './ARAgingSummaryTable'; import { ARAgingSummaryTable } from './ARAgingSummaryTable';
import { ARAgingSummaryService } from './ARAgingSummaryService'; import { ARAgingSummaryService } from './ARAgingSummaryService';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import { IARAgingSummaryTable } from './ARAgingSummary.types';
IARAgingSummaryQuery, import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
IARAgingSummaryTable,
} from './ARAgingSummary.types';
@Injectable() @Injectable()
export class ARAgingSummaryTableInjectable { export class ARAgingSummaryTableInjectable {
@@ -12,11 +10,11 @@ export class ARAgingSummaryTableInjectable {
/** /**
* Retrieves A/R aging summary in table format. * Retrieves A/R aging summary in table format.
* @param {IARAgingSummaryQuery} query - Aging summary query. * @param {ARAgingSummaryQueryDto} query - Aging summary query.
* @returns {Promise<IARAgingSummaryTable>} * @returns {Promise<IARAgingSummaryTable>}
*/ */
public async table( public async table(
query: IARAgingSummaryQuery, query: ARAgingSummaryQueryDto,
): Promise<IARAgingSummaryTable> { ): Promise<IARAgingSummaryTable> {
const report = await this.ARAgingSummarySheet.ARAgingSummary(query); const report = await this.ARAgingSummarySheet.ARAgingSummary(query);
const table = new ARAgingSummaryTable(report.data, query, {}); const table = new ARAgingSummaryTable(report.data, query, {});

View File

@@ -13,7 +13,7 @@ export class CustomerBalanceSummaryPdf {
/** /**
* Converts the given customer balance summary sheet table to pdf. * Converts the given customer balance summary sheet table to pdf.
* @param {IAPAgingSummaryQuery} query - Balance sheet query. * @param {ICustomerBalanceSummaryQuery} query - Balance sheet query.
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
public async pdf(query: ICustomerBalanceSummaryQuery): Promise<Buffer> { public async pdf(query: ICustomerBalanceSummaryQuery): Promise<Buffer> {