Skip to content

Commit

Permalink
fix(stock): improve Stock Value Report
Browse files Browse the repository at this point in the history
This commit fixes various components of the stock value report cited in
issue #6150.  Notably, the query now uses the stock_movement_status
table to pull in the quantities in stock for a given depot and loads the
stock_value table to get the value of the items in stock.  This speeds
the query up signficantly.

Closes #6150.
  • Loading branch information
jniles committed Apr 7, 2022
1 parent 0e6ec7c commit aa59dc6
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,40 @@ StockValueConfigController.$inject = [
'LanguageService', 'moment',
];

function StockValueConfigController($sce, Notify, SavedReports,
AppCache, reportData, $state, Languages, moment) {
function StockValueConfigController(
$sce, Notify, SavedReports,
AppCache, reportData, $state, Languages, moment,
) {

const vm = this;
const cache = new AppCache('configure_stock_value_report');
const reportUrl = 'reports/stock/value';

vm.reportDetails = {
dateTo : new Date(),
excludeZeroValue : 0,
};

// Default values
vm.previewGenerated = false;
vm.orderByCreatedAt = 0;
vm.dateTo = new Date();
vm.excludeZeroValue = 0;

vm.onDateChange = (date) => {
vm.dateTo = date;
vm.reportDetails.dateTo = date;
};

// check cached configuration
checkCachedConfiguration();

vm.onSelectDepot = function onSelectDepot(depot) {
vm.depot = depot;
vm.reportDetails.depot_uuid = depot.uuid;
};

vm.onSelectCronReport = report => {
vm.reportDetails = angular.copy(report);
};

vm.clear = function clear(key) {
delete vm[key];
delete vm.reportDetails[key];
};

vm.clearPreview = function clearPreview() {
Expand All @@ -45,38 +50,24 @@ function StockValueConfigController($sce, Notify, SavedReports,

vm.onSelectCurrency = (currency) => {
vm.reportDetails.currency_id = currency.id;
vm.currency_id = currency.id;
};

vm.onExcludeZeroValue = () => {
vm.reportDetails.exclude_zero_value = vm.excludeZeroValue;
};

function formatData() {
const params = {
depot_uuid : vm.depot.uuid,
dateTo : vm.dateTo,
currency_id : vm.currency_id,
exclude_zero_value : vm.excludeZeroValue,
};
cache.reportDetails = angular.copy(params);
params.dateTo = moment(params.dateTo).format('YYYY-MM-DD');
vm.preview = function preview(form) {
if (form.$invalid) { return 0; }

const dateTo = moment(vm.reportDetails.dateTo).format('YYYY-MM-DD');

const options = {
params,
...vm.reportDetails,
lang : Languages.key,
dateTo,
};

vm.reportDetails = options;
return vm.reportDetails;
}

vm.preview = function preview(form) {
if (form.$invalid) { return 0; }

vm.reportDetails = formatData();

return SavedReports.requestPreview(reportUrl, reportData.id, angular.copy(vm.reportDetails))
return SavedReports.requestPreview(reportUrl, reportData.id, angular.copy(options))
.then((result) => {
vm.previewGenerated = true;
vm.previewResult = $sce.trustAsHtml(result);
Expand All @@ -85,7 +76,6 @@ function StockValueConfigController($sce, Notify, SavedReports,
};

vm.requestSaveAs = function requestSaveAs() {
vm.reportDetails = formatData();
const options = {
url : reportUrl,
report : reportData,
Expand All @@ -100,6 +90,8 @@ function StockValueConfigController($sce, Notify, SavedReports,
};

function checkCachedConfiguration() {
vm.reportDetails = angular.copy(cache.reportDetails || {});
if (cache.reportDetails) {
vm.reportDetails = angular.copy(cache.reportDetails);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ <h3 translate>REPORT.STOCK_VALUE.TITLE</h3>

<!-- select depot -->
<bh-depot-select
depot-uuid="ReportConfigCtrl.depot.uuid"
depot-uuid="ReportConfigCtrl.reportDetails.depot_uuid"
on-select-callback="ReportConfigCtrl.onSelectDepot(depot)"
required="true">
<bh-clear on-clear="ReportConfigCtrl.clear('depot')"></bh-clear>
Expand All @@ -36,22 +36,22 @@ <h3 translate>REPORT.STOCK_VALUE.TITLE</h3>
<bh-date-editor
label="FORM.LABELS.UNTIL_DATE"
limit-min-fiscal
date-value="ReportConfigCtrl.dateTo"
date-value="ReportConfigCtrl.reportDetails.dateTo"
on-change="ReportConfigCtrl.onDateChange(date)">
</bh-date-editor>

<div class="checkbox">
<label>
<input type="checkbox" ng-true-value="1" ng-false-value="0"
ng-model="ReportConfigCtrl.excludeZeroValue"
ng-model="ReportConfigCtrl.reportDetails.excludeZeroValue"
ng-change="ReportConfigCtrl.onExcludeZeroValue()">
<span translate>REPORT.STOCK_VALUE.EXCLUDE_INVENTORIES_ZERO_VALUE</span>
</label>
</div>

<!-- the currency to be used in the footer -->
<bh-currency-select
currency-id="ReportConfigCtrl.currency_id"
currency-id="ReportConfigCtrl.reportDetails.currency_id"
on-change="ReportConfigCtrl.onSelectCurrency(currency)">
</bh-currency-select>

Expand Down
145 changes: 54 additions & 91 deletions server/controllers/stock/reports/stock/value.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,106 +29,69 @@ async function reporting(_options, session) {

const report = new ReportManager(STOCK_VALUE_REPORT_TEMPLATE, session, optionReport);

const options = (typeof (_options.params) === 'string') ? JSON.parse(_options.params) : _options.params;
data.dateTo = options.dateTo;
data.depot = await db.one('SELECT * FROM depot WHERE uuid=?', [db.bid(options.depot_uuid)]);
const currencyId = Number(options.currency_id);

// Get inventories movemented
const sqlGetInventories = `
SELECT DISTINCT(BUID(mov.inventory_uuid)) AS inventory_uuid, mov.inventory_uuid AS uuid,
mov.text AS inventory_name, mov.code AS inventory_code, mov.inventory_price
FROM(
SELECT inv.uuid AS inventory_uuid, inv.text, inv.code,
inv.price as inventory_price, sm.date
FROM stock_movement AS sm
JOIN lot AS l ON l.uuid = sm.lot_uuid
JOIN inventory AS inv ON inv.uuid = l.inventory_uuid
WHERE sm.depot_uuid = ? AND DATE(sm.date) <= DATE(?)
) AS mov
ORDER BY mov.text ASC;
`;

/*
* Here we first search for all the products that have
* been stored in stock in a warehouse,
* then we collect all the movements of stocks linked to a warehouse
*/
const stockValues = await db.exec(sqlGetInventories, [db.bid(options.depot_uuid), options.dateTo]);

// Compute stock value only for concerned inventories for having updated wac
const mapRecomputeInventoryStockValue = stockValues.map(inventory => {
return db.exec('CALL RecomputeInventoryStockValue(?, NULL);', [inventory.uuid]);
});
const options = (typeof (_options.params) === 'string') ? JSON.parse(_options.params) : _options;

await Promise.all(mapRecomputeInventoryStockValue);

const sqlGetMovementByDepot = `
SELECT sm.document_uuid, sm.depot_uuid, sm.lot_uuid, sm.quantity, sm.unit_cost, sm.date, sm.is_exit,
sm.created_at, BUID(inv.uuid) AS inventory_uuid, inv.text AS inventory_text, map.text AS docRef,
sv.wac
FROM stock_movement AS sm
JOIN lot AS l ON l.uuid = sm.lot_uuid
JOIN inventory AS inv ON inv.uuid = l.inventory_uuid
JOIN document_map AS map ON map.uuid = sm.document_uuid
JOIN stock_value AS sv ON sv.inventory_uuid = inv.uuid
WHERE sm.depot_uuid = ? AND DATE(sm.date) <= DATE(?)
ORDER BY inv.text, DATE(sm.date), sm.created_at ASC
data.dateTo = options.dateTo;
data.isEnterpriseCurrency = options.currency_id === session.enterprise.currency_id;

const depot = await db.one('SELECT * FROM depot WHERE uuid = ?', [db.bid(options.depot_uuid)]);
const exchangeRate = await Exchange.getExchangeRate(enterpriseId, options.currency_id, new Date());

// get the current quantities in stock
const currentQuantitiesInStockSQL = `
SELECT sms.date, BUID(sms.inventory_uuid) AS uuid, sms.sum_quantity AS quantity,
inventory.code, inventory.text, inventory.consumable,
sv.wac, inventory.price,
(sv.wac * sms.sum_quantity) AS total_value,
(sv.wac * IFNULL(GetExchangeRate(${enterpriseId}, ?, NOW()), 1)) AS exchanged_wac,
(sv.wac * sms.sum_quantity) * IFNULL(GetExchangeRate(${enterpriseId}, ?, NOW()), 1) AS exchanged_value,
(inventory.price * IFNULL(GetExchangeRate(${enterpriseId}, ?, NOW()), 1)) AS exchanged_price,
(inventory.price * IFNULL(GetExchangeRate(${enterpriseId}, ?, NOW()), 1)) * sms.sum_quantity
AS exchanged_sales_value
FROM stock_movement_status AS sms JOIN (
SELECT inside.inventory_uuid, MAX(inside.date) AS date
FROM stock_movement_status AS inside
WHERE inside.depot_uuid = ?
AND inside.date <= DATE(?)
GROUP BY inside.inventory_uuid
) AS outside
ON outside.date = sms.date
AND sms.inventory_uuid = outside.inventory_uuid
JOIN inventory ON inventory.uuid = sms.inventory_uuid
JOIN stock_value sv ON sv.inventory_uuid = inventory.uuid
WHERE sms.depot_uuid = ?
ORDER BY inventory.text;
`;

const allMovements = await db.exec(sqlGetMovementByDepot, [db.bid(options.depot_uuid), options.dateTo]);

stockValues.forEach(stock => {
stock.movements = allMovements.filter(movement => (movement.inventory_uuid === stock.inventory_uuid));
const currentQuantitiesInStock = await db.exec(currentQuantitiesInStockSQL, [
options.currency_id,
options.currency_id,
options.currency_id,
options.currency_id,
depot.uuid,
options.dateTo,
depot.uuid,
]);

// filter out 0s if necessary
const filtered = currentQuantitiesInStock.filter(row => {
if (options.excludeZeroValue === '1') { return row.quantity !== 0; }
return true;
});

let stockTotalValue = 0;
let stockTotalSaleValue = 0;
const exchangeRate = await Exchange.getExchangeRate(enterpriseId, currencyId, new Date());
const rate = exchangeRate.rate || 1;

// calculate quantity in stock since wac is globally calculated
stockValues.forEach(stock => {
let quantityInStock = 0;
let weightedAverageUnitCost = 0;

stock.movements.forEach(item => {
const isExit = item.is_exit ? (-1) : 1;
quantityInStock += (item.quantity * isExit);
item.quantityInStock = quantityInStock;
weightedAverageUnitCost = item.wac;
});
const totals = filtered.reduce((agg, row) => {
agg.stockTotalValue += row.exchanged_value;
agg.stockTotalSaleValue += row.exchanged_sales_value;
return agg;
}, { stockTotalValue : 0, stockTotalSaleValue : 0 });

stock.inventoryPrice = stock.inventory_price * rate;

stock.stockQtyPurchased = quantityInStock;
stock.stockUnitCost = weightedAverageUnitCost;
stock.stockValue = (quantityInStock * weightedAverageUnitCost);
stock.saleValue = (quantityInStock * stock.inventoryPrice);

// warn the user if the stock sales price is equivalent or _lower_ than the value of the stock
// the enterprise will not make money on this stock.
stock.hasWarning = stock.inventoryPrice <= weightedAverageUnitCost;

stockTotalValue += stock.stockValue;
stockTotalSaleValue += stock.saleValue;
});

const stockValueElements = options.exclude_zero_value
? stockValues.filter(item => item.stockValue > 0) : stockValues;

data.stockValues = stockValueElements || [];

data.stockTotalValue = stockTotalValue;
data.stockTotalSaleValue = stockTotalSaleValue;
data.stockValues = filtered;
data.emptyResult = data.stockValues.length === 0;

data.exclude_zero_value = options.exclude_zero_value;

data.currencyId = currencyId;
data.exchangeRate = rate;
data.exchangeRate = exchangeRate.rate || 1;
data.currency_id = options.currency_id;

return report.render(data);
return report.render({ ...data, depot, totals });
}

module.exports.document = stockValue;
Expand Down
24 changes: 13 additions & 11 deletions server/controllers/stock/reports/stock_value.report.handlebars
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{> head title="TREE.STOCK_INVENTORY" }}
{{> head title="TREE.STOCK_VALUE" }}

<body>

Expand All @@ -21,7 +21,9 @@
{{date dateTo}}
</h4>

{{> exchangeRate rate=exchangeRate currencyId=currencyId}}
{{#unless isEntepriseCurrency}}
{{> exchangeRate rate=exchangeRate currencyId=currency_id}}
{{/unless}}

<!-- list of data -->
<table class="table table-condensed table-report">
Expand All @@ -39,22 +41,22 @@
<tbody>
{{#each stockValues}}
<tr {{#if hasWarning}}class="text-danger bg-danger"{{/if}} >
<td>{{inventory_code}}</td>
<td style="width:50%">{{inventory_name}}</td>
<td class="text-right">{{stockQtyPurchased}}</td>
<td class="text-right">{{currency stockUnitCost ../currency_id 4}}</td>
<td class="text-right">{{currency stockValue ../currency_id 2}}</td>
<td class="text-right">{{currency inventoryPrice ../currency_id 4}}</td>
<td class="text-right">{{currency saleValue ../currency_id 2}}</td>
<td>{{code}}</td>
<td style="width:50%">{{text}}</td>
<td class="text-right">{{quantity}}</td>
<td class="text-right">{{currency exchanged_wac ../currency_id 4}}</td>
<td class="text-right">{{currency exchanged_value ../currency_id 2}}</td>
<td class="text-right">{{currency exchanged_price ../currency_id 4}}</td>
<td class="text-right">{{currency exchanged_sales_value ../currency_id 2}}</td>
</tr>
{{else}}
{{> emptyTable columns=7}}
{{/each}}
<tr>
<th colspan="4" class="text-right">{{ translate "FORM.LABELS.VALUE_IN_STOCK"}}</th>
<th class="text-right">{{currency stockTotalValue currency_id 2}}</th>
<th class="text-right">{{currency totals.stockTotalValue currency_id 2}}</th>
<th class="text-right">{{ translate "FORM.LABELS.SALE_VALUE"}}</th>
<th class="text-right">{{ currency stockTotalSaleValue currency_id 2}}</th>
<th class="text-right">{{ currency totals.stockTotalSaleValue currency_id 2}}</th>
</tr>
</tbody>
</table>
Expand Down

0 comments on commit aa59dc6

Please sign in to comment.