refactor: financial statements to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-21 11:38:07 +02:00
parent 8e36aab529
commit b46f2a91c3
21 changed files with 743 additions and 373 deletions

View File

@@ -1,35 +1,138 @@
import { isEmpty } from 'lodash';
import { Bill } from '@/modules/Bills/models/Bill';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
import { Inject } from '@nestjs/common';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { groupBy } from 'ramda';
export class APAgingSummaryRepository { export class APAgingSummaryRepository {
@Inject(Vendor.name)
private readonly vendorModel: typeof Vendor;
@Inject(Bill.name)
private readonly billModel: typeof Bill;
asyncInit() { @Inject(TenancyContext)
// Settings tenant service. private readonly tenancyContext: TenancyContext;
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
/**
* Filter.
* @param {IAPAgingSummaryQuery} filter
*/
filter: IAPAgingSummaryQuery;
/**
* Due bills.
* @param {Bill[]} dueBills
*/
dueBills: Bill[];
/**
* Due bills by vendor id.
* @param {Record<string, Bill[]>} dueBillsByVendorId
*/
dueBillsByVendorId: Record<number, Bill[]>;
/**
* Overdue bills.
* @param {Bill[]} overdueBills
*/
overdueBills: Bill[];
/**
* Overdue bills by vendor id.
* @param {Record<string, Bill[]>} overdueBillsByVendorId
*/
overdueBillsByVendorId: Record<number, Bill[]>;
/**
* Vendors.
* @param {Vendor[]} vendors
*/
vendors: Vendor[];
/**
* Base currency.
* @param {string} baseCurrency
*/
baseCurrency: string;
/**
* Set the filter.
* @param {IAPAgingSummaryQuery} filter
*/
setFilter(filter: IAPAgingSummaryQuery) {
this.filter = filter;
}
/**
* Load the data.
*/
async load() {
await this.asyncBaseCurrency();
await this.asyncVendors();
await this.asyncDueBills();
await this.asyncOverdueBills();
}
/**
* Retrieve the base currency.
* @returns {Promise<string>}
*/
async asyncBaseCurrency() {
const metadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = metadata.baseCurrency;
}
/**
* Retrieve all vendors from the storage.
*/
async asyncVendors() {
// Retrieve all vendors from the storage. // Retrieve all vendors from the storage.
const vendors = const vendors =
filter.vendorsIds.length > 0 this.filter.vendorsIds.length > 0
? await vendorRepository.findWhereIn('id', filter.vendorsIds) ? await this.vendorModel.query().whereIn('id', this.filter.vendorsIds)
: await vendorRepository.all(); : await this.vendorModel.query();
// Common query. this.vendors = vendors;
}
/**
* Retrieve all overdue bills from the storage.
*/
async asyncOverdueBills() {
const commonQuery = (query) => { const commonQuery = (query) => {
if (!isEmpty(filter.branchesIds)) { if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', filter.branchesIds); query.modify('filterByBranches', this.filter.branchesIds);
} }
}; };
// Retrieve all overdue vendors bills. const overdueBills = await this.billModel
const overdueBills = await Bill.query() .query()
.modify('overdueBillsFromDate', filter.asDate) .modify('overdueBillsFromDate', this.filter.asDate)
.onBuild(commonQuery); .onBuild(commonQuery);
this.overdueBills = overdueBills;
this.overdueBillsByVendorId = groupBy(overdueBills, 'vendorId');
}
/**
* Retrieve all due bills from the storage.
*/
async asyncDueBills() {
const commonQuery = (query) => {
if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', this.filter.branchesIds);
}
};
// Retrieve all due vendors bills. // Retrieve all due vendors bills.
const dueBills = await Bill.query() const dueBills = await this.billModel
.modify('dueBillsFromDate', filter.asDate) .query()
.modify('dueBillsFromDate', this.filter.asDate)
.onBuild(commonQuery); .onBuild(commonQuery);
this.dueBills = dueBills;
this.dueBillsByVendorId = groupBy(dueBills, 'vendorId');
} }
} }

View File

