Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
5361: Add page for finding and merging duplicate lots r=jmcameron a=jmcameron

Create a page where operators can find and merge potential duplicate lots. 

**Note** that this PR adds a new page under the Stock navigation menu:  "Find Duplicate Lots".  In order to use or test this, be sure to edit the user role permissions and enable the new page (under the Stock section).

Closes Third-Culture-Software#5166

**Testing**
- Use a production database such as IMCK
- Start Bhima
- In Adminstration > Role Management , enable user access to the "Find Duplicate Lots" page.
- Go to Stock > Find Duplicate Lots
   - Note the number of duplicates found (see the grid footer)
   - Click on the Action menu of any row and click on  "Find duplicate lots".  (I'm not 100% happy about the wording here; I'm open to suggestions).
   - A modal will pop up.  Note that form will show all lots that have matching lot labels AND inventory UUIDs.  Only one lot is shown for all the lots that match it.
   - Select the lot to keep and the lot(s) to merge
   - Click on the [Merge] button
   - When you return to the main grid, notice the offending row is now gone (since there is only one of them)


Co-authored-by: Jonathan Cameron <jmcameron@gmail.com>
  • Loading branch information
bors[bot] and jmcameron authored Feb 8, 2021
2 parents 4394f28 + f07f4c7 commit f2d92ac
Show file tree
Hide file tree
Showing 14 changed files with 283 additions and 4 deletions.
1 change: 1 addition & 0 deletions client/src/i18n/en/stock.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
"EXCLUDE_EXHAUSTED_LOTS": "Exclude exhausted lots",
"DETAILS":"Lot Information",
"FIND_DUPLICATE_LOTS" : "Find duplicate lots",
"MERGE_DUPLICATE_LOTS" : "Merge duplicate lots",
"LOT_LABEL":"Lot label",
"SUCCESSFULLY_EDITED":"Lot updated with success",
"ASSIGNMENT_HISTORIC":"Assignment History",
Expand Down
1 change: 1 addition & 0 deletions client/src/i18n/en/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"NR": "Nr",
"NO_PAYMENTS": "No payments made against this invoice.",
"NUM": "Number",
"NUM_DUPLICATE_LOTS": "# Duplicates",
"NUMBER_CASES": "Number of cases",
"OF": "of",
"PASSIVE": "Passive",
Expand Down
1 change: 1 addition & 0 deletions client/src/i18n/en/tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"DISPLAY_METADATA": "Display Metadata",
"DISTRIBUTION_KEYS": "Distribution Keys Management",
"DITRIBUTION_AUX_FEES_CENTERS" : "Distribution of Auxiliary Fee Centers",
"DUPLICATE_LOTS" : "Find Duplicate Lots",
"EMPLOYEE" : "Employee",
"EMPLOYEE_REGISTRY" : "Employee Registry",
"EMPLOYEE_STANDING_REPORT" : "Employee Standing Report",
Expand Down
1 change: 1 addition & 0 deletions client/src/i18n/fr/stock.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
"EXCLUDE_EXHAUSTED_LOTS": "Exclure les lots de stocks épuisés",
"DETAILS":"Information sur le lot",
"FIND_DUPLICATE_LOTS" : "Trouver lots en double",
"MERGE_DUPLICATE_LOTS" : "fusionner des lots en double",
"LOT_LABEL":"Designation du lot",
"SUCCESSFULLY_EDITED":"Mise à jour du lot avec succès",
"ASSIGNMENT_HISTORIC":"Historique d'assignation",
Expand Down
1 change: 1 addition & 0 deletions client/src/i18n/fr/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"NR": "",
"NO_PAYMENTS": "Aucun paiement n'a été effectué sur cette facture.",
"NUM": "Numéro",
"NUM_DUPLICATE_LOTS": "Nb de doublons",
"NUMBER_CASES": "Nombre de cas",
"OF": "sur",
"PASSIVE": "Passive",
Expand Down
1 change: 1 addition & 0 deletions client/src/i18n/fr/tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"DISPLAY_METADATA": "Afficher les métadonnées",
"DISTRIBUTION_KEYS": "Gestion des clés de répartitions",
"DITRIBUTION_AUX_FEES_CENTERS" : "Répartitions des centres de frais auxiliaires",
"DUPLICATE_LOTS" : "Trouver des lots en double",
"EMPLOYEE":"Gestion des Employés",
"EMPLOYEE_REGISTRY" : "Registre des employés",
"EMPLOYEE_STANDING_REPORT" : "Rapport des employés",
Expand Down
58 changes: 58 additions & 0 deletions client/src/modules/stock/lots-duplicates/lots-duplicates.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<div class="flex-header">
<div class="bhima-title">
<ol class="headercrumb">
<li class="static" translate>TREE.STOCK</li>
<li class="title" translate>TREE.DUPLICATE_LOTS</li>
</ol>

