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