├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── codeql.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── alerts ├── __init__.py ├── alerts │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── alert │ │ │ ├── __init__.py │ │ │ ├── alert.js │ │ │ ├── alert.json │ │ │ ├── alert.py │ │ │ └── alert_list.js │ │ ├── alert_for_role │ │ │ ├── __init__.py │ │ │ ├── alert_for_role.json │ │ │ └── alert_for_role.py │ │ ├── alert_for_user │ │ │ ├── __init__.py │ │ │ ├── alert_for_user.json │ │ │ └── alert_for_user.py │ │ ├── alert_seen_by │ │ │ ├── __init__.py │ │ │ ├── alert_seen_by.json │ │ │ └── alert_seen_by.py │ │ ├── alert_type │ │ │ ├── __init__.py │ │ │ ├── alert_type.js │ │ │ ├── alert_type.json │ │ │ ├── alert_type.py │ │ │ └── alert_type_list.js │ │ ├── alerts_settings │ │ │ ├── __init__.py │ │ │ ├── alerts_settings.js │ │ │ ├── alerts_settings.json │ │ │ └── alerts_settings.py │ │ └── alerts_update_receiver │ │ │ ├── __init__.py │ │ │ ├── alerts_update_receiver.json │ │ │ └── alerts_update_receiver.py │ └── report │ │ ├── __init__.py │ │ └── alert_report │ │ ├── __init__.py │ │ ├── alert_report.html │ │ ├── alert_report.js │ │ ├── alert_report.json │ │ └── alert_report.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── hooks.py ├── modules.txt ├── patches.txt ├── public │ ├── build.json │ └── js │ │ └── alerts.bundle.js ├── setup │ ├── __init__.py │ ├── install.py │ ├── migrate.py │ └── uninstall.py ├── utils │ ├── __init__.py │ ├── access.py │ ├── alert.py │ ├── background.py │ ├── cache.py │ ├── common.py │ ├── files.py │ ├── query.py │ ├── realtime.py │ ├── search.py │ ├── system.py │ ├── type.py │ └── update.py └── version.py ├── images ├── notice-alert-mobile.png ├── urgent-alert-mobile.png └── warning-alert-mobile.png ├── pyproject.toml ├── requirements.txt └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]: " 5 | labels: bug 6 | assignees: kid1194 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQ]: " 5 | labels: enhancement 6 | assignees: kid1194 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'javascript', 'python' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | - name: Set up Python 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: '3.x' 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | setup-python-dependencies: false 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 56 | # queries: security-extended,security-and-quality 57 | 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v2 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 66 | 67 | # If the Autobuild fails above, remove it and uncomment the following three lines. 68 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 69 | 70 | # - run: | 71 | # echo "Run, Build Application using script" 72 | # ./location_of_script_within_repo/buildscript.sh 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v2 76 | with: 77 | category: "/language:${{matrix.language}}" 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | .gitignore 5 | .ruff.toml 6 | package-lock.json 7 | package.json 8 | eslint.config.mjs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Level Up Marketing & Development Services © 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies 12 | or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 19 | USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include alerts *.css 8 | recursive-include alerts *.csv 9 | recursive-include alerts *.html 10 | recursive-include alerts *.ico 11 | recursive-include alerts *.js 12 | recursive-include alerts *.json 13 | recursive-include alerts *.md 14 | recursive-include alerts *.png 15 | recursive-include alerts *.py 16 | recursive-include alerts *.svg 17 | recursive-include alerts *.txt 18 | recursive-exclude alerts *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frappe Alerts 2 | 3 | A small **Frappe** module that displays custom alerts to specific recipients after login. 4 | 5 |  6 | 7 | **Apologies in advance for any problem or bug you face with this module.** 8 | **Please report any problem or bug you face so it can be fixed.** 9 | 10 | --- 11 | 12 |
13 |
14 |
16 |
17 |
19 |
20 |
\ 51 | ' + __('Disabling the alert type will prevent all linked alerts from being displayed.') + '\ 52 |
\ 53 | '); 54 | }, 55 | toggle_toolbar: function(frm) { 56 | var label = __('Preview'), 57 | del = frm.is_new() || cint(frm.doc.disabled) > 0; 58 | if (frm.custom_buttons[label]) { 59 | if (!del) return; 60 | frm.custom_buttons[label].remove(); 61 | delete frm.custom_buttons[label]; 62 | } 63 | if (del || frm.custom_buttons[label]) return; 64 | frm.add_custom_button(label, function() { 65 | frappe.alerts.mock().build(frm.doc); 66 | }); 67 | frm.change_custom_button_type(label, null, 'info'); 68 | }, 69 | }); -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_type/alert_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "autoname": "Prompt", 5 | "creation": "2022-04-04 04:04:04", 6 | "description": "Alert type for Alerts", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "disabled", 12 | "main_column", 13 | "disable_note", 14 | "options_section", 15 | "display_priority", 16 | "display_timeout", 17 | "options_column", 18 | "display_sound", 19 | "custom_display_sound", 20 | "style_section", 21 | "background", 22 | "border_color", 23 | "style_column", 24 | "title_color", 25 | "content_color", 26 | "dark_style_section", 27 | "dark_background", 28 | "dark_border_color", 29 | "dark_style_column", 30 | "dark_title_color", 31 | "dark_content_color" 32 | ], 33 | "fields": [ 34 | { 35 | "fieldname": "main_section", 36 | "fieldtype": "Section Break", 37 | "depends_on": "eval:!doc.__islocal" 38 | }, 39 | { 40 | "fieldname": "disabled", 41 | "fieldtype": "Check", 42 | "label": "Is Disabled", 43 | "default": "0", 44 | "search_index": 1 45 | }, 46 | { 47 | "fieldname": "main_column", 48 | "fieldtype": "Column Break" 49 | }, 50 | { 51 | "fieldname": "disable_note", 52 | "fieldtype": "HTML", 53 | "label": "", 54 | "read_only": 1 55 | }, 56 | { 57 | "fieldname": "options_section", 58 | "fieldtype": "Section Break", 59 | "label": "Options" 60 | }, 61 | { 62 | "fieldname": "display_priority", 63 | "fieldtype": "Int", 64 | "label": "Display Priority", 65 | "description": "Alerts of higher priority types gets displayed first.", 66 | "default": "0", 67 | "non_negative": 1, 68 | "in_list_view": 1 69 | }, 70 | { 71 | "fieldname": "display_timeout", 72 | "fieldtype": "Int", 73 | "label": "Display Timeout (Seconds)", 74 | "description": "Auto close alerts after a specific time. Default: 0 - No Timeout.", 75 | "default": "0", 76 | "non_negative": 1, 77 | "in_list_view": 1 78 | }, 79 | { 80 | "fieldname": "options_column", 81 | "fieldtype": "Column Break" 82 | }, 83 | { 84 | "fieldname": "display_sound", 85 | "fieldtype": "Select", 86 | "label": "Display Sound", 87 | "options": "None\nAlert\nError\nClick\nCancel\nSubmit\nCustom", 88 | "default": "None" 89 | }, 90 | { 91 | "fieldname": "custom_display_sound", 92 | "fieldtype": "Attach", 93 | "label": "Custom Display Sound", 94 | "description": "A small and short audio file is recommended. Supported formats: MP3 (Best), WAV, OGG.", 95 | "depends_on": "eval:doc.display_sound === 'Custom'", 96 | "mandatory_depends_on": "eval:doc.display_sound === 'Custom'", 97 | "read_only_depends_on": "eval:doc.display_sound !== 'Custom'" 98 | }, 99 | { 100 | "fieldname": "style_section", 101 | "fieldtype": "Section Break", 102 | "label": "Light Theme Style" 103 | }, 104 | { 105 | "fieldname": "background", 106 | "fieldtype": "Color", 107 | "label": "Background Color" 108 | }, 109 | { 110 | "fieldname": "border_color", 111 | "fieldtype": "Color", 112 | "label": "Border Color" 113 | }, 114 | { 115 | "fieldname": "style_column", 116 | "fieldtype": "Column Break" 117 | }, 118 | { 119 | "fieldname": "title_color", 120 | "fieldtype": "Color", 121 | "label": "Title Color" 122 | }, 123 | { 124 | "fieldname": "content_color", 125 | "fieldtype": "Color", 126 | "label": "Content Color" 127 | }, 128 | { 129 | "fieldname": "dark_style_section", 130 | "fieldtype": "Section Break", 131 | "label": "Dark Theme Style" 132 | }, 133 | { 134 | "fieldname": "dark_background", 135 | "fieldtype": "Color", 136 | "label": "Background Color" 137 | }, 138 | { 139 | "fieldname": "dark_border_color", 140 | "fieldtype": "Color", 141 | "label": "Border Color" 142 | }, 143 | { 144 | "fieldname": "dark_style_column", 145 | "fieldtype": "Column Break" 146 | }, 147 | { 148 | "fieldname": "dark_title_color", 149 | "fieldtype": "Color", 150 | "label": "Title Color" 151 | }, 152 | { 153 | "fieldname": "dark_content_color", 154 | "fieldtype": "Color", 155 | "label": "Content Color" 156 | } 157 | ], 158 | "icon": "fa fa-bell-o", 159 | "modified": "2024-06-19 04:04:04", 160 | "modified_by": "Administrator", 161 | "module": "Alerts", 162 | "name": "Alert Type", 163 | "naming_rule": "Set by user", 164 | "owner": "Administrator", 165 | "permissions": [ 166 | { 167 | "amend": 1, 168 | "cancel": 1, 169 | "create": 1, 170 | "delete": 1, 171 | "email": 1, 172 | "export": 1, 173 | "if_owner": 0, 174 | "import": 1, 175 | "permlevel": 0, 176 | "print": 1, 177 | "read": 1, 178 | "report": 1, 179 | "role": "System Manager", 180 | "set_user_permissions": 1, 181 | "share": 1, 182 | "submit": 1, 183 | "write": 1 184 | }, 185 | { 186 | "amend": 1, 187 | "cancel": 1, 188 | "create": 1, 189 | "delete": 1, 190 | "email": 1, 191 | "export": 1, 192 | "if_owner": 0, 193 | "import": 1, 194 | "permlevel": 0, 195 | "print": 1, 196 | "read": 1, 197 | "report": 1, 198 | "role": "Administrator", 199 | "set_user_permissions": 1, 200 | "share": 1, 201 | "submit": 1, 202 | "write": 1 203 | } 204 | ], 205 | "sort_field": "modified", 206 | "sort_order": "DESC", 207 | "translate_link_fields": 1, 208 | "track_changes": 1 209 | } -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_type/alert_type.py: -------------------------------------------------------------------------------- 1 | # Alerts © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe import _ 8 | from frappe.utils import cint 9 | from frappe.model.document import Document 10 | 11 | from alerts.utils import clear_doc_cache 12 | 13 | 14 | class AlertType(Document): 15 | _type_filter = {"fieldtype": ["in", ["Check", "Int", "Select", "Attach", "Color"]]} 16 | _int_types = ["Check", "Int"] 17 | 18 | 19 | def before_validate(self): 20 | self._check_app_status() 21 | self._set_defaults() 22 | 23 | 24 | def validate(self): 25 | if self.display_sound == "Custom": 26 | if not self.custom_display_sound: 27 | self._error(_("A valid custom display sound is required.")) 28 | else: 29 | from alerts.utils import is_sound_file 30 | 31 | if not is_sound_file(self.custom_display_sound): 32 | self._error(_("Custom display sound must be of a supported format (MP3, WAV, OGG).")) 33 | 34 | 35 | def before_rename(self, olddn, newdn, merge=False): 36 | self._check_app_status() 37 | clear_doc_cache(self.doctype, olddn) 38 | self._clean_flags() 39 | 40 | 41 | def before_save(self): 42 | clear_doc_cache(self.doctype, self.name) 43 | 44 | for f in self.meta.get("fields"): 45 | if self.has_value_changed(f.fieldname): 46 | self.flags.emit_change = 1 47 | self.flags.emit_action = "add" if self.is_new() else "change" 48 | break 49 | 50 | old = self._get_old_doc() 51 | if ( 52 | old and old.custom_display_sound and 53 | old.custom_display_sound != self.custom_display_sound 54 | ): 55 | self._delete_files([self.name, old.name], old.custom_display_sound) 56 | 57 | 58 | def on_update(self): 59 | if self.flags.get("emit_change", 0): 60 | self._emit_change() 61 | 62 | self._clean_flags() 63 | 64 | 65 | def on_trash(self): 66 | from alerts.utils import type_alerts_exist 67 | 68 | if type_alerts_exist(self.name): 69 | self._error(_("Alert type with linked alerts can't be removed.")) 70 | 71 | if self.custom_display_sound: 72 | self._delete_files() 73 | 74 | self.flags.emit_change = 1 75 | self.flags.emit_action = "trash" 76 | 77 | 78 | def after_delete(self): 79 | clear_doc_cache(self.doctype, self.name) 80 | if self.flags.get("emit_change", 0): 81 | self._emit_change() 82 | 83 | self._clean_flags() 84 | 85 | 86 | @property 87 | def _is_disabled(self): 88 | return cint(self.disabled) > 0 89 | 90 | 91 | @property 92 | def _display_priority(self): 93 | return cint(self.display_priority) 94 | 95 | 96 | @property 97 | def _display_timeout(self): 98 | return cint(self.display_timeout) 99 | 100 | 101 | def _set_defaults(self): 102 | if self._display_priority < 0: 103 | self.display_priority = 0 104 | if self._display_timeout < 0: 105 | self.display_timeout = 0 106 | if self.display_sound != "Custom": 107 | self.custom_display_sound = None 108 | 109 | 110 | def _emit_change(self): 111 | from alerts.utils import emit_type_changed 112 | 113 | data = { 114 | "name": self.name, 115 | "action": self.flags.get("emit_action", "change") 116 | } 117 | if data["action"] != "trash": 118 | for f in self.meta.get("fields", self._type_filter): 119 | data[f.fieldname] = self.get(f.fieldname) 120 | 121 | emit_type_changed(data) 122 | 123 | 124 | def _get_old_doc(self): 125 | if self.is_new(): 126 | return None 127 | doc = self.get_doc_before_save() 128 | if not doc: 129 | self.load_doc_before_save() 130 | doc = self.get_doc_before_save() 131 | return doc 132 | 133 | 134 | def _delete_files(self, name=None, files=None): 135 | from alerts.utils import delete_files 136 | 137 | delete_files(self.doctype, name or self.name, files or self.custom_display_sound) 138 | 139 | 140 | def _check_app_status(self): 141 | if not self.flags.get("status_checked", 0): 142 | from alerts.utils import check_app_status 143 | 144 | check_app_status() 145 | self.flags.status_checked = 1 146 | 147 | 148 | def _clean_flags(self): 149 | keys = [ 150 | "emit_change", 151 | "emit_action", 152 | "status_checked" 153 | ] 154 | for i in range(len(keys)): 155 | self.flags.pop(keys.pop(0), 0) 156 | 157 | 158 | def _error(self, msg): 159 | from alerts.utils import error 160 | 161 | self._clean_flags() 162 | error(msg, _(self.doctype)) -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_type/alert_type_list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Alerts © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.provide('frappe.listview_settings'); 10 | 11 | 12 | frappe.listview_settings['Alert Type'] = { 13 | onload: function(list) { 14 | frappe.alerts.on('ready change', function() { this.setup_list(list); }); 15 | }, 16 | get_indicator: function(doc) { 17 | return cint(doc.disabled) > 0 18 | ? ['Disabled', 'red', 'disabled,=,1|docstatus,=,0'] 19 | : ['Enabled', 'green', 'disabled,=,0|docstatus,=,0']; 20 | }, 21 | }; -------------------------------------------------------------------------------- /alerts/alerts/doctype/alerts_settings/__init__.py: -------------------------------------------------------------------------------- 1 | # Alerts © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /alerts/alerts/doctype/alerts_settings/alerts_settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Alerts © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.ui.form.on('Alerts Settings', { 10 | onload: function(frm) { 11 | frappe.alerts.on('on_alert', function(d, t) { 12 | frm._sets.errs.includes(t) && (d.title = __(frm.doctype)); 13 | }); 14 | frm._sets = { 15 | errs: ['fatal', 'error'], 16 | update: 0, 17 | }; 18 | }, 19 | refresh: function(frm) { 20 | !frm._sets.update && frm.events.setup_update_note(frm); 21 | }, 22 | check_for_update: function(frm) { 23 | frappe.alerts.request('check_for_update', null, function(ret) { 24 | if (!ret) return; 25 | frm._sets.update = 0; 26 | frm.reload_doc(); 27 | }); 28 | }, 29 | validate: function(frm) { 30 | var errs = []; 31 | if (cint(frm.doc.use_fallback_sync)) { 32 | if (cint(frm.doc.fallback_sync_delay) < 1) 33 | errs.push(__('Fallback sync delay must be greater than or equals to 1 minute.')); 34 | } 35 | if (cint(frm.doc.send_update_notification)) { 36 | if (!frappe.alerts.$isStrVal(frm.doc.update_notification_sender)) 37 | errs.push(__('A valid update notification sender is required.')); 38 | if (!frappe.alerts.$isArrVal(frm.doc.update_notification_receivers)) 39 | errs.push(__('At least one valid update notification receiver is required.')); 40 | } 41 | if (errs.length) { 42 | frappe.alerts.fatal(errs); 43 | return false; 44 | } 45 | }, 46 | setup_update_note: function(frm) { 47 | frm._sets.update = 1; 48 | frm.get_field('update_note').$wrapper.empty().append('\ 49 |{%= __("Alert") %} | 31 |{%= __("Title") %} | 32 |{%= __("Alert Type") %} | 33 |{%= __("From Date") %} | 34 |{%= __("Until Date") %} | 35 |{%= __("Repeatable") %} | 36 |{%= __("Reached") %} | 37 |{%= __("Status") %} | 38 |{%= frappe.format(data[i].alert, {fieldtype: "Link"}) || " " %} | 44 |{%= data[i].title || " " %} | 45 |{%= frappe.format(data[i].alert_type, {fieldtype: "Link"}) || " " %} | 46 |{%= frappe.datetime.str_to_user(data[i].from_date) %} | 47 |{%= frappe.datetime.str_to_user(data[i].until_date) %} | 48 |{%= data[i].is_repeatable || __("No") %} | 49 |{%= format_number(data[i].reached || 0, null, 0) %} | 50 |{%= data[i].status || " " %} | 51 | 52 | {% } %} 53 |
---|
{%= __("Printed On") %}: {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
-------------------------------------------------------------------------------- /alerts/alerts/report/alert_report/alert_report.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Alerts © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.query_reports['Alert Report'] = { 10 | filters: [ 11 | { 12 | 'fieldname': 'alert_type', 13 | 'label': __('Alert Type'), 14 | 'fieldtype': 'Link', 15 | 'options': 'Alert Type' 16 | }, 17 | { 18 | 'fieldname': 'from_date', 19 | 'label': __('From Date'), 20 | 'fieldtype': 'Date' 21 | }, 22 | { 23 | 'fieldname': 'until_date', 24 | 'label': __('Until Date'), 25 | 'fieldtype': 'Date' 26 | }, 27 | { 28 | 'fieldtype': 'Break', 29 | }, 30 | { 31 | 'fieldname': 'is_repeatable', 32 | 'label': __('Repeatable'), 33 | 'fieldtype': 'Check' 34 | }, 35 | { 36 | 'fieldname': 'status', 37 | 'label': __('Status'), 38 | 'fieldtype': 'Select', 39 | 'options': '\nDraft\nPending\nActive\nFinished\nCancelled' 40 | }, 41 | ] 42 | }; -------------------------------------------------------------------------------- /alerts/alerts/report/alert_report/alert_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "apply_user_permissions": 1, 4 | "creation": "2024-06-19 04:04:04", 5 | "disabled": 0, 6 | "docstatus": 0, 7 | "doctype": "Report", 8 | "is_standard": "Yes", 9 | "modified": "2024-06-19 04:04:04", 10 | "modified_by": "Administrator", 11 | "module": "Alerts", 12 | "name": "Alert Report", 13 | "owner": "Administrator", 14 | "ref_doctype": "Alert", 15 | "report_name": "Alert Report", 16 | "report_type": "Script Report", 17 | "roles": [ 18 | { 19 | "role": "System Manager" 20 | }, 21 | { 22 | "role": "Administrator" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /alerts/alerts/report/alert_report/alert_report.py: -------------------------------------------------------------------------------- 1 | # Alerts © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _, throw 9 | from frappe.utils import cint 10 | 11 | 12 | def execute(filters=None): 13 | if not filters: 14 | return [], [] 15 | 16 | validate_filters(filters) 17 | columns = get_columns() 18 | data = get_result(filters) 19 | totals = get_totals(data) 20 | chart = get_chart_data(totals) 21 | summary = get_report_summary(totals) 22 | return columns, data, None, chart, summary 23 | 24 | 25 | def validate_filters(filters): 26 | if ( 27 | filters.get("from_date", "") and 28 | filters.get("until_date", "") and 29 | filters.from_date > filters.until_date 30 | ): 31 | throw(_("From Date must be before Until Date.")) 32 | 33 | 34 | def get_columns(): 35 | dt = "Alert" 36 | return [ 37 | { 38 | "label": _(dt), 39 | "fieldname": "alert", 40 | "fieldtype": "Link", 41 | "options": dt, 42 | }, 43 | { 44 | "label": _("Title"), 45 | "fieldname": "title", 46 | }, 47 | { 48 | "label": _("Alert Type"), 49 | "fieldname": "alert_type", 50 | "fieldtype": "Link", 51 | "options": "Alert Type" 52 | }, 53 | { 54 | "label": _("From Date"), 55 | "fieldname": "from_date", 56 | "fieldtype": "Date" 57 | }, 58 | { 59 | "label": _("Until Date"), 60 | "fieldname": "until_date", 61 | "fieldtype": "Date" 62 | }, 63 | { 64 | "label": _("is_repeatable"), 65 | "fieldname": "Repeatable" 66 | }, 67 | { 68 | "label": _("Reached"), 69 | "fieldname": "Reached", 70 | "fieldtype": "Int" 71 | }, 72 | { 73 | "label": _("Status"), 74 | "fieldname": "status" 75 | }, 76 | ] 77 | 78 | 79 | def get_result(filters): 80 | from pypika.enums import Order 81 | 82 | doc = frappe.qb.DocType("Alert") 83 | qry = ( 84 | frappe.qb.from_(doc) 85 | .select( 86 | doc.name.as_("alert"), 87 | doc.title, 88 | doc.alert_type, 89 | doc.from_date, 90 | doc.until_date, 91 | doc.is_repeatable, 92 | doc.reached, 93 | doc.status 94 | ) 95 | .orderby(doc.from_date, order=Order.asc) 96 | ) 97 | 98 | if filters.get("alert_type", ""): 99 | qry = qry.where(doc.alert_type == filters.alert_type) 100 | if filters.get("from_date", ""): 101 | qry = qry.where(doc.from_date.gte(filters.from_date)) 102 | if filters.get("until_date", ""): 103 | qry = qry.where(doc.until_date.lte(filters.until_date)) 104 | if cint(filters.get("is_repeatable", 0)): 105 | qry = qry.where(doc.is_repeatable == 1) 106 | if filters.get("status", ""): 107 | qry = qry.where(doc.status == filters.status) 108 | 109 | data = qry.run(as_dict=True) 110 | for i in range(len(data)): 111 | data[i]["is_repeatable"] = "Yes" if cint(data[i]["is_repeatable"]) else "No" 112 | 113 | return data 114 | 115 | 116 | def get_totals(data): 117 | totals = {"*": len(data)} 118 | for v in get_types(): 119 | totals[v] = 0 120 | for v in data: 121 | if v["alert_type"] in totals: 122 | totals[v["alert_type"]] += 1 123 | 124 | return totals 125 | 126 | 127 | def get_chart_data(totals): 128 | labels = [] 129 | datasets = [] 130 | for k, v in totals.items(): 131 | if k != "*": 132 | labels.append(k) 133 | datasets.append({ 134 | "name": k, 135 | "values": [v], 136 | }) 137 | 138 | return { 139 | "data": { 140 | "labels": labels, 141 | "datasets": datasets 142 | }, 143 | "type": "bar", 144 | "fieldtype": "Int" 145 | } 146 | 147 | 148 | def get_report_summary(totals): 149 | summary = [] 150 | for k, v in totals.items(): 151 | label = "Total" 152 | if k != "*": 153 | label += f" ({k})" 154 | summary.append({ 155 | "value": v, 156 | "label": _(label), 157 | "datatype": "Int" 158 | }) 159 | 160 | return summary 161 | 162 | 163 | def get_types(): 164 | return frappe.get_all( 165 | "Alert Type", 166 | fields=["name"], 167 | pluck="name", 168 | ignore_permissions=True, 169 | strict=False 170 | ) -------------------------------------------------------------------------------- /alerts/config/__init__.py: -------------------------------------------------------------------------------- 1 | # Alerts © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /alerts/config/desktop.py: -------------------------------------------------------------------------------- 1 | # Alerts © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe import _ 8 | 9 | from alerts import __module__ 10 | 11 | 12 | def get_data(): 13 | return [ 14 | { 15 | "module_name": __module__, 16 | "color": "blue", 17 | "icon": "octicon octicon-bell", 18 | "type": "module", 19 | "label": _(__module__) 20 | } 21 | ] -------------------------------------------------------------------------------- /alerts/config/docs.py: -------------------------------------------------------------------------------- 1 | # Alerts © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from alerts import __module__ 8 | 9 | 10 | """ 11 | Configuration for docs 12 | """ 13 | 14 | 15 | def get_context(context): 16 | context.brand_html = __module__ 17 | -------------------------------------------------------------------------------- /alerts/hooks.py: -------------------------------------------------------------------------------- 1 | # Alerts © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from .version import is_version_gt 8 | 9 | 10 | app_name = "alerts" 11 | app_title = "Alerts" 12 | app_publisher = "Ameen Ahmed (Level Up)" 13 | app_description = "Frappe app that displays custom alerts to specific recipients." 14 | app_icon = "octicon octicon-bell" 15 | app_color = "blue" 16 | app_email = "kid1194@gmail.com" 17 | app_license = "MIT" 18 | 19 | 20 | app_include_js = [ 21 | 'alerts.bundle.js' 22 | ] if is_version_gt(13) else [ 23 | '/assets/alerts/js/alerts.js' 24 | ] 25 | 26 | 27 | after_sync = "alerts.setup.install.after_sync" 28 | after_migrate = "alerts.setup.migrate.after_migrate" 29 | after_uninstall = "alerts.setup.uninstall.after_uninstall" 30 | 31 | 32 | on_login = ["alerts.utils.access.on_login"] 33 | 34 | 35 | scheduler_events = { 36 | "cron": { 37 | "0 0 * * *": [ 38 | "alerts.utils.alert.update_alerts", 39 | ] 40 | }, 41 | "daily": [ 42 | "alerts.utils.update.auto_check_for_update" 43 | ] 44 | } -------------------------------------------------------------------------------- /alerts/modules.txt: -------------------------------------------------------------------------------- 1 | Alerts -------------------------------------------------------------------------------- /alerts/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kid1194/frappe_alerts/c0552c02f1e1724e0f55caf469cef04b02a987e4/alerts/patches.txt -------------------------------------------------------------------------------- /alerts/public/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "alerts/js/alerts.js": [ 3 | "public/js/alerts.bundle.js" 4 | ] 5 | } -------------------------------------------------------------------------------- /alerts/public/js/alerts.bundle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Alerts © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | (function() { 10 | var LU = { 11 | $type(v) { 12 | if (v == null) return v === null ? 'Null' : 'Undefined'; 13 | var t = Object.prototype.toString.call(v).slice(8, -1); 14 | return t === 'Number' && isNaN(v) ? 'NaN' : t; 15 | }, 16 | $hasProp(k, o) { return Object.prototype.hasOwnProperty.call(o || this, k); }, 17 | $is(v, t) { return v != null && this.$type(v) === t; }, 18 | $of(v, t) { return typeof v === t; }, 19 | $isObjLike(v) { return v != null && this.$of(v, 'object'); }, 20 | $isStr(v) { return this.$is(v, 'String'); }, 21 | $isStrVal(v) { return this.$isStr(v) && v.length; }, 22 | $isNum(v) { return this.$is(v, 'Number') && isFinite(v); }, 23 | $isBool(v) { return v === true || v === false; }, 24 | $isBoolLike(v) { return this.$isBool(v) || v === 0 || v === 1; }, 25 | $isFunc(v) { return this.$of(v, 'function') || /(Function|^Proxy)$/.test(this.$type(v)); }, 26 | $isArr(v) { return this.$is(v, 'Array'); }, 27 | $isArrVal(v) { return this.$isArr(v) && v.length; }, 28 | $isArgs(v) { return this.$is(v, 'Arguments'); }, 29 | $isArgsVal(v) { return this.$isArgs(v) && v.length; }, 30 | $isArrLike(v) { return this.$isArr(v) || this.$isArgs(v); }, 31 | $isArrLikeVal(v) { return this.$isArrLike(v) && v.length; }, 32 | $isEmptyObj(v) { 33 | if (this.$isObjLike(v)) for (var k in v) { if (this.$hasProp(k, v)) return false; } 34 | return true; 35 | }, 36 | $isBaseObj(v) { return !this.$isArgs(v) && this.$is(v, 'Object'); }, 37 | $isBaseObjVal(v) { return this.$isBaseObj(v) && !this.$isEmptyObj(v); }, 38 | $fnStr(v) { return Function.prototype.toString.call(v); }, 39 | $isObj(v, _) { 40 | if (!this.$isObjLike(v)) return false; 41 | var k = 'constructor'; 42 | v = Object.getPrototypeOf(v); 43 | return this.$hasProp(k, v) && this.$isFunc(v[k]) 44 | && (!_ || this.$fnStr(v[k]) === this.$fnStr(Object)); 45 | }, 46 | $isObjVal(v) { return this.$isObj(v) && !this.$isEmptyObj(v); }, 47 | $isDataObj(v) { return this.$isObj(v, 1); }, 48 | $isDataObjVal(v) { return this.$isDataObj(v) && !this.$isEmptyObj(v); }, 49 | $isIter(v) { return this.$isBaseObj(v) || this.$isArrLike(v); }, 50 | $isEmpty(v) { 51 | return v == null || v === '' || v === 0 || v === false 52 | || (this.$isArrLike(v) && v.length < 1) || this.$isEmptyObj(v); 53 | }, 54 | $toArr(v, s, e) { try { return Array.prototype.slice.call(v, s, e); } catch(_) { return []; } }, 55 | $toJson(v, d) { try { return JSON.stringify(v); } catch(_) { return d; } }, 56 | $parseJson(v, d) { try { return JSON.parse(v); } catch(_) { return d; } }, 57 | $clone(v) { return this.$parseJson(this.$toJson(v)); }, 58 | $filter(v, fn) { 59 | fn = this.$fn(fn) || function(v) { return v != null; }; 60 | var o = this.$isBaseObj(v), r = o ? {} : []; 61 | if (o) for (var k in v) { if (this.$hasProp(k, v) && fn(v[k], k) !== false) r[k] = v[k]; } 62 | else for (var i = 0, x = 0, l = v.length; i < l; i++) { if (fn(v[i], i) !== false) r[x++] = v[i]; } 63 | return r; 64 | }, 65 | $map(v, fn) { 66 | if (!(fn = this.$fn(fn))) return this.$clone(v); 67 | var o = this.$isBaseObj(v), r = o ? {} : []; 68 | if (o) for (var k in v) { if (this.$hasProp(k, v)) r[k] = fn(v[k], k); } 69 | else for (var i = 0, x = 0, l = v.length; i < l; i++) { r[x++] = fn(v[i], i); } 70 | return r; 71 | }, 72 | $reduce(v, fn, r) { 73 | if (!(fn = this.$fn(fn))) return r; 74 | if (this.$isBaseObj(v)) for (var k in v) { this.$hasProp(k, v) && fn(v[k], k, r); } 75 | else for (var i = 0, x = 0, l = v.length; i < l; i++) { fn(v[i], i, r); } 76 | return r; 77 | }, 78 | $assign() { 79 | var a = arguments.length && this.$filter(arguments, this.$isBaseObj); 80 | if (!a || !a.length) return {}; 81 | a.length > 1 && Object.assign.apply(null, a); 82 | return a[0]; 83 | }, 84 | $extend() { 85 | var a = arguments.length && this.$filter(arguments, this.$isBaseObj); 86 | if (!a || a.length < 2) return a && a.length ? a[0] : {}; 87 | var d = this.$isBoolLike(arguments[0]) && arguments[0], 88 | t = this.$map(a[0], this.$isBaseObj); 89 | for (var i = 1, l = a.length; i < l; i++) 90 | for (var k in a[i]) { 91 | if (!this.$hasProp(k, a[i]) || a[i][k] == null) continue; 92 | d && t[k] && this.$isBaseObj(a[i][k]) ? this.$extend(d, a[0][k], a[i][k]) : (a[0][k] = a[i][k]); 93 | } 94 | return a[0]; 95 | }, 96 | $fn(fn, o) { if (this.$isFunc(fn)) return fn.bind(o || this); }, 97 | $afn(fn, a, o) { 98 | if (!this.$isFunc(fn)) return; 99 | a = this.$isArrLike(a) ? this.$toArr(a) : (a != null ? [a] : a); 100 | return a && a.unshift(o || this) ? fn.bind.apply(fn, a) : fn.bind(o || this); 101 | }, 102 | $call(fn, a, o) { 103 | if (!this.$isFunc(fn)) return; 104 | a = a == null || this.$isArrLike(a) ? a : [a]; 105 | o = o || this; 106 | var l = a != null && a.length; 107 | return !l ? fn.call(o) : (l < 2 ? fn.call(o, a[0]) : (l < 3 ? fn.call(o, a[0], a[1]) 108 | : (l < 4 ? fn.call(o, a[0], a[1], a[2]) : fn.apply(o, a)))); 109 | }, 110 | $try(fn, a, o) { try { return this.$call(fn, a, o); } catch(e) { console.error(e.message, e.stack); } }, 111 | $xtry(fn, a, o) { return this.$fn(function() { return this.$try(fn, a, o); }); }, 112 | $timeout(fn, tm, a, o) { 113 | return tm != null ? setTimeout(this.$afn(fn, a, o), tm) : ((fn && clearTimeout(fn)) || this); 114 | }, 115 | $proxy(fn, tm) { 116 | var me = this; 117 | return { 118 | _fn(a, d) { this.cancel() || (d ? (this._r = me.$timeout(fn, tm, a)) : me.$call(fn, a)); }, 119 | call() { this._fn(arguments); }, 120 | delay() { this._fn(arguments, 1); }, 121 | cancel() { me.$timeout(this._r); delete this._r; }, 122 | }; 123 | }, 124 | $def(v, o) { return this.$ext(v, o, 0); }, 125 | $xdef(v, o) { return this.$ext(v, o, 0, 1); }, 126 | $static(v, o) { return this.$ext(v, o, 1); }, 127 | $ext(v, o, s, e) { 128 | for (var k in v) { this.$hasProp(k, v) && this.$getter(k, v[k], s, e, o); } 129 | return this; 130 | }, 131 | $getter(k, v, s, e, o) { 132 | o = o || this; 133 | if (!s && k[0] === '_') k = k.substring(1); 134 | if (!s) o['_' + k] = v; 135 | if (s || (e && o[k] == null)) Object.defineProperty(o, k, s ? {value: v} : {get() { return this['_' + k]; }}); 136 | return this; 137 | }, 138 | $getElem(k) { return document.getElementById(k); }, 139 | $hasElem(k) { return !!this.$getElem(k); }, 140 | $makeElem(t, o) { 141 | t = document.createElement(t); 142 | if (o) for (var k in o) { if (this.$hasProp(k, o)) t[k] = o[k]; } 143 | return t; 144 | }, 145 | $loadJs(s, o) { 146 | o = this.$assign(o || {}, {src: s, type: 'text/javascript', 'async': true}); 147 | document.getElementsByTagName('body')[0].appendChild(this.$makeElem('script', o)); 148 | return this; 149 | }, 150 | $loadCss(h, o) { 151 | o = this.$assign(o || {}, {href: h, type: 'text/css', rel: 'stylesheet', 'async': true}); 152 | document.getElementsByTagName('head')[0].appendChild(this.$makeElem('link', o)); 153 | return this; 154 | }, 155 | $load(c, o) { 156 | o = this.$assign(o || {}, {innerHTML: c, type: 'text/css'}); 157 | document.getElementsByTagName('head')[0].appendChild(this.$makeElem('style', o)); 158 | return this; 159 | } 160 | }; 161 | 162 | 163 | class LevelUpCore { 164 | destroy() { for (var k in this) { if (this.$hasProp(k)) delete this[k]; } } 165 | } 166 | LU.$extend(LevelUpCore.prototype, LU); 167 | frappe.LevelUpCore = LevelUpCore; 168 | 169 | 170 | class LevelUpBase extends LevelUpCore { 171 | constructor(mod, key, doc, ns) { 172 | super(); 173 | this._mod = mod; 174 | this._key = key; 175 | this._tmp = '_' + this._key; 176 | this._doc = new RegExp('^' + doc); 177 | this._real = this._key + '_'; 178 | this._pfx = '[' + this._key.toUpperCase() + ']'; 179 | this._ns = ns + (!ns.endsWith('.') ? '.' : ''); 180 | this._prod = 0; 181 | this._exit = 0; 182 | this._events = { 183 | list: {}, 184 | real: {}, 185 | once: 'ready page_change page_clean destroy after_destroy'.split(' ') 186 | }; 187 | } 188 | get module() { return this._mod; } 189 | get key() { return this._key; } 190 | get is_debug() { return !this._prod; } 191 | $alert(t, m, i, x) { 192 | m == null && (m = t) && (t = this._mod); 193 | t = this.$assign({title: t, indicator: i}, this.$isBaseObj(m) ? m : {message: m, as_list: this.$isArr(m)}); 194 | this.call('on_alert', t, x); 195 | (x === 'fatal' && (this._err = 1) ? frappe.throw : frappe.msgprint)(t); 196 | return this; 197 | } 198 | debug(t, m) { return this.is_debug ? this.$alert(t, m, 'gray', 'debug') : this; } 199 | log(t, m) { return this.is_debug ? this.$alert(t, m, 'cyan', 'log') : this; } 200 | info(t, m) { return this.$alert(t, m, 'light-blue', 'info'); } 201 | warn(t, m) { return this.$alert(t, m, 'orange', 'warn'); } 202 | error(t, m) { return this.$alert(t, m, 'red', 'error'); } 203 | fatal(t, m) { return this.$alert(t, m, 'red', 'fatal'); } 204 | $toast(m, i, s, a) { 205 | this.$isBaseObj(s) && (a = s) && (s = 0); 206 | frappe.show_alert(this.$assign({indicator: i}, this.$isBaseObj(m) ? m : {message: m}), s || 4, a); 207 | return this; 208 | } 209 | success_(m, s, a) { return this.$toast(m, 'green', s, a); } 210 | info_(m, s, a) { return this.$toast(m, 'blue', s, a); } 211 | warn_(m, s, a) { return this.$toast(m, 'orange', s, a); } 212 | error_(m, s, a) { return this.$toast(m, 'red', s, a); } 213 | $console(fn, a) { 214 | if (!this.is_debug) return this; 215 | !this.$isStr(a[0]) ? Array.prototype.unshift.call(a, this._pfx) 216 | : (a[0] = (this._pfx + ' ' + a[0]).trim()); 217 | (console[fn] || console.log).apply(null, a); 218 | return this; 219 | } 220 | _debug() { return this.$console('debug', arguments); } 221 | _log() { return this.$console('log', arguments); } 222 | _info() { return this.$console('info', arguments); } 223 | _warn() { return this.$console('warn', arguments); } 224 | _error() { return this.$console('error', arguments); } 225 | ajax(u, o, s, f) { 226 | o = this.$extend(1, { 227 | url: u, method: 'GET', cache: false, 'async': true, crossDomain: true, 228 | headers: {'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest'}, 229 | success: this.$fn(function(r, t) { 230 | if (this._exit) return; 231 | (s = this.$fn(s)) && s(r, t); 232 | }), 233 | error: this.$fn(function(r, t) { 234 | if (this._exit) return; 235 | r = this.$isStrVal(r) ? __(r) : (this.$isStrVal(t) ? __(t) 236 | : __('The ajax request sent raised an error.')); 237 | (f = this.$fn(f)) ? f({message: r}) : this._error(r); 238 | }) 239 | }, o); 240 | if (o.contentType == null) 241 | o.contentType = 'application/' + (o.method == 'post' ? 'json' : 'x-www-form-urlencoded') + '; charset=utf-8'; 242 | this.call('on_ajax', o); 243 | try { $.ajax(o); } catch(e) { 244 | (f = this.$fn(f)) ? f(e) : this._error(e.message); 245 | if (this._err) throw e; 246 | } finally { this._err = 0; } 247 | return this; 248 | } 249 | get_method(v) { return this._ns + v; } 250 | request(m, a, s, f, x) { 251 | s = this.$fn(s); 252 | f = this.$fn(f); 253 | var d = { 254 | method: m.includes('.') ? m : this.get_method(m), 255 | callback: this.$fn(function(r) { 256 | if (!x && this._exit) return; 257 | r = (this.$isBaseObj(r) && r.message) || r; 258 | if (!this.$isBaseObj(r) || !r.error) return s && s(r); 259 | if (!this.$isBaseObj(r)) r = {}; 260 | var k = ['list', 'message', 'error', 'self'], 261 | m = r[k[0]] != null ? r[k[0]] : (r[k[1]] != null ? r[k[1]] : (r[k[2]] != null ? r[k[2]] : null)); 262 | m = this.$isArrVal(m) ? this.$map(m, function(v) { return __(v); }).join('\n') : (this.$isStrVal(m) ? __(m) : ''); 263 | if (!m.trim().length) m = __('The request sent returned an invalid response.'); 264 | if (f) r = this.$assign({message: m, list: m.split('\n'), self: 1}, this.$filter(r, function(_, x) { return !k.includes(x); })); 265 | f ? f(r) : this._error(m); 266 | }), 267 | error: this.$fn(function(r, t) { 268 | if (!x && this._exit) return; 269 | r = this.$isStrVal(r) ? __(r) : (this.$isStrVal(t) ? __(t) : __('The request sent raised an error.')); 270 | f ? f({message: r}) : this._error(r); 271 | }) 272 | }; 273 | this.$isBaseObj(a) && this.call('on_request', a); 274 | this.$isBaseObjVal(a) && this.$assign(d, {type: 'POST', args: a}); 275 | try { frappe.call(d); } catch(e) { 276 | f ? f(e) : this._error(e.message); 277 | if (this._err) throw e; 278 | } finally { this._err = 0; } 279 | return this; 280 | } 281 | on(e, fn) { return this._on(e, fn); } 282 | xon(e, fn) { return this._on(e, fn, 0, 1); } 283 | once(e, fn) { return this._on(e, fn, 1); } 284 | xonce(e, fn) { return this._on(e, fn, 1, 1); } 285 | real(e, fn, n) { return this._on(e, fn, n, 0, 1); } 286 | xreal(e, fn, n) { return this._on(e, fn, n, 1, 1); } 287 | off(e, fn, rl) { 288 | if (e == null) return this._off(); 289 | if (this.$isBoolLike(e)) return this._off(0, 1); 290 | e = e.trim().split(' '); 291 | for (var i = 0, l = e.length, ev; i < l; i++) 292 | (ev = (rl ? this._real : '') + e[i]) && this._events.list[ev] && this._off(ev, fn); 293 | return this; 294 | } 295 | emit(e) { 296 | e = e.trim().split(' '); 297 | for (var a = this.$toArr(arguments, 1), p = Promise.resolve(), i = 0, l = e.length; i < l; i++) 298 | this._events.list[e[i]] && this._emit(e[i], a, p); 299 | return this; 300 | } 301 | call(e) { 302 | e = e.trim().split(' '); 303 | for (var a = this.$toArr(arguments, 1), i = 0, l = e.length; i < l; i++) 304 | this._events.list[e[i]] && this._emit(e[i], a); 305 | return this; 306 | } 307 | _on(ev, fn, o, s, r) { 308 | ev = ev.trim().split(' '); 309 | var rd = []; 310 | for (var es = this._events, i = 0, l = ev.length, e; i < l; i++) { 311 | e = (r ? this._real : '') + ev[i]; 312 | e === es.once[0] && this._is_ready && rd.push(es.once[0]); 313 | !s && es.once.includes(e) && (o = 1); 314 | !es.list[e] && (es.list[e] = []) && (!r || frappe.realtime.on(e, (es.real[e] = this._rfn(e)))); 315 | es.list[e].push({f: fn, o, s}); 316 | } 317 | return rd.length ? this.emit(rd.join(' ')) : this; 318 | } 319 | _rfn(e) { 320 | return this.$fn(function(r) { 321 | (r = (this.$isBaseObj(r) && r.message) || r) && this._emit(e, r != null ? [r] : r, Promise.wait(300)); 322 | return true; 323 | }); 324 | } 325 | _off(e, fn) { 326 | if (e && fn) this._del(e, fn); 327 | else if (!e && fn) for (var ev in this._events.list) { this._del(ev); } 328 | else { 329 | var es = this._events; 330 | es.real[e] && frappe.realtime.off(e, es.real[e]); 331 | delete es.list[e]; 332 | delete es.real[e]; 333 | } 334 | return this; 335 | } 336 | _del(e, fn) { 337 | var ev = this._events.list[e].slice(), ret = []; 338 | for (var x = 0, i = 0, l = ev.length; i < l; i++) 339 | (fn ? ev[i].f !== fn : ev[i].s) && (ret[x++] = ev[i]); 340 | !ret.length ? this._off(e) : (this._events.list[e] = ret); 341 | } 342 | _emit(e, a, p) { 343 | var ev = this._events.list[e].slice(), ret = []; 344 | p && p.catch(this.$fn(function(z) { this._error('Events emit', e, a, z.message); })); 345 | for (var x = 0, i = 0, l = ev.length; i < l; i++) { 346 | p ? p.then(this.$xtry(ev[i].f, a)) : this.$try(ev[i].f, a); 347 | !ev[i].o && (ret[x++] = ev[i]); 348 | } 349 | !ret.length ? this._off(e) : (this._events.list[e] = ret); 350 | } 351 | } 352 | 353 | 354 | var LUR = { 355 | on(o) { 356 | for (var k = ['router', 'route'], i = 0, l = k.length; i < l; i++) 357 | (o._router.obj = frappe[k[i]]) && (i < 1 || (o._router.old = 1)); 358 | this._reg(o, 'on'); 359 | this.get(o); 360 | }, 361 | off(o) { this._reg(o, 'off'); o._router.obj = o._router.old = null; }, 362 | get(o) { 363 | var d = ['app'], v; 364 | try { v = !o._router.old ? frappe.get_route() : (o._router.obj ? o._router.obj.parse() : null); } catch(_) {} 365 | v = LU.$isArrVal(v) ? LU.$filter(LU.$map(v, function(z) { return (z = cstr(z).trim()).length && !/(\#|\?|\&)$/.test(z) ? z : null; })) : d; 366 | if (v.length) v[0] = v[0].toLowerCase(); 367 | var r = 0; 368 | for (var i = 0, l = v.length; i < l; i++) 369 | if ((!o._router.val || o._router.val.indexOf(v[i]) !== i) && ++r) break; 370 | if (r) o._router.val = v; 371 | return r > 0; 372 | }, 373 | _reg(o, a) { 374 | o._router.obj && LU.$isFunc(o._router.obj[a]) && o._router.obj[a]('change', o._win.e.change); 375 | }, 376 | }, 377 | LUF = { 378 | has_flow(f) { try { return f && !f.is_new() && f.states && !!f.states.get_state(); } catch(_) {} }, 379 | is_field(f) { return f && f.df && !/^((Tab|Section|Column) Break|Table)$/.test(f.df.fieldtype); }, 380 | is_table(f) { return f && f.df && f.df.fieldtype === 'Table'; }, 381 | }, 382 | LUC = { 383 | get(f, k, g, r, c, d) { 384 | try { 385 | f = f.get_field(k); 386 | if (g || r != null) f = f.grid; 387 | if (r != null) f = f.get_row(r); 388 | if (c) f = (r != null && d && f.grid_form.fields_dict[c]) || f.get_field(c); 389 | return f; 390 | } catch(_) {} 391 | }, 392 | reload(f, k, r, c) { 393 | if (r != null && !c) { 394 | try { (LU.$isNum(r) || LU.$isStrVal(r)) && (f = this.get(f, k, 1)) && f.refresh_row(r); } catch(_) {} 395 | return; 396 | } 397 | if (r != null && c) { 398 | if (!LU.$isStrVal(c) || !(f = this.get(f, k, 1, r))) return; 399 | try { 400 | (r = f.on_grid_fields_dict[c]) && r.refresh && r.refresh(); 401 | (r = f.grid_form && f.grid_form.refresh_field) && r(c); 402 | } catch(_) {} 403 | return; 404 | } 405 | if (!c) { 406 | try { LU.$isStrVal(k) && f.refresh_field && f.refresh_field(k); } catch(_) {} 407 | return; 408 | } 409 | if (!LU.$isStrVal(c) || !(f = this.get(f, k, 1)) || !LU.$isArrVal(f.grid_rows)) return; 410 | try { 411 | for (var i = 0, l = f.grid_rows, x; i < l; i++) { 412 | r = f.grid_rows[i]; 413 | (x = r.on_grid_fields_dict[c]) && x.refresh && x.refresh(); 414 | (x = r.grid_form && r.grid_form.refresh_field) && x(c); 415 | } 416 | } catch(_) {} 417 | }, 418 | prop(f, k, g, r, c, p, v) { 419 | if (LU.$isBaseObj(k)) for (var x in k) { this.prop(f, x, g, r, c, k[x]); } 420 | else if (LU.$isBaseObj(c)) for (var x in c) { this.prop(f, k, g, r, x, c[x]); } 421 | else { 422 | (g || r != null) && (f = this.get(f, k, g, r)) && (k = c); 423 | var m = r != null ? 'set_field_property' : (g ? 'update_docfield_property' : 'set_df_property'); 424 | try { 425 | if (!LU.$isBaseObj(p)) f[m](k, p, v); 426 | else for (var x in p) { f[m](k, x, p[x]); } 427 | g && r == null && f.debounced_refresh(); 428 | } catch(_) {} 429 | } 430 | }, 431 | toggle(f, k, g, r, c, e, i) { 432 | var tf = this.get(f, k, g, r, c, 1); 433 | e = e ? 0 : 1; 434 | if (!tf || !tf.df || cint(tf.df.hidden) || (i && tf.df._ignore) || cint(tf.df.read_only) === e) return; 435 | this.prop(f, k, g, r, c, 'read_only', e); 436 | try { 437 | tf.df.translatable && tf.$wrapper 438 | && (tf = tf.$wrapper.find('.clearfix .btn-translation')) && tf.hidden(e ? 0 : 1); 439 | } catch(_) {} 440 | }, 441 | get_desc(f, k, g, r, c, b) { 442 | k && (f = this.get(f, k, g, r, c, 1)); 443 | return cstr((f && f.df && ((b && f.df._description) || (!b && f.df.description))) || ''); 444 | }, 445 | desc(f, k, g, r, c, m) { 446 | var x = 0; 447 | k && (f = this.get(f, k, g, r, c, 1)); 448 | if (f.df._description == null) f.df._description = f.df.description || ''; 449 | if (!LU.$isStr(m)) m = ''; 450 | try { 451 | if (m.length && f.set_new_description) ++x && f.set_new_description(m); 452 | else if (f.set_description) { 453 | if (!m.length) { m = f.df._description || ''; delete f.df._description; } 454 | ++x && f.set_description(m); 455 | } 456 | f.toggle_description && f.toggle_description(m.length > 0); 457 | } catch(_) {} 458 | return x; 459 | }, 460 | status(f, k, g, r, c, m) { 461 | var v = LU.$isStrVal(m), tf = this.get(f, k, g, r, c, 1), x = 0; 462 | if ((!v && tf.df.invalid) || (v && !tf.df.invalid)) 463 | try { 464 | ++x && ((tf.df.invalid = v ? 1 : 0) || 1) && tf.set_invalid && tf.set_invalid(); 465 | } catch(_) {} 466 | this.desc(tf, 0, 0, null, 0, m) && x++; 467 | x && this.reload(f, k, r, c); 468 | }, 469 | }, 470 | LUT = { 471 | setup(f, k) { 472 | cint(k.df.read_only) && (k.df._ignore = 1); 473 | for (var ds = this._fields(this._grid(f, k.df.fieldname)), i = 0, l = ds.length, d; i < l; i++) 474 | (d = ds[i]) && cint(d.read_only) && (d._ignore = 1); 475 | }, 476 | toggle(f, k, e, o, i) { 477 | var tf = this._grid(f, k, i), x; 478 | if (!tf) return; 479 | x = !e || !!tf._; 480 | x && (!o || !o.add) && this.toggle_add(tf, 0, e); 481 | x && (!o || !o.del) && this.toggle_del(tf, 0, e); 482 | x && (!o || !o.edit) && this.toggle_edit(tf, 0, o && o.keep, e); 483 | x && (!o || !o.sort) && this.toggle_sort(tf, 0, e); 484 | LUC.reload(f, k); 485 | x && (!o || !o.del) && this.toggle_check(tf, 0, e); 486 | if (e && tf._) delete tf._; 487 | }, 488 | toggle_add(f, k, e) { 489 | var tf = k ? this._grid(f, k) : f; 490 | if (!tf) return; 491 | if (e) { 492 | (!tf._ || tf._.add != null) && (tf.df.cannot_add_rows = tf._ ? tf._.add : false); 493 | if (k && tf._) delete tf._.add; 494 | } else { 495 | (tf._ = tf._ || {}) && tf._.add == null && (tf._.add = tf.df.cannot_add_rows); 496 | tf.df.cannot_add_rows = true; 497 | } 498 | k && LUC.reload(f, k); 499 | return 1; 500 | }, 501 | toggle_del(f, k, e) { 502 | var tf = k ? this._grid(f, k) : f; 503 | if (!tf) return; 504 | if (e) { 505 | (!tf._ || tf._.del != null) && (tf.df.cannot_delete_rows = tf._ ? tf._.del : false); 506 | if (k && tf._) delete tf._.del; 507 | } else { 508 | (tf._ = tf._ || {}) && tf._.del == null && (tf._.del = tf.df.cannot_delete_rows); 509 | tf.df.cannot_delete_rows = true; 510 | } 511 | k && LUC.reload(f, k); 512 | k && this.toggle_check(tf, 0, e); 513 | return 1; 514 | }, 515 | toggle_edit(f, k, g, e) { 516 | var tf = k ? this._grid(f, k) : f; 517 | if (!tf) return; 518 | if (e) { 519 | (!tf._ || tf._.edit != null) && (tf.df.in_place_edit = tf._ ? tf._.edit : true); 520 | tf._ && tf._.grid != null && tf.meta && (tf.meta.editable_grid = tf._.grid); 521 | (!tf._ || tf._.static != null) && (tf.static_rows = tf._ ? tf._.static : false); 522 | if (tf._ && tf._.read && tf._.read.length) { 523 | for (var ds = this._fields(tf), i = 0, l = ds.length, d; i < l; i++) 524 | (d = ds[i]) && !d._ignore && tf._.read.includes(d.fieldname) && (d.read_only = 0); 525 | } 526 | if (k && tf._) for (var x = ['edit', 'grid', 'static', 'read'], i = 0; i < 4; i++) { delete tf._[x[i]]; } 527 | k && LUC.reload(f, k); 528 | return 1; 529 | } 530 | (tf._ = tf._ || {}) && tf._.edit == null && (tf._.edit = tf.df.in_place_edit); 531 | tf.df.in_place_edit = false; 532 | tf.meta && tf._.grid == null && (tf._.grid = tf.meta.editable_grid); 533 | tf.meta && (tf.meta.editable_grid = false); 534 | tf._.static == null && (tf._.static = tf.static_rows); 535 | tf.static_rows = true; 536 | tf._.read == null && (tf._.read = []); 537 | for (var ds = this._fields(tf), i = 0, x = 0, l = ds.length, d; i < l; i++) 538 | (d = ds[i]) && !d._ignore && !tf._.read.includes(d.fieldname) 539 | && (!g || !g.includes(d.fieldname)) && (d.read_only = 1) 540 | && (tf._.read[x++] = d.fieldname); 541 | k && LUC.reload(f, k); 542 | return 1; 543 | }, 544 | toggle_sort(f, k, e) { 545 | var tf = k ? this._grid(f, k) : f; 546 | if (!tf) return; 547 | if (e) { 548 | (!tf._ || tf._.sort != null) && (tf.sortable_status = tf._ ? tf._.sort : true); 549 | if (k && tf._) delete tf._.sort; 550 | } else { 551 | (tf._ = tf._ || {}) && tf._.sort == null && (tf._.sort = tf.sortable_status); 552 | tf.sortable_status = false; 553 | } 554 | k && LUC.reload(f, k); 555 | return 1; 556 | }, 557 | toggle_check(f, k, e) { 558 | var tf = k ? this._grid(f, k) : f; 559 | if (!tf) return; 560 | if (e) { 561 | (!tf._ || tf._.check) && tf.toggle_checkboxes(1); 562 | if (k && tf._) delete tf._.check; 563 | } else { 564 | (tf._ = tf._ || {}) && (tf._.check = 1) && tf.toggle_checkboxes(0); 565 | } 566 | return 1; 567 | }, 568 | _grid(f, k, i) { 569 | return ((!k && (f = f.grid)) || (f = LUC.get(f, k, 1))) && !cint(f.df.hidden) && (!i || !f.df._ignore) && f; 570 | }, 571 | _fields(f) { 572 | var ds = []; 573 | if (LU.$isArrVal(f.grid_rows)) { 574 | for (var i = 0, l = f.grid_rows.length, r; i < l; i++) 575 | (r = f.grid_rows[i]) && LU.$isArrVal(r.docfields) && ds.push.apply(ds, r.docfields); 576 | } 577 | LU.$isArrVal(f.docfields) && ds.push.apply(ds, f.docfields); 578 | return ds; 579 | }, 580 | }; 581 | 582 | 583 | class LevelUp extends LevelUpBase { 584 | constructor(mod, key, doc, ns) { 585 | super(mod, key, doc, ns); 586 | this.$xdef({is_enabled: true}); 587 | this._router = {obj: null, old: 0, val: null}; 588 | this._win = { 589 | e: { 590 | unload: this.$fn(this.destroy), 591 | change: this.$fn(function() { !this._win.c && this._win.fn(); }), 592 | }, 593 | c: 0, 594 | fn: this.$fn(function() { 595 | if (this._win.c || !LUR.get(this)) return; 596 | this._win.c++; 597 | this._exit++; 598 | this.emit('page_change page_clean'); 599 | this.$timeout(function() { 600 | this._win.c--; 601 | this._exit--; 602 | }, 2000); 603 | }), 604 | }; 605 | addEventListener('beforeunload', this._win.e.unload); 606 | LUR.on(this); 607 | } 608 | options(opts) { return this.$static(opts); } 609 | destroy() { 610 | this._win.fn.cancel(); 611 | LUR.off(this); 612 | removeEventListener('beforeunload', this._win.e.unload); 613 | this.emit('page_clean destroy after_destroy').off(1); 614 | super.destroy(); 615 | } 616 | route(i) { return this._router.val[i] || this._router.val[0]; } 617 | get is_list() { return this.route(0) === 'list'; } 618 | get is_tree() { return this.route(0) === 'tree'; } 619 | get is_form() { return this.route(0) === 'form'; } 620 | get is_self() { return this._doc.test(this.route(1)); } 621 | is_doctype(v) { return this.route(1) === v; } 622 | _is_self_view(f) { return this._doc.test((f && f.doctype) || this.route(1)); } 623 | get_list(f) { return this.$isObjLike((f = f || cur_list)) ? f : null; } 624 | get_tree(f) { return this.$isObjLike((f = f || cur_tree)) ? f : null; } 625 | get_form(f) { return this.$isObjLike((f = f || cur_frm)) ? f : null; } 626 | get app_disabled_note() { return __('{0} app is disabled.', [this._mod]); } 627 | setup_list(f) { 628 | if (!this.is_list || !(f = this.get_list(f)) || !this._is_self_view(f)) return this; 629 | var n = !f[this._tmp]; 630 | if (this._is_enabled) this.enable_list(f); 631 | else this.disable_list(f, this.app_disabled_note); 632 | n && this.off('page_clean').once('page_clean', function() { this.enable_list(f); }); 633 | return this; 634 | } 635 | enable_list(f) { 636 | if (!(f = this.get_list(f)) || (f[this._tmp] && !f[this._tmp].disabled)) return this; 637 | var k = 'toggle_actions_menu_button'; 638 | f[this._tmp] && f[this._tmp][k] && (f[k] = f[this._tmp][k]); 639 | f.page.clear_inner_toolbar(); 640 | f.set_primary_action(); 641 | delete f[this._tmp]; 642 | return this; 643 | } 644 | disable_list(f, m) { 645 | if (!(f = this.get_list(f)) || (f[this._tmp] && f[this._tmp].disabled)) return this; 646 | f.page.hide_actions_menu(); 647 | f.page.clear_primary_action(); 648 | f.page.clear_inner_toolbar(); 649 | m && f.page.add_inner_message(m).removeClass('text-muted').addClass('text-danger'); 650 | var k = 'toggle_actions_menu_button'; 651 | !f[this._tmp] && (f[this._tmp] = {}); 652 | (f[this._tmp][k] = f[k]) && (f[k] = function() {}) && (f[this._tmp].disabled = 1); 653 | return this; 654 | } 655 | setup_tree(f) { 656 | if (!this.is_tree || !(f = this.get_tree(f)) || !this._is_self_view(f)) return this; 657 | var n = !f[this._tmp]; 658 | if (this._is_enabled) this.enable_tree(f); 659 | else this.disable_tree(f, this.app_disabled_note); 660 | n && this.$xdef({tree: f}).off('page_clean').once('page_clean', function() { this.$xdef({tree: null}).enable_tree(f); }); 661 | return this; 662 | } 663 | enable_tree(f) { 664 | if (!(f = this.get_tree(f)) || (f[this._tmp] && !f[this._tmp].disabled)) return this; 665 | var k = 'can_create'; 666 | f[this._tmp] && f[this._tmp][k] && (f[k] = f[this._tmp][k]); 667 | f.page.clear_inner_toolbar(); 668 | f.set_primary_action(); 669 | delete f[this._tmp]; 670 | f.refresh(); 671 | return this; 672 | } 673 | disable_tree(f, m) { 674 | if (!(f = this.get_tree(f)) || (f[this._tmp] && f[this._tmp].disabled)) return this; 675 | f.page.hide_actions_menu(); 676 | f.page.clear_primary_action(); 677 | f.page.clear_inner_toolbar(); 678 | m && f.page.add_inner_message(m).removeClass('text-muted').addClass('text-danger'); 679 | var k = 'can_create'; 680 | !f[this._tmp] && (f[this._tmp] = {}); 681 | (f[this._tmp][k] = f[k]) && (f[k] = false) && (f[this._tmp].disabled = 1); 682 | f.refresh(); 683 | return this; 684 | } 685 | setup_form(f) { 686 | if (!this.is_form || !(f = this.get_form(f)) || !this._is_self_view(f)) return this; 687 | var n = !f[this._tmp]; 688 | if (n && this.$isArrVal(f.fields)) 689 | try { 690 | for (var i = 0, l = f.fields.length, c; i < l; i++) { 691 | if (!(c = f.fields[i])) continue; 692 | if (LUF.is_table(c)) LUT.setup(f, c); 693 | else if (LUF.is_field(c) && cint(c.df.read_only)) c.df._ignore = 1; 694 | } 695 | } catch(_) {} 696 | if (this._is_enabled) this.enable_form(f); 697 | else this.disable_form(f, {message: this.app_disabled_note}); 698 | n && this.off('page_clean').once('page_clean', function() { this.enable_form(f); }); 699 | return this; 700 | } 701 | enable_form(f) { 702 | if (!(f = this.get_form(f)) || (f[this._tmp] && !f[this._tmp].disabled)) return this; 703 | try { 704 | if (this.$isArrVal(f.fields)) 705 | for (var i = 0, l = f.fields.length, c; i < l; i++) { 706 | if (!(c = f.fields[i]) || !c.df.fieldname) continue; 707 | if (LUF.is_table(c)) LUT.toggle(f, c.df.fieldname, 1, 0, 1); 708 | else if (LUF.is_field(c)) LUC.toggle(f, c.df.fieldname, 0, null, 0, 1, 1); 709 | } 710 | LUF.has_flow(f) ? f.page.show_actions_menu() : f.enable_save(); 711 | f.set_intro(); 712 | } catch(e) { this._error('Enable form', e.message, e.stack); } 713 | finally { 714 | delete f[this._tmp]; 715 | this.emit('form_enabled', f); 716 | } 717 | return this; 718 | } 719 | disable_form(f, o) { 720 | if (!(f = this.get_form(f)) || (f[this._tmp] && f[this._tmp].disabled)) return this; 721 | o = this.$assign({ignore: []}, o); 722 | try { 723 | if (this.$isArrVal(f.fields)) 724 | for (var i = 0, l = f.fields.length, c; i < l; i++) { 725 | if (!(c = f.fields[i]) || !c.df.fieldname || o.ignore.includes(c.df.fieldname)) continue; 726 | if (LUF.is_table(c)) LUT.toggle(f, c.df.fieldname, 0, 0, 1); 727 | else if (LUF.is_field(c)) LUC.toggle(f, c.df.fieldname, 0, null, 0, 0, 1); 728 | } 729 | LUF.has_flow(f) ? f.page.hide_actions_menu() : f.disable_save(); 730 | if (o.message) f.set_intro(o.message, o.color || 'red'); 731 | } catch(e) { this._error('Disable form', e.message, e.stack); } 732 | finally { 733 | (f[this._tmp] || (f[this._tmp] = {})) && (f[this._tmp].disabled = 1); 734 | this.emit('form_disabled', f); 735 | } 736 | return this; 737 | } 738 | get_field(f, k) { if ((f = this.get_form(f))) return LUC.get(f, k); } 739 | get_grid(f, k) { if ((f = this.get_form(f))) return LUC.get(f, k, 1); } 740 | get_tfield(f, k, c) { if ((f = this.get_form(f))) return LUC.get(f, k, 1, null, c); } 741 | get_row(f, k, r) { if ((f = this.get_form(f))) return LUC.get(f, k, 1, r); } 742 | get_rfield(f, k, r, c) { if ((f = this.get_form(f))) return LUC.get(f, k, 1, r, c); } 743 | get_rmfield(f, k, r, c) { if ((f = this.get_form(f))) return LUC.get(f, k, 1, r, c, 1); } 744 | reload_field(f, k) { 745 | (f = this.get_form(f)) && LUC.reload(f, k); 746 | return this; 747 | } 748 | reload_tfield(f, k, c) { 749 | (f = this.get_form(f)) && LUC.reload(f, k, null, c); 750 | return this; 751 | } 752 | reload_row(f, k, r) { 753 | (f = this.get_form(f)) && LUC.reload(f, k, r); 754 | return this; 755 | } 756 | reload_rfield(f, k, r, c) { 757 | (f = this.get_form(f)) && LUC.reload(f, k, r, c); 758 | return this; 759 | } 760 | field_prop(f, k, p, v) { 761 | (f = this.get_form(f)) && LUC.prop(f, k, 0, null, 0, p, v); 762 | return this; 763 | } 764 | tfield_prop(f, k, c, p, v) { 765 | (f = this.get_form(f)) && LUC.prop(f, k, 1, null, c, p, v); 766 | return this; 767 | } 768 | rfield_prop(f, k, r, c, p, v) { 769 | (f = this.get_form(f)) && LUC.prop(f, k, 1, r, c, p, v); 770 | return this; 771 | } 772 | toggle_field(f, k, e) { 773 | (f = this.get_form(f)) && LUC.toggle(f, k, 0, null, 0, e); 774 | return this; 775 | } 776 | toggle_table(f, k, e, o) { 777 | (f = this.get_form(f)) && LUT.toggle(f, k, e ? 1 : 0, o); 778 | return this; 779 | } 780 | toggle_tfield(f, k, c, e) { 781 | (f = this.get_form(f)) && LUC.toggle(f, k, 1, null, c, e); 782 | return this; 783 | } 784 | toggle_rfield(f, k, r, c, e) { 785 | (f = this.get_form(f)) && LUC.toggle(f, k, 1, r, c, e); 786 | return this; 787 | } 788 | table_add(f, k, e) { 789 | (f = this.get_form(f)) && LUT.toggle_add(f, k, e ? 1 : 0); 790 | return this; 791 | } 792 | table_edit(f, k, i, e) { 793 | if (!this.$isArrVal(i)) { 794 | if (e == null) e = i; 795 | i = null; 796 | } 797 | (f = this.get_form(f)) && LUT.toggle_edit(f, k, i, e ? 1 : 0); 798 | return this; 799 | } 800 | table_del(f, k, e) { 801 | (f = this.get_form(f)) && LUT.toggle_del(f, k, e ? 1 : 0); 802 | return this; 803 | } 804 | table_sort(f, k, e) { 805 | (f = this.get_form(f)) && LUT.toggle_sort(f, k, e ? 1 : 0); 806 | return this; 807 | } 808 | field_view(f, k, s) { return this.field_prop(f, k, 'hidden', s ? 0 : 1); } 809 | tfield_view(f, k, c, s) { return this.tfield_prop(f, k, c, 'hidden', s ? 0 : 1); } 810 | rfield_view(f, k, r, c, s) { return this.rfield_prop(f, k, r, c, 'hidden', s ? 0 : 1); } 811 | field_desc(f, k, m) { 812 | (f = this.get_form(f)) && LUC.desc(f, k, 0, null, 0, m); 813 | return this; 814 | } 815 | tfield_desc(f, k, c, m) { 816 | (f = this.get_form(f)) && LUC.desc(f, k, 1, null, c, m); 817 | return this; 818 | } 819 | rfield_desc(f, k, r, c, m) { 820 | (f = this.get_form(f)) && LUC.desc(f, k, 1, r, c, m); 821 | return this; 822 | } 823 | get_field_desc(f, k, b) { 824 | return ((f = this.get_form(f)) && LUC.get_desc(f, k, 0, null, 0, b)) || ''; 825 | } 826 | get_tfield_desc(f, k, c, b) { 827 | return ((f = this.get_form(f)) && LUC.get_desc(f, k, 1, null, c, b)) || ''; 828 | } 829 | get_rfield_desc(f, k, r, c, b) { 830 | return ((f = this.get_form(f)) && LUC.get_desc(f, k, 1, r, c, b)) || ''; 831 | } 832 | field_status(f, k, m) { 833 | (f = this.get_form(f)) && LUC.status(f, k, 0, null, 0, m); 834 | return this; 835 | } 836 | tfield_status(f, k, c, m) { 837 | (f = this.get_form(f)) && LUC.status(f, k, 1, null, c, m); 838 | return this; 839 | } 840 | rfield_status(f, k, r, c, m) { 841 | (f = this.get_form(f)) && LUC.status(f, k, 1, r, c, m); 842 | return this; 843 | } 844 | } 845 | 846 | 847 | class Alerts extends LevelUp { 848 | constructor() { 849 | super(__('Alerts'), 'alerts', 'Alert', 'alerts.utils'); 850 | this.$xdef({ 851 | id: frappe.utils.get_random(5), 852 | is_ready: 0, 853 | is_enabled: 0, 854 | use_fallback_sync: 0, 855 | fallback_sync_delay: 5, 856 | }); 857 | this._dialog = null; 858 | this._is_init = 0; 859 | this._is_first = 1; 860 | this._in_req = 0; 861 | this._styles = {}; 862 | this._list = []; 863 | this._seen = []; 864 | this._mock = null; 865 | 866 | this.request('get_settings', null, this._setup, null, true); 867 | } 868 | get has_alerts() { return this._list.length > 0; } 869 | get _has_seen() { return this._seen.length > 0; } 870 | mock() { return (this._mock || (this._mock = new AlertsMock())).show(); } 871 | show() { return this.has_alerts ? this._render() : this; } 872 | _setup(opts) { 873 | this._is_ready = 1; 874 | this._options(opts); 875 | this.xon('page_change', function() { 876 | this.off('change on_alert'); 877 | this._is_enabled && this._init(); 878 | }) 879 | .xreal('status_changed', function(ret, sync) { 880 | if (!this.$isBaseObj(ret)) return this._error('Invalid status change event data.', ret); 881 | var old = this._is_enabled; 882 | this._options(ret); 883 | if (this._is_enabled !== old) this.emit('change'); 884 | this._is_enabled && !sync && this._init(); 885 | if (!sync) this._queue_sync(); 886 | }) 887 | .xreal('type_changed', function(ret) { 888 | this._debug('real type_changed', ret); 889 | if (!this.$isDataObjVal(ret)) return; 890 | var name = cstr(ret.name); 891 | if (cstr(ret.action) === 'trash') { 892 | delete this._types[name]; 893 | delete this._styles[name]; 894 | } else { 895 | this._types[name] = { 896 | name: name, 897 | priority: cint(ret.display_priority), 898 | timeout: cint(ret.display_timeout), 899 | sound: cstr(ret.display_sound), 900 | custom_sound: cstr(ret.custom_display_sound) 901 | }; 902 | if (!this._styles[name]) 903 | this._styles[name] = new AlertsStyle(this._id, this._slug(name)); 904 | 905 | this._styles[name].build(ret); 906 | } 907 | }) 908 | .xreal('show_alert', function(ret) { 909 | this._debug('real show_alert', ret); 910 | if (this._is_enabled && this._is_valid(ret)) this._queue(ret); 911 | }) 912 | .emit('ready'); 913 | this._is_enabled && this._init(1); 914 | } 915 | _options(opts) { 916 | this.$xdef(opts); 917 | this._sync_tm && this.$timeout(this._sync_tm) && (this._sync_tm = null); 918 | if (this._use_fallback_sync) 919 | this._sync_delay = this._fallback_sync_delay * 60 * 1000; 920 | } 921 | _init(s) { 922 | if (this._is_init) return this.show(); 923 | this._is_init = 1; 924 | this.$timeout(this._get_alerts, s ? 300 : 700); 925 | } 926 | _queue_sync() { 927 | if (!this._use_fallback_sync || !this._is_init || this._sync_tm) return; 928 | this._sync_tm = this.$timeout(this._get_alerts, this._sync_delay); 929 | } 930 | _get_alerts() { 931 | if (this._in_req) return; 932 | this._in_req = 1; 933 | this._sync_tm && this.$timeout(this._sync_tm) && (this._sync_tm = null); 934 | this.request( 935 | 'sync_alerts', 936 | {init: this._is_first}, 937 | function(ret) { 938 | this._in_req = 0; 939 | if (!this.$isBaseObj(ret)) { 940 | this._queue_sync(); 941 | return this._error('Invalid alerts sync data.', ret); 942 | } 943 | this._is_first = 0; 944 | if (this.$isBaseObjVal(ret.system)) 945 | this.call('alerts_status_changed', ret.system, 1); 946 | if (!this._is_enabled) return this._queue_sync(); 947 | if (this.$isBaseObjVal(ret.events)) 948 | for (var k in ret.events) { this.emit(k, ret.events[k], 1); } 949 | if (this.$isBaseObj(ret.types)) this._build_types(ret.types); 950 | if (this.$isArrVal(ret.alerts)) this._queue(ret.alerts); 951 | this._queue_sync(); 952 | }, 953 | function(e) { 954 | this._in_req = 0; 955 | this._queue_sync(); 956 | this._error(e.self ? e.message : 'Getting user alerts failed.', e.message); 957 | }, 958 | true 959 | ); 960 | } 961 | _get_type(name) { return this._types[name] || {name}; } 962 | _build_types(data) { 963 | this._destroy_types(); 964 | for (var k in data) { 965 | this._types[k] = { 966 | name: k, 967 | priority: cint(data[k].display_priority), 968 | timeout: cint(data[k].display_timeout), 969 | sound: cstr(data[k].display_sound), 970 | custom_sound: cstr(data[k].custom_display_sound) 971 | }; 972 | this._styles[k] = new AlertsStyle(this._id, this._slug(k)); 973 | this._styles[k].build(data[k]); 974 | } 975 | } 976 | _destroy_types() { 977 | this.$reduce(this._styles, function(o, k) { 978 | try { o.destroy(); } catch(_) {} 979 | delete this._types[k]; 980 | delete this._styles[k]; 981 | }); 982 | } 983 | _is_valid(data) { 984 | if (!this.$isBaseObjVal(data)) return 0; 985 | if (this._seen.includes(data.name)) return 0; 986 | var user = frappe.session.user, score = 0; 987 | if (this.$isArrVal(data.users) && data.users.includes(user)) score++; 988 | if (!score && this.$isArrVal(data.roles) && frappe.user.has_role(data.roles)) score++; 989 | if (!score) return 0; 990 | if (this.$isArr(data.seen_today) && data.seen_today.includes(user)) return 0; 991 | var seen_by = this.$isBaseObj(data.seen_by) ? data.seen_by : {}, 992 | seen = seen_by[user] != null ? cint(seen_by[user]) : -1; 993 | if (cint(data.is_repeatable) < 1 && seen > 0) return 0; 994 | if (seen >= cint(data.number_of_repeats)) return 0; 995 | return 1; 996 | } 997 | _queue(data) { 998 | if (this.$isBaseObj(data)) this._list.push(data); 999 | else if (this.$isArrVal(data)) this._list.push.apply(this._list, data); 1000 | this.has_alerts && this._list.sort(this.$fn(function(a, b) { 1001 | a = this._get_type(a.alert_type).priority || 0; 1002 | b = this._get_type(b.alert_type).priority || 0; 1003 | return cint(b) - cint(a); 1004 | }, this)); 1005 | this.show(); 1006 | } 1007 | _render() { 1008 | if (this.has_alerts) this._render_dialog(); 1009 | else if (this._has_seen) this._process_seen(); 1010 | return this; 1011 | } 1012 | _mark_seen() { 1013 | this._dialog && this._seen.push(this._dialog.name); 1014 | } 1015 | _render_dialog() { 1016 | if (!this._dialog) { 1017 | this._dialog = new AlertsDialog(this._id, 'alerts-' + this._id); 1018 | this._rerender = this.$fn(this._render); 1019 | this._remark_seen = this.$fn(this._mark_seen); 1020 | } 1021 | 1022 | var data = this._list.shift(), 1023 | type = this._get_type(data.alert_type); 1024 | this._dialog 1025 | .reset() 1026 | .setName(data.name) 1027 | .setTitle(data.title) 1028 | .setMessage(data.message) 1029 | .setTimeout(type.timeout || 0) 1030 | .setSound(type.sound, type.custom_sound) 1031 | .onShow(this._remark_seen) 1032 | .onHide(this._rerender, 200) 1033 | .render(this._slug(type.name)) 1034 | .show(); 1035 | } 1036 | _process_seen() { 1037 | var seen = this._seen.splice(0, this._seen.length); 1038 | this.request( 1039 | 'sync_seen', 1040 | {names: seen}, 1041 | function(ret) { 1042 | if (ret) return; 1043 | this._seen = seen; 1044 | this._error('Marking alerts as seen error.', ret, seen); 1045 | this._retry_mark_seens(); 1046 | }, 1047 | function(e) { 1048 | this._seen = seen; 1049 | this._error(e.self ? e.message : 'Marking alerts as seen error.', seen, e && e.message); 1050 | this._retry_mark_seens(); 1051 | }, 1052 | true 1053 | ); 1054 | } 1055 | _retry_mark_seens() { 1056 | if (!this._seen_retry) { 1057 | this._seen_retry = 1; 1058 | this.$timeout(this._process_seen, 2000); 1059 | } else { 1060 | delete this._seen_retry; 1061 | this.error(__('{0} app is currently facing a problem.', [this.module])); 1062 | } 1063 | } 1064 | _slug(v) { return v.toLowerCase().replace(/ /g, '-'); } 1065 | destroy() { 1066 | //frappe.alerts = null; 1067 | //this._destroy_types(); 1068 | //if (this._dialog) try { this._dialog.destroy(); } catch(_) {} 1069 | if (this._mock) try { this._mock.destroy(); } catch(_) {} 1070 | this._mock = null; 1071 | //super.destroy(); 1072 | } 1073 | } 1074 | 1075 | 1076 | class AlertsMock extends LevelUpCore { 1077 | constructor() { 1078 | super(); 1079 | this._id = frappe.utils.get_random(5); 1080 | } 1081 | build(data) { 1082 | if (!this.$isDataObjVal(data)) return this; 1083 | if (!this._dialog) 1084 | this._dialog = new AlertsDialog(this._id, 'alerts-mock-' + this._id); 1085 | 1086 | this._dialog 1087 | .setTitle(__('Mock Alert')) 1088 | .setMessage(data.message || __('This is a mock alert message.')) 1089 | .setTimeout(data.display_timeout) 1090 | .setSound(data.display_sound, data.custom_display_sound); 1091 | 1092 | if (!data.alert_type) this._dialog.setStyle(data).render(); 1093 | else this._dialog.render(this._slug(data.alert_type)); 1094 | 1095 | return this; 1096 | } 1097 | show() { 1098 | this._dialog && this._dialog.show(); 1099 | return this; 1100 | } 1101 | hide() { 1102 | this._dialog && this._dialog.hide(); 1103 | return this; 1104 | } 1105 | _slug(v) { return v.toLowerCase().replace(/ /g, '-'); } 1106 | destroy() { 1107 | this._dialog && this._dialog.destroy(); 1108 | super.destroy(); 1109 | } 1110 | } 1111 | 1112 | 1113 | class AlertsDialog extends LevelUpCore { 1114 | constructor(id, _class) { 1115 | super(); 1116 | this._id = id; 1117 | this._class = _class; 1118 | this._def_class = _class; 1119 | this._body = null; 1120 | this.$getter('name', '', 0, 1); 1121 | this._title = null; 1122 | this._message = null; 1123 | this._timeout = 0; 1124 | this._sound = {loaded: 0, playing: 0, tm: null}; 1125 | this._visible = 0; 1126 | 1127 | } 1128 | setName(text) { 1129 | if (this.$isStrVal(text)) this._name = text; 1130 | return this; 1131 | } 1132 | setTitle(text, args) { 1133 | if (this.$isStrVal(text)) this._title = __(text, args); 1134 | return this; 1135 | } 1136 | setMessage(text, args) { 1137 | if (this.$isStrVal(text)) this._message = __(text, args); 1138 | return this; 1139 | } 1140 | setStyle(data) { 1141 | if (!this._style) this._style = new AlertsStyle(this._id, this._class); 1142 | this._style.build(data); 1143 | return this; 1144 | } 1145 | setTimeout(sec) { 1146 | if (this.$isNum(sec) && sec > 0) this._timeout = sec * 1000; 1147 | return this; 1148 | } 1149 | setSound(file, fallback) { 1150 | this.stopSound(); 1151 | this._sound.loaded = 0; 1152 | if (!this.$isStrVal(file) || file === 'None') return this; 1153 | if (file === 'Custom') { 1154 | if (!this.$isStrVal(fallback) || !(/\.(mp3|wav|ogg)$/i).test(fallback)) return this; 1155 | file = frappe.utils.get_file_link(fallback); 1156 | } else file = '/assets/frappe/sounds/' + file.toLowerCase() + '.mp3'; 1157 | if (!this._$sound) { 1158 | this._$sound = $('