diff --git a/client/src/i18n/en/cron.json b/client/src/i18n/en/cron.json new file mode 100644 index 0000000000..b469e75ed6 --- /dev/null +++ b/client/src/i18n/en/cron.json @@ -0,0 +1,24 @@ +{ + "CRON":{ + "TITLE":"Cron", + "AUTO_EMAIL_REPORT":"Auto sending report by email", + "FREQUENCY":"Frequency", + "RECEIVERS":"Receivers", + "DAILY":"Daily", + "WEEKLY":"Weekly", + "MONTHLY":"Monthly", + "YEARLY":"Yearly", + "EACH_MINUTE":"Each Minute", + "SAVE":"Save", + "SAVED_AUTO_REPORT":"Saved report for mailing", + "LAST":"Last Sent", + "NEXT":"Next Send", + "PLEASE_FILL_REPORT_FORM":"Oops! It looks like there is an error in the report configuration form. Please fill it in correctly before saving the email configuration.", + "PLEASE_FILL_CRON_FORM":"The email form isn't filled out correctly. Please check the values and resubmit", + "DATES_BEHAVIOR":"Dates behaviors", + "FIXED_DATES":"Fixed dates", + "DYNAMIC_DATES":"Dynamic dates", + "EMAIL_SENT_SUCCESSFULLY":"Email sent successfully", + "SEND":"Send" + } +} \ No newline at end of file diff --git a/client/src/i18n/en/enterprise.json b/client/src/i18n/en/enterprise.json index eb9dc37953..47b5757d5a 100644 --- a/client/src/i18n/en/enterprise.json +++ b/client/src/i18n/en/enterprise.json @@ -24,7 +24,9 @@ "ENABLE_BARCODES_LABEL" : "Enables barcodes throughout the application", "ENABLE_BARCODES_HELP_TEXT" : "Enable to place barcodes on most printed records and enable options to scan barcodes for document inputs.", "ENABLE_AUTO_STOCK_ACCOUNTING_LABEL" : "Enables realtime stock accounting", - "ENABLE_AUTO_STOCK_ACCOUNTING_HELP_TEXT" : "Enabling this feature will write stock movement transactions into the posting journal in real time. It requires all inventory accounts to be correctly configured." + "ENABLE_AUTO_STOCK_ACCOUNTING_HELP_TEXT" : "Enabling this feature will write stock movement transactions into the posting journal in real time. It requires all inventory accounts to be correctly configured.", + "ENABLE_AUTO_EMAIL_REPORT_LABEL" : "Enable sending automatic reports by email", + "ENABLE_AUTO_EMAIL_REPORT_HELP_TEXT" : "Enabling this option allows users to configure schedules to automatically send reports to a list of email addresses." } } } diff --git a/client/src/i18n/en/entity.json b/client/src/i18n/en/entity.json index bc4d91ca74..7581393576 100644 --- a/client/src/i18n/en/entity.json +++ b/client/src/i18n/en/entity.json @@ -1,8 +1,8 @@ { "ENTITY":{ - "MANAGEMENT":"Entity Management", - "ADD_ENTITY":"Add Entity", - "EDIT_ENTITY":"Edit Entity", + "MANAGEMENT":"Contact Management", + "ADD_ENTITY":"Add Contact", + "EDIT_ENTITY":"Edit Contact", "NAME":"Name", "SEX":"Sex", "PHONE":"Phone", @@ -11,13 +11,20 @@ "CREATED":"Successfully created", "SUCCESS":"Successfully Done", "UPDATED":"Successfully Updated", - "LABEL":"Entity", + "LABEL":"Contact", "TYPE":{ - "LABEL":"Entity Type", + "LABEL":"Contact Type", "PERSON":"Person", "ENTERPRISE":"Enterprise", "SERVICE":"Service", "OFFICE":"Office" + }, + "GROUP":{ + "TITLE":"Contact Group Management", + "ADD":"Add Group", + "EDIT":"Edit Group", + "DELETE":"Delete Group", + "GROUP":"Entity Group" } } } \ No newline at end of file diff --git a/client/src/i18n/en/form.json b/client/src/i18n/en/form.json index 7cbb32b6c9..76c12f61b5 100644 --- a/client/src/i18n/en/form.json +++ b/client/src/i18n/en/form.json @@ -789,7 +789,9 @@ "ACCOUNT_REFERENCE" : "Select an Account Reference", "ACCOUNT_REFERENCE_TYPE" : "Select an Account Reference Type", "ENTITY":"Select an entity", + "ENTITY_GROUP":"Select an entity group", "ENTITY_TYPE":"Select an entity type", + "FREQUENCY":"Select a frequency", "GENDER":"Select a gender", "EMPTY": "Empty", "EMPLOYEE": "Select an Employee", diff --git a/client/src/i18n/fr/cron.json b/client/src/i18n/fr/cron.json new file mode 100644 index 0000000000..25c97d73ee --- /dev/null +++ b/client/src/i18n/fr/cron.json @@ -0,0 +1,24 @@ +{ + "CRON":{ + "TITLE":"Cron", + "AUTO_EMAIL_REPORT":"Envoi automatique de rapport par email", + "FREQUENCY":"Fréquence", + "RECEIVERS":"Recepteurs", + "DAILY":"Chaque jour", + "WEEKLY":"Chaque semaine", + "MONTHLY":"Chaque mois", + "YEARLY":"Chaque année", + "EACH_MINUTE":"Chaque minute", + "SAVE":"Sauvegarder", + "SAVED_AUTO_REPORT":"Rapports sauvegardés pour le mailing", + "LAST":"Dernier envoi", + "NEXT":"Prochain envoi", + "PLEASE_FILL_REPORT_FORM":"Oops! Il semble y avoir une erreur dans le formulaire de configuration du rapport. Veuillez le remplir correctement avant de sauvegarder la configuration du courrier électronique.", + "PLEASE_FILL_CRON_FORM":"The email form isn't filled out correctly. Please check the values and resubmit", + "DATES_BEHAVIOR":"Comportement des dates", + "FIXED_DATES":"Dates fixes", + "DYNAMIC_DATES":"Dates dynamiques", + "EMAIL_SENT_SUCCESSFULLY":"Email envoyé avec succès", + "SEND":"Envoyer" + } +} \ No newline at end of file diff --git a/client/src/i18n/fr/enterprise.json b/client/src/i18n/fr/enterprise.json index 09fed71ee3..f6a4ff3397 100644 --- a/client/src/i18n/fr/enterprise.json +++ b/client/src/i18n/fr/enterprise.json @@ -24,7 +24,9 @@ "ENABLE_BARCODES_LABEL" : "Activer les codes à barres dans l'application", "ENABLE_BARCODES_HELP_TEXT" : "L'activation de cette fonction placera des codes à barres sur la plupart des enregistrements imprimés et activera les options permettant de numériser des codes à barres pour les entrées de document.", "ENABLE_AUTO_STOCK_ACCOUNTING_LABEL" : "Activer la comptabilisation de stock en temps réel", - "ENABLE_AUTO_STOCK_ACCOUNTING_HELP_TEXT" : "L'activation de cette option va écrire automatiquement et en temps réel dans le journal toutes les transactions liées aux mouvements de stock. Les comptes des inventaires et groupes d'inventaire doivent être bien configurés au préalable" + "ENABLE_AUTO_STOCK_ACCOUNTING_HELP_TEXT" : "L'activation de cette option va écrire automatiquement et en temps réel dans le journal toutes les transactions liées aux mouvements de stock. Les comptes des inventaires et groupes d'inventaire doivent être bien configurés au préalable", + "ENABLE_AUTO_EMAIL_REPORT_LABEL" : "Activer l'envoie des rapports automatiques par email", + "ENABLE_AUTO_EMAIL_REPORT_HELP_TEXT" : "L'activation de cette option permet aux utilisateurs de configurer des planifications pour envoyer automatiquement des rapports à une liste d'adresses électroniques." } } } diff --git a/client/src/i18n/fr/entity.json b/client/src/i18n/fr/entity.json index bfd39c2f0c..11edd42f6e 100644 --- a/client/src/i18n/fr/entity.json +++ b/client/src/i18n/fr/entity.json @@ -1,23 +1,30 @@ { "ENTITY":{ - "MANAGEMENT":"Gestion des entités", - "ADD_ENTITY":"Ajouter une entité", - "EDIT_ENTITY":"Modifier l'entité", + "MANAGEMENT":"Gestion des contacts", + "ADD_ENTITY":"Ajouter un contact", + "EDIT_ENTITY":"Modifier le contact", "NAME":"Nom", "SEX":"Sex", "PHONE":"Téléphone", "EMAIL":"Email", "ADDRESS":"Adresse", - "CREATED":"Création de l'entité réussie avec succès", + "CREATED":"Création du contact réussie avec succès", "SUCCESS":"Opération réussie avec succès", "UPDATED":"Mise à jour réussie avec succès", "LABEL":"Entité", "TYPE":{ - "LABEL":"Type d'entité", + "LABEL":"Type de contact", "PERSON":"Personne", "ENTERPRISE":"Entreprise", "SERVICE":"Service", "OFFICE":"Bureau" + }, + "GROUP":{ + "TITLE":"Gestion des groupe de contacts", + "ADD":"Ajouter un groupe", + "EDIT":"Editer le groupe", + "DELETE":"Supprimer le groupe", + "GROUP":"Groupe des contacts" } } } \ No newline at end of file diff --git a/client/src/i18n/fr/form.json b/client/src/i18n/fr/form.json index 83a385f967..600e5c1e0f 100644 --- a/client/src/i18n/fr/form.json +++ b/client/src/i18n/fr/form.json @@ -789,7 +789,9 @@ "ACCOUNT_REFERENCE" : "Sélectionner la référence des comptes", "ACCOUNT_REFERENCE_TYPE" : "Sélectionner le type de référence des comptes", "ENTITY":"Sélectionner une entité", + "ENTITY_GROUP":"Sélectionner un groupe d'entités", "ENTITY_TYPE":"Sélectionner un type d'entité", + "FREQUENCY":"Sélectionner une frequence", "GENDER":"Sélectionner un genre", "EMPTY": "Vide", "EMPLOYEE": "Sélectionner un Employé", diff --git a/client/src/js/components/bhCronEmailReport/bhCronEmailReport.html b/client/src/js/components/bhCronEmailReport/bhCronEmailReport.html new file mode 100644 index 0000000000..58572e3dc1 --- /dev/null +++ b/client/src/js/components/bhCronEmailReport/bhCronEmailReport.html @@ -0,0 +1,77 @@ +
+ +
+
+ + CRON.AUTO_EMAIL_REPORT +
+
+
+ + + + + + + + + + + + + + + + +
+ +
+
+
+
+ + +
+
+ + CRON.SAVED_AUTO_REPORT +
+ + + + + + + + + + + +
{{ item.label}}{{ item.entity_group_label }} ({{ item.cron_label }})CRON.LAST
{{ item.last_send | date:'dd MMMM yyyy, hh:mm' }}
CRON.NEXT
{{ item.next_send | date:'dd MMMM yyyy, hh:mm' }}
+ + CRON.SEND + + + + + FORM.BUTTONS.DELETE + +
+
+
diff --git a/client/src/js/components/bhCronEmailReport/bhCronEmailReport.js b/client/src/js/components/bhCronEmailReport/bhCronEmailReport.js new file mode 100644 index 0000000000..c747fc44d6 --- /dev/null +++ b/client/src/js/components/bhCronEmailReport/bhCronEmailReport.js @@ -0,0 +1,113 @@ +angular.module('bhima.components') + .component('bhCronEmailReport', { + templateUrl : 'js/components/bhCronEmailReport/bhCronEmailReport.html', + controller : bhCronEmailReportController, + transclude : true, + bindings : { + reportId : '@', + reportForm : '<', + reportDetails : '<', + onSelectReport : '&', + }, + }); + +bhCronEmailReportController.$inject = [ + 'CronEmailReportService', 'NotifyService', 'SessionService', +]; + +function bhCronEmailReportController(CronEmailReports, Notify, Session) { + const $ctrl = this; + + $ctrl.submit = submit; + $ctrl.remove = remove; + $ctrl.details = details; + $ctrl.send = send; + + $ctrl.onSelectEntityGroup = entityGroup => { + $ctrl.cron.entity_group_uuid = entityGroup.uuid; + }; + + $ctrl.onSelectCron = cron => { + $ctrl.cron.cron_id = cron.id; + }; + + $ctrl.onChangeDynamicDates = value => { + $ctrl.cron.has_dynamic_dates = value; + }; + + $ctrl.$onInit = init; + + function init() { + $ctrl.cron = { + report_id : $ctrl.reportId, + report_url : $ctrl.reportUrl, + has_dynamic_dates : 0, + }; + $ctrl.isEmailFeatureEnabled = Session.enterprise.settings.enable_auto_email_report; + load($ctrl.reportId); + } + + function load(id) { + CronEmailReports.read(null, { report_id : id }) + .then(rows => { + $ctrl.list = rows; + }) + .catch(Notify.handleError); + } + + function details(id) { + CronEmailReports.read(id) + .then(row => { + if (!row) { return; } + const report = JSON.parse(row.params); + $ctrl.onSelectReport({ report }); + }) + .catch(Notify.handleError); + } + + function send(id) { + $ctrl.sendingPending = true; + CronEmailReports.send(id) + .then(() => { + $ctrl.sendingPending = false; + Notify.success('CRON.EMAIL_SENT_SUCCESSFULLY'); + }) + .catch(Notify.handleError) + .finally(() => { + $ctrl.sendingPending = false; + }); + } + + function remove(id) { + CronEmailReports.delete(id) + .then(() => load($ctrl.reportId)) + .catch(Notify.handleError); + } + + function submit(cronForm) { + if ($ctrl.reportForm.$invalid) { + Notify.warn('CRON.PLEASE_FILL_REPORT_FORM'); + return; + } + + if (cronForm.$invalid) { + return; + } + + const params = { + cron : $ctrl.cron, + reportOptions : $ctrl.reportDetails, + }; + + CronEmailReports.create(params) + .then(() => reset(cronForm)) + .then(() => init()) + .catch(Notify.handleError); + } + + function reset(form) { + form.CronForm.$setPristine(); + form.EntityGroupForm.$setPristine(); + form.textValueForm.$setPristine(); + } +} diff --git a/client/src/js/components/bhCronSelect/bhCronSelect.html b/client/src/js/components/bhCronSelect/bhCronSelect.html new file mode 100644 index 0000000000..5b8f200078 --- /dev/null +++ b/client/src/js/components/bhCronSelect/bhCronSelect.html @@ -0,0 +1,28 @@ +
+
+ + + + + + + {{$select.selected.hrLabel}} + + + + + + +
+
+
+
+
diff --git a/client/src/js/components/bhCronSelect/bhCronSelect.js b/client/src/js/components/bhCronSelect/bhCronSelect.js new file mode 100644 index 0000000000..0d75c3636e --- /dev/null +++ b/client/src/js/components/bhCronSelect/bhCronSelect.js @@ -0,0 +1,41 @@ +angular.module('bhima.components') + .component('bhCronSelect', { + templateUrl : 'js/components/bhCronSelect/bhCronSelect.html', + controller : CronSelectController, + transclude : true, + bindings : { + id : '<', + onSelectCallback : '&', + required : ' { + $ctrl.crons = data.map(item => { + item.hrLabel = $translate.instant(item.label); + return item; + }); + + }) + .catch(Notify.handleError); + }; + + // fires the onSelectCallback bound to the component boundary + $ctrl.onSelect = $item => { + $ctrl.onSelectCallback({ cron : $item }); + }; +} diff --git a/client/src/js/components/bhEntityGroupSelect/bhEntityGroupSelect.js b/client/src/js/components/bhEntityGroupSelect/bhEntityGroupSelect.js new file mode 100644 index 0000000000..d95ed234cc --- /dev/null +++ b/client/src/js/components/bhEntityGroupSelect/bhEntityGroupSelect.js @@ -0,0 +1,38 @@ +angular.module('bhima.components') + .component('bhEntityGroupSelect', { + templateUrl : 'js/components/bhEntityGroupSelect/bhEntityGroupSelect.tmpl.html', + controller : EntityGroupSelectController, + transclude : true, + bindings : { + uuid : '<', + onSelectCallback : '&', + required : ' { + $ctrl.entityGroups = data; + }) + .catch(Notify.handleError); + }; + + // fires the onSelectCallback bound to the component boundary + $ctrl.onSelect = $item => { + $ctrl.onSelectCallback({ entityGroup : $item }); + }; +} diff --git a/client/src/js/components/bhEntityGroupSelect/bhEntityGroupSelect.tmpl.html b/client/src/js/components/bhEntityGroupSelect/bhEntityGroupSelect.tmpl.html new file mode 100644 index 0000000000..5a6d91e6d1 --- /dev/null +++ b/client/src/js/components/bhEntityGroupSelect/bhEntityGroupSelect.tmpl.html @@ -0,0 +1,28 @@ +
+
+ + + + + + + {{$select.selected.label}} + + + + + + +
+
+
+
+
diff --git a/client/src/js/components/bhEntitySelect/bhEntitySelect.js b/client/src/js/components/bhEntitySelect/bhEntitySelect.js index 4ef01aa7fd..51e69252cd 100644 --- a/client/src/js/components/bhEntitySelect/bhEntitySelect.js +++ b/client/src/js/components/bhEntitySelect/bhEntitySelect.js @@ -21,6 +21,7 @@ function EntitySelectController(Entities, Notify) { $ctrl.$onInit = function onInit() { $ctrl.label = $ctrl.label || 'ENTITY.LABEL'; + $ctrl.multiple = $ctrl.multiple || false; // load all depots Entities.read(null) diff --git a/client/src/js/components/bhEntitySelectMultiple/bhEntitySelectMultiple.js b/client/src/js/components/bhEntitySelectMultiple/bhEntitySelectMultiple.js new file mode 100644 index 0000000000..29f14e80f5 --- /dev/null +++ b/client/src/js/components/bhEntitySelectMultiple/bhEntitySelectMultiple.js @@ -0,0 +1,49 @@ +angular.module('bhima.components') + .component('bhEntitySelectMultiple', { + templateUrl : 'js/components/bhEntitySelectMultiple/bhEntitySelectMultiple.tmpl.html', + controller : EntitySelectMultipleController, + transclude : true, + bindings : { + entityUuids : '<', + onSelectCallback : '&', + required : ' { + $ctrl.onSelectCallback({ entities : $ctrl.entityUuids }); + loadEntities(); + }; + + function loadEntities() { + Entities.read(null) + .then(entities => { + if ($ctrl.entityUuids.length) { + const givenEntities = $ctrl.entityUuids.map(e => e.uuid); + $ctrl.entities = entities.filter(e => { + return givenEntities.includes(e.uuid) === false; + }); + } else { + $ctrl.entities = entities; + } + + }) + .catch(Notify.handleError); + } +} diff --git a/client/src/js/components/bhEntitySelectMultiple/bhEntitySelectMultiple.tmpl.html b/client/src/js/components/bhEntitySelectMultiple/bhEntitySelectMultiple.tmpl.html new file mode 100644 index 0000000000..eb1376d548 --- /dev/null +++ b/client/src/js/components/bhEntitySelectMultiple/bhEntitySelectMultiple.tmpl.html @@ -0,0 +1,34 @@ +
+
+ + + + + + + + {{$item.display_name}} + + + +
+ +
+
+ +
+
+
+
+
diff --git a/client/src/js/services/CronEmailReportService.js b/client/src/js/services/CronEmailReportService.js new file mode 100644 index 0000000000..4b6c4fae4c --- /dev/null +++ b/client/src/js/services/CronEmailReportService.js @@ -0,0 +1,17 @@ +angular.module('bhima.services') + .service('CronEmailReportService', CronEmailReportService); + +CronEmailReportService.$inject = ['PrototypeApiService']; + +function CronEmailReportService(Api) { + const service = new Api('/cron_email_reports/'); + + service.send = send; + + function send(id) { + return service.$http.post(`/cron_email_reports/${id}`) + .then(service.util.unwrapHttpResponse); + } + + return service; +} diff --git a/client/src/js/services/CronService.js b/client/src/js/services/CronService.js new file mode 100644 index 0000000000..12f1508e80 --- /dev/null +++ b/client/src/js/services/CronService.js @@ -0,0 +1,9 @@ +angular.module('bhima.services') + .service('CronService', CronService); + +CronService.$inject = ['PrototypeApiService']; + +function CronService(Api) { + const service = new Api('/crons/'); + return service; +} diff --git a/client/src/modules/enterprises/enterprises.html b/client/src/modules/enterprises/enterprises.html index 07d4d3ef99..b07d4cd02c 100644 --- a/client/src/modules/enterprises/enterprises.html +++ b/client/src/modules/enterprises/enterprises.html @@ -265,6 +265,13 @@ help-text="ENTERPRISE.SETTINGS.ENABLE_AUTO_STOCK_ACCOUNTING_HELP_TEXT" on-change-callback="EnterpriseCtrl.enableAutoStockAccountingSetting(value)"> + + + diff --git a/client/src/modules/enterprises/enterprises.js b/client/src/modules/enterprises/enterprises.js index 7d01c7beca..38d6079242 100644 --- a/client/src/modules/enterprises/enterprises.js +++ b/client/src/modules/enterprises/enterprises.js @@ -221,6 +221,7 @@ function EnterpriseController(Enterprises, util, Notify, Projects, Modal, Scroll vm.enableBalanceOnInvoiceReceipSetting = proxy('enable_balance_on_invoice_receipt'); vm.enableBarcodesSetting = proxy('enable_barcodes'); vm.enableAutoStockAccountingSetting = proxy('enable_auto_stock_accounting'); + vm.enableAutoEmailReportSetting = proxy('enable_auto_email_report'); startup(); } diff --git a/client/src/modules/entities/entities.js b/client/src/modules/entities/entities.js index 7a30b8e6ea..14d358816b 100644 --- a/client/src/modules/entities/entities.js +++ b/client/src/modules/entities/entities.js @@ -105,7 +105,7 @@ function EntityController( Entity.delete(entity.uuid) .then(() => { - Notify.success('DEPOT.DELETED'); + Notify.success('FORM.INFO.DELETE_SUCCESS'); loadEntities(); }) .catch(Notify.handleError); diff --git a/client/src/modules/entity_group/entity_group.html b/client/src/modules/entity_group/entity_group.html new file mode 100644 index 0000000000..9324742f56 --- /dev/null +++ b/client/src/modules/entity_group/entity_group.html @@ -0,0 +1,40 @@ +
+
+
    +
  1. TREE.ADMIN
  2. +
  3. ENTITY.GROUP.TITLE
  4. +
+ +
+
+ +
+ +
+ + +
+
+
+
+ + +
+
+
+ + + +
+
+
diff --git a/client/src/modules/entity_group/entity_group.js b/client/src/modules/entity_group/entity_group.js new file mode 100644 index 0000000000..f98be3d770 --- /dev/null +++ b/client/src/modules/entity_group/entity_group.js @@ -0,0 +1,102 @@ +angular.module('bhima.controllers') + .controller('EntityGroupController', EntityGroupController); + +EntityGroupController.$inject = [ + 'EntityGroupService', 'ModalService', 'NotifyService', 'uiGridConstants', + '$state', +]; + +/** + * EntityGroup Controller + * + * This controller is responsible of handling entity group + */ +function EntityGroupController( + EntityGroup, ModalService, Notify, uiGridConstants, $state +) { + const vm = this; + + // bind methods + vm.deleteEntityGroup = deleteEntityGroup; + vm.editEntityGroup = editEntityGroup; + vm.toggleFilter = toggleFilter; + + // global variables + vm.gridApi = {}; + vm.filterEnabled = false; + + // options for the UI grid + vm.gridOptions = { + appScopeProvider : vm, + enableColumnMenus : false, + fastWatch : true, + flatEntityGroupAccess : true, + enableSorting : true, + onRegisterApi : onRegisterApiFn, + columnDefs : [ + { + field : 'label', + displayName : 'ENTITY.GROUP.GROUP', + headerCellFilter : 'translate', + }, + { + field : 'entities', + displayName : 'ENTITY.LABEL', + headerCellFilter : 'translate', + }, + { + field : 'action', + width : 80, + displayName : '', + cellTemplate : '/modules/entity_group/templates/action.tmpl.html', + enableSorting : false, + enableFiltering : false, + }, + ], + }; + + function onRegisterApiFn(gridApi) { + vm.gridApi = gridApi; + } + + function toggleFilter() { + vm.filterEnabled = !vm.filterEnabled; + vm.gridOptions.enableFiltering = vm.filterEnabled; + vm.gridApi.core.notifyDataChange(uiGridConstants.dataChange.COLUMN); + } + + function loadEntities() { + vm.loading = true; + + EntityGroup.read() + .then(data => { + vm.gridOptions.data = data; + }) + .catch(Notify.handleError) + .finally(() => { + vm.loading = false; + }); + } + + // switch to delete warning mode + function deleteEntityGroup(uuid) { + ModalService.confirm('FORM.DIALOGS.CONFIRM_DELETE') + .then(bool => { + if (!bool) { return; } + + EntityGroup.delete(uuid) + .then(() => { + Notify.success('FORM.INFO.DELETE_SUCCESS'); + loadEntities(); + }) + .catch(Notify.handleError); + }); + } + + // update an existing entity + function editEntityGroup(uuid) { + $state.go('entityGroup.edit', { uuid }); + } + + loadEntities(); +} diff --git a/client/src/modules/entity_group/entity_group.routes.js b/client/src/modules/entity_group/entity_group.routes.js new file mode 100644 index 0000000000..f9cfc20fea --- /dev/null +++ b/client/src/modules/entity_group/entity_group.routes.js @@ -0,0 +1,42 @@ +angular.module('bhima.routes') + .config(['$stateProvider', $stateProvider => { + $stateProvider + .state('entityGroup', { + url : '/entity_group', + controller : 'EntityGroupController as EntityGroupCtrl', + templateUrl : 'modules/entity_group/entity_group.html', + }) + + .state('entityGroup.create', { + url : '/create', + params : { + uuid : { value : null }, + creating : { value : true }, + }, + onEnter : ['$uibModal', entityGroupModal], + onExit : ['$uibModalStack', closeModal], + }) + + .state('entityGroup.edit', { + url : '/edit', + params : { + uuid : { value : null }, + creating : { value : false }, + }, + onEnter : ['$uibModal', entityGroupModal], + onExit : ['$uibModalStack', closeModal], + }); + }]); + +function entityGroupModal($modal) { + $modal.open({ + keyboard : false, + backdrop : 'static', + templateUrl : 'modules/entity_group/modals/entity_group.modal.html', + controller : 'EntityGroupModalController as EntityGroupModalCtrl', + }); +} + +function closeModal(ModalStack) { + ModalStack.dismissAll(); +} diff --git a/client/src/modules/entity_group/entity_group.service.js b/client/src/modules/entity_group/entity_group.service.js new file mode 100644 index 0000000000..c3311647c8 --- /dev/null +++ b/client/src/modules/entity_group/entity_group.service.js @@ -0,0 +1,17 @@ +angular.module('bhima.services') + .service('EntityGroupService', EntityGroupService); + +EntityGroupService.$inject = ['PrototypeApiService', 'util']; + +/** + * @class EntityGroupService + * @extends PrototypeApiService + * + * @description + * Encapsulates common requests to the /entities/groups/ URL. + */ +function EntityGroupService(Api) { + const baseUrl = '/entities/groups/'; + const service = new Api(baseUrl); + return service; +} diff --git a/client/src/modules/entity_group/modals/entity_group.modal.html b/client/src/modules/entity_group/modals/entity_group.modal.html new file mode 100644 index 0000000000..0dd601df93 --- /dev/null +++ b/client/src/modules/entity_group/modals/entity_group.modal.html @@ -0,0 +1,45 @@ +
+ + + + + +
diff --git a/client/src/modules/entity_group/modals/entity_group.modal.js b/client/src/modules/entity_group/modals/entity_group.modal.js new file mode 100644 index 0000000000..063756c072 --- /dev/null +++ b/client/src/modules/entity_group/modals/entity_group.modal.js @@ -0,0 +1,66 @@ +angular.module('bhima.controllers') + .controller('EntityGroupModalController', EntityGroupModalController); + +EntityGroupModalController.$inject = [ + '$state', 'EntityGroupService', 'NotifyService', +]; + +function EntityGroupModalController($state, EntityGroup, Notify) { + const vm = this; + const entityGroupUuid = $state.params.uuid || {}; + + vm.group = {}; + vm.isCreating = !!($state.params.creating); + + // exposed methods + vm.submit = submit; + + vm.onSelectEntities = entities => { + vm.group.entities = entities; + }; + + vm.clear = (key) => { + delete vm[key]; + }; + + function init() { + if (vm.isCreating) { return; } + + EntityGroup.read(entityGroupUuid) + .then(group => { + vm.group = group; + }) + .catch(Notify.handleError); + } + + // submit the data to the server from all two forms (update, create) + function submit(entityForm) { + if (entityForm.$invalid) { + return 0; + } + + if (entityForm.$pristine) { + cancel(); + return 0; + } + + const params = vm.group; + const promise = (vm.isCreating) + ? EntityGroup.create(params) + : EntityGroup.update(entityGroupUuid, params); + + return promise + .then(() => { + const translateKey = (vm.isCreating) ? 'ENTITY.CREATED' : 'ENTITY.UPDATED'; + Notify.success(translateKey); + $state.go('entityGroup', null, { reload : true }); + }) + .catch(Notify.handleError); + } + + function cancel() { + $state.go('entityGroup'); + } + + init(); +} diff --git a/client/src/modules/entity_group/templates/action.tmpl.html b/client/src/modules/entity_group/templates/action.tmpl.html new file mode 100644 index 0000000000..c082e13d59 --- /dev/null +++ b/client/src/modules/entity_group/templates/action.tmpl.html @@ -0,0 +1,25 @@ +
+ + FORM.BUTTONS.ACTIONS + + + + +
diff --git a/client/src/modules/fiscal/templates/modals/fiscal.closing.modal.html b/client/src/modules/fiscal/templates/modals/fiscal.closing.modal.html index 3aa2e940ae..f6d9a6f6e4 100644 --- a/client/src/modules/fiscal/templates/modals/fiscal.closing.modal.html +++ b/client/src/modules/fiscal/templates/modals/fiscal.closing.modal.html @@ -68,7 +68,10 @@

-

FISCAL.ACCOUNT_HAS_VALUE : {{ $ctrl.accountBalance.balance | currency : $ctrl.currency_id }}

+

+ FISCAL.ACCOUNT_HAS_VALUE : {{ $ctrl.accountBalance.balance | currency : $ctrl.currency_id }} + ({{ $ctrl.accountBalance.balance }}) +

diff --git a/client/src/modules/reports/generate/balance_report/balance_report.config.js b/client/src/modules/reports/generate/balance_report/balance_report.config.js index a11b973b14..38b2df1bf7 100644 --- a/client/src/modules/reports/generate/balance_report/balance_report.config.js +++ b/client/src/modules/reports/generate/balance_report/balance_report.config.js @@ -41,6 +41,10 @@ function BalanceReportConfigController($sce, Notify, SavedReports, AppCache, rep vm.reportDetails.includeClosingBalances = bool; }; + vm.onSelectCronReport = report => { + vm.reportDetails = angular.copy(report); + }; + vm.preview = function preview(form) { if (form.$invalid) { Notify.danger('FORM.ERRORS.RECORD_ERROR'); diff --git a/client/src/modules/reports/generate/balance_report/balance_report.html b/client/src/modules/reports/generate/balance_report/balance_report.html index e7d7270ebe..083653ef29 100644 --- a/client/src/modules/reports/generate/balance_report/balance_report.html +++ b/client/src/modules/reports/generate/balance_report/balance_report.html @@ -77,5 +77,14 @@

REPORT.BALANCE_REPORT.TITLE

+ +
+ + +
diff --git a/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.config.js b/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.config.js index ac26ae2aa4..4665144ea5 100644 --- a/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.config.js +++ b/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.config.js @@ -18,6 +18,10 @@ function OhadaBalanceSheetReportConfigController($sce, Notify, SavedReports, App vm.reportDetails.fiscal_id = fiscalYear.id; }; + vm.onSelectCronReport = report => { + vm.reportDetails = angular.copy(report); + }; + vm.clearPreview = function clearPreview() { vm.previewGenerated = false; vm.previewResult = null; diff --git a/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.html b/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.html index 6689724b18..80f42a533a 100644 --- a/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.html +++ b/client/src/modules/reports/generate/ohada_balance_sheet_report/ohada_balance_sheet_report.html @@ -35,5 +35,14 @@

REPORT.OHADA.BALANCE_SHEET

+ +
+ + +
diff --git a/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss.html b/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss.html index 29e5505bca..f80320cdf7 100644 --- a/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss.html +++ b/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss.html @@ -35,5 +35,14 @@

REPORT.OHADA.PROFIT_LOSS

+ +
+ + +
diff --git a/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss_report.config.js b/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss_report.config.js index 1145a2ee8b..5a17799f71 100644 --- a/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss_report.config.js +++ b/client/src/modules/reports/generate/ohada_profit_loss/ohada_profit_loss_report.config.js @@ -18,6 +18,10 @@ function OhadaProfitLossReportConfigController($sce, Notify, SavedReports, AppCa vm.reportDetails.fiscal_id = fiscalYear.id; }; + vm.onSelectCronReport = report => { + vm.reportDetails = angular.copy(report); + }; + vm.clearPreview = function clearPreview() { vm.previewGenerated = false; vm.previewResult = null; diff --git a/client/src/modules/reports/generate/operating/operating.config.js b/client/src/modules/reports/generate/operating/operating.config.js index 3ab161fa63..a80f03c2f9 100644 --- a/client/src/modules/reports/generate/operating/operating.config.js +++ b/client/src/modules/reports/generate/operating/operating.config.js @@ -30,8 +30,12 @@ function OperatingConfigController($sce, Notify, SavedReports, AppCache, reportD vm.reportDetails.periodTo = period.id; }; + vm.onSelectCronReport = report => { + vm.reportDetails = angular.copy(report); + }; + vm.preview = function preview(form) { - if (form.$invalid) { return; } + if (form.$invalid) { return null; } // update cached configuration cache.reportDetails = angular.copy(vm.reportDetails); diff --git a/client/src/modules/reports/generate/operating/operating.html b/client/src/modules/reports/generate/operating/operating.html index f784554bf8..c682a18d11 100644 --- a/client/src/modules/reports/generate/operating/operating.html +++ b/client/src/modules/reports/generate/operating/operating.html @@ -40,5 +40,14 @@

TREE.OPERATING_ACCOUNT

+ +
+ + +
diff --git a/package.json b/package.json index 16bd4e3da5..d92848802e 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "chai-spies": "^1.0.0", "chai-spies-next": "^0.9.3", "connect-redis": "^3.4.0", + "cron": "^1.7.1", "cropper": "^4.0.0", "cross-env": "^5.2.0", "csvtojson": "^2.0.8", diff --git a/server/config/routes.js b/server/config/routes.js index c8f6ea0d86..0b9583f857 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -38,6 +38,8 @@ const languages = require('../controllers/admin/languages'); const locations = require('../controllers/admin/locations'); const groups = require('../controllers/groups'); const entities = require('../controllers/admin/entities'); +const cron = require('../controllers/admin/cron'); +const cronEmailReport = require('../controllers/admin/cronEmailReport'); // payroll routes const payrollConfig = require('../controllers/payroll/configuration'); @@ -101,8 +103,6 @@ const transactions = require('../controllers/finance/transactions'); // looking up an entity by it reference const referenceLookup = require('../lib/referenceLookup'); -const operating = require('../controllers/finance/reports/operating/index'); - const department = require('../controllers/admin/department'); const tags = require('../controllers/admin/tags'); @@ -395,7 +395,6 @@ exports.configure = function configure(app) { app.get('/reports/finance/financialPatient/:uuid', financeReports.patient); app.get('/reports/finance/income_expense', financeReports.income_expense.document); app.get('/reports/finance/unpaid-invoice-payments', unpaidInvoicePayments.document); - app.get('/reports/finance/income_expense_by_month', financeReports.income_expense_by_month.document); app.get('/reports/finance/income_expense_by_year', financeReports.income_expense_by_year.document); app.get('/reports/finance/cash_report', financeReports.cashReport.document); @@ -415,6 +414,7 @@ exports.configure = function configure(app) { app.get('/reports/finance/employeeStanding/', financeReports.employee); app.get('/reports/finance/break_even', financeReports.breakEven.report); app.get('/reports/finance/break_even_fee_center', financeReports.breakEvenFeeCenter.report); + app.get('/reports/finance/operating', financeReports.operating.document); // visits reports app.get('/reports/visits', medicalReports.visitsReports.document); @@ -789,9 +789,6 @@ exports.configure = function configure(app) { app.post('/install', install.proceedInstall); app.get('/diagnoses', diagnoses.list); - - app.get('/reports/finance/operating', operating.document); - app.get('/roles', rolesCtrl.list); app.get('/roles/:uuid', rolesCtrl.detail); @@ -823,6 +820,13 @@ exports.configure = function configure(app) { app.delete('/entities/types/:id', entities.types.remove); app.post('/entities/types', entities.types.create); + // entities groups API + app.get('/entities/groups', entities.groups.list); + app.get('/entities/groups/:uuid', entities.groups.details); + app.put('/entities/groups/:uuid', entities.groups.update); + app.delete('/entities/groups/:uuid', entities.groups.remove); + app.post('/entities/groups/', entities.groups.create); + // entities API app.get('/entities', entities.list); app.get('/entities/:uuid', entities.details); @@ -920,4 +924,18 @@ exports.configure = function configure(app) { // API dashboard app.get('/indicators/dashboards', dashboard.getIndicators); app.get('/reports/indicatorsReport', indicatorRerpor.report); + + // API cron + app.get('/crons', cron.list); + app.get('/crons/:id', cron.details); + app.post('/crons', cron.create); + app.put('/crons/:id', cron.update); + app.delete('/crons/:id', cron.remove); + + // API cron_email_report + app.get('/cron_email_reports', cronEmailReport.list); + app.get('/cron_email_reports/:id', cronEmailReport.details); + app.post('/cron_email_reports', cronEmailReport.create); + app.post('/cron_email_reports/:id', cronEmailReport.send); + app.delete('/cron_email_reports/:id', cronEmailReport.remove); }; diff --git a/server/controllers/admin/cron/index.js b/server/controllers/admin/cron/index.js new file mode 100644 index 0000000000..0586fa04b8 --- /dev/null +++ b/server/controllers/admin/cron/index.js @@ -0,0 +1,69 @@ +/** + * HTTP END POINT + * API for the crons http end point + */ +const db = require('../../../lib/db'); + +exports.list = list; +exports.details = details; +exports.update = update; +exports.remove = remove; +exports.create = create; + +function list(req, res, next) { + const query = ` + SELECT id, label FROM cron + `; + db.exec(query) + .then(rows => res.status(200).json(rows)) + .catch(next) + .done(); +} + +function details(req, res, next) { + const query = ` + SELECT id, label FROM cron + WHERE id = ?; + `; + db.one(query, [req.params.id]) + .then(rows => res.status(200).json(rows)) + .catch(next) + .done(); +} + +function update(req, res, next) { + const query = ` + UPDATE cron SET ? WHERE id = ?; + `; + const params = req.body; + if (params.id) { + delete params.id; + } + db.exec(query, [params, req.params.id]) + .then(() => res.sendStatus(204)) + .catch(next) + .done(); +} + +function remove(req, res, next) { + const query = ` + DELETE FROM cron WHERE id = ?; + `; + db.exec(query, [req.params.id]) + .then(() => res.sendStatus(204)) + .catch(next) + .done(); +} + +function create(req, res, next) { + const query = ` + INSERT INTO cron SET ?; + `; + const params = req.body; + db.exec(query, [params]) + .then((result) => { + res.status(201).json({ id : result.insertId }); + }) + .catch(next) + .done(); +} diff --git a/server/controllers/admin/cronEmailReport/index.js b/server/controllers/admin/cronEmailReport/index.js new file mode 100644 index 0000000000..02fa491d5a --- /dev/null +++ b/server/controllers/admin/cronEmailReport/index.js @@ -0,0 +1,298 @@ +/** + * HTTP END POINT + * API controller for the table cron_email_report + */ +const debug = require('debug')('app'); +const Cron = require('cron').CronJob; + +const db = require('../../../lib/db'); +const BhMoment = require('../../../lib/bhMoment'); +const FilterParser = require('../../../lib/filter'); + +const mailer = require('../../../lib/mailer'); +const auth = require('../../auth'); +const dbReports = require('../../report.handlers'); + +const CURRENT_JOBS = []; + +function find(options = {}) { + const filters = new FilterParser(options, { tableAlias : 'cer' }); + const sql = ` + SELECT + cer.id, cer.entity_group_uuid, cer.cron_id, cer.report_id, + cer.params, cer.label, cer.last_send, + cer.next_send, cer.has_dynamic_dates, + eg.label AS entity_group_label, + c.label AS cron_label, c.value AS cron_value, + r.report_key, r.title_key + FROM cron_email_report cer + JOIN entity_group eg ON eg.uuid = cer.entity_group_uuid + JOIN cron c ON c.id = cer.cron_id + JOIN report r ON r.id = cer.report_id + `; + + filters.equals('id'); + filters.equals('report_id'); + const query = filters.applyQuery(sql); + const parameters = filters.parameters(); + return db.exec(query, parameters); +} + +function lookup(id) { + const query = ` + SELECT + cer.id, cer.entity_group_uuid, cer.cron_id, cer.report_id, + cer.params, cer.label, cer.last_send, + cer.next_send, cer.has_dynamic_dates, + eg.label AS entity_group_label, + c.label AS cron_label, c.value AS cron_value, + r.report_key, r.title_key + FROM cron_email_report cer + JOIN entity_group eg ON eg.uuid = cer.entity_group_uuid + JOIN cron c ON c.id = cer.cron_id + JOIN report r ON r.id = cer.report_id + WHERE cer.id = ?; + `; + return db.one(query, [id]); +} + +function list(req, res, next) { + find(req.query) + .then(rows => res.status(200).json(rows)) + .catch(next) + .done(); +} + +function details(req, res, next) { + lookup(req.params.id) + .then((data) => res.status(200).json(data)) + .catch(next) + .done(); +} + +function remove(req, res, next) { + const query = ` + DELETE FROM cron_email_report WHERE id = ?; + `; + db.exec(query, [req.params.id]) + .then(() => { + const [jobToStop] = CURRENT_JOBS.filter(item => { + return item.id === parseInt(req.params.id, 10); + }); + + if (jobToStop) { + jobToStop.job.stop(); + debug(`The job for "${jobToStop.label}" is stopped`); + } + }) + .then(() => res.sendStatus(204)) + .catch(next) + .done(); +} + +async function create(req, res, next) { + try { + const query = 'INSERT INTO cron_email_report SET ?;'; + const { cron, reportOptions } = req.body; + + db.convert(cron, ['entity_group_uuid']); + cron.params = JSON.stringify(reportOptions); + + const result = await db.exec(query, [cron]); + const created = await lookup(result.insertId); + await createEmailReportJob(created, sendEmailReportDocument, created); + + res.status(201).json({ id : result.insertId }); + } catch (error) { + next(error); + } +} + +function send(req, res, next) { + const { id } = req.params; + lookup(id) + .then(record => sendEmailReportDocument(record)) + .then(() => res.sendStatus(201)) + .catch(next); +} + +/** + * @function addJob + * @description add a cron job to run each time for the given pattern + * @param {string} frequency Cron job pattern * * * * * + * @param {function} cb the function to run + * @param {any} params params of the function + */ +function addJob(frequency, cb, ...params) { + const cj = new Cron(frequency, () => cb(...params)); + cj.start(); + return cj; +} + +/** + * @method launchCronEmailReportJobs + * @description at the startup, read all cron email reports + * in the database and create jobs for them + */ +async function launchCronEmailReportJobs() { + try { + const session = await loadSession(); + if (!session.enterprise.settings.enable_auto_email_report) { return; } + + const records = await find(); + if (!records.length) { return; } + + const jobs = records.map(record => createEmailReportJob(record, sendEmailReportDocument, record)); + await Promise.all(jobs); + + debug('Reports scanned successfully'); + } catch (error) { + // NEED TO BE HANDLED FOR AVOIDING THE CRASH OF THE APPLICATION + throw error; + } +} + +/** + * @function createEmailReportJob + * @param {object} record A row of cron email report + * @param {*} cb The function to run + */ +function createEmailReportJob(record, cb, ...params) { + const job = addJob(record.cron_value, cb, ...params); + CURRENT_JOBS.push({ id : record.id, label : record.label, job }); + return updateCronEmailReportNextSend(record.id, job); +} + +/** + * @function sendEmailReportDocument + * @param {object} record A row of cron email report + * @param {object} options The report options + */ +async function sendEmailReportDocument(record) { + try { + const reportOptions = JSON.parse(record.params); + // dynamic dates in the report params if needed + const options = addDynamicDatesOptions(record.cron_id, record.has_dynamic_dates, reportOptions); + const fn = dbReports[record.report_key]; + const contacts = await loadContacts(record.entity_group_uuid); + const session = await loadSession(); + const document = await fn(options, session); + const filename = replaceSlash(document.headers.filename); + + if (contacts.length) { + const attachments = [ + { filename, stream : document.report }, + ]; + const content = ` + Hi, + + We have attached to this email the ${filename} file + + Thank you, + `; + const mails = contacts.map(c => { + return mailer.email(c, record.label, content, { + attachments, + }); + }); + + await Promise.all(mails); + await updateCronEmailReportLastSend(record.id); + debug(`(${record.label}) report sent by email to ${contacts.length} contacts`); + } + } catch (e) { + throw e; + } +} + +function replaceSlash(name = '', value = '_') { + const regex = /\//gi; + return name.replace(regex, value); +} + +function loadContacts(entityGroupUuid) { + const query = ` + SELECT e.email FROM entity e + JOIN entity_group_entity ege ON ege.entity_uuid = e.uuid + JOIN entity_group eg ON eg.uuid = ege.entity_group_uuid + WHERE eg.uuid = ?; + `; + return db.exec(query, [entityGroupUuid]) + .then(contacts => contacts.map(c => c.email)); +} + +function loadSession() { + const query = ` + SELECT + user.id, user.username, user.display_name, user.email, user.deactivated, + project.enterprise_id , project.id AS project_id + FROM user + JOIN project_permission + JOIN project ON user.id = project_permission.user_id + AND project.id = project_permission.project_id + LIMIT 1`; + + return db.one(query) + .then(user => auth.loadSessionInformation(user)); +} + +function addDynamicDatesOptions(cronId, hasDynamicDates, options) { + // cron ids + const DAILY = 1; + const WEEKLY = 2; + const MONTHLY = 3; + const YEARLY = 4; + + const period = new BhMoment(new Date()); + + if (hasDynamicDates) { + if (cronId === DAILY) { + options.dateFrom = period.day().dateFrom; + options.dateTo = period.day().dateTo; + } + + if (cronId === WEEKLY) { + options.dateFrom = period.week().dateFrom; + options.dateTo = period.week().dateTo; + } + + if (cronId === MONTHLY) { + options.dateFrom = period.month().dateFrom; + options.dateTo = period.month().dateTo; + } + + if (cronId === YEARLY) { + options.dateFrom = period.year().dateFrom; + options.dateTo = period.year().dateTo; + } + } + return options; +} + +function updateCronEmailReportNextSend(id, job) { + const sql = ` + UPDATE cron_email_report SET ? WHERE id = ?; + `; + const params = { + next_send : job.nextDate() ? job.nextDate().toDate() : null, + }; + return db.exec(sql, [params, id]); +} + +function updateCronEmailReportLastSend(id) { + const sql = ` + UPDATE cron_email_report SET ? WHERE id = ?; + `; + const params = { + last_send : new Date(), + }; + return db.exec(sql, [params, id]); +} + +launchCronEmailReportJobs(); + +exports.list = list; +exports.details = details; +exports.remove = remove; +exports.create = create; +exports.send = send; diff --git a/server/controllers/admin/enterprises.js b/server/controllers/admin/enterprises.js index 5746b46c05..5bb8fcb5c1 100644 --- a/server/controllers/admin/enterprises.js +++ b/server/controllers/admin/enterprises.js @@ -25,7 +25,7 @@ exports.list = function list(req, res, next) { BUID(location_id) AS location_id, logo, currency_id, gain_account_id, loss_account_id, enable_price_lock, enable_prepayments, enable_delete_records, enable_password_validation, enable_balance_on_invoice_receipt, - enable_barcodes, enable_auto_stock_accounting + enable_barcodes, enable_auto_stock_accounting, enable_auto_email_report FROM enterprise LEFT JOIN enterprise_setting ON enterprise.id = enterprise_setting.enterprise_id ;`; @@ -48,6 +48,7 @@ exports.list = function list(req, res, next) { 'enable_balance_on_invoice_receipt', 'enable_barcodes', 'enable_auto_stock_accounting', + 'enable_auto_email_report', ]; row.settings = _.pick(row, settings); diff --git a/server/controllers/admin/entities/groups/index.js b/server/controllers/admin/entities/groups/index.js new file mode 100644 index 0000000000..2bd191903a --- /dev/null +++ b/server/controllers/admin/entities/groups/index.js @@ -0,0 +1,135 @@ +/** + * HTTP END POINT + * API for the entities/groups http end point + */ +const _ = require('lodash'); + +const db = require('../../../../lib/db'); +const util = require('../../../../lib/util'); + +exports.list = list; +exports.details = details; +exports.update = update; +exports.remove = remove; +exports.create = create; + +function list(req, res, next) { + const query = ` + SELECT BUID(eg.uuid) AS uuid, eg.label, GROUP_CONCAT(e.display_name, '') AS entities FROM entity_group_entity ege + JOIN entity_group eg ON eg.uuid = ege.entity_group_uuid + JOIN entity e ON e.uuid = ege.entity_uuid + GROUP BY eg.uuid; + `; + db.exec(query) + .then(rows => res.status(200).json(rows)) + .catch(next) + .done(); +} + +function details(req, res, next) { + const bundle = {}; + const uuid = db.bid(req.params.uuid); + const query = ` + SELECT BUID(uuid) AS uuid, label FROM entity_group + WHERE uuid = ?; + `; + db.one(query, [uuid]) + .then(rows => { + _.extend(bundle, rows); + const queryEntities = ` + SELECT + BUID(ege.entity_uuid) AS uuid, e.display_name + FROM entity_group_entity ege + JOIN entity e ON e.uuid = ege.entity_uuid + WHERE ege.entity_group_uuid = ?; + `; + return db.exec(queryEntities, [uuid]); + }) + .then(rows => { + bundle.entities = rows; + res.status(200).json(bundle); + }) + .catch(next) + .done(); +} + +function update(req, res, next) { + const { entities } = req.body; + const { uuid } = req.params; + const entityGroupUuid = db.bid(uuid); + + delete req.body.uuid; + delete req.body.entities; + + const transaction = db.transaction(); + transaction.addQuery( + 'DELETE FROM entity_group_entity WHERE entity_group_uuid = ?;', + [entityGroupUuid] + ); + transaction.addQuery( + 'UPDATE entity_group SET ? WHERE uuid = ?;', + [req.body, entityGroupUuid] + ); + entities.forEach(entityUuid => { + const value = { + entity_uuid : db.bid(entityUuid), + entity_group_uuid : entityGroupUuid, + }; + transaction.addQuery( + 'INSERT INTO entity_group_entity SET ?;', + [value] + ); + }); + + transaction.execute() + .then(() => res.sendStatus(204)) + .catch(next) + .done(); +} + +function remove(req, res, next) { + const queryEntityGroup = ` + DELETE FROM entity_group WHERE uuid = ?; + `; + const queryDropEntities = ` + DELETE FROM entity_group_entity WHERE entity_group_uuid = ?; + `; + + const transaction = db.transaction(); + transaction.addQuery(queryDropEntities, [db.bid(req.params.uuid)]); + transaction.addQuery(queryEntityGroup, [db.bid(req.params.uuid)]); + transaction.execute() + .then(() => res.sendStatus(204)) + .catch(next) + .done(); +} + +function create(req, res, next) { + const { entities } = req.body; + delete req.body.entities; + + const params = { + uuid : db.bid(util.uuid()), + label : req.body.label, + }; + + db.exec('INSERT INTO entity_group SET ?;', [params]) + .then(() => { + const transaction = db.transaction(); + + entities.forEach(entityUuid => { + const value = { + entity_uuid : db.bid(entityUuid), + entity_group_uuid : params.uuid, + }; + transaction.addQuery('INSERT INTO entity_group_entity SET ?;', [value]); + }); + + return transaction.execute(); + }) + .then(() => { + res.status(201).json({ uuid : params.uuid }); + }) + .catch(next) + .done(); +} diff --git a/server/controllers/admin/entities/index.js b/server/controllers/admin/entities/index.js index a043abae03..6337a81b6f 100644 --- a/server/controllers/admin/entities/index.js +++ b/server/controllers/admin/entities/index.js @@ -5,8 +5,10 @@ const util = require('../../../lib/util'); const db = require('../../../lib/db'); const types = require('./types'); +const groups = require('./groups'); exports.types = types; +exports.groups = groups; exports.list = list; exports.details = details; exports.update = update; diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 273c6650cf..ee41cf0539 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -25,6 +25,9 @@ exports.logout = logout; // POST /auth/reload exports.reload = reload; +// expose session locally +exports.loadSessionInformation = loadSessionInformation; + function loginRoute(req, res, next) { const { username, password, project } = req.body; diff --git a/server/controllers/finance/reports/balance/index.js b/server/controllers/finance/reports/balance/index.js index 4134b10123..7517cace0a 100644 --- a/server/controllers/finance/reports/balance/index.js +++ b/server/controllers/finance/reports/balance/index.js @@ -21,6 +21,7 @@ const TEMPLATE = './server/controllers/finance/reports/balance/report.handlebars // expose to the API exports.document = document; +exports.reporting = reporting; // default report parameters const DEFAULT_PARAMS = { @@ -32,6 +33,48 @@ const DEFAULT_PARAMS = { const TITLE_ID = 6; +/** + * @description this function helps to get html document of the report in server side + * so that we can use it with others modules on the server side + * @param {*} options the report options + * @param {*} session the session + */ +async function reporting(options, session) { + try { + const params = options; + const context = {}; + + _.defaults(params, DEFAULT_PARAMS); + + context.useSeparateDebitsAndCredits = Number.parseInt(params.useSeparateDebitsAndCredits, 10); + context.shouldPruneEmptyRows = Number.parseInt(params.shouldPruneEmptyRows, 10); + context.shouldHideTitleAccounts = Number.parseInt(params.shouldHideTitleAccounts, 10); + context.includeClosingBalances = Number.parseInt(params.includeClosingBalances, 10); + + const report = new ReportManager(TEMPLATE, session, params); + const currencyId = session.enterprise.currency_id; + + const period = await getPeriodFromParams(params.fiscal_id, params.period_id, context.includeClosingBalances); + _.merge(context, { period }); + + const balance = await getBalanceForFiscalYear(period, currencyId); + context.accounts = balance.accounts; + context.totals = balance.totals; + + const tree = await computeBalanceTree(balance.accounts, balance.totals, context, context.shouldPruneEmptyRows); + context.accounts = tree.accounts; + context.totals = tree.totals; + + if (context.shouldHideTitleAccounts) { + context.accounts = context.accounts.filter(account => account.isTitleAccount === 0); + } + + return report.render(context); + } catch (error) { + throw error; + } +} + /** * @function document * @@ -44,44 +87,7 @@ const TITLE_ID = 6; * NOTE(@jniles): This file corresponds to the "Balance Report" on the client. */ function document(req, res, next) { - const params = req.query; - const context = {}; - let report; - - _.defaults(params, DEFAULT_PARAMS); - - context.useSeparateDebitsAndCredits = Number.parseInt(params.useSeparateDebitsAndCredits, 10); - context.shouldPruneEmptyRows = Number.parseInt(params.shouldPruneEmptyRows, 10); - context.shouldHideTitleAccounts = Number.parseInt(params.shouldHideTitleAccounts, 10); - context.includeClosingBalances = Number.parseInt(params.includeClosingBalances, 10); - - try { - report = new ReportManager(TEMPLATE, req.session, params); - } catch (e) { - next(e); - return; - } - - const currencyId = req.session.enterprise.currency_id; - - getPeriodFromParams(params.fiscal_id, params.period_id, context.includeClosingBalances) - .then(period => { - _.merge(context, { period }); - return getBalanceForFiscalYear(period, currencyId); - }) - .then(({ accounts, totals }) => { - _.merge(context, { accounts, totals }); - return computeBalanceTree(accounts, totals, context, context.shouldPruneEmptyRows); - }) - .then(({ accounts, totals }) => { - _.merge(context, { accounts, totals }); - - if (context.shouldHideTitleAccounts) { - context.accounts = accounts.filter(account => account.isTitleAccount === 0); - } - - return report.render(context); - }) + reporting(req.query, req.session) .then(result => { res.set(result.headers).send(result.report); }) diff --git a/server/controllers/finance/reports/index.js b/server/controllers/finance/reports/index.js index 1ce74528a6..066108f1fe 100644 --- a/server/controllers/finance/reports/index.js +++ b/server/controllers/finance/reports/index.js @@ -34,3 +34,4 @@ exports.feeCenter = require('./fee_center'); exports.annualClientsReport = require('./debtors/annual-clients-report').annualClientsReport; exports.breakEven = require('./break_even'); exports.breakEvenFeeCenter = require('./break_even_fee_center'); +exports.operating = require('./operating'); diff --git a/server/controllers/finance/reports/ohada_balance_sheet/index.js b/server/controllers/finance/reports/ohada_balance_sheet/index.js index 0826bd81c1..29c63e34c7 100644 --- a/server/controllers/finance/reports/ohada_balance_sheet/index.js +++ b/server/controllers/finance/reports/ohada_balance_sheet/index.js @@ -36,26 +36,29 @@ const balanceSheetLiabilityTable = balanceSheetElement.balanceSheetLiabilityTabl // expose to the API exports.document = document; +exports.reporting = reporting; exports.aggregateReferences = aggregateReferences; + /** - * @function document - * @description process and render the balance report document + * @description this function helps to get html document of the report in server side + * so that we can use it with others modules on the server side + * @param {object} options the report options + * @param {object} session the session */ -function document(req, res, next) { - const params = req.query; +function reporting(options, session) { + const params = options; const context = {}; let report; _.defaults(params, DEFAULT_PARAMS); try { - report = new ReportManager(TEMPLATE, req.session, params); + report = new ReportManager(TEMPLATE, session, params); } catch (e) { - next(e); - return; + throw e; } - balanceSheetElement.getFiscalYearDetails(params.fiscal_id) + return balanceSheetElement.getFiscalYearDetails(params.fiscal_id) .then(fiscalYear => { _.merge(context, { fiscalYear }); const currentPeriodReferences = AccountReference.computeAllAccountReference(fiscalYear.current.period_id); @@ -254,7 +257,15 @@ function document(req, res, next) { _.merge(context, { assetTable, liabilityTable }); return report.render(context); - }) + }); +} + +/** + * @function document + * @description process and render the balance report document + */ +function document(req, res, next) { + reporting(req.query, req.session) .then(result => { res.set(result.header).send(result.report); }) diff --git a/server/controllers/finance/reports/ohada_profit_loss/index.js b/server/controllers/finance/reports/ohada_profit_loss/index.js index 95bd6144df..fd693097a2 100644 --- a/server/controllers/finance/reports/ohada_profit_loss/index.js +++ b/server/controllers/finance/reports/ohada_profit_loss/index.js @@ -17,6 +17,11 @@ const _ = require('lodash'); const db = require('../../../../lib/db'); const AccountReference = require('../../accounts/references'); const ReportManager = require('../../../../lib/ReportManager'); + +// expose to the API +exports.document = document; +exports.reporting = reporting; + // report template const TEMPLATE = './server/controllers/finance/reports/ohada_profit_loss/report.handlebars'; @@ -163,28 +168,27 @@ const mapTable = {}; profitLossTable.forEach(item => { mapTable[item.ref] = item.sign; }); -// expose to the API -exports.document = document; /** - * @function document - * @description process and render the balance report document + * @description this function helps to get html document of the report in server side + * so that we can use it with others modules on the server side + * @param {*} options the report options + * @param {*} session the session */ -function document(req, res, next) { - const params = req.query; +function reporting(options, session) { + const params = options; const context = {}; let report; _.defaults(params, DEFAULT_PARAMS); try { - report = new ReportManager(TEMPLATE, req.session, params); + report = new ReportManager(TEMPLATE, session, params); } catch (e) { - next(e); - return; + throw e; } - getFiscalYearDetails(params.fiscal_id) + return getFiscalYearDetails(params.fiscal_id) .then(fiscalYear => { _.merge(context, { fiscalYear }); @@ -297,7 +301,15 @@ function document(req, res, next) { _.merge(context, { assetTable }, { totals }); return report.render(context); - }) + }); +} + +/** + * @function document + * @description process and render the balance report document + */ +function document(req, res, next) { + reporting(req.query, req.session) .then(result => { res.set(result.header).send(result.report); }) diff --git a/server/controllers/finance/reports/operating/index.js b/server/controllers/finance/reports/operating/index.js index f59cd415fa..3037c6b92e 100644 --- a/server/controllers/finance/reports/operating/index.js +++ b/server/controllers/finance/reports/operating/index.js @@ -16,26 +16,31 @@ const TEMPLATE = './server/controllers/finance/reports/operating/report.handleba exports.document = document; exports.formatData = formatData; +exports.reporting = reporting; const EXPENSE_ACCOUNT_TYPE = 5; const INCOME_ACCOUNT_TYPE = 4; const DECIMAL_PRECISION = 2; // ex: 12.4567 => 12.46 - -function document(req, res, next) { - const params = req.query; +/** + * @description this function helps to get html document of the report in server side + * so that we can use it with others modules on the server side + * @param {*} options the report options + * @param {*} session the session + */ +function reporting(opts, session) { + const params = opts; let docReport; - const options = _.extend(req.query, { + const options = _.extend(opts, { filename : 'TREE.OPERATING_ACCOUNT', csvKey : 'rows', - user : req.session.user, + user : session.user, }); try { - docReport = new ReportManager(TEMPLATE, req.session, options); + docReport = new ReportManager(TEMPLATE, session, options); } catch (e) { - next(e); - return; + throw e; } let queries; @@ -43,8 +48,8 @@ function document(req, res, next) { let lastRateUsed; let firstCurrency; let secondCurrency; - const enterpriseId = req.session.enterprise.id; - const enterpriseCurrencyId = req.session.enterprise.currency_id; + const enterpriseId = session.enterprise.id; + const enterpriseCurrencyId = session.enterprise.currency_id; const getQueryIncome = fiscal.getAccountBalancesByTypeId; const periods = { @@ -52,7 +57,7 @@ function document(req, res, next) { periodTo : params.periodTo, }; - fiscal.getDateRangeFromPeriods(periods).then(dateRange => { + return fiscal.getDateRangeFromPeriods(periods).then(dateRange => { range = dateRange; return Exchange.getExchangeRate(enterpriseId, params.currency_id, range.dateTo); }).then(exchangeRate => { @@ -125,7 +130,11 @@ function document(req, res, next) { context.total = diff; return docReport.render(context); - }) + }); +} + +function document(req, res, next) { + reporting(req.query, req.session) .then((result) => { res.set(result.headers).send(result.report); }) diff --git a/server/controllers/report.handlers.js b/server/controllers/report.handlers.js new file mode 100644 index 0000000000..718d24721c --- /dev/null +++ b/server/controllers/report.handlers.js @@ -0,0 +1,8 @@ +const financeReports = require('./finance/reports'); + +module.exports = { + balance_report : financeReports.balance.reporting, + ohada_balance_sheet_report : financeReports.ohadaBalanceSheet.reporting, + ohada_profit_loss : financeReports.ohadaProfitLoss.reporting, + operating : financeReports.operating.reporting, +}; diff --git a/server/lib/bhMoment.js b/server/lib/bhMoment.js new file mode 100644 index 0000000000..9832d7c9e6 --- /dev/null +++ b/server/lib/bhMoment.js @@ -0,0 +1,40 @@ +/** + * this class helps to get start date and end date of a period + */ +const moment = require('moment'); + +class BhMoment { + constructor(date) { + this.value = moment(date); + } + + day() { + return { + dateFrom : moment(this.value).startOf('day'), + dateTo : moment(this.value).endOf('day'), + }; + } + + week() { + return { + dateFrom : moment(this.value).startOf('week'), + dateTo : moment(this.value).endOf('week'), + }; + } + + month() { + return { + dateFrom : moment(this.value).startOf('month'), + dateTo : moment(this.value).endOf('month'), + }; + } + + year() { + return { + dateFrom : moment(this.value).startOf('year'), + dateTo : moment(this.value).endOf('year'), + }; + } +} + +module.exports = BhMoment; diff --git a/server/lib/mailer.js b/server/lib/mailer.js index 18147ce81d..4619229d20 100644 --- a/server/lib/mailer.js +++ b/server/lib/mailer.js @@ -37,15 +37,19 @@ function processAttachments(attachments = []) { // default to the name of the file if the name has not been specified attach.filename = attach.filename || path.parse(attach.path).base; - debug(`#processAttachments() loading ${attach.path}`); + if (!attach.stream) { + debug(`#processAttachments() loading ${attach.path}`); + + // asynchronously load the file and add it as as an attachment + return fs.readFile(attach.path) + .then(file => new mailgun.Attachment({ filename : attach.filename, data : file })); + } + + debug(`#processAttachments() attach stream ${attach.filename}`); // asynchronously load the file and add it as as an attachment - return fs.readFile(attach.path) - .then(file => - new mailgun.Attachment({ - filename : attach.filename, - data : file, - })); + return new mailgun.Attachment({ filename : attach.filename, data : attach.stream }); + })); } diff --git a/server/models/bhima.sql b/server/models/bhima.sql index f900cbb7d6..afecfac422 100644 --- a/server/models/bhima.sql +++ b/server/models/bhima.sql @@ -98,7 +98,6 @@ INSERT INTO unit VALUES (209, 'Accounts Report Multiple','TREE.REPORTS_MULTIPLE_ACCOUNTS','',144,'/modules/reports/account_report_multiple','/reports/account_report_multiple'), (210, 'Unbalanced Invoice Payments','REPORT.UNPAID_INVOICE_PAYMENTS_REPORT.TITLE','',144,'/modules/reports/unpaid-invoice-payments','/reports/unpaid-invoice-payments'), (211, 'Income Expenses by Month', 'REPORT.PROFIT_AND_LOSS_BY_MONTH', 'The Report of income and expenses', 144, '/modules/finance/income_expense_by_month', '/reports/income_expense_by_month'), - (212, 'Entity Management','ENTITY.MANAGEMENT','',1,'/modules/entities','/entities'), (213, 'Stock value Report','TREE.STOCK_VALUE','',144,'/modules/reports/stock_value','/reports/stock_value'), (214, '[OHADA] Compte de resultat','TREE.OHADA_RESULT_ACCOUNT','',144,'/modules/reports/ohada_profit_loss','/reports/ohada_profit_loss'), (215, 'Department management','TREE.DEPARTMENT_MANAGEMENT','Department Management', 1,'/modules/department/','/departments'), @@ -125,7 +124,10 @@ INSERT INTO unit VALUES (237, 'Finances dashboard', 'TREE.DASHBOARDS.FINANCES', 'Tableau de bord des finances', 233, '/modules/dashboards/finances/', '/dashboards/finances'), (238, 'Indicators report', 'TREE.INDICATORS_REPORT', 'Rapport sur les indicateurs', 144,'/modules/reports/indicatorsReport', '/reports/indicatorsReport'), (239, 'Visits Report', 'TREE.VISITS_REPORT', 'Visits registry', 144, '/modules/reports/visit_report', '/reports/visit_report'), - (240, '[Stock] Stock Entry Report','TREE.STOCK_ENTRY_REPORT','Stock Entry Report', 144,'/modules/reports/generated/stock_entry','/reports/stock_entry'); + (240, '[Stock] Stock Entry Report','TREE.STOCK_ENTRY_REPORT','Stock Entry Report', 144,'/modules/reports/generated/stock_entry','/reports/stock_entry'), + (241, 'Entity Folder', 'ENTITY.MANAGEMENT', 'Entity Folder', 0, '/modules/entities', '/ENTITY_FOLDER'), + (242, 'Entity Management','ENTITY.MANAGEMENT','',241,'/modules/entities','/entities'), + (243, 'Entity Group', 'ENTITY.GROUP.TITLE', 'Entity Group', 241, '/modules/entity_group', '/entity_group'); -- Reserved system account type INSERT INTO `account_category` VALUES @@ -333,3 +335,11 @@ INSERT INTO `indicator_type`(`id`, `text`,`translate_key`)VALUES (1, 'hospitalization', 'DASHBOARD.HOSPITALIZATION'), (2, 'staff', 'DASHBOARD.STAFF'), (3, 'fianance', 'DASHBOARD.FINANCE'); + +-- cron +INSERT INTO `cron` (`label`, `value`) VALUES + ('CRON.DAILY', '0 1 * * *'), + ('CRON.WEEKLY', '0 1 * * 0'), + ('CRON.MONTHLY', '0 1 30 * *'), + ('CRON.YEARLY', '0 1 31 12 *'), + ('CRON.EACH_MINUTE', '* * * * *'); \ No newline at end of file diff --git a/server/models/migrations/next/migrations.sql b/server/models/migrations/next/migrations.sql index 4de7aa9ef9..fe8c23e45b 100644 --- a/server/models/migrations/next/migrations.sql +++ b/server/models/migrations/next/migrations.sql @@ -210,6 +210,83 @@ ALTER TABLE `service` ADD COLUMN project_id SMALLINT(5) UNSIGNED NOT NULL; */ UPDATE unit SET path="/depots" WHERE `name`="Depot Management" AND `key`="DEPOT.TITLE"; +/* + * @date: 2019-06-14 + * description: entity and entity groups units + */ +INSERT INTO `unit` VALUES + (240, 'Entity Folder', 'ENTITY.MANAGEMENT', 'Entity Folder', 0, '/modules/entities', '/ENTITY_FOLDER'), + (241, 'Entity Management','ENTITY.MANAGEMENT','',240,'/modules/entities','/entities'), + (242, 'Entity Group', 'ENTITY.GROUP.TITLE', 'Entity Group', 240, '/modules/entity_group', '/entity_group'); + +/* + * @author: mbayopanda + * @date: 2019-06-14 + * @description: entity group +*/ +DROP TABLE IF EXISTS `entity_group`; +CREATE TABLE `entity_group` ( + `uuid` BINARY(16) NOT NULL, + `label` VARCHAR(190) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`uuid`), + UNIQUE KEY `label` (`label`) +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `entity_group_entity`; +CREATE TABLE `entity_group_entity` ( + `id` SMALLINT(5) NOT NULL AUTO_INCREMENT, + `entity_uuid` BINARY(16) NOT NULL, + `entity_group_uuid` BINARY(16) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +/* + * @author: mbayopanda + * @date: 2019-06-10 + * @description: cron emailing tables + */ +DROP TABLE IF EXISTS `cron`; +CREATE TABLE `cron` ( + `id` SMALLINT(5) NOT NULL AUTO_INCREMENT, + `label` VARCHAR(150) NOT NULL, + `value` VARCHAR(20) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `cron_email_report`; +CREATE TABLE `cron_email_report` ( + `id` SMALLINT(5) NOT NULL AUTO_INCREMENT, + `entity_group_uuid` BINARY(16) NOT NULL, + `cron_id` SMALLINT(5) NOT NULL, + `report_id` SMALLINT(5) NOT NULL, + `report_url` VARCHAR(200) NOT NULL, + `params` TEXT NULL, + `label` VARCHAR(200) NOT NULL, + `last_send` DATETIME NULL, + `next_send` DATETIME NULL, + `has_dynamic_dates` TINYINT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `label` (`label`, `report_id`), + KEY `entity_group_uuid` (`entity_group_uuid`), + FOREIGN KEY (`entity_group_uuid`) REFERENCES `entity_group` (`uuid`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +-- cron +INSERT INTO `cron` (`label`, `value`) VALUES + ('CRON.DAILY', '0 1 * * *'), + ('CRON.WEEKLY', '0 1 * * 0'), + ('CRON.MONTHLY', '0 1 30 * *'), + ('CRON.YEARLY', '0 1 31 12 *'), + ('CRON.EVERY_MINUTE', '* * * * *'); + +/* + * @author: mbayopanda + * @date: 2019-06-13 + * @description: enable enterprise settings for auto email report + */ +ALTER TABLE `enterprise_setting` ADD COLUMN `enable_auto_email_report` TINYINT(1) NOT NULL DEFAULT 0; /* * @author: jeremielodi @@ -250,3 +327,14 @@ DELIMITER ; -- update columns call UpdatePeriodLabels(); + +/* + @author: mbayopanda + @date: 2019-05-13 + @description: stock entries report +*/ +INSERT INTO unit VALUES + (240, '[Stock] Stock Entry Report','TREE.STOCK_ENTRY_REPORT','Stock Entry Report', 144,'/modules/reports/generated/stock_entry','/reports/stock_entry'); + +INSERT INTO `report` (`id`, `report_key`, `title_key`) VALUES + (33, 'stock_entry', 'REPORT.STOCK.ENTRY_REPORT'); diff --git a/server/models/migrations/v0.8.0-v1.0.0/migrate.sql b/server/models/migrations/v0.8.0-v1.0.0/migrate.sql index 574e7d5b72..855977b208 100644 --- a/server/models/migrations/v0.8.0-v1.0.0/migrate.sql +++ b/server/models/migrations/v0.8.0-v1.0.0/migrate.sql @@ -796,6 +796,7 @@ BEGIN -- lock the fiscal year and associated periods UPDATE fiscal_year SET locked = 1 WHERE id = fiscalYearId; UPDATE period SET locked = 1 WHERE fiscal_year_id = fiscalYearId; +END $$ DELIMITER $$ /* diff --git a/server/models/migrations/v1.1.0-v1.1.1/migrate.sql b/server/models/migrations/v1.1.0-v1.1.1/migrate.sql index a1ff80202f..518b269554 100644 --- a/server/models/migrations/v1.1.0-v1.1.1/migrate.sql +++ b/server/models/migrations/v1.1.0-v1.1.1/migrate.sql @@ -143,13 +143,3 @@ INSERT INTO unit VALUES INSERT INTO `report` (`id`, `report_key`, `title_key`) VALUES (32, 'visit_report', 'PATIENT_RECORDS.REPORT.VISITS'); -/* - @author: mbayopanda - @date: 2019-05-13 - @description: stock entries report -*/ -INSERT INTO unit VALUES - (240, '[Stock] Stock Entry Report','TREE.STOCK_ENTRY_REPORT','Stock Entry Report', 144,'/modules/reports/generated/stock_entry','/reports/stock_entry'); - -INSERT INTO `report` (`id`, `report_key`, `title_key`) VALUES - (33, 'stock_entry', 'REPORT.STOCK.ENTRY_REPORT'); diff --git a/server/models/procedures/time_period.sql b/server/models/procedures/time_period.sql index e053308a3a..a1157e8a2d 100644 --- a/server/models/procedures/time_period.sql +++ b/server/models/procedures/time_period.sql @@ -6,6 +6,7 @@ This procedure help to create fiscal year and fiscal year's periods periods include period `0` and period `13` */ +DROP PROCEDURE IF EXISTS CreateFiscalYear$$ CREATE PROCEDURE CreateFiscalYear( IN p_enterprise_id SMALLINT(5), IN p_previous_fiscal_year_id MEDIUMINT(8), @@ -30,6 +31,7 @@ BEGIN CALL CreatePeriods(fiscalYearId); END $$ +DROP PROCEDURE IF EXISTS GetPeriodRange$$ CREATE PROCEDURE GetPeriodRange( IN fiscalYearStartDate DATE, IN periodNumberIndex SMALLINT(5), @@ -43,6 +45,7 @@ CREATE PROCEDURE GetPeriodRange( SET periodEndDate = (SELECT LAST_DAY(innerDate)); END $$ +DROP PROCEDURE IF EXISTS CreatePeriods$$ CREATE PROCEDURE CreatePeriods( IN fiscalYearId MEDIUMINT(8) ) @@ -108,7 +111,7 @@ END $$ DROP PROCEDURE IF EXISTS `UpdatePeriodLabels`$$ -CREATE PROCEDURE `UpdatePeriodLabels`() +CREATE PROCEDURE `UpdatePeriodLabels`() BEGIN DECLARE _id mediumint(8) unsigned; DECLARE _start_date DATE; @@ -151,6 +154,7 @@ to get the final opening balance. TODO - check that there are no unposted records from previous years. */ +DROP PROCEDURE IF EXISTS CloseFiscalYear$$ CREATE PROCEDURE CloseFiscalYear( IN fiscalYearId MEDIUMINT UNSIGNED, IN closingAccountId INT UNSIGNED diff --git a/server/models/schema.sql b/server/models/schema.sql index 3895ab6d04..a77b9f385a 100644 --- a/server/models/schema.sql +++ b/server/models/schema.sql @@ -663,6 +663,7 @@ CREATE TABLE `enterprise_setting` ( `enable_balance_on_invoice_receipt` TINYINT(1) NOT NULL DEFAULT 0, `enable_barcodes` TINYINT(1) NOT NULL DEFAULT 1, `enable_auto_stock_accounting` TINYINT(1) NOT NULL DEFAULT 0, + `enable_auto_email_report` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`enterprise_id`), FOREIGN KEY (`enterprise_id`) REFERENCES `enterprise` (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; @@ -1351,6 +1352,24 @@ CREATE TABLE `entity_type` ( UNIQUE KEY `label` (`label`) ) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; +DROP TABLE IF EXISTS `entity_group`; +CREATE TABLE `entity_group` ( + `uuid` BINARY(16) NOT NULL, + `label` VARCHAR(190) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`uuid`), + UNIQUE KEY `label` (`label`) +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `entity_group_entity`; +CREATE TABLE `entity_group_entity` ( + `id` SMALLINT(5) NOT NULL AUTO_INCREMENT, + `entity_uuid` BINARY(16) NOT NULL, + `entity_group_uuid` BINARY(16) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + DROP TABLE IF EXISTS `entity`; CREATE TABLE `entity` ( `uuid` BINARY(16) NOT NULL, @@ -1369,6 +1388,31 @@ CREATE TABLE `entity` ( KEY `entity_type_id` (`entity_type_id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; +DROP TABLE IF EXISTS `cron`; +CREATE TABLE `cron` ( + `id` SMALLINT(5) NOT NULL AUTO_INCREMENT, + `label` VARCHAR(150) NOT NULL, + `value` VARCHAR(20) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `cron_email_report`; +CREATE TABLE `cron_email_report` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `entity_group_uuid` BINARY(16) NOT NULL, + `cron_id` SMALLINT(5) NOT NULL, + `report_id` SMALLINT(5) NOT NULL, + `params` TEXT NULL, + `label` VARCHAR(100) NOT NULL, + `last_send` DATETIME NULL, + `next_send` DATETIME NULL, + `has_dynamic_dates` TINYINT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `label` (`label`, `report_id`), + KEY `entity_group_uuid` (`entity_group_uuid`), + FOREIGN KEY (`entity_group_uuid`) REFERENCES `entity_group` (`uuid`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci; + DROP TABLE IF EXISTS `posting_journal`; CREATE TABLE `posting_journal` ( diff --git a/test/data.sql b/test/data.sql index 21049c5912..e43ec5bf92 100644 --- a/test/data.sql +++ b/test/data.sql @@ -9,8 +9,8 @@ SET NAMES 'utf8'; INSERT INTO `enterprise` VALUES (1, 'Test Enterprise', 'TE', '243 81 504 0540', 'enterprise@test.org', HUID('1f162a10-9f67-4788-9eff-c1fea42fcc9b'), NULL, 2, 103, NULL, NULL); -INSERT INTO `enterprise_setting` (enterprise_id, enable_price_lock, enable_password_validation, enable_delete_records) VALUES - (1, 0, 1, 1); +INSERT INTO `enterprise_setting` (enterprise_id, enable_price_lock, enable_password_validation, enable_delete_records, enable_auto_email_report) VALUES + (1, 0, 1, 1, 1); -- Project INSERT INTO `project` VALUES @@ -1047,6 +1047,15 @@ INSERT INTO entity (uuid, display_name, gender, email, phone, address, entity_ty (HUID('00099B1D184A48DEB93D45FBD0AB3790'), 'Bruce Wayne', 'M', 'thebat@bhi.ma', '+243000000', 'Gotham City', 1), (HUID('037AC6C6B75A4E328E9DCDE5DA22BACE'), 'Wayne Enterprise', 'o', 'thebat@bhi.ma', '+243000000', 'Gotham City', 4); +-- default entity groups +INSERT INTO entity_group (uuid, label) VALUES + (HUID('00099B1D184A48DEB93D45FBD0AB3898'), 'Developers'); + +-- entity group entity +INSERT INTO entity_group_entity (entity_uuid, entity_group_uuid) VALUES + (HUID('00099B1D184A48DEB93D45FBD0AB3790'), HUID('00099B1D184A48DEB93D45FBD0AB3898')), + (HUID('037AC6C6B75A4E328E9DCDE5DA22BACE'), HUID('00099B1D184A48DEB93D45FBD0AB3898')); + -- default room type INSERT INTO room_type VALUES (1, 'Public Room'), diff --git a/test/end-to-end/reports/balance_report/balance_report.page.js b/test/end-to-end/reports/balance_report/balance_report.page.js index ac98efc16d..02a389d6d5 100644 --- a/test/end-to-end/reports/balance_report/balance_report.page.js +++ b/test/end-to-end/reports/balance_report/balance_report.page.js @@ -50,6 +50,23 @@ class BalanceReportPage { async closeBalanceReportPreview() { await this.page.closePreview(); } + + async fillReportOptions(year, month) { + await components.fiscalYearSelect.set(year); + await components.periodSelection.set(month); + await components.yesNoRadios.set('yes', 'useSeparateDebitsAndCredits'); + await components.yesNoRadios.set('no', 'includeClosingBalances'); + await components.yesNoRadios.set('yes', 'shouldPruneEmptyRows'); + await components.yesNoRadios.set('yes', 'shouldHideTitleAccounts'); + } + + // save for the auto emailing + async saveCronEmailReport(title, entityGroupName, cronFrequencyName) { + await components.inpuText.set('label', title); + await components.entityGroupSelect.set(entityGroupName); + await components.cronSelect.set(cronFrequencyName); + await this.page.saveAutoMailing(); + } } module.exports = BalanceReportPage; diff --git a/test/end-to-end/reports/balance_report/balance_report.spec.js b/test/end-to-end/reports/balance_report/balance_report.spec.js index 14f3e0fbbc..d8549c3b54 100644 --- a/test/end-to-end/reports/balance_report/balance_report.spec.js +++ b/test/end-to-end/reports/balance_report/balance_report.spec.js @@ -12,11 +12,22 @@ describe('Balance Report', () => { renderer : 'PDF', }; - before(async () => { + const cron = { + title : 'Balance report 2018', + group : 'Developers', + frequency : 'Chaque mois', + }; + + beforeEach(async () => { await helpers.navigate(`#!/reports/${key}`); Page = new BalanceReportPage(key); }); + it('save report for cron task of emailing', async () => { + await Page.fillReportOptions(dataset.year, dataset.month); + await Page.saveCronEmailReport(cron.title, cron.group, cron.frequency); + }); + it('preview a new balance report', async () => { await Page.showBalanceReportPreview(dataset.year, dataset.month); }); diff --git a/test/end-to-end/reports/page.js b/test/end-to-end/reports/page.js index d0a03eb763..c3100a5935 100644 --- a/test/end-to-end/reports/page.js +++ b/test/end-to-end/reports/page.js @@ -14,6 +14,7 @@ class ReportPage { constructor(key) { this.url = `/reports/${key}`; this.previewAnchor = '[data-id="report-preview"]'; + this.cronEmailReportwAnchor = '[data-element="cron-email-report"]'; this.configAnchor = '[data-method="report-config"]'; this.archiveAnchor = '[data-method="archive"]'; } @@ -36,6 +37,12 @@ class ReportPage { await anchor.element(by.css('[data-method="save"]')).click(); } + // save for auto mailing + async saveAutoMailing() { + const anchor = $(this.cronEmailReportwAnchor); + await anchor.element(by.css('[data-method="save-cron-report"]')).click(); + } + // config report async backToConfig() { await $(this.configAnchor).click(); diff --git a/test/end-to-end/shared/components/bhCronSelect.js b/test/end-to-end/shared/components/bhCronSelect.js new file mode 100644 index 0000000000..8d07f8ce24 --- /dev/null +++ b/test/end-to-end/shared/components/bhCronSelect.js @@ -0,0 +1,14 @@ +/* global element, by */ +const FU = require('../FormUtils'); + +const selector = '[bh-cron-select]'; + +function set(cron, id) { + const locator = (id) ? by.id(id) : by.css(selector); + const target = element(locator); + return FU.uiSelect('$ctrl.id', cron, target); +} + +module.exports = { + set, +}; diff --git a/test/end-to-end/shared/components/bhEntityGroupSelect.js b/test/end-to-end/shared/components/bhEntityGroupSelect.js new file mode 100644 index 0000000000..205aad2c73 --- /dev/null +++ b/test/end-to-end/shared/components/bhEntityGroupSelect.js @@ -0,0 +1,14 @@ +/* global element, by */ +const FU = require('../FormUtils'); + +const selector = '[bh-entity-group-select]'; + +function set(entityGroup, id) { + const locator = (id) ? by.id(id) : by.css(selector); + const target = element(locator); + return FU.uiSelect('$ctrl.uuid', entityGroup, target); +} + +module.exports = { + set, +}; diff --git a/test/end-to-end/shared/components/index.js b/test/end-to-end/shared/components/index.js index 55ff7271ad..a1cf22ab2f 100644 --- a/test/end-to-end/shared/components/index.js +++ b/test/end-to-end/shared/components/index.js @@ -69,4 +69,6 @@ module.exports = { roomSelect : require('./bhRoomSelect'), fiscalYearPeriodSelect : require('./bhFiscalYearPeriodSelect'), diagnosisSelect : require('./bhDiagnosisSelect'), + entityGroupSelect : require('./bhEntityGroupSelect'), + cronSelect : require('./bhCronSelect'), }; diff --git a/test/integration/cronEmailReport/cronEmailReport.js b/test/integration/cronEmailReport/cronEmailReport.js new file mode 100644 index 0000000000..1cc8466e23 --- /dev/null +++ b/test/integration/cronEmailReport/cronEmailReport.js @@ -0,0 +1,71 @@ +/* global agent */ +const helpers = require('../helpers'); + +// The /cron_email_reports API endpoint +describe('(/cron_email_reports) The cron_email_reports API ', () => { + // new cron_email_report object + const record = { + cron : + { + report_id : '4', + has_dynamic_dates : 1, + label : 'Balance 2018', + entity_group_uuid : '00099B1D184A48DEB93D45FBD0AB3898', + cron_id : 1, + }, + reportOptions : + { + useSeparateDebitsAndCredits : 1, + shouldPruneEmptyRows : 1, + shouldHideTitleAccounts : 1, + fiscal_id : 1, + includeClosingBalances : 0, + period_id : 1, + }, + }; + + it('POST /cron_email_reports create a new cron_email_report in the database', () => { + return agent.post('/cron_email_reports') + .send(record) + .then((res) => { + helpers.api.created(res); + record.id = res.body.id; + }) + .catch(helpers.handler); + }); + + it('GET /cron_email_reports list registered cron email reports', () => { + return agent.get('/cron_email_reports') + .send(record) + .then((res) => { + helpers.api.listed(res, 1); + }) + .catch(helpers.handler); + }); + + it('GET /cron_email_reports list registered cron email reports based on report id', () => { + return agent.get('/cron_email_reports') + .query({ report_id : record.cron.report_id }) + .then((res) => { + helpers.api.listed(res, 1); + }) + .catch(helpers.handler); + }); + + // Need Mailgun must be configured correctly + it.skip('POST /cron_email_reports/:id send an email for a report', () => { + return agent.post(`/cron_email_reports/${record.id}`) + .then((res) => { + helpers.api.created(res); + }) + .catch(helpers.handler); + }); + + it('DELETE /cron_email_reports should delete an existing cron_email_report', () => { + return agent.delete(`/cron_email_reports/${record.id}`) + .then((res) => { + helpers.api.deleted(res); + }) + .catch(helpers.handler); + }); +}); diff --git a/test/server-unit/bhMoment.spec.js b/test/server-unit/bhMoment.spec.js new file mode 100644 index 0000000000..591e78fd33 --- /dev/null +++ b/test/server-unit/bhMoment.spec.js @@ -0,0 +1,48 @@ +const { expect } = require('chai'); +const BhMoment = require('../../server/lib/bhMoment'); + +const dateString = '2019-08-24'; +const startTime = '00:00:00.000'; +const endTime = '23:59:59.999'; + +const date = new BhMoment(dateString); + +function evaluate(startDate, endDate, dateBhMoment) { + const expected = { + dateFrom : new Date(`${startDate}T${startTime}`), + dateTo : new Date(`${endDate}T${endTime}`), + }; + const formated = { + dateFrom : dateBhMoment.dateFrom.toDate(), + dateTo : dateBhMoment.dateTo.toDate(), + }; + expect(formated).to.deep.equal(expected); +} + +const bhMomentUnitTest = () => { + it('#day() should return the start and stop dates of the given date', () => { + const startDate = '2019-08-24'; + const endDate = '2019-08-24'; + evaluate(startDate, endDate, date.day()); + }); + + it('#week() should return the start and stop dates of the week for the given date', () => { + const startDate = '2019-08-18'; + const endDate = '2019-08-24'; + evaluate(startDate, endDate, date.week()); + }); + + it('#month() should return the start and stop dates of the month for the given date', () => { + const startDate = '2019-08-01'; + const endDate = '2019-08-31'; + evaluate(startDate, endDate, date.month()); + }); + + it('#year() should return the start and stop dates of the year for the given date', () => { + const startDate = '2019-01-01'; + const endDate = '2019-12-31'; + evaluate(startDate, endDate, date.year()); + }); +}; + +describe('/lib/bhMoment.spec.js', bhMomentUnitTest); diff --git a/yarn.lock b/yarn.lock index 134cc8571c..53c5d685d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1876,6 +1876,21 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" +cron-parser@^2.7.3: + version "2.12.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.12.0.tgz#05ae8c008025cecdd8778746626a183249ca300b" + integrity sha512-1GU6CQJ6gT9XDEGeTuzfhZgFMf82BSs3ihFA3i2wr4qGKJLhO1kOvaIF9biIo39CaPgzZ17U8FgYxRv/+UR50A== + dependencies: + is-nan "^1.2.1" + moment-timezone "^0.5.25" + +cron@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/cron/-/cron-1.7.1.tgz#e85ee9df794d1bc6579896ee382053c3ce33778f" + integrity sha512-gmMB/pJcqUVs/NklR1sCGlNYM7TizEw+1gebz20BMc/8bTm/r7QUp3ZPSPlG8Z5XRlvb7qhjEjq/+bdIfUCL2A== + dependencies: + moment-timezone "^0.5.x" + cropper@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cropper/-/cropper-4.0.0.tgz#77a0c1f8989049f6b9a9137acb865dca5a1e9f62" @@ -2252,7 +2267,7 @@ default-resolution@^2.0.0: resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= -define-properties@^1.1.2, define-properties@^1.1.3: +define-properties@^1.1.1, define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -4742,6 +4757,13 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-nan@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2" + integrity sha1-n69ltvttskt/XAYoR16nH5iEAeI= + dependencies: + define-properties "^1.1.1" + is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -5574,6 +5596,11 @@ loglevel@^1.6.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= +long-timeout@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + integrity sha1-lyHXiLR+C8taJMLivuGg2lXatRQ= + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -6015,7 +6042,14 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -"moment@>=2.8.0 <3.0.0", moment@^2.24.0: +moment-timezone@^0.5.25, moment-timezone@^0.5.x: + version "0.5.25" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.25.tgz#a11bfa2f74e088327f2cd4c08b3e7bdf55957810" + integrity sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", "moment@>=2.8.0 <3.0.0", moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== @@ -6227,6 +6261,15 @@ node-releases@^1.1.17: dependencies: semver "^5.3.0" +node-schedule@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.2.tgz#d774b383e2a6f6ade59eecc62254aea07cd758cb" + integrity sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw== + dependencies: + cron-parser "^2.7.3" + long-timeout "0.1.1" + sorted-array-functions "^1.0.0" + nodemon@^1.18.10: version "1.19.1" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071" @@ -8503,6 +8546,11 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" +sorted-array-functions@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz#43265b21d6e985b7df31621b1c11cc68d8efc7c3" + integrity sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg== + source-map-resolve@^0.5.0: version "0.5.2" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"