mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat(reports): inventory valuation report.
feat(reports): sales by items report. feat(reports): purchases by items report.
This commit is contained in:
@@ -8,6 +8,9 @@ import JournalSheetController from './FinancialStatements/JournalSheet';
|
|||||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||||
import ARAgingSummary from './FinancialStatements/ARAgingSummary';
|
import ARAgingSummary from './FinancialStatements/ARAgingSummary';
|
||||||
import APAgingSummary from './FinancialStatements/APAgingSummary';
|
import APAgingSummary from './FinancialStatements/APAgingSummary';
|
||||||
|
import PurchasesByItemsController from './FinancialStatements/PurchasesByItem';
|
||||||
|
import SalesByItemsController from './FinancialStatements/SalesByItems';
|
||||||
|
import InventoryValuationController from './FinancialStatements/InventoryValuationSheet';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class FinancialStatementsService {
|
export default class FinancialStatementsService {
|
||||||
@@ -42,7 +45,18 @@ export default class FinancialStatementsService {
|
|||||||
'/payable_aging_summary',
|
'/payable_aging_summary',
|
||||||
Container.get(APAgingSummary).router()
|
Container.get(APAgingSummary).router()
|
||||||
);
|
);
|
||||||
|
router.use(
|
||||||
|
'/purchases-by-items',
|
||||||
|
Container.get(PurchasesByItemsController).router()
|
||||||
|
);
|
||||||
|
router.use(
|
||||||
|
'/sales-by-items',
|
||||||
|
Container.get(SalesByItemsController).router()
|
||||||
|
);
|
||||||
|
router.use(
|
||||||
|
'/inventory-valuation',
|
||||||
|
Container.get(InventoryValuationController).router()
|
||||||
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { query, ValidationChain } from 'express-validator';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||||
|
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||||
|
import InventoryValuationService from 'services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class InventoryValuationReportController extends BaseFinancialReportController {
|
||||||
|
@Inject()
|
||||||
|
inventoryValuationService: InventoryValuationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
this.validationSchema,
|
||||||
|
this.validationResult,
|
||||||
|
asyncMiddleware(this.inventoryValuation.bind(this))
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation schema.
|
||||||
|
*/
|
||||||
|
get validationSchema(): ValidationChain[] {
|
||||||
|
return [
|
||||||
|
query('from_date').optional().isISO8601(),
|
||||||
|
query('to_date').optional().isISO8601(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||||
|
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||||
|
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||||
|
query('order').optional().isIn(['desc', 'asc']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the general ledger financial statement.
|
||||||
|
* @param {Request} req -
|
||||||
|
* @param {Response} res -
|
||||||
|
*/
|
||||||
|
async inventoryValuation(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const filter = this.matchedQueryData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
meta,
|
||||||
|
} = await this.inventoryValuationService.inventoryValuationSheet(
|
||||||
|
tenantId,
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
meta: this.transfromToResponse(meta),
|
||||||
|
data: this.transfromToResponse(data),
|
||||||
|
query: this.transfromToResponse(query),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { query, ValidationChain } from 'express-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||||
|
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||||
|
import PurchasesByItemsService from 'services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class PurchasesByItemReportController extends BaseFinancialReportController {
|
||||||
|
@Inject()
|
||||||
|
purchasesByItemsService: PurchasesByItemsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
this.validationSchema,
|
||||||
|
this.validationResult,
|
||||||
|
asyncMiddleware(this.purchasesByItems.bind(this))
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation schema.
|
||||||
|
*/
|
||||||
|
get validationSchema(): ValidationChain[] {
|
||||||
|
return [
|
||||||
|
query('from_date').optional().isISO8601(),
|
||||||
|
query('to_date').optional().isISO8601(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||||
|
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||||
|
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||||
|
query('order').optional().isIn(['desc', 'asc']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the general ledger financial statement.
|
||||||
|
* @param {Request} req -
|
||||||
|
* @param {Response} res -
|
||||||
|
*/
|
||||||
|
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const filter = this.matchedQueryData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
meta,
|
||||||
|
} = await this.purchasesByItemsService.purchasesByItems(
|
||||||
|
tenantId,
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
meta: this.transfromToResponse(meta),
|
||||||
|
data: this.transfromToResponse(data),
|
||||||
|
query: this.transfromToResponse(query),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { query, ValidationChain } from 'express-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||||
|
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||||
|
import SalesByItemsReportService from 'services/FinancialStatements/SalesByItems/SalesByItemsService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SalesByItemsReportController extends BaseFinancialReportController {
|
||||||
|
@Inject()
|
||||||
|
salesByItemsService: SalesByItemsReportService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
this.validationSchema,
|
||||||
|
this.validationResult,
|
||||||
|
asyncMiddleware(this.purchasesByItems.bind(this))
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation schema.
|
||||||
|
*/
|
||||||
|
get validationSchema(): ValidationChain[] {
|
||||||
|
return [
|
||||||
|
query('from_date').optional().isISO8601(),
|
||||||
|
query('to_date').optional().isISO8601(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||||
|
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||||
|
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||||
|
query('order').optional().isIn(['desc', 'asc']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the general ledger financial statement.
|
||||||
|
* @param {Request} req -
|
||||||
|
* @param {Response} res -
|
||||||
|
*/
|
||||||
|
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const filter = this.matchedQueryData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
meta,
|
||||||
|
} = await this.salesByItemsService.salesByItems(
|
||||||
|
tenantId,
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
meta: this.transfromToResponse(meta),
|
||||||
|
data: this.transfromToResponse(data),
|
||||||
|
query: this.transfromToResponse(query),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
|
|
||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.createTable('accounts_transactions', (table) => {
|
return knex.schema
|
||||||
|
.createTable('accounts_transactions', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.decimal('credit', 13, 3);
|
table.decimal('credit', 13, 3);
|
||||||
table.decimal('debit', 13, 3);
|
table.decimal('debit', 13, 3);
|
||||||
table.string('transaction_type').index();
|
table.string('transaction_type').index();
|
||||||
table.string('reference_type').index();
|
table.string('reference_type').index();
|
||||||
table.integer('reference_id').index();
|
table.integer('reference_id').index();
|
||||||
table.integer('account_id').unsigned().index().references('id').inTable('accounts');
|
table
|
||||||
|
.integer('account_id')
|
||||||
|
.unsigned()
|
||||||
|
.index()
|
||||||
|
.references('id')
|
||||||
|
.inTable('accounts');
|
||||||
table.string('contact_type').nullable().index();
|
table.string('contact_type').nullable().index();
|
||||||
table.integer('contact_id').unsigned().nullable().index();
|
table.integer('contact_id').unsigned().nullable().index();
|
||||||
table.string('transaction_number').nullable().index();
|
table.string('transaction_number').nullable().index();
|
||||||
table.string('reference_number').nullable().index();
|
table.string('reference_number').nullable().index();
|
||||||
table.integer('item_id').unsigned().nullable().index();
|
table.integer('item_id').unsigned().nullable().index();
|
||||||
|
table.integer('item_quantity').unsigned().nullable().index(),
|
||||||
table.string('note');
|
table.string('note');
|
||||||
table.integer('user_id').unsigned().index();
|
table.integer('user_id').unsigned().index();
|
||||||
|
|
||||||
@@ -21,7 +27,8 @@ exports.up = function(knex) {
|
|||||||
|
|
||||||
table.date('date').index();
|
table.date('date').index();
|
||||||
table.datetime('created_at').index();
|
table.datetime('created_at').index();
|
||||||
}).raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000');
|
})
|
||||||
|
.raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = function (knex) {
|
exports.down = function (knex) {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export interface IBalanceSheetQuery {
|
|||||||
fromDate: Date | string;
|
fromDate: Date | string;
|
||||||
toDate: Date | string;
|
toDate: Date | string;
|
||||||
numberFormat: INumberFormatQuery;
|
numberFormat: INumberFormatQuery;
|
||||||
noneZero: boolean;
|
|
||||||
noneTransactions: boolean;
|
noneTransactions: boolean;
|
||||||
|
noneZero: boolean;
|
||||||
basis: 'cash' | 'accural';
|
basis: 'cash' | 'accural';
|
||||||
accountIds: number[];
|
accountIds: number[];
|
||||||
}
|
}
|
||||||
|
|||||||
42
server/src/interfaces/IInventoryValuationSheet.ts
Normal file
42
server/src/interfaces/IInventoryValuationSheet.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
INumberFormatQuery,
|
||||||
|
IFormatNumberSettings,
|
||||||
|
} from './FinancialStatements';
|
||||||
|
|
||||||
|
export interface IInventoryValuationReportQuery {
|
||||||
|
asDate: Date | string;
|
||||||
|
numberFormat: INumberFormatQuery;
|
||||||
|
noneTransactions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IInventoryValuationSheetMeta {
|
||||||
|
organizationName: string,
|
||||||
|
baseCurrency: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IInventoryValuationItem {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
code: string,
|
||||||
|
valuation: number,
|
||||||
|
quantity: number,
|
||||||
|
average: number,
|
||||||
|
valuationFormatted: string,
|
||||||
|
quantityFormatted: string,
|
||||||
|
averageFormatted: string,
|
||||||
|
currencyCode: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IInventoryValuationTotal {
|
||||||
|
valuation: number,
|
||||||
|
quantity: number,
|
||||||
|
|
||||||
|
valuationFormatted: string,
|
||||||
|
quantityFormatted: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInventoryValuationStatement {
|
||||||
|
items: IInventoryValuationItem[],
|
||||||
|
total: IInventoryValuationTotal
|
||||||
|
};
|
||||||
|
|
||||||
43
server/src/interfaces/SalesByItemsSheet.ts
Normal file
43
server/src/interfaces/SalesByItemsSheet.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
INumberFormatQuery,
|
||||||
|
} from './FinancialStatements';
|
||||||
|
|
||||||
|
export interface ISalesByItemsReportQuery {
|
||||||
|
fromDate: Date | string;
|
||||||
|
toDate: Date | string;
|
||||||
|
numberFormat: INumberFormatQuery;
|
||||||
|
noneTransactions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISalesByItemsSheetMeta {
|
||||||
|
organizationName: string,
|
||||||
|
baseCurrency: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISalesByItemsItem {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
code: string,
|
||||||
|
quantitySold: number,
|
||||||
|
soldCost: number,
|
||||||
|
averageSellPrice: number,
|
||||||
|
|
||||||
|
quantitySoldFormatted: string,
|
||||||
|
soldCostFormatted: string,
|
||||||
|
averageSellPriceFormatted: string,
|
||||||
|
currencyCode: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISalesByItemsTotal {
|
||||||
|
quantitySold: number,
|
||||||
|
soldCost: number,
|
||||||
|
quantitySoldFormatted: string,
|
||||||
|
soldCostFormatted: string,
|
||||||
|
currencyCode: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISalesByItemsSheetStatement {
|
||||||
|
items: ISalesByItemsItem[],
|
||||||
|
total: ISalesByItemsTotal
|
||||||
|
};
|
||||||
|
|
||||||
@@ -41,3 +41,5 @@ export * from './APAgingSummaryReport';
|
|||||||
export * from './Mailable';
|
export * from './Mailable';
|
||||||
export * from './InventoryAdjustment';
|
export * from './InventoryAdjustment';
|
||||||
export * from './Setup'
|
export * from './Setup'
|
||||||
|
export * from './IInventoryValuationSheet';
|
||||||
|
export * from './SalesByItemsSheet';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Model } from 'objection';
|
import { Model, raw } from 'objection';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
@@ -34,6 +34,22 @@ export default class InventoryTransaction extends TenantModel {
|
|||||||
query.where('date', '<=', toDate);
|
query.where('date', '<=', toDate);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
itemsTotals(builder) {
|
||||||
|
builder.select('itemId');
|
||||||
|
builder.sum('rate as rate');
|
||||||
|
builder.sum('quantity as quantity');
|
||||||
|
builder.select(raw('SUM(`QUANTITY` * `RATE`) as COST'));
|
||||||
|
builder.groupBy('itemId');
|
||||||
|
},
|
||||||
|
|
||||||
|
INDirection(builder) {
|
||||||
|
builder.where('direction', 'IN');
|
||||||
|
},
|
||||||
|
|
||||||
|
OUTDirection(builder) {
|
||||||
|
builder.where('direction', 'OUT');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default class JournalCommands {
|
|||||||
: entry.costAccountId,
|
: entry.costAccountId,
|
||||||
index: index + 2,
|
index: index + 2,
|
||||||
itemId: entry.itemId,
|
itemId: entry.itemId,
|
||||||
|
itemQuantity: entry.quantity,
|
||||||
});
|
});
|
||||||
this.journal.debit(debitEntry);
|
this.journal.debit(debitEntry);
|
||||||
});
|
});
|
||||||
@@ -416,6 +417,7 @@ export default class JournalCommands {
|
|||||||
note: entry.description,
|
note: entry.description,
|
||||||
index: index + 2,
|
index: index + 2,
|
||||||
itemId: entry.itemId,
|
itemId: entry.itemId,
|
||||||
|
itemQuantity: entry.quantity,
|
||||||
});
|
});
|
||||||
this.journal.credit(incomeEntry);
|
this.journal.credit(incomeEntry);
|
||||||
});
|
});
|
||||||
@@ -465,6 +467,8 @@ export default class JournalCommands {
|
|||||||
account: entry.item.sellAccountId,
|
account: entry.item.sellAccountId,
|
||||||
note: entry.description,
|
note: entry.description,
|
||||||
index: index + 2,
|
index: index + 2,
|
||||||
|
itemId: entry.itemId,
|
||||||
|
itemQuantity: entry.quantity,
|
||||||
});
|
});
|
||||||
this.journal.credit(incomeEntry);
|
this.journal.credit(incomeEntry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { sumBy, get } from 'lodash';
|
||||||
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
import {
|
||||||
|
IItem,
|
||||||
|
IInventoryValuationReportQuery,
|
||||||
|
IInventoryValuationItem,
|
||||||
|
IAccountTransaction,
|
||||||
|
IInventoryValuationTotal
|
||||||
|
} from 'interfaces';
|
||||||
|
import { transformToMap } from 'utils'
|
||||||
|
|
||||||
|
export default class InventoryValuationSheet extends FinancialSheet {
|
||||||
|
readonly query: IInventoryValuationReportQuery;
|
||||||
|
readonly items: IItem[];
|
||||||
|
readonly INInventoryCostLots: Map<number, IAccountTransaction>;
|
||||||
|
readonly OUTInventoryCostLots: Map<number, IAccountTransaction>;
|
||||||
|
readonly baseCurrency: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {IInventoryValuationReportQuery} query
|
||||||
|
* @param items
|
||||||
|
* @param INInventoryCostLots
|
||||||
|
* @param OUTInventoryCostLots
|
||||||
|
* @param baseCurrency
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
query: IInventoryValuationReportQuery,
|
||||||
|
items: IItem[],
|
||||||
|
INInventoryCostLots: IAccountTransaction[],
|
||||||
|
OUTInventoryCostLots: IAccountTransaction[],
|
||||||
|
baseCurrency: string
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.query = query;
|
||||||
|
this.items = items;
|
||||||
|
this.INInventoryCostLots = transformToMap(INInventoryCostLots, 'itemId');
|
||||||
|
this.OUTInventoryCostLots = transformToMap(OUTInventoryCostLots, 'itemId');
|
||||||
|
this.baseCurrency = baseCurrency;
|
||||||
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemTransaction(
|
||||||
|
transactionsMap,
|
||||||
|
itemId: number,
|
||||||
|
): { cost: number, quantity: number } {
|
||||||
|
const meta = transactionsMap.get(itemId);
|
||||||
|
|
||||||
|
const cost = get(meta, 'cost', 0);
|
||||||
|
const quantity = get(meta, 'cost', 0);
|
||||||
|
|
||||||
|
return { cost, quantity };
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemINTransaction(
|
||||||
|
itemId: number,
|
||||||
|
): { cost: number, quantity: number } {
|
||||||
|
return this.getItemTransaction(this.INInventoryCostLots, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemOUTTransaction(
|
||||||
|
itemId: number,
|
||||||
|
): { cost: number, quantity: number } {
|
||||||
|
return this.getItemTransaction(this.OUTInventoryCostLots, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemValuation(itemId: number): number {
|
||||||
|
const { cost: INValuation } = this.getItemINTransaction(itemId);
|
||||||
|
const { cost: OUTValuation } = this.getItemOUTTransaction(itemId);
|
||||||
|
|
||||||
|
return INValuation - OUTValuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemQuantity(itemId: number): number {
|
||||||
|
const { quantity: INQuantity } = this.getItemINTransaction(itemId);
|
||||||
|
const { quantity: OUTQuantity } = this.getItemOUTTransaction(itemId);
|
||||||
|
|
||||||
|
return INQuantity - OUTQuantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcAverage(valuation: number, quantity: number): number {
|
||||||
|
return quantity ? valuation / quantity : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMapper(item: IItem): IInventoryValuationItem {
|
||||||
|
const valuation = this.getItemValuation(item.id);
|
||||||
|
const quantity = this.getItemQuantity(item.id);
|
||||||
|
const average = this.calcAverage(valuation, quantity);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
code: item.code,
|
||||||
|
valuation,
|
||||||
|
quantity,
|
||||||
|
average,
|
||||||
|
valuationFormatted: this.formatNumber(valuation),
|
||||||
|
quantityFormatted: this.formatNumber(quantity, { money: false }),
|
||||||
|
averageFormatted: this.formatNumber(average, { money: false }),
|
||||||
|
currencyCode: this.baseCurrency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
itemsSection() {
|
||||||
|
return this.items.map(this.itemMapper.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal {
|
||||||
|
const valuation = sumBy(items, item => item.valuation);
|
||||||
|
const quantity = sumBy(items, item => item.quantity);
|
||||||
|
|
||||||
|
return {
|
||||||
|
valuation,
|
||||||
|
quantity,
|
||||||
|
valuationFormatted: this.formatNumber(valuation),
|
||||||
|
quantityFormatted: this.formatNumber(quantity, { money: false }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
reportData() {
|
||||||
|
const items = this.itemsSection();
|
||||||
|
const total = this.totalSection(items);
|
||||||
|
|
||||||
|
return { items, total };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import moment from 'moment';
|
||||||
|
import {
|
||||||
|
IInventoryValuationReportQuery,
|
||||||
|
IInventoryValuationSheetMeta,
|
||||||
|
} from 'interfaces';
|
||||||
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import InventoryValuationSheet from './InventoryValuationSheet';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class InventoryValuationSheetService {
|
||||||
|
@Inject()
|
||||||
|
tenancy: TenancyService;
|
||||||
|
|
||||||
|
@Inject('logger')
|
||||||
|
logger: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults balance sheet filter query.
|
||||||
|
* @return {IBalanceSheetQuery}
|
||||||
|
*/
|
||||||
|
get defaultQuery(): IInventoryValuationReportQuery {
|
||||||
|
return {
|
||||||
|
asDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
numberFormat: {
|
||||||
|
precision: 2,
|
||||||
|
divideOn1000: false,
|
||||||
|
showZero: false,
|
||||||
|
formatMoney: 'always',
|
||||||
|
negativeFormat: 'mines',
|
||||||
|
},
|
||||||
|
noneTransactions: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the balance sheet meta.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @returns {IBalanceSheetMeta}
|
||||||
|
*/
|
||||||
|
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
|
const organizationName = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'name',
|
||||||
|
});
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizationName,
|
||||||
|
baseCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inventory valuation sheet.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {IInventoryValuationReportQuery} query - Valuation query.
|
||||||
|
*/
|
||||||
|
public async inventoryValuationSheet(
|
||||||
|
tenantId: number,
|
||||||
|
query: IInventoryValuationReportQuery,
|
||||||
|
) {
|
||||||
|
const { Item, InventoryCostLotTracker } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const inventoryItems = await Item.query().where('type', 'inventory');
|
||||||
|
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||||
|
|
||||||
|
// Settings tenant service.
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
...this.defaultQuery,
|
||||||
|
...query,
|
||||||
|
};
|
||||||
|
const commonQuery = (builder) => {
|
||||||
|
builder.whereIn('item_id', inventoryItemsIds);
|
||||||
|
builder.sum('rate as rate');
|
||||||
|
builder.sum('quantity as quantity');
|
||||||
|
builder.sum('cost as cost');
|
||||||
|
builder.select('itemId');
|
||||||
|
builder.groupBy('itemId');
|
||||||
|
};
|
||||||
|
|
||||||
|
const INTransactions = await InventoryCostLotTracker.query()
|
||||||
|
.onBuild(commonQuery)
|
||||||
|
.where('direction', 'IN');
|
||||||
|
|
||||||
|
const OUTTransactions = await InventoryCostLotTracker.query()
|
||||||
|
.onBuild(commonQuery)
|
||||||
|
.where('direction', 'OUT');
|
||||||
|
|
||||||
|
const inventoryValuationInstance = new InventoryValuationSheet(
|
||||||
|
filter,
|
||||||
|
inventoryItems,
|
||||||
|
INTransactions,
|
||||||
|
OUTTransactions,
|
||||||
|
baseCurrency,
|
||||||
|
);
|
||||||
|
|
||||||
|
const inventoryValuationData = inventoryValuationInstance.reportData();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: inventoryValuationData,
|
||||||
|
query: filter,
|
||||||
|
meta: this.reportMetadata(tenantId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { get, sumBy } from 'lodash';
|
||||||
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
import { transformToMap } from 'utils';
|
||||||
|
import {
|
||||||
|
IAccountTransaction,
|
||||||
|
IInventoryValuationTotal,
|
||||||
|
IInventoryValuationItem,
|
||||||
|
IInventoryValuationReportQuery,
|
||||||
|
IInventoryValuationStatement,
|
||||||
|
IItem,
|
||||||
|
} from 'interfaces';
|
||||||
|
|
||||||
|
export default class InventoryValuationReport extends FinancialSheet {
|
||||||
|
readonly baseCurrency: string;
|
||||||
|
readonly items: IItem[];
|
||||||
|
readonly itemsTransactions: Map<number, IAccountTransaction>;
|
||||||
|
readonly query: IInventoryValuationReportQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {IInventoryValuationReportQuery} query
|
||||||
|
* @param {IItem[]} items
|
||||||
|
* @param {IAccountTransaction[]} itemsTransactions
|
||||||
|
* @param {string} baseCurrency
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
query: IInventoryValuationReportQuery,
|
||||||
|
items: IItem[],
|
||||||
|
itemsTransactions: IAccountTransaction[],
|
||||||
|
baseCurrency: string
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.baseCurrency = baseCurrency;
|
||||||
|
this.items = items;
|
||||||
|
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
|
||||||
|
this.query = query;
|
||||||
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the item purchase item, cost and average cost price.
|
||||||
|
* @param {number} itemId
|
||||||
|
*/
|
||||||
|
getItemTransaction(
|
||||||
|
itemId: number
|
||||||
|
): { quantity: number; cost: number; average: number } {
|
||||||
|
const transaction = this.itemsTransactions.get(itemId);
|
||||||
|
|
||||||
|
const quantity = get(transaction, 'quantity', 0);
|
||||||
|
const cost = get(transaction, 'cost', 0);
|
||||||
|
|
||||||
|
const average = cost / quantity;
|
||||||
|
|
||||||
|
return { quantity, cost, average };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping the given item section.
|
||||||
|
* @param {IInventoryValuationItem} item
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
itemSectionMapper(item: IItem): IInventoryValuationItem {
|
||||||
|
const meta = this.getItemTransaction(item.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
code: item.code,
|
||||||
|
quantityPurchased: meta.quantity,
|
||||||
|
purchaseCost: meta.cost,
|
||||||
|
averageCostPrice: meta.average,
|
||||||
|
quantityPurchasedFormatted: this.formatNumber(meta.quantity, {
|
||||||
|
money: false,
|
||||||
|
}),
|
||||||
|
purchaseCostFormatted: this.formatNumber(meta.cost),
|
||||||
|
averageCostPriceFormatted: this.formatNumber(meta.average),
|
||||||
|
currencyCode: this.baseCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the items sections.
|
||||||
|
* @returns {IInventoryValuationItem[]}
|
||||||
|
*/
|
||||||
|
itemsSection(): IInventoryValuationItem[] {
|
||||||
|
return this.items.map(this.itemSectionMapper.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the total section of the sheet.
|
||||||
|
* @param {IInventoryValuationItem[]} items
|
||||||
|
* @returns {IInventoryValuationTotal}
|
||||||
|
*/
|
||||||
|
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal {
|
||||||
|
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
|
||||||
|
const purchaseCost = sumBy(items, (item) => item.purchaseCost);
|
||||||
|
|
||||||
|
return {
|
||||||
|
quantityPurchased,
|
||||||
|
purchaseCost,
|
||||||
|
quantityPurchasedFormatted: this.formatNumber(quantityPurchased, {
|
||||||
|
money: false,
|
||||||
|
}),
|
||||||
|
purchaseCostFormatted: this.formatNumber(purchaseCost),
|
||||||
|
currencyCode: this.baseCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the sheet data.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
reportData(): IInventoryValuationStatement {
|
||||||
|
const items = this.itemsSection();
|
||||||
|
const total = this.totalSection(items);
|
||||||
|
|
||||||
|
return { items, total };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import moment from 'moment';
|
||||||
|
import {
|
||||||
|
IInventoryValuationReportQuery,
|
||||||
|
IInventoryValuationStatement,
|
||||||
|
IInventoryValuationSheetMeta,
|
||||||
|
} from 'interfaces';
|
||||||
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import PurchasesByItems from './PurchasesByItems';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class InventoryValuationReportService {
|
||||||
|
@Inject()
|
||||||
|
tenancy: TenancyService;
|
||||||
|
|
||||||
|
@Inject('logger')
|
||||||
|
logger: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults balance sheet filter query.
|
||||||
|
* @return {IBalanceSheetQuery}
|
||||||
|
*/
|
||||||
|
get defaultQuery(): IInventoryValuationReportQuery {
|
||||||
|
return {
|
||||||
|
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
|
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
numberFormat: {
|
||||||
|
precision: 2,
|
||||||
|
divideOn1000: false,
|
||||||
|
showZero: false,
|
||||||
|
formatMoney: 'always',
|
||||||
|
negativeFormat: 'mines',
|
||||||
|
},
|
||||||
|
noneTransactions: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the balance sheet meta.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @returns {IBalanceSheetMeta}
|
||||||
|
*/
|
||||||
|
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
|
const organizationName = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'name',
|
||||||
|
});
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizationName,
|
||||||
|
baseCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve balance sheet statement.
|
||||||
|
* -------------
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IBalanceSheetQuery} query
|
||||||
|
*
|
||||||
|
* @return {IBalanceSheetStatement}
|
||||||
|
*/
|
||||||
|
public async purchasesByItems(
|
||||||
|
tenantId: number,
|
||||||
|
query: IInventoryValuationReportQuery
|
||||||
|
): Promise<{
|
||||||
|
data: IInventoryValuationStatement,
|
||||||
|
query: IInventoryValuationReportQuery,
|
||||||
|
meta: IInventoryValuationSheetMeta,
|
||||||
|
}> {
|
||||||
|
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Settings tenant service.
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
...this.defaultQuery,
|
||||||
|
...query,
|
||||||
|
};
|
||||||
|
this.logger.info('[purchases_by_items] trying to calculate the report.', {
|
||||||
|
filter,
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
const inventoryItems = await Item.query().where('type', 'inventory');
|
||||||
|
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||||
|
|
||||||
|
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||||
|
const inventoryTransactions = await InventoryTransaction.query().onBuild(
|
||||||
|
(builder: any) => {
|
||||||
|
builder.modify('itemsTotals');
|
||||||
|
builder.modify('INDirection');
|
||||||
|
|
||||||
|
// Filter the inventory items only.
|
||||||
|
builder.whereIn('itemId', inventoryItemsIds);
|
||||||
|
|
||||||
|
// Filter the date range of the sheet.
|
||||||
|
builder.modify('filterDateRange', filter.fromDate, filter.toDate)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const purchasesByItemsInstance = new PurchasesByItems(
|
||||||
|
filter,
|
||||||
|
inventoryItems,
|
||||||
|
inventoryTransactions,
|
||||||
|
baseCurrency
|
||||||
|
);
|
||||||
|
const purchasesByItemsData = purchasesByItemsInstance.reportData();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: purchasesByItemsData,
|
||||||
|
query: filter,
|
||||||
|
meta: this.reportMetadata(tenantId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { get, sumBy } from 'lodash';
|
||||||
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
import { transformToMap } from 'utils';
|
||||||
|
import {
|
||||||
|
ISalesByItemsReportQuery,
|
||||||
|
IAccountTransaction,
|
||||||
|
ISalesByItemsItem,
|
||||||
|
ISalesByItemsTotal,
|
||||||
|
ISalesByItemsSheetStatement,
|
||||||
|
IItem,
|
||||||
|
} from 'interfaces';
|
||||||
|
|
||||||
|
export default class SalesByItemsReport extends FinancialSheet {
|
||||||
|
readonly baseCurrency: string;
|
||||||
|
readonly items: IItem[];
|
||||||
|
readonly itemsTransactions: Map<number, IAccountTransaction>;
|
||||||
|
readonly query: ISalesByItemsReportQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {ISalesByItemsReportQuery} query
|
||||||
|
* @param {IItem[]} items
|
||||||
|
* @param {IAccountTransaction[]} itemsTransactions
|
||||||
|
* @param {string} baseCurrency
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
query: ISalesByItemsReportQuery,
|
||||||
|
items: IItem[],
|
||||||
|
itemsTransactions: IAccountTransaction[],
|
||||||
|
baseCurrency: string
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.baseCurrency = baseCurrency;
|
||||||
|
this.items = items;
|
||||||
|
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
|
||||||
|
this.query = query;
|
||||||
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the item purchase item, cost and average cost price.
|
||||||
|
* @param {number} itemId - Item id.
|
||||||
|
*/
|
||||||
|
getItemTransaction(
|
||||||
|
itemId: number
|
||||||
|
): { quantity: number; cost: number; average: number } {
|
||||||
|
const transaction = this.itemsTransactions.get(itemId);
|
||||||
|
|
||||||
|
const quantity = get(transaction, 'quantity', 0);
|
||||||
|
const cost = get(transaction, 'cost', 0);
|
||||||
|
|
||||||
|
const average = cost / quantity;
|
||||||
|
|
||||||
|
return { quantity, cost, average };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping the given item section.
|
||||||
|
* @param {ISalesByItemsItem} item
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
itemSectionMapper(item: IItem): ISalesByItemsItem {
|
||||||
|
const meta = this.getItemTransaction(item.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
code: item.code,
|
||||||
|
quantitySold: meta.quantity,
|
||||||
|
soldCost: meta.cost,
|
||||||
|
averageSellPrice: meta.average,
|
||||||
|
quantitySoldFormatted: this.formatNumber(meta.quantity, {
|
||||||
|
money: false,
|
||||||
|
}),
|
||||||
|
soldCostFormatted: this.formatNumber(meta.cost),
|
||||||
|
averageSellPriceFormatted: this.formatNumber(meta.average),
|
||||||
|
currencyCode: this.baseCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the items sections.
|
||||||
|
* @returns {ISalesByItemsItem[]}
|
||||||
|
*/
|
||||||
|
itemsSection(): ISalesByItemsItem[] {
|
||||||
|
return this.items.map(this.itemSectionMapper.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the total section of the sheet.
|
||||||
|
* @param {IInventoryValuationItem[]} items
|
||||||
|
* @returns {IInventoryValuationTotal}
|
||||||
|
*/
|
||||||
|
totalSection(items: ISalesByItemsItem[]): ISalesByItemsTotal {
|
||||||
|
const quantitySold = sumBy(items, (item) => item.quantitySold);
|
||||||
|
const soldCost = sumBy(items, (item) => item.soldCost);
|
||||||
|
|
||||||
|
return {
|
||||||
|
quantitySold,
|
||||||
|
soldCost,
|
||||||
|
quantitySoldFormatted: this.formatNumber(quantitySold, {
|
||||||
|
money: false,
|
||||||
|
}),
|
||||||
|
soldCostFormatted: this.formatNumber(soldCost),
|
||||||
|
currencyCode: this.baseCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the sheet data.
|
||||||
|
* @returns {ISalesByItemsSheetStatement}
|
||||||
|
*/
|
||||||
|
reportData(): ISalesByItemsSheetStatement {
|
||||||
|
const items = this.itemsSection();
|
||||||
|
const total = this.totalSection(items);
|
||||||
|
|
||||||
|
return { items, total };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import moment from 'moment';
|
||||||
|
import {
|
||||||
|
ISalesByItemsReportQuery,
|
||||||
|
ISalesByItemsSheetStatement,
|
||||||
|
ISalesByItemsSheetMeta
|
||||||
|
} from 'interfaces';
|
||||||
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import SalesByItems from './SalesByItems';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SalesByItemsReportService {
|
||||||
|
@Inject()
|
||||||
|
tenancy: TenancyService;
|
||||||
|
|
||||||
|
@Inject('logger')
|
||||||
|
logger: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults balance sheet filter query.
|
||||||
|
* @return {IBalanceSheetQuery}
|
||||||
|
*/
|
||||||
|
get defaultQuery(): ISalesByItemsReportQuery {
|
||||||
|
return {
|
||||||
|
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
|
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
numberFormat: {
|
||||||
|
precision: 2,
|
||||||
|
divideOn1000: false,
|
||||||
|
showZero: false,
|
||||||
|
formatMoney: 'always',
|
||||||
|
negativeFormat: 'mines',
|
||||||
|
},
|
||||||
|
noneTransactions: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the balance sheet meta.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @returns {IBalanceSheetMeta}
|
||||||
|
*/
|
||||||
|
reportMetadata(tenantId: number): ISalesByItemsSheetMeta {
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
|
const organizationName = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'name',
|
||||||
|
});
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizationName,
|
||||||
|
baseCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve balance sheet statement.
|
||||||
|
* -------------
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IBalanceSheetQuery} query
|
||||||
|
*
|
||||||
|
* @return {IBalanceSheetStatement}
|
||||||
|
*/
|
||||||
|
public async salesByItems(
|
||||||
|
tenantId: number,
|
||||||
|
query: ISalesByItemsReportQuery
|
||||||
|
): Promise<{
|
||||||
|
data: ISalesByItemsSheetStatement,
|
||||||
|
query: ISalesByItemsReportQuery,
|
||||||
|
meta: ISalesByItemsSheetMeta,
|
||||||
|
}> {
|
||||||
|
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Settings tenant service.
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
...this.defaultQuery,
|
||||||
|
...query,
|
||||||
|
};
|
||||||
|
this.logger.info('[sales_by_items] trying to calculate the report.', {
|
||||||
|
filter,
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
const inventoryItems = await Item.query().where('type', 'inventory');
|
||||||
|
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||||
|
|
||||||
|
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||||
|
const inventoryTransactions = await InventoryTransaction.query().onBuild(
|
||||||
|
(builder: any) => {
|
||||||
|
builder.modify('itemsTotals');
|
||||||
|
builder.modify('OUTDirection');
|
||||||
|
|
||||||
|
// Filter the inventory items only.
|
||||||
|
builder.whereIn('itemId', inventoryItemsIds);
|
||||||
|
|
||||||
|
// Filter the date range of the sheet.
|
||||||
|
builder.modify('filterDateRange', filter.fromDate, filter.toDate)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const purchasesByItemsInstance = new SalesByItems(
|
||||||
|
filter,
|
||||||
|
inventoryItems,
|
||||||
|
inventoryTransactions,
|
||||||
|
baseCurrency
|
||||||
|
);
|
||||||
|
const purchasesByItemsData = purchasesByItemsInstance.reportData();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: purchasesByItemsData,
|
||||||
|
query: filter,
|
||||||
|
meta: this.reportMetadata(tenantId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,8 +165,6 @@ export default class InventoryAverageCostMethod
|
|||||||
'transactionId',
|
'transactionId',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'sellAccountId',
|
|
||||||
'costAccountId',
|
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
switch (invTransaction.direction) {
|
switch (invTransaction.direction) {
|
||||||
|
|||||||
Reference in New Issue
Block a user