<div class="toolbar">
<div class="toolbar-item">
<div uib-dropdown dropdown-append-to-body data-action="open-menu">
<a class="btn btn-default" uib-dropdown-toggle>
<span class="fa fa-bars"></span> <span class="hidden-xs" translate>FORM.LABELS.MENU</span> <span class="caret"></span>
</a>

<ul uib-dropdown-menu role="menu" class="dropdown-menu-right">
<li role="menuitem">
<a href data-method="configure" ng-click="DupeLotsCtrl.openColumnConfigModal()">
<i class="fa fa-columns"></i> <span translate>FORM.LABELS.COLUMNS</span>
</a>
</li>

<li role="separator" class="divider"></li>
<li role="menuitem">
<a href data-method="save-state" ng-click="DupeLotsCtrl.saveGridState()">
<i class="fa fa-save"></i> <span translate>FORM.BUTTONS.SAVE_GRID_CONFIGURATION</span>
</a>
</li>

<li role="menuitem">
<a href data-method="clear-state" ng-click="DupeLotsCtrl.clearGridState()">
<i class="fa fa-close"></i> <span translate>FORM.BUTTONS.CLEAR_GRID_CONFIGURATION</span>
</a>
</li>

</ul>
</div>
</div>
</div>
</div>
</div>

<div class="flex-content">
<div class="container-fluid">
<div
id="duplicate-lots-grid"
class="grid-full-height-with-filters"
ui-grid="DupeLotsCtrl.gridOptions"
ui-grid-resize-columns
ui-grid-auto-resize
ui-grid-save-state>
<bh-grid-loading-indicator
loading-state="DupeLotsCtrl.loading"
empty-state="DupeLotsCtrl.gridOptions.data.length === 0"
error-state="DupeLotsCtrl.hasError">
</bh-grid-loading-indicator>
</div>
</div>
</div>
158 changes: 158 additions & 0 deletions client/src/modules/stock/lots-duplicates/lots-duplicates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
angular.module('bhima.controllers')
.controller('DuplicateLotsController', DuplicateLotsController);

DuplicateLotsController.$inject = [
'LotService', 'NotifyService', 'StockModalService', 'SessionService',
'GridColumnService', 'GridStateService', '$state',
];