@@ -1,5 +1,6 @@
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { import {
IAPAgingSummaryQuery, IAPAgingSummaryQuery,
IAPAgingSummarySheet, IAPAgingSummarySheet,
@@ -7,19 +8,19 @@ import {
import { APAgingSummarySheet } from './APAgingSummarySheet'; import { APAgingSummarySheet } from './APAgingSummarySheet';
import { APAgingSummaryMeta } from './APAgingSummaryMeta'; import { APAgingSummaryMeta } from './APAgingSummaryMeta';
import { getAPAgingSummaryDefaultQuery } from './utils'; import { getAPAgingSummaryDefaultQuery } from './utils';
import { events } from '@/common/events/events'; import { APAgingSummaryRepository } from './APAgingSummaryRepository';
@Injectable() @Injectable()
export class APAgingSummaryService { export class APAgingSummaryService {
constructor( constructor(
private readonly APAgingSummaryMeta: APAgingSummaryMeta, private readonly APAgingSummaryMeta: APAgingSummaryMeta,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
private readonly APAgingSummaryRepository: APAgingSummaryRepository,
) {} ) {}
/** /**
* Retrieve A/P aging summary report. * Retrieve A/P aging summary report.
* @param {number} tenantId - * @param {IAPAgingSummaryQuery} query - A/P aging summary query.
* @param {IAPAgingSummaryQuery} query -
* @returns {Promise<IAPAgingSummarySheet>} * @returns {Promise<IAPAgingSummarySheet>}
*/ */
public async APAgingSummary( public async APAgingSummary(
@@ -30,13 +31,14 @@ export class APAgingSummaryService {
...getAPAgingSummaryDefaultQuery(), ...getAPAgingSummaryDefaultQuery(),
...query, ...query,
}; };
// Load the data.
this.APAgingSummaryRepository.setFilter(filter);
await this.APAgingSummaryRepository.load();
// A/P aging summary report instance. // A/P aging summary report instance.
const APAgingSummaryReport = new APAgingSummarySheet( const APAgingSummaryReport = new APAgingSummarySheet(
filter, filter,
vendors, this.APAgingSummaryRepository,
overdueBills,
dueBills,
tenant.metadata.baseCurrency,
); );
// A/P aging summary report data and columns. // A/P aging summary report data and columns.
const data = APAgingSummaryReport.reportData(); const data = APAgingSummaryReport.reportData();

View File

@@ -1,4 +1,4 @@
import { groupBy, sum, isEmpty } from 'lodash'; import { sum, isEmpty } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import { import {
IAPAgingSummaryQuery, IAPAgingSummaryQuery,
@@ -10,20 +10,13 @@ import {
import { AgingSummaryReport } from '../AgingSummary/AgingSummary'; import { AgingSummaryReport } from '../AgingSummary/AgingSummary';
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types'; import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { Bill } from '@/modules/Bills/models/Bill';
import { Vendor } from '@/modules/Vendors/models/Vendor'; import { Vendor } from '@/modules/Vendors/models/Vendor';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed'; import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
export class APAgingSummarySheet extends AgingSummaryReport { export class APAgingSummarySheet extends AgingSummaryReport {
readonly tenantId: number; readonly repository: APAgingSummaryRepository;
readonly query: IAPAgingSummaryQuery; readonly query: IAPAgingSummaryQuery;
readonly contacts: ModelObject<Vendor>[];
readonly unpaidBills: ModelObject<Bill>[];
readonly baseCurrency: string;
readonly overdueInvoicesByContactId: Record<number, Array<ModelObject<Bill>>>;
readonly currentInvoicesByContactId: Record<number, Array<ModelObject<Bill>>>;
readonly agingPeriods: IAgingPeriod[]; readonly agingPeriods: IAgingPeriod[];
/** /**
@@ -34,23 +27,14 @@ export class APAgingSummarySheet extends AgingSummaryReport {
* @param {string} baseCurrency - Base currency of the organization. * @param {string} baseCurrency - Base currency of the organization.
*/ */
constructor( constructor(
tenantId: number,
query: IAPAgingSummaryQuery, query: IAPAgingSummaryQuery,
vendors: ModelObject<Vendor>[], repository: APAgingSummaryRepository,
overdueBills: ModelObject<Bill>[],
unpaidBills: ModelObject<Bill>[],
baseCurrency: string,
) { ) {
super(); super();
this.tenantId = tenantId;
this.query = query; this.query = query;
this.repository = repository;
this.numberFormat = this.query.numberFormat; 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. // Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods( this.agingPeriods = this.agingRangePeriods(
@@ -170,7 +154,7 @@ export class APAgingSummarySheet extends AgingSummaryReport {
* @return {IAPAgingSummaryData} * @return {IAPAgingSummaryData}
*/ */
public reportData = (): IAPAgingSummaryData => { public reportData = (): IAPAgingSummaryData => {
const vendorsAgingPeriods = this.vendorsSection(this.contacts); const vendorsAgingPeriods = this.vendorsSection(this.repository.vendors);
const vendorsTotal = this.getVendorsTotal(vendorsAgingPeriods); const vendorsTotal = this.getVendorsTotal(vendorsAgingPeriods);
return { return {

View File

@@ -1,38 +1,146 @@
import { isEmpty, groupBy } from 'lodash';
import { Customer } from '@/modules/Customers/models/Customer';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { ModelObject } from 'objection';
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { Inject } from '@nestjs/common';
export class ARAgingSummaryRepository { export class ARAgingSummaryRepository {
@Inject(TenancyContext)
private tenancyContext: TenancyContext;
@Inject(Customer.name)
private customerModel: typeof Customer;
init(){ @Inject(SaleInvoice.name)
const tenant = await Tenant.query() private saleInvoiceModel: typeof SaleInvoice;
.findById(tenantId)
.withGraphFetched('metadata');
/**
* Filter.
* @param {IARAgingSummaryQuery} filter
*/
filter: IARAgingSummaryQuery;
/**
* Base currency.
* @param {string} baseCurrency
*/
baseCurrency: string;
/**
* Customers.
* @param {ModelObject<Customer>[]} customers
*/
customers: ModelObject<Customer>[];
/**
* Overdue sale invoices.
* @param {ModelObject<SaleInvoice>[]} overdueSaleInvoices
*/
overdueSaleInvoices: ModelObject<SaleInvoice>[];
/**
* Current sale invoices.
* @param {ModelObject<SaleInvoice>[]} currentInvoices
*/
currentInvoices: ModelObject<SaleInvoice>[];
/**
* Current sale invoices by contact id.
* @param {Record<string, ModelObject<SaleInvoice>[]>} currentInvoicesByContactId
*/
currentInvoicesByContactId: Record<string, ModelObject<SaleInvoice>[]>;
/**
* Overdue sale invoices by contact id.
* @param {Record<string, ModelObject<SaleInvoice>[]>} overdueInvoicesByContactId
*/
overdueInvoicesByContactId: Record<string, ModelObject<SaleInvoice>[]>;
/**
* Set the filter.
* @param {IARAgingSummaryQuery} filter
*/
setFilter(filter: IARAgingSummaryQuery) {
this.filter = filter;
}
/**
* Initialize the repository.
*/
async load() {
await this.initBaseCurrency();
await this.initCustomers();
await this.initOverdueSaleInvoices();
await this.initCurrentInvoices();
}
/**
* Initialize the base currency.
*/
async initBaseCurrency() {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = tenantMetadata.baseCurrency;
}
/**
* Initialize the customers.
*/
async initCustomers() {
// Retrieve all customers from the storage. // Retrieve all customers from the storage.
const customers = const customers =
filter.customersIds.length > 0 this.filter.customersIds.length > 0
? await customerRepository.findWhereIn('id', filter.customersIds) ? await this.customerModel
: await customerRepository.all(); .query()
.whereIn('id', this.filter.customersIds)
: await this.customerModel.query();
// Common query. this.customers = customers;
}
/**
* Initialize the overdue sale invoices.
*/
async initOverdueSaleInvoices() {
const commonQuery = (query) => { const commonQuery = (query) => {
if (!isEmpty(filter.branchesIds)) { if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', filter.branchesIds); query.modify('filterByBranches', this.filter.branchesIds);
} }
}; };
// Retrieve all overdue sale invoices. // Retrieve all overdue sale invoices.
const overdueSaleInvoices = await SaleInvoice.query() const overdueSaleInvoices = await this.saleInvoiceModel
.modify('overdueInvoicesFromDate', filter.asDate) .query()
.modify('overdueInvoicesFromDate', this.filter.asDate)
.onBuild(commonQuery); .onBuild(commonQuery);
this.overdueSaleInvoices = overdueSaleInvoices;
this.overdueInvoicesByContactId = groupBy(
overdueSaleInvoices,
'customerId',
);
}
/**
* Initialize the current sale invoices.
*/
async initCurrentInvoices() {
const commonQuery = (query) => {
if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', this.filter.branchesIds);
}
};
// Retrieve all due sale invoices. // Retrieve all due sale invoices.
const currentInvoices = await SaleInvoice.query() const currentInvoices = await this.saleInvoiceModel
.modify('dueInvoicesFromDate', filter.asDate) .query()
.modify('dueInvoicesFromDate', this.filter.asDate)
.onBuild(commonQuery); .onBuild(commonQuery);
this.currentInvoices = currentInvoices;
this.currentInvoicesByContactId = groupBy(
currentInvoices,
'customerId',
);
} }
} }

View File

@@ -5,11 +5,13 @@ import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { IARAgingSummaryQuery } from './ARAgingSummary.types'; import { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
@Injectable() @Injectable()
export class ARAgingSummaryService { export class ARAgingSummaryService {
constructor( constructor(
private readonly ARAgingSummaryMeta: ARAgingSummaryMeta, private readonly ARAgingSummaryMeta: ARAgingSummaryMeta,
private readonly ARAgingSummaryRepository: ARAgingSummaryRepository,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
) {} ) {}
@@ -22,13 +24,14 @@ export class ARAgingSummaryService {
...getARAgingSummaryDefaultQuery(), ...getARAgingSummaryDefaultQuery(),
...query, ...query,
}; };
// Load the A/R aging summary repository.
this.ARAgingSummaryRepository.setFilter(filter);
await this.ARAgingSummaryRepository.load();
// AR aging summary report instance. // AR aging summary report instance.
const ARAgingSummaryReport = new ARAgingSummarySheet( const ARAgingSummaryReport = new ARAgingSummarySheet(
filter, filter,
customers, this.ARAgingSummaryRepository,
overdueSaleInvoices,
currentInvoices,
tenant.metadata.baseCurrency,
); );
// AR aging summary report data and columns. // AR aging summary report data and columns.
const data = ARAgingSummaryReport.reportData(); const data = ARAgingSummaryReport.reportData();
@@ -40,9 +43,7 @@ export class ARAgingSummaryService {
// Triggers `onReceivableAgingViewed` event. // Triggers `onReceivableAgingViewed` event.
await this.eventPublisher.emitAsync( await this.eventPublisher.emitAsync(
events.reports.onReceivableAgingViewed, events.reports.onReceivableAgingViewed,
{ { query },
query,
},
); );
return { return {

View File

@@ -1,5 +1,5 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { Dictionary, groupBy, isEmpty, sum } from 'lodash'; import { isEmpty, sum } from 'lodash';
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types'; import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
import { import {
IARAgingSummaryQuery, IARAgingSummaryQuery,
@@ -12,17 +12,12 @@ import { AgingSummaryReport } from '../AgingSummary/AgingSummary';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed'; import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { Customer } from '@/modules/Customers/models/Customer'; import { Customer } from '@/modules/Customers/models/Customer';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
export class ARAgingSummarySheet extends AgingSummaryReport { export class ARAgingSummarySheet extends AgingSummaryReport {
readonly tenantId: number;
readonly query: IARAgingSummaryQuery; readonly query: IARAgingSummaryQuery;
readonly contacts: ModelObject<Customer>[];
readonly agingPeriods: IAgingPeriod[]; readonly agingPeriods: IAgingPeriod[];
readonly baseCurrency: string; readonly repository: ARAgingSummaryRepository;
readonly overdueInvoicesByContactId: Dictionary<ModelObject<SaleInvoice>[]>;
readonly currentInvoicesByContactId: Dictionary<ModelObject<SaleInvoice>[]>;
/** /**
* Constructor method. * Constructor method.
@@ -32,29 +27,15 @@ export class ARAgingSummarySheet extends AgingSummaryReport {
* @param {IJournalPoster} journal * @param {IJournalPoster} journal
*/ */
constructor( constructor(
tenantId: number,
query: IARAgingSummaryQuery, query: IARAgingSummaryQuery,
customers: ModelObject<Customer>[], repository: ARAgingSummaryRepository,
overdueSaleInvoices: ModelObject<SaleInvoice>[],
currentSaleInvoices: ModelObject<SaleInvoice>[],
baseCurrency: string,
) { ) {
super(); super();
this.tenantId = tenantId;
this.contacts = customers;
this.query = query; this.query = query;
this.baseCurrency = baseCurrency; this.repository = repository;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.overdueInvoicesByContactId = groupBy(
overdueSaleInvoices,
'customerId',
);
this.currentInvoicesByContactId = groupBy(
currentSaleInvoices,
'customerId',
);
// Initializes the aging periods. // Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods( this.agingPeriods = this.agingRangePeriods(
this.query.asDate, this.query.asDate,
@@ -179,7 +160,9 @@ export class ARAgingSummarySheet extends AgingSummaryReport {
* @return {IARAgingSummaryData} * @return {IARAgingSummaryData}
*/ */
public reportData = (): IARAgingSummaryData => { public reportData = (): IARAgingSummaryData => {
const customersAgingPeriods = this.customersWalker(this.contacts); const customersAgingPeriods = this.customersWalker(
this.repository.customers,
);
const customersTotal = this.getCustomersTotal(customersAgingPeriods); const customersTotal = this.getCustomersTotal(customersAgingPeriods);
return { return {

View File

@@ -1,5 +1,5 @@
import { Controller, Headers, Query, Res } from '@nestjs/common'; import { Controller, Headers, Query, Res } from '@nestjs/common';
import { InventortyDetailsApplication } from './InventoryItemDetailsApplication'; import { InventoryItemDetailsApplication } from './InventoryItemDetailsApplication';
import { IInventoryDetailsQuery } from './InventoryItemDetails.types'; import { IInventoryDetailsQuery } from './InventoryItemDetails.types';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express'; import { Response } from 'express';
@@ -7,7 +7,7 @@ import { Response } from 'express';
@Controller('reports/inventory-item-details') @Controller('reports/inventory-item-details')
export class InventoryItemDetailsController { export class InventoryItemDetailsController {
constructor( constructor(
private readonly inventoryItemDetailsApp: InventortyDetailsApplication, private readonly inventoryItemDetailsApp: InventoryItemDetailsApplication,
) {} ) {}
async inventoryItemDetails( async inventoryItemDetails(

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { InventoryItemDetailsController } from './InventoryItemDetails.controller'; import { InventoryItemDetailsController } from './InventoryItemDetails.controller';
import { InventoryDetailsTablePdf } from './InventoryItemDetailsTablePdf'; import { InventoryDetailsTablePdf } from './InventoryItemDetailsTablePdf';
import { InventoryDetailsService } from './InventoryItemDetailsService'; import { InventoryDetailsService } from './InventoryItemDetails.service';
import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable'; import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable';
import { InventoryItemDetailsExportInjectable } from './InventoryItemDetailsExportInjectable'; import { InventoryItemDetailsExportInjectable } from './InventoryItemDetailsExportInjectable';
import { InventoryItemDetailsApplication } from './InventoryItemDetailsApplication'; import { InventoryItemDetailsApplication } from './InventoryItemDetailsApplication';

View File

@@ -0,0 +1,46 @@
import { I18nService } from 'nestjs-i18n';
import { Injectable } from '@nestjs/common';
import {
IInventoryDetailsQuery,
IInvetoryItemDetailDOO,
} from './InventoryItemDetails.types';
import { InventoryDetails } from './InventoryItemDetails';
import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
import { InventoryDetailsMetaInjectable } from './InventoryItemDetailsMeta';
import { getInventoryItemDetailsDefaultQuery } from './constant';
@Injectable()
export class InventoryDetailsService {
constructor(
private readonly inventoryItemDetailsRepository: InventoryItemDetailsRepository,
private readonly inventoryDetailsMeta: InventoryDetailsMetaInjectable,
private readonly i18n: I18nService,
) {}
/**
* Retrieve the inventory details report data.
* @param {IInventoryDetailsQuery} query - Inventory details query.
* @return {Promise<IInvetoryItemDetailDOO>}
*/
public async inventoryDetails(
query: IInventoryDetailsQuery,
): Promise<IInvetoryItemDetailDOO> {
const filter = {
...getInventoryItemDetailsDefaultQuery(),
...query,
};
// Inventory details report mapper.
const inventoryDetailsInstance = new InventoryDetails(
filter,
this.inventoryItemDetailsRepository,
this.i18n,
);
const meta = await this.inventoryDetailsMeta.meta(query);
return {
data: inventoryDetailsInstance.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -1,6 +1,7 @@
import * as R from 'ramda'; import * as R from 'ramda';
import * as moment from 'moment';
import { defaultTo, sumBy, get } from 'lodash'; import { defaultTo, sumBy, get } from 'lodash';
import moment from 'moment'; import { I18nService } from 'nestjs-i18n';
import { import {
IInventoryDetailsQuery, IInventoryDetailsQuery,
IInventoryDetailsNumber, IInventoryDetailsNumber,
@@ -11,56 +12,42 @@ import {
IInventoryDetailsOpening, IInventoryDetailsOpening,
IInventoryDetailsItemTransaction, IInventoryDetailsItemTransaction,
} from './InventoryItemDetails.types'; } from './InventoryItemDetails.types';
import FinancialSheet from '../FinancialSheet'; import { ModelObject } from 'objection';
import { transformToMapBy, transformToMapKeyValue } from 'utils'; import { Item } from '@/modules/Items/models/Item';
import { filterDeep } from 'utils/deepdash'; import {
IFormatNumberSettings,
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' }; INumberFormatQuery,
} from '../../types/Report.types';
enum INodeTypes { import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
ITEM = 'item', import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
TRANSACTION = 'transaction', import { TInventoryTransactionDirection } from '@/modules/InventoryCost/types/InventoryCost.types';
OPENING_ENTRY = 'OPENING_ENTRY', import { FinancialSheet } from '../../common/FinancialSheet';
CLOSING_ENTRY = 'CLOSING_ENTRY', import { filterDeep } from '@/utils/deepdash';
} import { INodeTypes, MAP_CONFIG } from './constant';
export class InventoryDetails extends FinancialSheet { export class InventoryDetails extends FinancialSheet {
readonly inventoryTransactionsByItemId: Map<number, IInventoryTransaction[]>; readonly repository: InventoryItemDetailsRepository;
readonly openingBalanceTransactions: Map<number, IInventoryTransaction>;
readonly query: IInventoryDetailsQuery; readonly query: IInventoryDetailsQuery;
readonly numberFormat: INumberFormatQuery; readonly numberFormat: INumberFormatQuery;
readonly baseCurrency: string; readonly items: ModelObject<Item>[];
readonly items: IItem[]; readonly i18n: I18nService;
/** /**
* Constructor method. * Constructor method.
* @param {IItem[]} items - Items. * @param {InventoryItemDetailsRepository} repository - The repository.
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions. * @param {I18nService} i18n - The i18n service.
* @param {IInventoryDetailsQuery} query - Report query.
* @param {string} baseCurrency - The base currency.
*/ */
constructor( constructor(
items: IItem[], filter: IInventoryDetailsQuery,
openingBalanceTransactions: IInventoryTransaction[], repository: InventoryItemDetailsRepository,
inventoryTransactions: IInventoryTransaction[], i18n: I18nService,
query: IInventoryDetailsQuery,
baseCurrency: string,
i18n: any
) { ) {
super(); super();
this.inventoryTransactionsByItemId = transformToMapBy( this.repository = repository;
inventoryTransactions,
'itemId' this.query = filter;
);
this.openingBalanceTransactions = transformToMapKeyValue(
openingBalanceTransactions,
'itemId'
);
this.query = query;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.items = items;
this.baseCurrency = baseCurrency;
this.i18n = i18n; this.i18n = i18n;
} }
@@ -69,9 +56,9 @@ export class InventoryDetails extends FinancialSheet {
* @param {number} number * @param {number} number
* @returns * @returns
*/ */
private getNumberMeta( public getNumberMeta(
number: number, number: number,
settings?: IFormatNumberSettings settings?: IFormatNumberSettings,
): IInventoryDetailsNumber { ): IInventoryDetailsNumber {
return { return {
formattedNumber: this.formatNumber(number, { formattedNumber: this.formatNumber(number, {
@@ -89,9 +76,9 @@ export class InventoryDetails extends FinancialSheet {
* @param {IFormatNumberSettings} settings - * @param {IFormatNumberSettings} settings -
* @retrun {IInventoryDetailsNumber} * @retrun {IInventoryDetailsNumber}
*/ */
private getTotalNumberMeta( public getTotalNumberMeta(
number: number, number: number,
settings?: IFormatNumberSettings settings?: IFormatNumberSettings,
): IInventoryDetailsNumber { ): IInventoryDetailsNumber {
return this.getNumberMeta(number, { excerptZero: false, ...settings }); return this.getNumberMeta(number, { excerptZero: false, ...settings });
} }
@@ -101,7 +88,7 @@ export class InventoryDetails extends FinancialSheet {
* @param {Date|string} date * @param {Date|string} date
* @returns {IInventoryDetailsDate} * @returns {IInventoryDetailsDate}
*/ */
private getDateMeta(date: Date | string): IInventoryDetailsDate { public getDateMeta(date: Date | string): IInventoryDetailsDate {
return { return {
formattedDate: moment(date).format('YYYY-MM-DD'), formattedDate: moment(date).format('YYYY-MM-DD'),
date: moment(date).toDate(), date: moment(date).toDate(),
@@ -110,14 +97,14 @@ export class InventoryDetails extends FinancialSheet {
/** /**
* Adjusts the movement amount. * Adjusts the movement amount.
* @param {number} amount * @param {number} amount - The amount.
* @param {TInventoryTransactionDirection} direction * @param {TInventoryTransactionDirection} direction - The transaction direction.
* @returns {number} * @returns {number}
*/ */
private adjustAmountMovement = R.curry( public adjustAmountMovement = R.curry(
(direction: TInventoryTransactionDirection, amount: number): number => { (direction: TInventoryTransactionDirection, amount: number): number => {
return direction === 'OUT' ? amount * -1 : amount; return direction === 'OUT' ? amount * -1 : amount;
} },
); );
/** /**
@@ -125,8 +112,8 @@ export class InventoryDetails extends FinancialSheet {
* @param {IInventoryDetailsItemTransaction[]} transactions * @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction[]} * @returns {IInventoryDetailsItemTransaction[]}
*/ */
private mapAccumTransactionsRunningQuantity( public mapAccumTransactionsRunningQuantity(
transactions: IInventoryDetailsItemTransaction[] transactions: IInventoryDetailsItemTransaction[],
): IInventoryDetailsItemTransaction[] { ): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0); const initial = this.getNumberMeta(0);
@@ -140,7 +127,7 @@ export class InventoryDetails extends FinancialSheet {
return R.mapAccum( return R.mapAccum(
mapAccumAppender, mapAccumAppender,
{ runningQuantity: initial }, { runningQuantity: initial },
transactions transactions,
)[1]; )[1];
} }
@@ -149,8 +136,8 @@ export class InventoryDetails extends FinancialSheet {
* @param {IInventoryDetailsItemTransaction[]} transactions * @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction} * @returns {IInventoryDetailsItemTransaction}
*/ */
private mapAccumTransactionsRunningValuation( public mapAccumTransactionsRunningValuation(
transactions: IInventoryDetailsItemTransaction[] transactions: IInventoryDetailsItemTransaction[],
): IInventoryDetailsItemTransaction[] { ): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0); const initial = this.getNumberMeta(0);
@@ -165,16 +152,18 @@ export class InventoryDetails extends FinancialSheet {
return R.mapAccum( return R.mapAccum(
mapAccumAppender, mapAccumAppender,
{ runningValuation: initial }, { runningValuation: initial },
transactions transactions,
)[1]; )[1];
} }
/** /**
* Retrieve the inventory transaction total. * Retrieve the inventory transaction total.
* @param {IInventoryTransaction} transaction * @param {ModelObject<InventoryTransaction>} transaction
* @returns {number} * @returns {number}
*/ */
private getTransactionTotal = (transaction: IInventoryTransaction) => { public getTransactionTotal = (
transaction: ModelObject<InventoryTransaction>,
) => {
return transaction.quantity return transaction.quantity
? transaction.quantity * transaction.rate ? transaction.quantity * transaction.rate
: transaction.rate; : transaction.rate;
@@ -186,9 +175,9 @@ export class InventoryDetails extends FinancialSheet {
* @param {IInvetoryTransaction} transaction * @param {IInvetoryTransaction} transaction
* @returns {IInventoryDetailsItemTransaction} * @returns {IInventoryDetailsItemTransaction}
*/ */
private itemTransactionMapper( public itemTransactionMapper(
item: IItem, item: ModelObject<Item>,
transaction: IInventoryTransaction transaction: ModelObject<InventoryTransaction>,
): IInventoryDetailsItemTransaction { ): IInventoryDetailsItemTransaction {
const total = this.getTransactionTotal(transaction); const total = this.getTransactionTotal(transaction);
const amountMovement = this.adjustAmountMovement(transaction.direction); const amountMovement = this.adjustAmountMovement(transaction.direction);
@@ -209,7 +198,7 @@ export class InventoryDetails extends FinancialSheet {
return { return {
nodeType: INodeTypes.TRANSACTION, nodeType: INodeTypes.TRANSACTION,
date: this.getDateMeta(transaction.date), date: this.getDateMeta(transaction.date),
transactionType: this.i18n.__(transaction.transcationTypeFormatted), transactionType: this.i18n.t(transaction.transcationTypeFormatted),
transactionNumber: transaction?.meta?.transactionNumber, transactionNumber: transaction?.meta?.transactionNumber,
direction: transaction.direction, direction: transaction.direction,
@@ -232,13 +221,16 @@ export class InventoryDetails extends FinancialSheet {
/** /**
* Retrieve the inventory transcations by item id. * Retrieve the inventory transcations by item id.
* @param {number} itemId * @param {number} itemId - The item id.
* @returns {IInventoryTransaction[]} * @returns {ModelObject<InventoryTransaction>[]}
*/ */
private getInventoryTransactionsByItemId( public getInventoryTransactionsByItemId(
itemId: number itemId: number,
): IInventoryTransaction[] { ): ModelObject<InventoryTransaction>[] {
return defaultTo(this.inventoryTransactionsByItemId.get(itemId + ''), []); return defaultTo(
this.repository.inventoryTransactionsByItemId.get(itemId),
[],
);
} }
/** /**
@@ -246,13 +238,15 @@ export class InventoryDetails extends FinancialSheet {
* @param {IItem} item * @param {IItem} item
* @returns {IInventoryDetailsItemTransaction[]} * @returns {IInventoryDetailsItemTransaction[]}
*/ */
private getItemTransactions(item: IItem): IInventoryDetailsItemTransaction[] { public getItemTransactions(
item: ModelObject<Item>,
): ModelObject<InventoryTransaction>[] {
const transactions = this.getInventoryTransactionsByItemId(item.id); const transactions = this.getInventoryTransactionsByItemId(item.id);
return R.compose( return R.compose(
this.mapAccumTransactionsRunningQuantity.bind(this), this.mapAccumTransactionsRunningQuantity.bind(this),
this.mapAccumTransactionsRunningValuation.bind(this), this.mapAccumTransactionsRunningValuation.bind(this),
R.map(R.curry(this.itemTransactionMapper.bind(this))(item)) R.map(R.curry(this.itemTransactionMapper.bind(this))(item)),
)(transactions); )(transactions);
} }
@@ -265,8 +259,8 @@ export class InventoryDetails extends FinancialSheet {
* | IInventoryDetailsClosing * | IInventoryDetailsClosing
* )[]} * )[]}
*/ */
private itemTransactionsMapper( public itemTransactionsMapper(
item: IItem item: ModelObject<Item>,
): ( ): (
| IInventoryDetailsItemTransaction | IInventoryDetailsItemTransaction
| IInventoryDetailsOpening | IInventoryDetailsOpening
@@ -277,7 +271,7 @@ export class InventoryDetails extends FinancialSheet {
const closingValuation = this.getItemClosingValuation( const closingValuation = this.getItemClosingValuation(
item, item,
transactions, transactions,
openingValuation openingValuation,
); );
const hasTransactions = transactions.length > 0; const hasTransactions = transactions.length > 0;
const isItemHasOpeningBalance = this.isItemHasOpeningBalance(item.id); const isItemHasOpeningBalance = this.isItemHasOpeningBalance(item.id);
@@ -285,7 +279,7 @@ export class InventoryDetails extends FinancialSheet {
return R.pipe( return R.pipe(
R.concat(transactions), R.concat(transactions),
R.when(R.always(isItemHasOpeningBalance), R.prepend(openingValuation)), R.when(R.always(isItemHasOpeningBalance), R.prepend(openingValuation)),
R.when(R.always(hasTransactions), R.append(closingValuation)) R.when(R.always(hasTransactions), R.append(closingValuation)),
)([]); )([]);
} }
@@ -294,8 +288,8 @@ export class InventoryDetails extends FinancialSheet {
* @param {number} itemId - Item id. * @param {number} itemId - Item id.
* @return {boolean} * @return {boolean}
*/ */
private isItemHasOpeningBalance(itemId: number): boolean { public isItemHasOpeningBalance(itemId: number): boolean {
return !!this.openingBalanceTransactions.get(itemId); return !!this.repository.openingBalanceTransactionsByItemId.get(itemId);
} }
/** /**
@@ -303,8 +297,12 @@ export class InventoryDetails extends FinancialSheet {
* @param {IItem} item - * @param {IItem} item -
* @returns {IInventoryDetailsOpening} * @returns {IInventoryDetailsOpening}
*/ */
private getItemOpeingValuation(item: IItem): IInventoryDetailsOpening { public getItemOpeingValuation(
const openingBalance = this.openingBalanceTransactions.get(item.id); item: ModelObject<Item>,
): IInventoryDetailsOpening {
const openingBalance = this.repository.openingBalanceTransactionsByItemId.get(
item.id,
);
const quantity = defaultTo(get(openingBalance, 'quantity'), 0); const quantity = defaultTo(get(openingBalance, 'quantity'), 0);
const value = defaultTo(get(openingBalance, 'value'), 0); const value = defaultTo(get(openingBalance, 'value'), 0);
@@ -321,10 +319,10 @@ export class InventoryDetails extends FinancialSheet {
* @param {IItem} item - * @param {IItem} item -
* @returns {IInventoryDetailsOpening} * @returns {IInventoryDetailsOpening}
*/ */
private getItemClosingValuation( public getItemClosingValuation(
item: IItem, item: ModelObject<Item>,
transactions: IInventoryDetailsItemTransaction[], transactions: ModelObject<InventoryTransaction>[],
openingValuation: IInventoryDetailsOpening openingValuation: IInventoryDetailsOpening,
): IInventoryDetailsOpening { ): IInventoryDetailsOpening {
const value = sumBy(transactions, 'valueMovement.number'); const value = sumBy(transactions, 'valueMovement.number');
const quantity = sumBy(transactions, 'quantityMovement.number'); const quantity = sumBy(transactions, 'quantityMovement.number');
@@ -347,7 +345,7 @@ export class InventoryDetails extends FinancialSheet {
* @param {IItem} item * @param {IItem} item
* @returns {IInventoryDetailsItem} * @returns {IInventoryDetailsItem}
*/ */
private itemsNodeMapper(item: IItem): IInventoryDetailsItem { public itemsNodeMapper(item: ModelObject<Item>): IInventoryDetailsItem {
return { return {
id: item.id, id: item.id,
name: item.name, name: item.name,
@@ -363,9 +361,9 @@ export class InventoryDetails extends FinancialSheet {
* @param {IItem} node * @param {IItem} node
* @returns {boolean} * @returns {boolean}
*/ */
private isNodeTypeEquals( public isNodeTypeEquals(
nodeType: string, nodeType: string,
node: IInventoryDetailsItem node: IInventoryDetailsItem,
): boolean { ): boolean {
return nodeType === node.nodeType; return nodeType === node.nodeType;
} }
@@ -375,8 +373,8 @@ export class InventoryDetails extends FinancialSheet {
* @param {IInventoryDetailsItem} item * @param {IInventoryDetailsItem} item
* @returns {boolean} * @returns {boolean}
*/ */
private isItemNodeHasTransactions(item: IInventoryDetailsItem) { public isItemNodeHasTransactions(item: IInventoryDetailsItem) {
return !!this.inventoryTransactionsByItemId.get(item.id); return !!this.repository.inventoryTransactionsByItemId.get(item.id);
} }
/** /**
@@ -384,11 +382,11 @@ export class InventoryDetails extends FinancialSheet {
* @param {IInventoryDetailsItem} item * @param {IInventoryDetailsItem} item
* @return {boolean} * @return {boolean}
*/ */
private isFilterNode(item: IInventoryDetailsItem): boolean { public isFilterNode(item: IInventoryDetailsItem): boolean {
return R.ifElse( return R.ifElse(
R.curry(this.isNodeTypeEquals)(INodeTypes.ITEM), R.curry(this.isNodeTypeEquals)(INodeTypes.ITEM),
this.isItemNodeHasTransactions.bind(this), this.isItemNodeHasTransactions.bind(this),
R.always(true) R.always(true),
)(item); )(item);
} }
@@ -397,24 +395,24 @@ export class InventoryDetails extends FinancialSheet {
* @param {IInventoryDetailsItem[]} items - * @param {IInventoryDetailsItem[]} items -
* @returns {IInventoryDetailsItem[]} * @returns {IInventoryDetailsItem[]}
*/ */
private filterItemsNodes(items: IInventoryDetailsItem[]) { public filterItemsNodes(items: IInventoryDetailsItem[]) {
const filtered = filterDeep( const filtered = filterDeep(
items, items,
this.isFilterNode.bind(this), this.isFilterNode.bind(this),
MAP_CONFIG MAP_CONFIG,
); );
return defaultTo(filtered, []); return defaultTo(filtered, []);
} }
/** /**
* Retrieve the items nodes of the report. * Retrieve the items nodes of the report.
* @param {IItem} items * @param {ModelObject<Item>[]} items
* @returns {IInventoryDetailsItem[]} * @returns {IInventoryDetailsItem[]}
*/ */
private itemsNodes(items: IItem[]): IInventoryDetailsItem[] { public itemsNodes(items: ModelObject<Item>[]): IInventoryDetailsItem[] {
return R.compose( return R.compose(
this.filterItemsNodes.bind(this), this.filterItemsNodes.bind(this),
R.map(this.itemsNodeMapper.bind(this)) R.map(this.itemsNodeMapper.bind(this)),
)(items); )(items);
} }

View File

@@ -1,10 +1,10 @@
import { import {
IInventoryDetailsQuery, IInventoryDetailsQuery,
IInvetoryItemDetailsTable, IInvetoryItemDetailsTable,
} from '@/interfaces'; } from './InventoryItemDetails.types';
import { InventoryItemDetailsExportInjectable } from './InventoryItemDetailsExportInjectable'; import { InventoryItemDetailsExportInjectable } from './InventoryItemDetailsExportInjectable';
import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable'; import { InventoryDetailsTableInjectable } from './InventoryItemDetailsTableInjectable';
import { InventoryDetailsService } from './InventoryItemDetailsService'; import { InventoryDetailsService } from './InventoryItemDetails.service';
import { InventoryDetailsTablePdf } from './InventoryItemDetailsTablePdf'; import { InventoryDetailsTablePdf } from './InventoryItemDetailsTablePdf';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';

View File

@@ -4,19 +4,129 @@ import moment from 'moment';
import { IInventoryDetailsQuery } from './InventoryItemDetails.types'; import { IInventoryDetailsQuery } from './InventoryItemDetails.types';
import { Item } from '@/modules/Items/models/Item'; import { Item } from '@/modules/Items/models/Item';
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction'; import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
import { Injectable, Scope } from '@nestjs/common'; import { Inject, Injectable, Scope } from '@nestjs/common';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { transformToMapKeyValue } from '@/utils/transform-to-map-key-value';
import { transformToMapBy } from '@/utils/transform-to-map-by';
@Injectable({ scope: Scope.TRANSIENT }) @Injectable({ scope: Scope.TRANSIENT })
export class InventoryItemDetailsRepository { export class InventoryItemDetailsRepository {
@Inject(Item.name)
readonly itemModel: typeof Item;
@Inject(InventoryTransaction.name)
readonly inventoryTransactionModel: typeof InventoryTransaction;
@Inject(TenancyContext)
readonly tenancyContext: TenancyContext;
/** /**
* Constructor method. * The items.
* @param {typeof Item} itemModel - Item model. * @param {ModelObject<Item>[]} items - The items.
* @param {typeof InventoryTransaction} inventoryTransactionModel - Inventory transaction model.
*/ */
constructor( items: ModelObject<Item>[];
private readonly itemModel: typeof Item,
private readonly inventoryTransactionModel: typeof InventoryTransaction, /**
) {} * The opening balance transactions.
* @param {ModelObject<InventoryTransaction>[]} openingBalanceTransactions - The opening balance transactions.
*/
openingBalanceTransactions: ModelObject<InventoryTransaction>[];
/**
* The opening balance transactions by item id.
* @param {Map<number, ModelObject<InventoryTransaction>>} openingBalanceTransactionsByItemId - The opening balance transactions by item id.
*/
openingBalanceTransactionsByItemId: Map<number, ModelObject<InventoryTransaction>>;
/**
* The inventory transactions.
* @param {ModelObject<InventoryTransaction>[]} inventoryTransactions - The inventory transactions.
*/
inventoryTransactions: ModelObject<InventoryTransaction>[];
/**
* The inventory transactions by item id.
* @param {Map<number, ModelObject<InventoryTransaction>>} inventoryTransactionsByItemId - The inventory transactions by item id.
*/
inventoryTransactionsByItemId: Map<number, ModelObject<InventoryTransaction>[]>;
/**
* The filter.
* @param {IInventoryDetailsQuery} filter - The filter.
*/
filter: IInventoryDetailsQuery;
/**
* The base currency.
* @param {string} baseCurrency - The base currency.
*/
baseCurrency: string;
/**
* Set the filter.
* @param {IInventoryDetailsQuery} filter - The filter.
*/
setFilter(filter: IInventoryDetailsQuery) {
this.filter = filter;
}
/**
* Initialize the repository.
*/
async asyncInit() {
await this.initItems();
await this.initOpeningBalanceTransactions();
await this.initInventoryTransactions();
await this.initBaseCurrency();
}
/**
* Initialize the items.
*/
async initItems() {
// Retrieves the items.
const items = await this.getInventoryItems(this.filter.itemsIds);
this.items = items;
}
/**
* Initialize the opening balance transactions.
*/
async initOpeningBalanceTransactions() {
// Retrieves the opening balance transactions.
const openingBalanceTransactions = await this.getOpeningBalanceTransactions(
this.filter,
);
this.openingBalanceTransactions = openingBalanceTransactions;
this.openingBalanceTransactionsByItemId = transformToMapKeyValue(
openingBalanceTransactions,
'itemId'
);
}
/**
* Initialize the inventory transactions.
*/
async initInventoryTransactions() {
// Retrieves the inventory transactions.
const inventoryTransactions = await this.getItemInventoryTransactions(
this.filter,
);
this.inventoryTransactions = inventoryTransactions;
this.inventoryTransactionsByItemId = transformToMapBy(
inventoryTransactions,
'itemId'
);
}
/**
* Initialize the base currency.
*/
async initBaseCurrency() {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = tenantMetadata.baseCurrency;
}
/** /**
* Retrieve inventory items. * Retrieve inventory items.
@@ -39,7 +149,7 @@ export class InventoryItemDetailsRepository {
* @param {IInventoryDetailsQuery} * @param {IInventoryDetailsQuery}
* @return {Promise<ModelObject<InventoryTransaction>>} * @return {Promise<ModelObject<InventoryTransaction>>}
*/ */
public async openingBalanceTransactions( public async getOpeningBalanceTransactions(
filter: IInventoryDetailsQuery, filter: IInventoryDetailsQuery,
): Promise<ModelObject<InventoryTransaction>[]> { ): Promise<ModelObject<InventoryTransaction>[]> {
const openingBalanceDate = moment(filter.fromDate) const openingBalanceDate = moment(filter.fromDate)
@@ -95,7 +205,7 @@ export class InventoryItemDetailsRepository {
* @param {IInventoryDetailsQuery} * @param {IInventoryDetailsQuery}
* @return {Promise<IInventoryTransaction>} * @return {Promise<IInventoryTransaction>}
*/ */
public async itemInventoryTransactions( public async getItemInventoryTransactions(
filter: IInventoryDetailsQuery, filter: IInventoryDetailsQuery,
): Promise<ModelObject<InventoryTransaction>[]> { ): Promise<ModelObject<InventoryTransaction>[]> {
const inventoryTransactions = this.inventoryTransactionModel const inventoryTransactions = this.inventoryTransactionModel

View File

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

View File

@@ -1,11 +1,11 @@
import { InventoryDetailsTable } from './InventoryItemDetailsTable'; import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { import {
IInventoryDetailsQuery, IInventoryDetailsQuery,
IInvetoryItemDetailsTable, IInvetoryItemDetailsTable,
} from './InventoryItemDetails.types'; } from './InventoryItemDetails.types';
import { InventoryDetailsService } from './InventoryItemDetailsService'; import { InventoryDetailsService } from './InventoryItemDetails.service';
import { Injectable } from '@nestjs/common'; import { InventoryItemDetailsTable } from './InventoryItemDetailsTable';
import { I18nService } from 'nestjs-i18n';
@Injectable() @Injectable()
export class InventoryDetailsTableInjectable { export class InventoryDetailsTableInjectable {
@@ -24,7 +24,8 @@ export class InventoryDetailsTableInjectable {
): Promise<IInvetoryItemDetailsTable> { ): Promise<IInvetoryItemDetailsTable> {
const inventoryDetails = const inventoryDetails =
await this.inventoryDetails.inventoryDetails(query); await this.inventoryDetails.inventoryDetails(query);
const table = new InventoryDetailsTable(inventoryDetails, this.i18n);
const table = new InventoryItemDetailsTable(inventoryDetails.data, this.i18n);
return { return {
table: { table: {

View File

@@ -1,3 +1,5 @@
import { IInventoryDetailsQuery } from "./InventoryItemDetails.types";
export const HtmlTableCustomCss = ` export const HtmlTableCustomCss = `
table tr.row-type--item td, table tr.row-type--item td,
table tr.row-type--opening-entry td, table tr.row-type--opening-entry td,
@@ -5,3 +7,32 @@ table tr.row-type--closing-entry td{
font-weight: 500; font-weight: 500;
} }
`; `;
export const getInventoryItemDetailsDefaultQuery =
(): 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: [],
};
};
export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
export enum INodeTypes {
ITEM = 'item',
TRANSACTION = 'transaction',
OPENING_ENTRY = 'OPENING_ENTRY',
CLOSING_ENTRY = 'CLOSING_ENTRY',
}

View File

@@ -1,3 +1,4 @@
import { ModelObject } from 'objection';
import { sumBy, get, isEmpty } from 'lodash'; import { sumBy, get, isEmpty } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import { import {
@@ -6,41 +7,29 @@ import {
IInventoryValuationStatement, IInventoryValuationStatement,
IInventoryValuationTotal, IInventoryValuationTotal,
} from './InventoryValuationSheet.types'; } from './InventoryValuationSheet.types';
import { ModelObject } from 'objection';
import { Item } from '@/modules/Items/models/Item'; import { Item } from '@/modules/Items/models/Item';
import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker'; import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker';
import { FinancialSheet } from '../../common/FinancialSheet'; import { FinancialSheet } from '../../common/FinancialSheet';
import { transformToMap } from '@/utils/transform-to-key'; import { InventoryValuationSheetRepository } from './InventoryValuationSheetRepository';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
export class InventoryValuationSheet extends FinancialSheet { export class InventoryValuationSheet extends FinancialSheet {
readonly query: IInventoryValuationReportQuery; readonly query: IInventoryValuationReportQuery;
readonly items: ModelObject<Item>[]; readonly repository: InventoryValuationSheetRepository;
readonly INInventoryCostLots: Map<number, InventoryCostLotTracker>;
readonly OUTInventoryCostLots: Map<number, InventoryCostLotTracker>;
readonly baseCurrency: string;
/** /**
* Constructor method. * Constructor method.
* @param {IInventoryValuationReportQuery} query * @param {IInventoryValuationReportQuery} query - Inventory valuation query.
* @param {ModelObject<Item>[]} items * @param {InventoryValuationSheetRepository} repository - Inventory valuation sheet repository.
* @param {Map<number, InventoryCostLotTracker[]>} INInventoryCostLots
* @param {Map<number, InventoryCostLotTracker[]>} OUTInventoryCostLots
* @param {string} baseCurrency
*/ */
constructor( constructor(
query: IInventoryValuationReportQuery, query: IInventoryValuationReportQuery,
items: ModelObject<Item>[], repository: InventoryValuationSheetRepository,
INInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
OUTInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
baseCurrency: string,
) { ) {
super(); super();
this.query = query; this.query = query;
this.items = items; this.repository = repository;
this.INInventoryCostLots = transformToMap(INInventoryCostLots, 'itemId');
this.OUTInventoryCostLots = transformToMap(OUTInventoryCostLots, 'itemId');
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
} }
@@ -70,7 +59,7 @@ export class InventoryValuationSheet extends FinancialSheet {
cost: number; cost: number;
quantity: number; quantity: number;
} { } {
return this.getItemTransaction(this.INInventoryCostLots, itemId); return this.getItemTransaction(this.repository.INInventoryCostLots, itemId);
} }
/** /**
@@ -81,7 +70,7 @@ export class InventoryValuationSheet extends FinancialSheet {
cost: number; cost: number;
quantity: number; quantity: number;
} { } {
return this.getItemTransaction(this.OUTInventoryCostLots, itemId); return this.getItemTransaction(this.repository.OUTInventoryCostLots, itemId);
} }
/** /**
@@ -148,10 +137,13 @@ export class InventoryValuationSheet extends FinancialSheet {
private filterNoneTransactions = ( private filterNoneTransactions = (
valuationItem: IInventoryValuationItem, valuationItem: IInventoryValuationItem,
): boolean => { ): boolean => {
const transactionIN = this.INInventoryCostLots.get(valuationItem.id); const transactionIN = this.repository.INInventoryCostLots.get(
const transactionOUT = this.OUTInventoryCostLots.get(valuationItem.id); valuationItem.id,
);
return transactionOUT || transactionIN; const transactionOUT = this.repository.OUTInventoryCostLots.get(
valuationItem.id,
);
return !isEmpty(transactionOUT) || !isEmpty(transactionIN);
}; };
/** /**
@@ -200,8 +192,8 @@ export class InventoryValuationSheet extends FinancialSheet {
* @param {IItem[]} items * @param {IItem[]} items
* @returns {IInventoryValuationItem[]} * @returns {IInventoryValuationItem[]}
*/ */
private itemsMapper = (items: IItem[]): IInventoryValuationItem[] => { private itemsMapper = (items: ModelObject<Item>[]): IInventoryValuationItem[] => {
return this.items.map(this.itemMapper.bind(this)); return this.repository.inventoryItems.map(this.itemMapper.bind(this));
}; };
/** /**
@@ -230,7 +222,7 @@ export class InventoryValuationSheet extends FinancialSheet {
return R.compose( return R.compose(
R.when(this.isItemsPostFilter, this.itemsFilter), R.when(this.isItemsPostFilter, this.itemsFilter),
this.itemsMapper, this.itemsMapper,
)(this.items); )(this.repository.inventoryItems);
} }
/** /**

View File

@@ -1,17 +1,109 @@
import { Injectable } from '@nestjs/common'; import { isEmpty } from 'lodash';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { IInventoryValuationReportQuery } from './InventoryValuationSheet.types';
import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker';
import { ModelObject } from 'objection';
import { Item } from '@/modules/Items/models/Item';
import { transformToMap } from '@/utils/transform-to-key';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable({ scope: Scope.TRANSIENT })
@Injectable()
export class InventoryValuationSheetRepository { export class InventoryValuationSheetRepository {
asyncInit() { @Inject(TenancyContext)
const inventoryItems = await Item.query().onBuild((q) => { private readonly tenancyContext: TenancyContext;
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: typeof InventoryCostLotTracker;
@Inject(Item.name)
private readonly itemModel: typeof Item;
/**
* The filter.
* @param {IInventoryValuationReportQuery} value
*/
filter: IInventoryValuationReportQuery;
/**
* The inventory items.
* @param {ModelObject<Item>[]} value
*/
inventoryItems: ModelObject<Item>[];
/**
* The inventory cost `IN` transactions.
* @param {ModelObject<InventoryCostLotTracker>[]} value
*/
INTransactions: ModelObject<InventoryCostLotTracker>[];
/**
* The inventory cost `IN` transactions map.
* @param {Map<number, InventoryCostLotTracker[]>} value
*/
INInventoryCostLots: Map<number, InventoryCostLotTracker[]>;
/**
* The inventory cost `OUT` transactions.
* @param {ModelObject<InventoryCostLotTracker>[]} value
*/
OUTTransactions: ModelObject<InventoryCostLotTracker>[];
/**
* The inventory cost `OUT` transactions map.
* @param {Map<number, InventoryCostLotTracker[]>} value
*/
OUTInventoryCostLots: Map<number, InventoryCostLotTracker[]>;
/**
* The base currency.
* @param {string} value
*/
baseCurrency: string;
/**
* Set the filter.
* @param {IInventoryValuationReportQuery} filter
*/
setFilter(filter: IInventoryValuationReportQuery) {
this.filter = filter;
}
/**
* Initialize the repository.
*/
async asyncInit() {
await this.asyncItems();
await this.asyncCostLotsTransactions();
await this.asyncBaseCurrency();
}
/**
* Retrieve the base currency.
*/
async asyncBaseCurrency() {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = tenantMetadata.baseCurrency;
}
/**
* Retrieve the inventory items.
*/
async asyncItems() {
const inventoryItems = await this.itemModel.query().onBuild((q) => {
q.where('type', 'inventory'); q.where('type', 'inventory');
if (filter.itemsIds.length > 0) { if (this.filter.itemsIds.length > 0) {
q.whereIn('id', filter.itemsIds); q.whereIn('id', this.filter.itemsIds);
} }
}); });
const inventoryItemsIds = inventoryItems.map((item) => item.id); this.inventoryItems = inventoryItems;
}
/**
* Retrieve the inventory cost `IN` and `OUT` transactions.
*/
async asyncCostLotsTransactions() {
const inventoryItemsIds = this.inventoryItems.map((item) => item.id);
const commonQuery = (builder) => { const commonQuery = (builder) => {
builder.whereIn('item_id', inventoryItemsIds); builder.whereIn('item_id', inventoryItemsIds);
@@ -21,23 +113,29 @@ export class InventoryValuationSheetRepository {
builder.select('itemId'); builder.select('itemId');
builder.groupBy('itemId'); builder.groupBy('itemId');
if (!isEmpty(query.branchesIds)) { if (!isEmpty(this.filter.branchesIds)) {
builder.modify('filterByBranches', query.branchesIds); builder.modify('filterByBranches', this.filter.branchesIds);
} }
if (!isEmpty(query.warehousesIds)) { if (!isEmpty(this.filter.warehousesIds)) {
builder.modify('filterByWarehouses', query.warehousesIds); builder.modify('filterByWarehouses', this.filter.warehousesIds);
} }
}; };
// Retrieve the inventory cost `IN` transactions. // Retrieve the inventory cost `IN` transactions.
const INTransactions = await InventoryCostLotTracker.query() const INTransactions = await this.inventoryCostLotTracker
.query()
.onBuild(commonQuery) .onBuild(commonQuery)
.where('direction', 'IN'); .where('direction', 'IN');
// Retrieve the inventory cost `OUT` transactions. // Retrieve the inventory cost `OUT` transactions.
const OUTTransactions = await InventoryCostLotTracker.query() const OUTTransactions = await this.inventoryCostLotTracker
.query()
.onBuild(commonQuery) .onBuild(commonQuery)
.where('direction', 'OUT'); .where('direction', 'OUT');
this.INTransactions = INTransactions;
this.OUTTransactions = OUTTransactions;
this.INInventoryCostLots = transformToMap(INTransactions, 'itemId');
this.OUTInventoryCostLots = transformToMap(OUTTransactions, 'itemId');
} }
} }

View File

@@ -9,19 +9,15 @@ import { InventoryValuationMetaInjectable } from './InventoryValuationSheetMeta'
import { getInventoryValuationDefaultQuery } from './_constants'; import { getInventoryValuationDefaultQuery } from './_constants';
import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker'; import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker';
import { Item } from '@/modules/Items/models/Item'; import { Item } from '@/modules/Items/models/Item';
import { InventoryValuationSheetRepository } from './InventoryValuationSheetRepository';
import { InventoryValuationSheet } from './InventoryValuationSheet';
@Injectable() @Injectable()
export class InventoryValuationSheetService { export class InventoryValuationSheetService {
constructor( constructor(
private readonly inventoryService: InventoryService,
private readonly inventoryValuationMeta: InventoryValuationMetaInjectable, private readonly inventoryValuationMeta: InventoryValuationMetaInjectable,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
private readonly inventoryValuationSheetRepository: InventoryValuationSheetRepository,
@Inject(Item.name)
private readonly itemModel: typeof Item,
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: typeof InventoryCostLotTracker,
) {} ) {}
/** /**
@@ -35,12 +31,12 @@ export class InventoryValuationSheetService {
...getInventoryValuationDefaultQuery(), ...getInventoryValuationDefaultQuery(),
...query, ...query,
}; };
this.inventoryValuationSheetRepository.setFilter(filter);
await this.inventoryValuationSheetRepository.asyncInit();
const inventoryValuationInstance = new InventoryValuationSheet( const inventoryValuationInstance = new InventoryValuationSheet(
filter, filter,
inventoryItems, this.inventoryValuationSheetRepository,
INTransactions,
OUTTransactions,
tenant.metadata.baseCurrency,
); );
// Retrieve the inventory valuation report data. // Retrieve the inventory valuation report data.
const inventoryValuationData = inventoryValuationInstance.reportData(); const inventoryValuationData = inventoryValuationInstance.reportData();
@@ -51,9 +47,7 @@ export class InventoryValuationSheetService {
// Triggers `onInventoryValuationViewed` event. // Triggers `onInventoryValuationViewed` event.
await this.eventPublisher.emitAsync( await this.eventPublisher.emitAsync(
events.reports.onInventoryValuationViewed, events.reports.onInventoryValuationViewed,
{ { query },
query,
},
); );
return { return {

View File

@@ -4,6 +4,7 @@ import * as moment from 'moment';
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils'; import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
import { TInventoryTransactionDirection } from '../types/InventoryCost.types'; import { TInventoryTransactionDirection } from '../types/InventoryCost.types';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { InventoryTransactionMeta } from './InventoryTransactionMeta';
export class InventoryTransaction extends TenantBaseModel { export class InventoryTransaction extends TenantBaseModel {
date: Date | string; date: Date | string;
@@ -21,6 +22,8 @@ export class InventoryTransaction extends TenantBaseModel {
warehouseId?: number; warehouseId?: number;
meta?: InventoryTransactionMeta;
/** /**
* Table name * Table name
*/ */

View File

@@ -2,6 +2,10 @@ import { BaseModel } from '@/models/Model';
import { Model, raw } from 'objection'; import { Model, raw } from 'objection';
export class InventoryTransactionMeta extends BaseModel { export class InventoryTransactionMeta extends BaseModel {
transactionNumber!: string;
description!: string;
inventoryTransactionId!: number;
/** /**
* Table name * Table name
*/ */

View File

@@ -0,0 +1,6 @@
export const transformToMapKeyValue = <T, K extends string | number>(
collection: T[],
key: keyof T,
): Map<K, T> => {
return new Map(collection.map((item) => [item[key], item]));
};