diff --git a/packages/server-nest/src/common/types/Constructor.ts b/packages/server-nest/src/common/types/Constructor.ts index 373c3f94e..2afa4fbe3 100644 --- a/packages/server-nest/src/common/types/Constructor.ts +++ b/packages/server-nest/src/common/types/Constructor.ts @@ -1,3 +1,2 @@ - export type Constructor = new (...args: any[]) => {}; -export type GConstructor = new (...args: any[]) => T; \ No newline at end of file +export type GConstructor = new (...args: any[]) => T; diff --git a/packages/server-nest/src/common/types/Date.ts b/packages/server-nest/src/common/types/Date.ts new file mode 100644 index 000000000..c6c114a7b --- /dev/null +++ b/packages/server-nest/src/common/types/Date.ts @@ -0,0 +1,3 @@ +import * as moment from 'moment'; + +export type DateInput = moment.MomentInput; \ No newline at end of file diff --git a/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts b/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts index c4d2ed713..921722b90 100644 --- a/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts +++ b/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts @@ -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 {} - diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts index 7661eef92..27e8e09a9 100644 --- a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts @@ -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 } : {}), diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.controller.ts new file mode 100644 index 000000000..95a68be3d --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.controller.ts @@ -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); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.module.ts new file mode 100644 index 000000000..0d7e72bf0 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.module.ts @@ -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 {} + diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.types.ts new file mode 100644 index 000000000..281594f55 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummary.types.ts @@ -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; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryApplication.ts new file mode 100644 index 000000000..537e38560 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryApplication.ts @@ -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} + */ + public pdf(query: IAPAgingSummaryQuery) { + return this.APAgingSumaryPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryExportInjectable.ts new file mode 100644 index 000000000..9bf249fd5 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryExportInjectable.ts @@ -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} + */ + 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} + */ + public async csv(query: IAPAgingSummaryQuery): Promise { + const table = await this.APAgingSummaryTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryMeta.ts new file mode 100644 index 000000000..4bcb1f4e2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryMeta.ts @@ -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 { + const commonMeta = await this.agingSummaryMeta.meta(query); + + return { + ...commonMeta, + sheetName: 'A/P Aging Summary', + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryPdfInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryPdfInjectable.ts new file mode 100644 index 000000000..f319442b4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryPdfInjectable.ts @@ -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} + */ + public async pdf(query: IAPAgingSummaryQuery): Promise { + const table = await this.APAgingSummaryTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedAsDate, + HtmlTableCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryRepository.ts new file mode 100644 index 000000000..69b03a2d0 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryRepository.ts @@ -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); + } +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryService.ts new file mode 100644 index 000000000..a1e121572 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryService.ts @@ -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} + */ + public async APAgingSummary( + query: IAPAgingSummaryQuery, + ): Promise { + + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummarySheet.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummarySheet.ts new file mode 100644 index 000000000..38a4ef6fb --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummarySheet.ts @@ -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[]; + readonly unpaidBills: ModelObject[]; + readonly baseCurrency: string; + + readonly overdueInvoicesByContactId: Record>>; + readonly currentInvoicesByContactId: Record>>; + + readonly agingPeriods: IAgingPeriod[]; + + /** + * Constructor method. + * @param {number} tenantId - Tenant id. + * @param {IAPAgingSummaryQuery} query - Report query. + * @param {ModelObject[]} vendors - Unpaid bills. + * @param {string} baseCurrency - Base currency of the organization. + */ + constructor( + tenantId: number, + query: IAPAgingSummaryQuery, + vendors: ModelObject[], + overdueBills: ModelObject[], + unpaidBills: ModelObject[], + 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 + * @return {IAPAgingSummaryVendor} + */ + private vendorTransformer = ( + vendor: ModelObject, + ): 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[]} vendors + * @returns {IAPAgingSummaryVendor[]} + */ + private vendorsMapper = ( + vendors: ModelObject[], + ): 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[], + ): 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; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryTable.ts new file mode 100644 index 000000000..9d12d0d0e --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryTable.ts @@ -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' }; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryTableInjectable.ts new file mode 100644 index 000000000..d83655205 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/APAgingSummaryTableInjectable.ts @@ -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} + */ + public async table( + query: IAPAgingSummaryQuery, + ): Promise { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/utils.ts new file mode 100644 index 000000000..2fe0a85a8 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/APAgingSummary/utils.ts @@ -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, + }; +}; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.controller.ts new file mode 100644 index 000000000..a3e41529a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.controller.ts @@ -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); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.module.ts new file mode 100644 index 000000000..98ff39b73 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.module.ts @@ -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 {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.types.ts new file mode 100644 index 000000000..638a285b7 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummary.types.ts @@ -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; +} + diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryApplication.ts new file mode 100644 index 000000000..908e29163 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryApplication.ts @@ -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} + */ + public pdf(query: IARAgingSummaryQuery) { + return this.ARAgingSummaryPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryExportInjectable.ts new file mode 100644 index 000000000..3730c97d1 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryExportInjectable.ts @@ -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} + */ + public async xlsx( + query: IARAgingSummaryQuery + ): Promise { + 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} + */ + public async csv( + query: IARAgingSummaryQuery + ): Promise { + const table = await this.ARAgingSummaryTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryMeta.ts new file mode 100644 index 000000000..c10c96597 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryMeta.ts @@ -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 { + const commonMeta = await this.agingSummaryMeta.meta(query); + + return { + ...commonMeta, + sheetName: 'A/R Aging Summary', + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryPdfInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryPdfInjectable.ts new file mode 100644 index 000000000..29c118ee2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryPdfInjectable.ts @@ -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} + */ + public async pdf(query: IARAgingSummaryQuery): Promise { + const table = await this.ARAgingSummaryTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryRepository.ts new file mode 100644 index 000000000..81181e9ed --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryRepository.ts @@ -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); + + } +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryService.ts new file mode 100644 index 000000000..aed7cc30a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryService.ts @@ -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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummarySheet.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummarySheet.ts new file mode 100644 index 000000000..9cef473ce --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummarySheet.ts @@ -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[]; + readonly agingPeriods: IAgingPeriod[]; + readonly baseCurrency: string; + + readonly overdueInvoicesByContactId: Dictionary[]>; + readonly currentInvoicesByContactId: Dictionary[]>; + + /** + * Constructor method. + * @param {number} tenantId + * @param {IARAgingSummaryQuery} query + * @param {ICustomer[]} customers + * @param {IJournalPoster} journal + */ + constructor( + tenantId: number, + query: IARAgingSummaryQuery, + customers: ModelObject[], + overdueSaleInvoices: ModelObject[], + currentSaleInvoices: ModelObject[], + 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, + ): 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[], + ): 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[], + ): 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; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryTable.ts new file mode 100644 index 000000000..45ae58963 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryTable.ts @@ -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' }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryTableInjectable.ts new file mode 100644 index 000000000..b0e977299 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/ARAgingSummaryTableInjectable.ts @@ -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} + */ + public async table( + query: IARAgingSummaryQuery, + ): Promise { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/utils.ts new file mode 100644 index 000000000..674f81928 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/ARAgingSummary/utils.ts @@ -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, + }; +}; \ No newline at end of file diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingReport.ts b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingReport.ts new file mode 100644 index 000000000..2f2cfe310 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingReport.ts @@ -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; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.module.ts new file mode 100644 index 000000000..ce88d5764 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { AgingSummaryMeta } from './AgingSummaryMeta'; + +@Module({ + exports: [AgingSummaryMeta], + providers: [AgingSummaryMeta], +}) +export class AgingSummaryModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.ts b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.ts new file mode 100644 index 000000000..137de738c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.ts @@ -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[]; + readonly agingPeriods: IAgingPeriod[] = []; + readonly baseCurrency: string; + readonly query: IAgingSummaryQuery; + readonly overdueInvoicesByContactId: Record< + number, + Array> + >; + readonly currentInvoicesByContactId: Record< + number, + Array> + >; + + /** + * 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 | ModelObject)[] { + 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 | ModelObject)[] { + 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); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.types.ts new file mode 100644 index 000000000..4e76d2768 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummary.types.ts @@ -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; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummaryMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummaryMeta.ts new file mode 100644 index 000000000..e3bf32736 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummaryMeta.ts @@ -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 { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummaryTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummaryTable.ts new file mode 100644 index 000000000..9f08ebbf7 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/AgingSummaryTable.ts @@ -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' }, + ]); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/_constants.ts b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/_constants.ts new file mode 100644 index 000000000..fd818e160 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/AgingSummary/_constants.ts @@ -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; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.controller.ts new file mode 100644 index 000000000..8b802df96 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.controller.ts @@ -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); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.module.ts new file mode 100644 index 000000000..1d792d4f7 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.module.ts @@ -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 {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.ts new file mode 100644 index 000000000..f796f23da --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.ts @@ -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; + readonly openingBalanceTransactions: Map; + 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); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.types.ts new file mode 100644 index 000000000..27b2e99b9 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetails.types.ts @@ -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 { + 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; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsApplication.ts new file mode 100644 index 000000000..95a02da2d --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsApplication.ts @@ -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} + */ + 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 { + return this.inventoryDetailsTable.table(query); + } + + /** + * Retrieves the inventory details report in XLSX format. + * @param {IInventoryDetailsQuery} query + * @returns {Promise} + */ + public xlsx(query: IInventoryDetailsQuery): Promise { + return this.inventoryDetailsExport.xlsx(query); + } + + /** + * Retrieves the inventory details report in CSV format. + * @param {IInventoryDetailsQuery} query + * @returns {Promise} + */ + public csv(query: IInventoryDetailsQuery): Promise { + return this.inventoryDetailsExport.csv(query); + } + + /** + * Retrieves the inventory details report in PDF format. + * @param {IInventoryDetailsQuery} query + * @returns {Promise} + */ + public pdf(query: IInventoryDetailsQuery) { + return this.inventoryDetailsPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsExportInjectable.ts new file mode 100644 index 000000000..1f8520f47 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsExportInjectable.ts @@ -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} + */ + 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} + */ + public async csv(query: IInventoryDetailsQuery): Promise { + const table = await this.inventoryDetailsTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsMeta.ts new file mode 100644 index 000000000..7515e1917 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsMeta.ts @@ -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 { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsRepository.ts new file mode 100644 index 000000000..495ec3125 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsRepository.ts @@ -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>} + */ + public async getInventoryItems( + itemsIds?: number[], + ): Promise[]> { + 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>} + */ + public async openingBalanceTransactions( + filter: IInventoryDetailsQuery, + ): Promise[]> { + 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} + */ + public async itemInventoryTransactions( + filter: IInventoryDetailsQuery, + ): Promise[]> { + 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; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsService.ts new file mode 100644 index 000000000..1a4204c58 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsService.ts @@ -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} + */ + public async inventoryDetails( + tenantId: number, + query: IInventoryDetailsQuery + ): Promise { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTable.ts new file mode 100644 index 000000000..ad95fd9aa --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTable.ts @@ -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') }, + ]; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTableInjectable.ts new file mode 100644 index 000000000..41580baac --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTableInjectable.ts @@ -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} + */ + public async table( + query: IInventoryDetailsQuery, + ): Promise { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTablePdf.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTablePdf.ts new file mode 100644 index 000000000..da52e73d9 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/InventoryItemDetailsTablePdf.ts @@ -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} + */ + public async pdf(query: IInventoryDetailsQuery): Promise { + const table = await this.inventoryDetailsTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCustomCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/constant.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/constant.ts new file mode 100644 index 000000000..eeb199236 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryItemDetails/constant.ts @@ -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; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuation.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuation.controller.ts new file mode 100644 index 000000000..4c3cf14b5 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuation.controller.ts @@ -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); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.module.ts new file mode 100644 index 000000000..b80464e09 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.module.ts @@ -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 {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.ts new file mode 100644 index 000000000..98452d349 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.ts @@ -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[]; + readonly INInventoryCostLots: Map; + readonly OUTInventoryCostLots: Map; + readonly baseCurrency: string; + + /** + * Constructor method. + * @param {IInventoryValuationReportQuery} query + * @param {ModelObject[]} items + * @param {Map} INInventoryCostLots + * @param {Map} OUTInventoryCostLots + * @param {string} baseCurrency + */ + constructor( + query: IInventoryValuationReportQuery, + items: ModelObject[], + INInventoryCostLots: Map, + OUTInventoryCostLots: Map, + 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} transactionsMap + * @param {number} itemId + * @returns + */ + private getItemTransaction( + transactionsMap: Map, + 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): 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 }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.types.ts new file mode 100644 index 000000000..33d219ebf --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheet.types.ts @@ -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; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetApplication.ts new file mode 100644 index 000000000..977744d82 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetApplication.ts @@ -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 { + return this.inventoryValuationSheet.inventoryValuationSheet(query); + } + + /** + * Retrieves the inventory valuation json table format. + * @param {number} tenantId + * @param {IInventoryValuationReportQuery} query + * @returns {Promise} + */ + public table( + query: IInventoryValuationReportQuery, + ): Promise { + return this.inventoryValuationTable.table(query); + } + + /** + * Retrieves the inventory valuation xlsx format. + * @param {number} tenantId + * @param {IInventoryValuationReportQuery} query + * @returns + */ + public xlsx(query: IInventoryValuationReportQuery): Promise { + return this.inventoryValuationExport.xlsx(query); + } + + /** + * Retrieves the inventory valuation csv format. + * @param {number} tenantId + * @param {IInventoryValuationReportQuery} query + * @returns + */ + public csv(query: IInventoryValuationReportQuery): Promise { + return this.inventoryValuationExport.csv(query); + } + + /** + * Retrieves the inventory valuation pdf format. + * @param {number} tenantId + * @param {IInventoryValuationReportQuery} query + * @returns {Promise} + */ + public pdf(query: IInventoryValuationReportQuery): Promise { + return this.inventoryValuationPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetExportable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetExportable.ts new file mode 100644 index 000000000..88a5116cf --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetExportable.ts @@ -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} + */ + public async xlsx(query: IInventoryValuationReportQuery): Promise { + 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} + */ + public async csv(query: IInventoryValuationReportQuery): Promise { + const table = await this.inventoryValuationTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetMeta.ts new file mode 100644 index 000000000..d383eaf09 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetMeta.ts @@ -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} + */ + public async meta( + query: IInventoryValuationReportQuery, + ): Promise { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetPdf.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetPdf.ts new file mode 100644 index 000000000..cf9e00033 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetPdf.ts @@ -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} + */ + public async pdf(query: IInventoryValuationReportQuery): Promise { + const table = await this.inventoryValuationTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCustomCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetRepository.ts new file mode 100644 index 000000000..4fe0c800a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetRepository.ts @@ -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'); + + + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetService.ts new file mode 100644 index 000000000..80c112370 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetService.ts @@ -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 { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetTable.ts new file mode 100644 index 000000000..c855a515f --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetTable.ts @@ -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); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetTableInjectable.ts new file mode 100644 index 000000000..75c0fd466 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/InventoryValuationSheetTableInjectable.ts @@ -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} + */ + public async table( + filter: IInventoryValuationReportQuery, + ): Promise { + const { data, query, meta } = + await this.sheet.inventoryValuationSheet(filter); + const table = new InventoryValuationSheetTable(data); + + return { + table: { + columns: table.tableColumns(), + rows: table.tableRows(), + }, + query, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/_constants.ts b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/_constants.ts new file mode 100644 index 000000000..09c379f2d --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/InventoryValuationSheet/_constants.ts @@ -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: [], + }; +}; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/SalesByItems/SalesByItemsTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/SalesByItems/SalesByItemsTable.ts index 2139ce70d..bd7a445bb 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/SalesByItems/SalesByItemsTable.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/SalesByItems/SalesByItemsTable.ts @@ -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; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.ts index 88f8390b3..0361d2d9d 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.ts @@ -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, }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactRepository.ts index d4c490610..eeef88bf1 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactRepository.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactRepository.ts @@ -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; } diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.controller.ts new file mode 100644 index 000000000..da6f6fd9f --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.controller.ts @@ -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); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.module.ts index cb5534a0a..624d06352 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.module.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.module.ts @@ -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 {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersMeta.ts index 04e16f1b2..8f003cd09 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersMeta.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersMeta.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import * as moment from 'moment'; import { Injectable } from '@nestjs/common'; import { FinancialSheetMeta } from '../../common/FinancialSheetMeta'; import { diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts index b7fad1e1e..548f4dd66 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts @@ -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} */ private async getCustomersPeriodsEntries( - fromDate: Date | string, - toDate: Date | string, + fromDate: DateInput, + toDate: DateInput, ): Promise { 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} */ @@ -187,7 +202,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo } /** - * Retrieve the accounts receivable. + * Retrieves the accounts receivable. * @returns {Promise} */ public async getReceivableAccounts(): Promise { @@ -204,7 +219,7 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo * @returns {Promise} */ public async getCustomersOpeningBalanceTransactions( - openingDate: Date, + openingDate: DateInput, customersIds?: number[], ): Promise { 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} */ public async getCustomersPeriodTransactions( - fromDate: Date, - toDate: Date, + fromDate: DateInput, + toDate: DateInput, ): Promise { const receivableAccounts = await this.getReceivableAccounts(); const receivableAccountsIds = map(receivableAccounts, 'id'); diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersService.ts index 958181a43..132a5937e 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersService.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersService.ts @@ -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 { - 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 { diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTable.ts index dd78e725a..1ba956ac6 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTable.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTable.ts @@ -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); }; /** diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts index a485ca03d..560f44a71 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts @@ -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} + * @param {ITransactionsByCustomersFilter} filter - Filter object. + * @returns {Promise} - Transactions by customers table. */ public async table( filter: ITransactionsByCustomersFilter, diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/utils.ts index 286615bda..a434b5590 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/utils.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/utils.ts @@ -1,5 +1,4 @@ - - +import * as moment from 'moment'; export const getTransactionsByCustomerDefaultQuery = () => { return { diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionByReference.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionByReference.module.ts index 600593574..347878b51 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionByReference.module.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionByReference.module.ts @@ -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], }) diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.controller.ts index 9f865ea3f..34c393cf4 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.controller.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.controller.ts @@ -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, diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.types.ts index 146a4cb74..8f942daef 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.types.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.types.ts @@ -9,7 +9,14 @@ export interface ITransactionsByReferenceAmount { currencyCode: string; } +interface ITransactionsByReferenceDate { + formattedDate: string; + date: Date; +} + export interface ITransactionsByReferenceTransaction { + date: ITransactionsByReferenceDate; + credit: ITransactionsByReferenceAmount; debit: ITransactionsByReferenceAmount; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceReport.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceReport.ts index e89483480..e1f5f8ec6 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceReport.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceReport.ts @@ -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, diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.ts index b2514749c..1c23a857c 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.ts @@ -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, ): 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[], ): ITransactionsByVendorsVendor[] { return R.compose( diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorApplication.ts index 25874d64e..c7650e15c 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorApplication.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorApplication.ts @@ -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} */ @@ -67,7 +66,6 @@ export class TransactionsByVendorApplication { /** * Retrieves the transactions by vendor in PDF format. - * @param {number} tenantId * @param {ITransactionsByVendorsFilter} query * @returns {Promise} */ diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorInjectable.ts index 59b83a3e3..ab9afe0bd 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorInjectable.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorInjectable.ts @@ -30,6 +30,12 @@ export class TransactionsByVendorsInjectable { ): Promise { 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, diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts index 633b4e2b4..0c1aeed79 100644 --- a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts @@ -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} + */ 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 { const transactions = await this.getVendorsPeriodTransactions( fromDate, @@ -172,8 +180,8 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit * @returns {Promise} */ public async getReportEntries( - fromDate: moment.MomentInput, - toDate: moment.MomentInput, + fromDate: DateInput, + toDate: DateInput, ): Promise { const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate(); @@ -240,8 +248,8 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit * @returns {Promise} */ public async getVendorsPeriodTransactions( - fromDate: moment.MomentInput, - toDate: moment.MomentInput, + fromDate: DateInput, + toDate: DateInput, ): Promise { const receivableAccounts = await this.getPayableAccounts(); const receivableAccountsIds = map(receivableAccounts, 'id'); diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.controller.ts new file mode 100644 index 000000000..9282d12c5 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.controller.ts @@ -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); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.module.ts new file mode 100644 index 000000000..d59599e45 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.module.ts @@ -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 {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.ts new file mode 100644 index 000000000..4c2891b1d --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.ts @@ -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 + * @returns {IVendorBalanceSummaryVendor} + */ + private vendorMapper = ( + vendor: ModelObject, + ): 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[]} vendors - Customers. + * @returns {IVendorBalanceSummaryVendor[]} + */ + private vendorsMapper = ( + vendors: ModelObject[], + ): 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} vendors + * @returns {IVendorBalanceSummaryVendor[]} + */ + private getVendorsSection( + vendors: ModelObject[], + ): 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 }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.types.ts new file mode 100644 index 000000000..192da060b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.types.ts @@ -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; +} + +export interface IVendorBalanceSummaryTable extends IFinancialTable { + query: IVendorBalanceSummaryQuery; + meta: IVendorBalanceSummaryMeta; +} + +export interface IVendorBalanceSummaryMeta extends IFinancialSheetCommonMeta { + formattedAsDate: string; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryApplication.ts new file mode 100644 index 000000000..e6d56b413 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryApplication.ts @@ -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} + */ + public xlsx(query: IVendorBalanceSummaryQuery): Promise { + return this.vendorBalanceSummaryExport.xlsx(query); + } + + /** + * Retrieves the vendor balance summary sheet in csv format. + * @param {IVendorBalanceSummaryQuery} query + * @returns {Promise} + */ + public csv(query: IVendorBalanceSummaryQuery): Promise { + return this.vendorBalanceSummaryExport.csv(query); + } + + /** + * Retrieves the vendor balance summary sheet in pdf format. + * @param {IVendorBalanceSummaryQuery} query + * @returns {Promise} + */ + public pdf(query: IVendorBalanceSummaryQuery) { + return this.vendorBalanceSummaryPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryExportInjectable.ts new file mode 100644 index 000000000..746244188 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryExportInjectable.ts @@ -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} + */ + 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} + */ + public async csv(query: IVendorBalanceSummaryQuery): Promise { + const table = await this.customerBalanceSummaryTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryMeta.ts new file mode 100644 index 000000000..206ab5f61 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryMeta.ts @@ -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 { + const commonMeta = await this.financialSheetMeta.meta(); + const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD'); + + return { + ...commonMeta, + sheetName: 'Vendor Balance Summary', + formattedAsDate, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryPdf.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryPdf.ts new file mode 100644 index 000000000..d2d58b3dd --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryPdf.ts @@ -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} + */ + public async pdf( + query: IVendorBalanceSummaryQuery, + ): Promise { + const table = await this.vendorBalanceSummaryTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedAsDate, + HtmlTableCustomCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryRepository.ts new file mode 100644 index 000000000..b4c3b54e4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryRepository.ts @@ -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} vendorEntries + */ + public vendorEntries: Array; + + /** + * Vendors list. + * @param {Array>} vendors + */ + public vendors: Array>; + + /** + * 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[]> { + 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} + */ + public async getPayableAccounts(): Promise[]> { + 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} + */ + private async getReportVendorsEntries( + date: Date | string, + ): Promise { + const transactions = await this.getVendorsTransactions(date); + const commonProps = { accountNormal: 'credit' }; + + return R.map(R.merge(commonProps))(transactions); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryService.ts new file mode 100644 index 000000000..3d9e88d88 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryService.ts @@ -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} + */ + public async vendorBalanceSummary( + query: IVendorBalanceSummaryQuery, + ): Promise { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryTableInjectable.ts new file mode 100644 index 000000000..f9e634096 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryTableInjectable.ts @@ -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} + */ + public async table( + query: IVendorBalanceSummaryQuery, + ): Promise { + 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, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryTableRows.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryTableRows.ts new file mode 100644 index 000000000..1409cc7e2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryTableRows.ts @@ -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) + )([]); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/constants.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/constants.ts new file mode 100644 index 000000000..513a3dcdb --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/constants.ts @@ -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; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/utils.ts new file mode 100644 index 000000000..c3b066857 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/VendorBalanceSummary/utils.ts @@ -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, + }; +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts b/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts index 1fe8f3552..70f4d02ff 100644 --- a/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts +++ b/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts @@ -13,6 +13,7 @@ export type ITableRow = { cells: ITableCell[]; rowTypes?: Array; id?: string; + children?: ITableRow[]; }; export interface ITableColumn { diff --git a/packages/server-nest/src/modules/Ledger/Ledger.ts b/packages/server-nest/src/modules/Ledger/Ledger.ts index 0fe2a173f..711c10b1c 100644 --- a/packages/server-nest/src/modules/Ledger/Ledger.ts +++ b/packages/server-nest/src/modules/Ledger/Ledger.ts @@ -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';