/**
* Stock Lots Duplicates
* This module is a stock lots page for finding and merging duplicate lots
*/
function DuplicateLotsController(
Lots, Notify, Modal, Session,
Columns, GridState, $state,
) {
const vm = this;
const cacheKey = 'duplicate-lots-grid';
vm.enterprise = Session.enterprise;

const columns = [
{
field : 'label',
displayName : 'TABLE.COLUMNS.LABEL',
headerTooltip : 'TABLE.COLUMNS.LABEL',
headerCellFilter : 'translate',

}, {
field : 'inventory_text',
displayName : 'TABLE.COLUMNS.INVENTORY',
headerTooltip : 'TABLE.COLUMNS.INVENTORY',
headerCellFilter : 'translate',
width : '20%',
}, {
field : 'quantity',
displayName : 'TABLE.COLUMNS.QUANTITY',
headerTooltip : 'TABLE.COLUMNS.QUANTITY',
headerCellFilter : 'translate',
cellClass : 'text-right',
type : 'number',
}, {
field : 'initial_quantity',
displayName : 'STOCK.INITIAL_QUANTITY',
headerTooltip : 'STOCK.INITIAL_QUANTITY',
headerCellFilter : 'translate',
cellClass : 'text-right',
type : 'number',
}, {
field : 'unit_cost',
displayName : 'TABLE.COLUMNS.UNIT_PRICE',
headerTooltip : 'TABLE.COLUMNS.UNIT_PRICE',
headerCellFilter : 'translate',
cellFilter : `currency:${vm.enterprise.currency_id}`,
cellClass : 'text-right',
type : 'number',
}, {
field : 'entry_date',
displayName : 'STOCK.ENTRY_DATE',
headerTooltip : 'STOCK.ENTRY_DATE',
headerCellFilter : 'translate',
cellFilter : 'date',
cellClass : 'text-right',
}, {
field : 'expiration_date',
displayName : 'STOCK.EXPIRATION_DATE',
headerTooltip : 'STOCK.EXPIRATION_DATE',
headerCellFilter : 'translate',
cellFilter : 'date:"mediumDate"',
cellClass : 'text-right',
}, {
field : 'num_duplicates',
displayName : 'TABLE.COLUMNS.NUM_DUPLICATE_LOTS',
headerTooltip : 'TABLE.COLUMNS.NUM_DUPLICATE_LOTS',
headerCellFilter : 'translate',
cellClass : 'text-right',
type : 'number',
}, {
field : 'action',
displayName : '',
enableFiltering : false,
enableSorting : false,
cellTemplate : 'modules/stock/lots-duplicates/templates/action.cell.html',
},
];

const footerTemplate = `
<div class="ui-grid-cell-contents">
<b>{{ grid.appScope.gridOptions.data.length }}</b>
<span translate>FORM.INFO.FOUND</span>
</div>
`;

vm.gridOptions = {
appScopeProvider : vm,
enableColumnMenus : false,
columnDefs : columns,
showGridFooter : true,
gridFooterTemplate : footerTemplate,
fastWatch : true,
flatEntityAccess : true,
};

const gridColumns = new Columns(vm.gridOptions, cacheKey);
const state = new GridState(vm.gridOptions, cacheKey);

vm.saveGridState = state.saveGridState;

function clearGridState() {
state.clearGridState();
$state.reload();
}

// expose view logic
vm.openColumnConfigModal = openColumnConfigModal;
vm.clearGridState = clearGridState;

// This function opens a modal through column service to let the user toggle
// the visibility of the inventories registry's columns.
function openColumnConfigModal() {
gridColumns.openConfigurationModal();
}

// load stock lots in the grid
function load() {
vm.hasError = false;
vm.loading = true;

Lots.dupes({ find_dupes : true })
.then((rows) => {
vm.gridOptions.data = rows;
})
.catch((err) => {
vm.hasError = true;
Notify.handleError(err);
})
.finally(() => {
vm.loading = false;
});
}

// lot duplicates modal
vm.openDuplicatesModal = (uuid, depotUuid) => {
// NOTE: depotUuid is undefined (for now)
Modal.openDuplicateLotsModal({ uuid, depotUuid })
.then((res) => {
if (res === 'success') {
// Reload the duplicate lots since some lots were merged
load();
}
});
};

function startup() {
load();
}

startup();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="ui-grid-cell-contents text-right" uib-dropdown dropdown-append-to-body uib-dropdown-toggle ng-if="!row.groupHeader">
<a href>
<span data-method="action" translate>FORM.BUTTONS.ACTIONS</span>
<span class="caret"></span>
</a>

<ul class="dropdown-menu-right" bh-dropdown-menu-auto-dropup uib-dropdown-menu>
<li class="bh-dropdown-header">{{row.entity.label}} - {{row.entity.inventory_text}}</li>
<li>
<a data-method="find-duplicates" ng-click="grid.appScope.openDuplicatesModal(row.entity.uuid, row.entity.depot_uuid)" href>
<i class="fa fa-search"></i> <span translate>LOTS.MERGE_DUPLICATE_LOTS</span>
</a>
</li>
</ul>
</div>
9 changes: 9 additions & 0 deletions client/src/modules/stock/stock.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ angular.module('bhima.routes')
},
})

.state('stockLotsDuplicates', {
url : '/stock/lots/duplicates',
controller : 'DuplicateLotsController as DupeLotsCtrl',
templateUrl : 'modules/stock/lots-duplicates/lots-duplicates.html',
params : {
filters : [],
},
})

