diff --git a/client/src/i18n/en/price_list.json b/client/src/i18n/en/price_list.json
index 18b44ac55b..03ca203d83 100644
--- a/client/src/i18n/en/price_list.json
+++ b/client/src/i18n/en/price_list.json
@@ -1,14 +1,24 @@
-{"PRICE_LIST":{"ADD_ITEMS":"Add an Item",
-"ADD_PRICE_LIST":"Add a Price List",
-"HELP_TXT_1":"Select an action on the left to edit price list properties",
-"HELP_TXT_2":"Edit the price list's metadata",
-"HELP_TXT_3":"Remove the price list",
-"ITEMS":"Items",
-"NEW_PRICE_LIST":"New Price List",
-"NO_RECORDS":"No Records",
-"NONE":"Apply no price list",
-"PRICE_LIST_ITEMS":"Price list items",
-"TITLE":"Price List Management",
-"UNABLE_TO_DELETE":"Price list cannot be deleted - it may currently be assigned to patient / debtor groups",
-"UPDATE":"Update a Price List",
-"ERRORS":{"HAS_NEGATIVE_PRICE":"You cannot assign a negative price to an item. Please either assign a positive percentage, a negative percentage, or a positive value."}}}
+{
+ "PRICE_LIST": {
+ "ADD_ITEMS": "Add an Item",
+ "ADD_PRICE_LIST": "Add a Price List",
+ "HELP_TXT_1": "Select an action on the left to edit price list properties",
+ "HELP_TXT_2": "Edit the price list's metadata",
+ "HELP_TXT_3": "Remove the price list",
+ "ITEMS": "Items",
+ "NEW_PRICE_LIST": "New Price List",
+ "NO_RECORDS": "No Records",
+ "NONE": "Apply no price list",
+ "PRICE_LIST_ITEMS": "Price list items",
+ "TITLE": "Price List Management",
+ "UNABLE_TO_DELETE": "Price list cannot be deleted - it may currently be assigned to patient / debtor groups",
+ "UPDATE": "Update a Price List",
+ "ERRORS": {
+ "HAS_NEGATIVE_PRICE": "You cannot assign a negative price to an item. Please either assign a positive percentage, a negative percentage, or a positive value."
+ },
+ "IMPORT": {
+ "PRICE_LIST_ITEMS_DESCRIPTION": "You are about to import inventories's prices for this price list from a csv file",
+ "PRICE_LIST_ITEMS": "Click here for downloading price list items template file"
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/src/i18n/fr/price_list.json b/client/src/i18n/fr/price_list.json
index 6379abc092..d9d3e922b9 100644
--- a/client/src/i18n/fr/price_list.json
+++ b/client/src/i18n/fr/price_list.json
@@ -1,14 +1,24 @@
-{"PRICE_LIST":{"ADD_ITEMS":"Ajouter un Item",
-"ADD_PRICE_LIST":"Ajouter une Liste",
-"HELP_TXT_1":"Choisir une action à gauche pour editer la liste de prix",
-"HELP_TXT_2":"Éditer les metadonnées de la Liste",
-"HELP_TXT_3":"Effacer la Liste",
-"ITEMS":"Items",
-"NEW_PRICE_LIST":"Nouvelle Liste des Prix",
-"NO_RECORDS":"Pas d'enregistrement",
-"NONE":"Appliquer aucune liste de prix",
-"PRICE_LIST_ITEMS":"Eléments de la liste des prix",
-"TITLE":"Liste des Prix",
-"UNABLE_TO_DELETE":"La liste de prix ne peut être supprimer - car elle peut être courament assignée à un groupe de patient / Groupes Débiteurs",
-"UPDATE":"Mettre à jour une liste de prix",
-"ERRORS":{"HAS_NEGATIVE_PRICE":"Vous ne pouvez pas assigner une valeur negatif."}}}
\ No newline at end of file
+{
+ "PRICE_LIST": {
+ "ADD_ITEMS": "Ajouter un Item",
+ "ADD_PRICE_LIST": "Ajouter une Liste",
+ "HELP_TXT_1": "Choisir une action à gauche pour editer la liste de prix",
+ "HELP_TXT_2": "Éditer les metadonnées de la Liste",
+ "HELP_TXT_3": "Effacer la Liste",
+ "ITEMS": "Items",
+ "NEW_PRICE_LIST": "Nouvelle Liste des Prix",
+ "NO_RECORDS": "Pas d'enregistrement",
+ "NONE": "Appliquer aucune liste de prix",
+ "PRICE_LIST_ITEMS": "Eléments de la liste des prix",
+ "TITLE": "Liste des Prix",
+ "UNABLE_TO_DELETE": "La liste de prix ne peut être supprimer - car elle peut être courament assignée à un groupe de patient / Groupes Débiteurs",
+ "UPDATE": "Mettre à jour une liste de prix",
+ "ERRORS": {
+ "HAS_NEGATIVE_PRICE": "Vous ne pouvez pas assigner une valeur negatif."
+ },
+ "IMPORT": {
+ "PRICE_LIST_ITEMS_DESCRIPTION": "Vous êtes sur le point d'importer les prix des inventaires pour cette liste des prix à partir d'un fichier CSV",
+ "PRICE_LIST_ITEMS": "Cliquez ici pour télécharger le fichier modèle pour l'importation des prix"
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/src/modules/prices/modal/import.html b/client/src/modules/prices/modal/import.html
new file mode 100644
index 0000000000..e4319912bc
--- /dev/null
+++ b/client/src/modules/prices/modal/import.html
@@ -0,0 +1,47 @@
+
diff --git a/client/src/modules/prices/modal/import.js b/client/src/modules/prices/modal/import.js
new file mode 100644
index 0000000000..5f6583cacc
--- /dev/null
+++ b/client/src/modules/prices/modal/import.js
@@ -0,0 +1,58 @@
+angular.module('bhima.controllers')
+ .controller('ImportPriceListModalController', ImportPriceListModalController);
+
+ImportPriceListModalController.$inject = [
+ 'data', '$uibModalInstance', 'InventoryService',
+ 'Upload', 'NotifyService', 'PriceListService',
+];
+
+function ImportPriceListModalController(data, Instance, Inventory, Upload, Notify, PriceList) {
+ const vm = this;
+
+ vm.downloadTemplate = Inventory.downloadInventoriesTemplate;
+ vm.cancel = Instance.close;
+ vm.priceList = data;
+ vm.select = (file) => {
+ vm.noSelectedFile = !file;
+ };
+
+ vm.downloadTemplate = PriceList.downloadTemplate;
+
+ vm.submit = () => {
+ // send data only when a file is selected
+ if (!vm.file) {
+ vm.noSelectedFile = true;
+ return;
+ }
+
+ uploadFile(vm.file);
+ };
+
+ /** upload the file to server */
+ function uploadFile(file) {
+ vm.uploadState = 'uploading';
+
+ const params = {
+ url : '/prices/item/import',
+ data : { file, pricelist_uuid : data.uuid },
+ };
+
+ // upload the file to the server
+ Upload.upload(params)
+ .then(handleSuccess, Notify.handleError, handleProgress);
+
+ // success upload handler
+ function handleSuccess() {
+ vm.uploadState = 'uploaded';
+ Notify.success('INVENTORY.UPLOAD_SUCCESS');
+ Instance.close();
+ }
+
+ // progress handler
+ function handleProgress(evt) {
+ file.progress = Math.min(100, parseInt((100.0 * evt.loaded) / evt.total, 10));
+ vm.progressStyle = { width : String(file.progress).concat('%') };
+ }
+
+ }
+}
diff --git a/client/src/modules/prices/prices.html b/client/src/modules/prices/prices.html
index 2561f98c60..64a1778e43 100644
--- a/client/src/modules/prices/prices.html
+++ b/client/src/modules/prices/prices.html
@@ -38,6 +38,7 @@
FORM.BUTTONS.PRINT
+
DOWNLOADS.CSV
diff --git a/client/src/modules/prices/prices.js b/client/src/modules/prices/prices.js
index 1ef3a2bf0b..4258ef5404 100644
--- a/client/src/modules/prices/prices.js
+++ b/client/src/modules/prices/prices.js
@@ -18,6 +18,7 @@ function PriceListController(
vm.download = download;
vm.openColumnConfigModal = openColumnConfigModal;
vm.toggleInlineFilter = toggleInlineFilter;
+ vm.ImportList = ImportList;
// set price list items
vm.addItem = addItem;
// delete a price list
@@ -172,6 +173,22 @@ function PriceListController(
});
}
+ // Add pricelist Item in a modal
+ function ImportList(pricelist) {
+ return $uibModal.open({
+ templateUrl : 'modules/prices/modal/import.html',
+ controller : 'ImportPriceListModalController as ModalCtrl',
+ keyboard : false,
+ backdrop : 'static',
+ size : 'md',
+ resolve : {
+ data : function dataProvider() {
+ return pricelist || {};
+ },
+ },
+ });
+ }
+
// refresh the displayed PriceList
function refreshPriceList() {
return PriceListService.read(null, { detailed : 1 }).then(data => {
diff --git a/client/src/modules/prices/prices.service.js b/client/src/modules/prices/prices.service.js
index 1a9fcd0535..863ca4ebb7 100644
--- a/client/src/modules/prices/prices.service.js
+++ b/client/src/modules/prices/prices.service.js
@@ -20,7 +20,7 @@ function PriceListService(Api) {
service.details = details;
service.deleteItem = deleteItem;
service.download = download;
-
+ service.downloadTemplate = downloadTemplate;
/**
* @method create
*
@@ -78,5 +78,13 @@ function PriceListService(Api) {
return service.$http.get(url, params)
.then(service.util.unwrapHttpResponse);
}
+
+ function downloadTemplate() {
+ const url = service.url.concat('download/template');
+ return service.$http.get(url)
+ .then(response => {
+ return service.util.download(response, 'Iventory item Template', 'csv');
+ });
+ }
return service;
}
diff --git a/client/src/modules/prices/templates/action.cell.html b/client/src/modules/prices/templates/action.cell.html
index 6925db8f50..34220485e0 100644
--- a/client/src/modules/prices/templates/action.cell.html
+++ b/client/src/modules/prices/templates/action.cell.html
@@ -22,6 +22,13 @@
+
+
+
+ FORM.BUTTONS.IMPORT FORM.LABELS.ITEMS
+
+
+
diff --git a/server/config/routes.js b/server/config/routes.js
index 2b04c462a6..350eb82341 100644
--- a/server/config/routes.js
+++ b/server/config/routes.js
@@ -578,9 +578,11 @@ exports.configure = function configure(app) {
app.get('/prices', priceList.list);
app.get('/prices/:uuid', priceList.details);
app.get('/prices/download/list', priceListPreport.downloadRegistry);
+ app.get('/prices/download/template', priceList.downloadTemplate);
app.get('/prices/report/:uuid', financeReports.priceList);
app.post('/prices', priceList.create);
app.post('/prices/item', priceList.createItem);
+ app.post('/prices/item/import', upload.middleware('csv', 'file'), priceList.importItem);
app.put('/prices/:uuid', priceList.update);
app.delete('/prices/:uuid', priceList.delete);
app.delete('/prices/item/:uuid', priceList.deleteItem);
diff --git a/server/controllers/finance/priceList.js b/server/controllers/finance/priceList.js
index 1ddf6cbfbe..1b9c1132ba 100644
--- a/server/controllers/finance/priceList.js
+++ b/server/controllers/finance/priceList.js
@@ -12,9 +12,13 @@
* PUT /prices/:uuid
* DELETE /prices/:uuid
*/
+const path = require('path');
const db = require('../../lib/db');
const { uuid } = require('../../lib/util');
+const BadRequest = require('../../lib/errors/BadRequest');
+const util = require('../../lib/util');
+
exports.lookup = lookup;
/**
* Lists all price lists in the database
@@ -229,6 +233,56 @@ exports.createItem = function createItem(req, res, next) {
.done();
};
+
+/**
+ * @method downloadTemplate
+ *
+ * @description send to the client the template file for price list item import
+*/
+exports.downloadTemplate = (req, res, next) => {
+ try {
+ const file = path.join(__dirname, '../../resources/templates/import-inventory-item-template.csv');
+ res.download(file);
+ } catch (error) {
+ next(error);
+ }
+};
+
+exports.importItem = async (req, res, next) => {
+ try {
+ if (!req.files || req.files.length === 0) {
+ const errorDescription = 'Expected at least one file upload but did not receive any files.';
+ throw new BadRequest(errorDescription, 'ERRORS.MISSING_UPLOAD_FILES');
+ }
+
+ const filePath = req.files[0].path;
+
+ const data = await util.formatCsvToJson(filePath);
+ if (!hasValidDataFormat(data)) {
+ throw new BadRequest('The given file has a bad data format for stock', 'ERRORS.BAD_DATA_FORMAT');
+ }
+ const priceListUuid = db.bid(req.body.pricelist_uuid);
+ const sql = 'CALL importPriceListItem(?,?,?,?);';
+ const transaction = db.transaction();
+ data.forEach(item => {
+
+ transaction.addQuery(sql, [priceListUuid, item.code, item.price, item.is_percentage]);
+ });
+ await transaction.execute();
+ res.sendStatus(200);
+ } catch (ex) {
+ next(ex);
+ }
+
+};
+
+function hasValidDataFormat(data) {
+ const invalids = data.filter(r => {
+ return (r.code && r.price);
+ });
+ return (invalids.length === data.length);
+}
+
exports.deleteItem = function deleteItem(req, res, next) {
const priceListDeleteItemSql = `DELETE FROM price_list_item WHERE uuid = ?`;
const itemUuid = db.bid(req.params.uuid);
diff --git a/server/models/migrations/next/migrate.sql b/server/models/migrations/next/migrate.sql
index 3788107639..0ccccb81f0 100644
--- a/server/models/migrations/next/migrate.sql
+++ b/server/models/migrations/next/migrate.sql
@@ -1,3 +1,34 @@
/*
* DATABASE CHANGES FOR VERSION 1.6.0 TO 1.7.0
- */
\ No newline at end of file
+ */
+/*
+*Pricelist importation
+by Jeremielodi
+2019-10-16
+*/
+
+DROP PROCEDURE IF EXISTS importPriceListItem;
+CREATE PROCEDURE importPriceListItem (
+ IN _price_list_uuid BINARY(16),
+ IN _inventory_code VARCHAR(30),
+ IN _value DOUBLE,
+ IN _is_percentage tinyint(1)
+)
+BEGIN
+ DECLARE _inventory_uuid BINARY(16);
+ DECLARE isIventory tinyint(5);
+ DECLARE inventoryLabel VARCHAR(100);
+
+ SELECT uuid, text, count(uuid)
+ INTO _inventory_uuid, inventoryLabel, isIventory
+ FROM inventory
+ WHERE code = _inventory_code;
+
+ IF isIventory = 1 THEN
+ DELETE FROM price_list_item
+ WHERE price_list_uuid = _price_list_uuid AND inventory_uuid = _inventory_uuid;
+ INSERT INTO price_list_item(uuid, inventory_uuid, price_list_uuid, label, value, is_percentage)
+ VALUES(HUID(uuid()), _inventory_uuid, _price_list_uuid, inventoryLabel, _value, _is_percentage);
+ END IF;
+
+END $$
\ No newline at end of file
diff --git a/server/models/procedures/inventory.sql b/server/models/procedures/inventory.sql
index f393e2d1b8..149605fb19 100644
--- a/server/models/procedures/inventory.sql
+++ b/server/models/procedures/inventory.sql
@@ -74,4 +74,30 @@ BEGIN
END IF;
END $$
+DROP PROCEDURE IF EXISTS importPriceListItem;
+CREATE PROCEDURE importPriceListItem (
+ IN _price_list_uuid BINARY(16),
+ IN _inventory_code VARCHAR(30),
+ IN _value DOUBLE,
+ IN _is_percentage tinyint(1)
+)
+BEGIN
+ DECLARE _inventory_uuid BINARY(16);
+ DECLARE isIventory tinyint(5);
+ DECLARE inventoryLabel VARCHAR(100);
+
+ SELECT uuid, text, count(uuid)
+ INTO _inventory_uuid, inventoryLabel, isIventory
+ FROM inventory
+ WHERE code = _inventory_code;
+
+ IF isIventory = 1 THEN
+ DELETE FROM price_list_item
+ WHERE price_list_uuid = _price_list_uuid AND inventory_uuid = _inventory_uuid;
+ INSERT INTO price_list_item(uuid, inventory_uuid, price_list_uuid, label, value, is_percentage)
+ VALUES(HUID(uuid()), _inventory_uuid, _price_list_uuid, inventoryLabel, _value, _is_percentage);
+ END IF;
+
+END $$
+
DELIMITER ;
diff --git a/server/resources/templates/import-inventory-item-template.csv b/server/resources/templates/import-inventory-item-template.csv
new file mode 100644
index 0000000000..b076930b34
--- /dev/null
+++ b/server/resources/templates/import-inventory-item-template.csv
@@ -0,0 +1 @@
+code, price, is_percentage
\ No newline at end of file
diff --git a/test/end-to-end/price_list/PriceListItemsModal.page.js b/test/end-to-end/price_list/PriceListItemsModal.page.js
index 5cf4641f3a..47414c4633 100644
--- a/test/end-to-end/price_list/PriceListItemsModal.page.js
+++ b/test/end-to-end/price_list/PriceListItemsModal.page.js
@@ -1,7 +1,9 @@
/* global by, element */
-
+const path = require('path');
const FU = require('../shared/FormUtils');
+const fixtures = path.resolve(__dirname, '../../fixtures/');
+
class PriceListItemsModalPage {
constructor() {
this.gridId = 'pricelist-items-grid';
@@ -20,6 +22,11 @@ class PriceListItemsModalPage {
return FU.input('ModalCtrl.data.value', value, this.modal);
}
+ uploadFile(fileToUpload) {
+ const absolutePath = path.resolve(fixtures, fileToUpload);
+ return element(by.id('import-input')).sendKeys(absolutePath);
+ }
+
// TODO(@jniles) - migrate this to bhYesNo
async setIsPercentage(bool) {
if (bool) {
diff --git a/test/end-to-end/price_list/price_list.page.js b/test/end-to-end/price_list/price_list.page.js
index efd7ab371a..3471afebd1 100644
--- a/test/end-to-end/price_list/price_list.page.js
+++ b/test/end-to-end/price_list/price_list.page.js
@@ -31,6 +31,12 @@ class PriceListPage {
await row.dropdown().click();
await row.method('edit-items').click();
}
+
+ async importItems(label) {
+ const row = new GridRow(label);
+ await row.dropdown().click();
+ await row.method('import-items').click();
+ }
}
module.exports = PriceListPage;
diff --git a/test/end-to-end/price_list/price_list.spec.js b/test/end-to-end/price_list/price_list.spec.js
index 05e936dbeb..5f29955279 100644
--- a/test/end-to-end/price_list/price_list.spec.js
+++ b/test/end-to-end/price_list/price_list.spec.js
@@ -1,5 +1,3 @@
-/* global element, by */
-
const helpers = require('../shared/helpers');
const FU = require('../shared/FormUtils');
@@ -7,10 +5,12 @@ const components = require('../shared/components');
const PriceListPage = require('./price_list.page');
const PriceListItemsModal = require('./PriceListItemsModal.page');
+const PRICE_LIST_ITEM_CSV_FILE = 'import-inventory-item-template.csv';
+
describe('Price Lists', () => {
const path = '#!/prices';
const page = new PriceListPage();
-
+ const modal = new PriceListItemsModal();
before(() => helpers.navigate(path));
const list = {
@@ -49,9 +49,6 @@ describe('Price Lists', () => {
it('prices should add a price list item', async () => {
await page.configure(updateListLabel);
-
- const modal = new PriceListItemsModal();
-
await modal.setLabel(priceListItem.label);
await modal.setValue(priceListItem.value);
await modal.setIsPercentage(priceListItem.is_percentage);
@@ -63,14 +60,22 @@ describe('Price Lists', () => {
await components.notification.hasSuccess();
});
+
it('prices should delete a price list item', async () => {
await page.configure(updateListLabel);
-
- const modal = new PriceListItemsModal();
await modal.remove(priceListItem.label);
await modal.submit();
await modal.close();
await components.notification.hasSuccess();
});
+
+ // import custom ohada accounts
+ it('import price list item from csv file into the system', async () => {
+ await page.importItems(updateListLabel);
+ await modal.uploadFile(PRICE_LIST_ITEM_CSV_FILE);
+ await FU.modal.submit();
+ await components.notification.hasSuccess();
+ });
+
});
diff --git a/test/fixtures/import-inventory-item-template.csv b/test/fixtures/import-inventory-item-template.csv
new file mode 100644
index 0000000000..6efbd5ace6
--- /dev/null
+++ b/test/fixtures/import-inventory-item-template.csv
@@ -0,0 +1,2 @@
+code, price, is_percentage
+100102, 200, 1
\ No newline at end of file