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 {
@Inject(Vendor.name)
private readonly vendorModel: typeof Vendor;
@Inject(Bill.name)
private readonly billModel: typeof Bill;
asyncInit() {
// Settings tenant service.
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
@Inject(TenancyContext)
private readonly tenancyContext: TenancyContext;
/**
* 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.
const vendors =
filter.vendorsIds.length > 0
? await vendorRepository.findWhereIn('id', filter.vendorsIds)
: await vendorRepository.all();
this.filter.vendorsIds.length > 0
? await this.vendorModel.query().whereIn('id', this.filter.vendorsIds)
: await this.vendorModel.query();
// Common query.
this.vendors = vendors;
}
/**
* Retrieve all overdue bills from the storage.
*/
async asyncOverdueBills() {
const commonQuery = (query) => {
if (!isEmpty(filter.branchesIds)) {
query.modify('filterByBranches', filter.branchesIds);
if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', this.filter.branchesIds);
}
};
// Retrieve all overdue vendors bills.
const overdueBills = await Bill.query()
.modify('overdueBillsFromDate', filter.asDate)
const overdueBills = await this.billModel
.query()
.modify('overdueBillsFromDate', this.filter.asDate)
.onBuild(commonQuery);
// Retrieve all due vendors bills.
const dueBills = await Bill.query()
.modify('dueBillsFromDate', filter.asDate)
.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.
const dueBills = await this.billModel
.query()
.modify('dueBillsFromDate', this.filter.asDate)
.onBuild(commonQuery);
this.dueBills = dueBills;
this.dueBillsByVendorId = groupBy(dueBills, 'vendorId');
}
}

View File

@@ -1,5 +1,6 @@
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import {
IAPAgingSummaryQuery,
IAPAgingSummarySheet,
@@ -7,19 +8,19 @@ import {
import { APAgingSummarySheet } from './APAgingSummarySheet';
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
import { getAPAgingSummaryDefaultQuery } from './utils';
import { events } from '@/common/events/events';
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
@Injectable()
export class APAgingSummaryService {
constructor(
private readonly APAgingSummaryMeta: APAgingSummaryMeta,
private readonly eventPublisher: EventEmitter2,
private readonly APAgingSummaryRepository: APAgingSummaryRepository,
) {}
/**
* Retrieve A/P aging summary report.
* @param {number} tenantId -
* @param {IAPAgingSummaryQuery} query -
* @param {IAPAgingSummaryQuery} query - A/P aging summary query.
* @returns {Promise<IAPAgingSummarySheet>}
*/
public async APAgingSummary(
@@ -30,13 +31,14 @@ export class APAgingSummaryService {
...getAPAgingSummaryDefaultQuery(),
...query,
};
// Load the data.
this.APAgingSummaryRepository.setFilter(filter);
await this.APAgingSummaryRepository.load();
// A/P aging summary report instance.
const APAgingSummaryReport = new APAgingSummarySheet(
filter,
vendors,
overdueBills,
dueBills,
tenant.metadata.baseCurrency,
this.APAgingSummaryRepository,
);
// A/P aging summary report data and columns.
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 {
IAPAgingSummaryQuery,
@@ -10,20 +10,13 @@ import {
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';
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
export class APAgingSummarySheet extends AgingSummaryReport {
readonly tenantId: number;
readonly repository: APAgingSummaryRepository;
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[];
/**
@@ -34,23 +27,14 @@ export class APAgingSummarySheet extends AgingSummaryReport {
* @param {string} baseCurrency - Base currency of the organization.
*/
constructor(
tenantId: number,
query: IAPAgingSummaryQuery,
vendors: ModelObject<Vendor>[],
overdueBills: ModelObject<Bill>[],
unpaidBills: ModelObject<Bill>[],
baseCurrency: string,
repository: APAgingSummaryRepository,
) {
super();
this.tenantId = tenantId;
this.query = query;
this.repository = repository;
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(
@@ -170,7 +154,7 @@ export class APAgingSummarySheet extends AgingSummaryReport {
* @return {IAPAgingSummaryData}
*/
public reportData = (): IAPAgingSummaryData => {
const vendorsAgingPeriods = this.vendorsSection(this.contacts);
const vendorsAgingPeriods = this.vendorsSection(this.repository.vendors);
const vendorsTotal = this.getVendorsTotal(vendorsAgingPeriods);
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 {
@Inject(TenancyContext)
private tenancyContext: TenancyContext;
@Inject(Customer.name)
private customerModel: typeof Customer;
init(){
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
@Inject(SaleInvoice.name)
private saleInvoiceModel: typeof SaleInvoice;
// Retrieve all customers from the storage.
const customers =
filter.customersIds.length > 0
? await customerRepository.findWhereIn('id', filter.customersIds)
: await customerRepository.all();
/**
* Filter.
* @param {IARAgingSummaryQuery} filter
*/
filter: IARAgingSummaryQuery;
// 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);
/**
* Base currency.
* @param {string} baseCurrency
*/
baseCurrency: string;
// Retrieve all due sale invoices.
const currentInvoices = await SaleInvoice.query()
.modify('dueInvoicesFromDate', filter.asDate)
.onBuild(commonQuery);
/**
* 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.
const customers =
this.filter.customersIds.length > 0
? await this.customerModel
.query()
.whereIn('id', this.filter.customersIds)
: await this.customerModel.query();
this.customers = customers;
}
/**
* Initialize the overdue sale invoices.
*/
async initOverdueSaleInvoices() {
const commonQuery = (query) => {
if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', this.filter.branchesIds);
}
};
// Retrieve all overdue sale invoices.
const overdueSaleInvoices = await this.saleInvoiceModel
.query()
.modify('overdueInvoicesFromDate', this.filter.asDate)
.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.
const currentInvoices = await this.saleInvoiceModel
.query()
.modify('dueInvoicesFromDate', this.filter.asDate)
.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 { IARAgingSummaryQuery } from './ARAgingSummary.types';
import { events } from '@/common/events/events';
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
@Injectable()
export class ARAgingSummaryService {
constructor(
private readonly ARAgingSummaryMeta: ARAgingSummaryMeta,
private readonly ARAgingSummaryRepository: ARAgingSummaryRepository,
private readonly eventPublisher: EventEmitter2,
) {}
@@ -22,13 +24,14 @@ export class ARAgingSummaryService {
...getARAgingSummaryDefaultQuery(),
...query,
};
// Load the A/R aging summary repository.
this.ARAgingSummaryRepository.setFilter(filter);
await this.ARAgingSummaryRepository.load();
// AR aging summary report instance.
const ARAgingSummaryReport = new ARAgingSummarySheet(
filter,
customers,
overdueSaleInvoices,
currentInvoices,
tenant.metadata.baseCurrency,
this.ARAgingSummaryRepository,
);
// AR aging summary report data and columns.
const data = ARAgingSummaryReport.reportData();
@@ -40,9 +43,7 @@ export class ARAgingSummaryService {
// Triggers `onReceivableAgingViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onReceivableAgingViewed,
{
query,
},
{ query },
);
return {

View File

@@ -1,5 +1,5 @@
import * as R from 'ramda';
import { Dictionary, groupBy, isEmpty, sum } from 'lodash';
import { isEmpty, sum } from 'lodash';
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
import {
IARAgingSummaryQuery,
@@ -12,17 +12,12 @@ 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';
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
export class ARAgingSummarySheet extends AgingSummaryReport {
readonly tenantId: number;
readonly query: IARAgingSummaryQuery;
readonly contacts: ModelObject<Customer>[];
readonly agingPeriods: IAgingPeriod[];
readonly baseCurrency: string;
readonly overdueInvoicesByContactId: Dictionary<ModelObject<SaleInvoice>[]>;
readonly currentInvoicesByContactId: Dictionary<ModelObject<SaleInvoice>[]>;
readonly repository: ARAgingSummaryRepository;
/**
* Constructor method.
@@ -32,29 +27,15 @@ export class ARAgingSummarySheet extends AgingSummaryReport {
* @param {IJournalPoster} journal
*/
constructor(
tenantId: number,
query: IARAgingSummaryQuery,
customers: ModelObject<Customer>[],
overdueSaleInvoices: ModelObject<SaleInvoice>[],
currentSaleInvoices: ModelObject<SaleInvoice>[],
baseCurrency: string,
repository: ARAgingSummaryRepository,
) {
super();
this.tenantId = tenantId;
this.contacts = customers;
this.query = query;
this.baseCurrency = baseCurrency;
this.repository = repository;
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,
@@ -179,7 +160,9 @@ export class ARAgingSummarySheet extends AgingSummaryReport {
* @return {IARAgingSummaryData}
*/
public reportData = (): IARAgingSummaryData => {
const customersAgingPeriods = this.customersWalker(this.contacts);
const customersAgingPeriods = this.customersWalker(
this.repository.customers,
);
const customersTotal = this.getCustomersTotal(customersAgingPeriods);
return {

View File

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

View File

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

View File

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

View File

@@ -4,19 +4,129 @@ 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';
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 })
export class InventoryItemDetailsRepository {
@Inject(Item.name)
readonly itemModel: typeof Item;
@Inject(InventoryTransaction.name)
readonly inventoryTransactionModel: typeof InventoryTransaction;
@Inject(TenancyContext)
readonly tenancyContext: TenancyContext;
/**
* Constructor method.
* @param {typeof Item} itemModel - Item model.
* @param {typeof InventoryTransaction} inventoryTransactionModel - Inventory transaction model.
* The items.
* @param {ModelObject<Item>[]} items - The items.
*/
constructor(
private readonly itemModel: typeof Item,
private readonly inventoryTransactionModel: typeof InventoryTransaction,
) {}
items: ModelObject<Item>[];
/**
* 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.
@@ -39,7 +149,7 @@ export class InventoryItemDetailsRepository {
* @param {IInventoryDetailsQuery}
* @return {Promise<ModelObject<InventoryTransaction>>}
*/
public async openingBalanceTransactions(
public async getOpeningBalanceTransactions(
filter: IInventoryDetailsQuery,
): Promise<ModelObject<InventoryTransaction>[]> {
const openingBalanceDate = moment(filter.fromDate)
@@ -95,7 +205,7 @@ export class InventoryItemDetailsRepository {
* @param {IInventoryDetailsQuery}
* @return {Promise<IInventoryTransaction>}
*/
public async itemInventoryTransactions(
public async getItemInventoryTransactions(
filter: IInventoryDetailsQuery,
): Promise<ModelObject<InventoryTransaction>[]> {
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 {
IInventoryDetailsQuery,
IInvetoryItemDetailsTable,
} from './InventoryItemDetails.types';
import { InventoryDetailsService } from './InventoryItemDetailsService';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { InventoryDetailsService } from './InventoryItemDetails.service';
import { InventoryItemDetailsTable } from './InventoryItemDetailsTable';
@Injectable()
export class InventoryDetailsTableInjectable {
@@ -24,7 +24,8 @@ export class InventoryDetailsTableInjectable {
): Promise<IInvetoryItemDetailsTable> {
const inventoryDetails =
await this.inventoryDetails.inventoryDetails(query);
const table = new InventoryDetailsTable(inventoryDetails, this.i18n);
const table = new InventoryItemDetailsTable(inventoryDetails.data, this.i18n);
return {
table: {

View File

@@ -1,3 +1,5 @@
import { IInventoryDetailsQuery } from "./InventoryItemDetails.types";
export const HtmlTableCustomCss = `
table tr.row-type--item td,
table tr.row-type--opening-entry td,
@@ -5,3 +7,32 @@ table tr.row-type--closing-entry td{
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 * as R from 'ramda';
import {
@@ -6,41 +7,29 @@ import {
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';
import { InventoryValuationSheetRepository } from './InventoryValuationSheetRepository';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
export class InventoryValuationSheet extends FinancialSheet {
readonly query: IInventoryValuationReportQuery;
readonly items: ModelObject<Item>[];
readonly INInventoryCostLots: Map<number, InventoryCostLotTracker>;
readonly OUTInventoryCostLots: Map<number, InventoryCostLotTracker>;
readonly baseCurrency: string;
readonly repository: InventoryValuationSheetRepository;
/**
* Constructor method.
* @param {IInventoryValuationReportQuery} query
* @param {ModelObject<Item>[]} items
* @param {Map<number, InventoryCostLotTracker[]>} INInventoryCostLots
* @param {Map<number, InventoryCostLotTracker[]>} OUTInventoryCostLots
* @param {string} baseCurrency
* @param {IInventoryValuationReportQuery} query - Inventory valuation query.
* @param {InventoryValuationSheetRepository} repository - Inventory valuation sheet repository.
*/
constructor(
query: IInventoryValuationReportQuery,
items: ModelObject<Item>[],
INInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
OUTInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
baseCurrency: string,
repository: InventoryValuationSheetRepository,
) {
super();
this.query = query;
this.items = items;
this.INInventoryCostLots = transformToMap(INInventoryCostLots, 'itemId');
this.OUTInventoryCostLots = transformToMap(OUTInventoryCostLots, 'itemId');
this.baseCurrency = baseCurrency;
this.repository = repository;
this.numberFormat = this.query.numberFormat;
}
@@ -70,7 +59,7 @@ export class InventoryValuationSheet extends FinancialSheet {
cost: 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;
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 = (
valuationItem: IInventoryValuationItem,
): boolean => {
const transactionIN = this.INInventoryCostLots.get(valuationItem.id);
const transactionOUT = this.OUTInventoryCostLots.get(valuationItem.id);
return transactionOUT || transactionIN;
const transactionIN = this.repository.INInventoryCostLots.get(
valuationItem.id,
);
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
* @returns {IInventoryValuationItem[]}
*/
private itemsMapper = (items: IItem[]): IInventoryValuationItem[] => {
return this.items.map(this.itemMapper.bind(this));
private itemsMapper = (items: ModelObject<Item>[]): IInventoryValuationItem[] => {
return this.repository.inventoryItems.map(this.itemMapper.bind(this));
};
/**
@@ -230,7 +222,7 @@ export class InventoryValuationSheet extends FinancialSheet {
return R.compose(
R.when(this.isItemsPostFilter, this.itemsFilter),
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()
@Injectable({ scope: Scope.TRANSIENT })
export class InventoryValuationSheetRepository {
asyncInit() {
const inventoryItems = await Item.query().onBuild((q) => {
@Inject(TenancyContext)
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');
if (filter.itemsIds.length > 0) {
q.whereIn('id', filter.itemsIds);
if (this.filter.itemsIds.length > 0) {
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) => {
builder.whereIn('item_id', inventoryItemsIds);
@@ -21,23 +113,29 @@ export class InventoryValuationSheetRepository {
builder.select('itemId');
builder.groupBy('itemId');
if (!isEmpty(query.branchesIds)) {
builder.modify('filterByBranches', query.branchesIds);
if (!isEmpty(this.filter.branchesIds)) {
builder.modify('filterByBranches', this.filter.branchesIds);
}
if (!isEmpty(query.warehousesIds)) {
builder.modify('filterByWarehouses', query.warehousesIds);
if (!isEmpty(this.filter.warehousesIds)) {
builder.modify('filterByWarehouses', this.filter.warehousesIds);
}
};
// Retrieve the inventory cost `IN` transactions.
const INTransactions = await InventoryCostLotTracker.query()
const INTransactions = await this.inventoryCostLotTracker
.query()
.onBuild(commonQuery)
.where('direction', 'IN');
// Retrieve the inventory cost `OUT` transactions.
const OUTTransactions = await InventoryCostLotTracker.query()
const OUTTransactions = await this.inventoryCostLotTracker
.query()
.onBuild(commonQuery)
.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 { InventoryCostLotTracker } from '@/modules/InventoryCost/models/InventoryCostLotTracker';
import { Item } from '@/modules/Items/models/Item';
import { InventoryValuationSheetRepository } from './InventoryValuationSheetRepository';
import { InventoryValuationSheet } from './InventoryValuationSheet';
@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,
private readonly inventoryValuationSheetRepository: InventoryValuationSheetRepository,
) {}
/**
@@ -35,12 +31,12 @@ export class InventoryValuationSheetService {
...getInventoryValuationDefaultQuery(),
...query,
};
this.inventoryValuationSheetRepository.setFilter(filter);
await this.inventoryValuationSheetRepository.asyncInit();
const inventoryValuationInstance = new InventoryValuationSheet(
filter,
inventoryItems,
INTransactions,
OUTTransactions,
tenant.metadata.baseCurrency,
this.inventoryValuationSheetRepository,
);
// Retrieve the inventory valuation report data.
const inventoryValuationData = inventoryValuationInstance.reportData();
@@ -51,9 +47,7 @@ export class InventoryValuationSheetService {
// Triggers `onInventoryValuationViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onInventoryValuationViewed,
{
query,
},
{ query },
);
return {

View File

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

View File

@@ -2,6 +2,10 @@ import { BaseModel } from '@/models/Model';
import { Model, raw } from 'objection';
export class InventoryTransactionMeta extends BaseModel {
transactionNumber!: string;
description!: string;
inventoryTransactionId!: number;
/**
* 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]));
};