.state('stockMovements', {
url : '/stock/movements',
controller : 'StockMovementsController as StockCtrl',
Expand Down
19 changes: 17 additions & 2 deletions server/controllers/stock/lots.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const FilterParser = require('../../lib/filter');
const detailsQuery = `
SELECT
BUID(l.uuid) AS uuid, l.label, l.quantity, l.initial_quantity,
l.unit_cost, l.description, l.expiration_date, l.entry_date,
l.unit_cost, l.description, l.entry_date, l.expiration_date,
BUID(i.uuid) AS inventory_uuid, i.text
FROM lot l
JOIN inventory i ON i.uuid = l.inventory_uuid
Expand Down Expand Up @@ -113,9 +113,24 @@ function getDupes(req, res, next) {
filters.equals('entry_date');
filters.equals('expiration_date');

const query = filters.applyQuery(detailsQuery);
let query = filters.applyQuery(detailsQuery);
const params = filters.parameters();

if ('find_dupes' in options) {
// For the 'find duplicate lots' search, we need a different query
query = `
SELECT
BUID(l.uuid) AS uuid, l.label, l.quantity, l.initial_quantity,
l.unit_cost, l.description, l.entry_date, l.expiration_date,
BUID(i.uuid) AS inventory_uuid, i.text as inventory_text,
COUNT(*) as num_duplicates
FROM lot l
JOIN inventory i ON i.uuid = l.inventory_uuid
GROUP BY label, inventory_uuid HAVING num_duplicates > 1
ORDER BY inventory_text, num_duplicates, label
`;
}

return db.exec(query, params)
.then(rows => {
res.status(200).json(rows);
Expand Down
3 changes: 2 additions & 1 deletion server/models/bhima.sql
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ INSERT INTO unit VALUES
(289, '[Stock] Expiration report','TREE.STOCK_EXPIRATION_REPORT','Stock expiration report', 282,'/reports/stock_expiration_report'),
(290, '[SETTINGS] Settings', 'TREE.STOCK_SETTINGS', 'Stock Settings', 160, '/stock/setting'),
(291, '[Stock] Dashboard', 'TREE.STOCK_DASHBOARD','Stock Dashboard', 160,'/stock/dashboard'),
(292, '[Stock] Changes Report', 'REPORT.STOCK_CHANGES.TITLE', 'Stock Changes Report', 282, '/reports/stock_changes');
(292, '[Stock] Changes Report', 'REPORT.STOCK_CHANGES.TITLE', 'Stock Changes Report', 282, '/reports/stock_changes'),
(293, 'Duplicate Lots','TREE.DUPLICATE_LOTS','The stock lots duplicates list',160,'/stock/lots/duplicates');

-- Reserved system account type
INSERT INTO `account_category` VALUES
Expand Down
8 changes: 8 additions & 0 deletions server/models/migrations/next/migrate.sql
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,11 @@ INSERT INTO `discharge_type` (`id`, `label`) VALUES
*/
CALL add_column_if_missing('depot', 'description', 'TEXT DEFAULT NULL AFTER `text`');
CALL add_column_if_missing('depot', 'dhis2_uid', 'VARCHAR(150) DEFAULT NULL AFTER `parent_uuid`');

/*
* @author: jmcameron
* @date: 2021-02-04
* @subject : Add nav entry for 'Find Duplicate Lots' page
*/
INSERT INTO unit VALUES
(293, 'Duplicate Lots','TREE.DUPLICATE_LOTS','The stock lots duplicates list',160,'/stock/lots/duplicates');
11 changes: 10 additions & 1 deletion test/integration/mergeLots.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe('Test merging lots', () => {
return agent.get('/lot/dupes')
.query({ label : lot1Label, inventory_uuid : vitamineUuid })
.then((res) => {
helpers.api.listed(res, 4); // 4 = he lot itself and 3 dupes
helpers.api.listed(res, 4); // 4 = the lot itself and 3 dupes
})
.catch(helpers.handler);
});
Expand Down Expand Up @@ -176,6 +176,15 @@ describe('Test merging lots', () => {
.catch(helpers.handler);
});

it(`Verify the 'find duplicate lots' query works`, () => {
return agent.get('/lot/dupes')
.query({ find_dupes : 1 })
.then((res) => {
helpers.api.listed(res, 1); // all dupes can be merged into one
})
.catch(helpers.handler);
});

// ===========================================================================
// NOW do the merge tests

Expand Down

0 comments on commit f2d92ac

Please sign in to comment.