├── .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 | ![v1.0.8](https://img.shields.io/badge/v1.0.8-2024/06/23-green?style=plastic) 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 | Alerts 14 |

15 |

16 | Alerts 17 |

18 |

19 | Alerts 20 |

21 | 22 | --- 23 | 24 | ### Contributors 25 | **The list of people who deserves more than a simple "Thank You".** 26 | - [![avc](https://img.shields.io/badge/avc-Debug_%7C_Test-green?style=plastic)](https://github.com/git-avc) 27 | - [![lan9635](https://img.shields.io/badge/lan9635-Debug_%7C_Test-green?style=plastic)](https://github.com/lan9635) 28 | - [![satishkakani](https://img.shields.io/badge/satishkakani-Debug-orange?style=plastic)](https://github.com/satishkakani) 29 | - [![neomrx](https://img.shields.io/badge/neomrx-Debug-orange?style=plastic)](https://github.com/neomrx) 30 | - [![rahulrajeevihg](https://img.shields.io/badge/rahulrajeevihg-Debug-orange?style=plastic)](https://github.com/rahulrajeevihg) 31 | 32 | --- 33 | 34 | ### Table of Contents 35 | - [Requirements](#requirements) 36 | - [Setup](#setup) 37 | - [Install](#install) 38 | - [Update](#update) 39 | - [Uninstall](#uninstall) 40 | - [Usage](#usage) 41 | - [Issues](#issues) 42 | - [License](#license) 43 | 44 | --- 45 | 46 | ### Requirements 47 | - Frappe >= v12.0.0 48 | 49 | --- 50 | 51 | ### Setup 52 | 53 | ⚠️ *Important* ⚠️ 54 | 55 | *Do not forget to replace [sitename] with the name of your site in all commands.* 56 | 57 | #### Install 58 | 1. Go to bench directory 59 | 60 | ``` 61 | cd ~/frappe-bench 62 | ``` 63 | 64 | 2. Get plugin from Github 65 | 66 | ``` 67 | bench get-app https://github.com/kid1194/frappe_alerts 68 | ``` 69 | 70 | 3. Build plugin files 71 | 72 | ``` 73 | bench build --app alerts 74 | ``` 75 | 76 | 4. Install plugin on a specific site 77 | 78 | ``` 79 | bench --site [sitename] install-app alerts 80 | ``` 81 | 82 | 5. Restart bench to clear cache 83 | 84 | ``` 85 | bench restart 86 | ``` 87 | 88 | #### Update 89 | 1. Go to app directory 90 | 91 | ``` 92 | cd ~/frappe-bench/apps/alerts 93 | ``` 94 | 95 | 2. Get updates from Github 96 | 97 | ``` 98 | git pull 99 | ``` 100 | 101 | 3. Go back to bench directory 102 | 103 | ``` 104 | cd ~/frappe-bench 105 | ``` 106 | 107 | 4. Build plugin files 108 | 109 | ``` 110 | bench build --app alerts 111 | ``` 112 | 113 | 5. Update a specific site 114 | 115 | ``` 116 | bench --site [sitename] migrate 117 | ``` 118 | 119 | 6. Restart bench to clear cache 120 | 121 | ``` 122 | bench restart 123 | ``` 124 | 125 | #### Uninstall 126 | 1. Go to bench directory 127 | 128 | ``` 129 | cd ~/frappe-bench 130 | ``` 131 | 132 | 2. Uninstall plugin from a specific site 133 | 134 | ``` 135 | bench --site [sitename] uninstall-app alerts 136 | ``` 137 | 138 | 3. Remove plugin from bench 139 | 140 | ``` 141 | bench remove-app alerts 142 | ``` 143 | 144 | 4. Restart bench to clear cache 145 | 146 | ``` 147 | bench restart 148 | ``` 149 | 150 | --- 151 | 152 | ### Usage 153 | ### Alert Type 154 | - Go to `Alert Type` then create a new type 155 | - Enter the type name 156 | - Set the `Display Priority` if needed 157 | - To make alerts of this type close automatically: 158 | - Set the `Display Timeout (Seconds)` or keep as `0` to disable the automatic close 159 | - Fraction numbers can also be used, like `1.5`, to set a more specific timeout 160 | - To customize the alert sound: 161 | - Select the `Display Sound` that you prefer 162 | - Select `Custom` option to be able to upload a `Custom Display Sound` 163 | - Select `None` to disable the `Display Sound` 164 | - To customize the look of the alert: 165 | - Change the `Background Color`, `Border Color`, `Title Color` and `Content Color` for both, **Light** and **Dark** themes 166 | - After saving form, click on `Preview` to see how the custom style will look 167 | 168 | ### Alert 169 | - Go to `Alert` and create new entry 170 | - Select an `Alert Type` and change the alert `Title`, if needed 171 | - Set the `From Date` and `Until Date` to specify the alert duration 172 | - Enter and format the `Message` of alert 173 | - Set the `For Roles` and/or the `For Users` to specify the alert recipients 174 | - Check `Is Repeatable` to display the alert more than once, and set the `Number of Repeats` 175 | - After submitting the alert: 176 | - The `Seen By` table will be visible and will include all the reached users and datetime of reach 177 | - In list view, check the total number of unique users the alert has `Reached` 178 | 179 | ### Alerts Settings 180 | - Go to `Alerts Settings` to enable or disable the module 181 | - If realtime events isn't working for some reason: 182 | - Check `Use Fallback Sync Method` field to enable using the fallback method 183 | - Set the `Fallback Sync Delay` field with the number of minutes to wait between sync requests 184 | 185 | ⚠️ *Note:* When using the fallback sync method, the lower the `Fallback Sync Delay`, the more sync requests sent to server. 186 | 187 | --- 188 | 189 | ### Issues 190 | If you find bug in the plugin, please create a [bug report](https://github.com/kid1194/frappe_alerts/issues/new?assignees=kid1194&labels=bug&template=bug_report.md&title=%5BBUG%5D) and let us know about it. 191 | 192 | --- 193 | 194 | ### License 195 | This repository has been released under the [MIT License](https://github.com/kid1194/frappe_alerts/blob/main/LICENSE). 196 | -------------------------------------------------------------------------------- /alerts/__init__.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 | __module__ = "Alerts" 8 | __version__ = "1.0.8" 9 | __update_api__ = "https://api.github.com/repos/kid1194/frappe_alerts/releases/latest" -------------------------------------------------------------------------------- /alerts/alerts/__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/__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/alert/__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/alert/alert.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('Alert', { 10 | onload: function(frm) { 11 | frappe.alerts 12 | .on('ready change', function() { this.setup_form(frm); }) 13 | .on('on_alert', function(d, t) { 14 | frm._alert.errs.includes(t) && (d.title = __(frm.doctype)); 15 | }); 16 | frm._alert = { 17 | errs: ['fatal', 'error'], 18 | ignore: 0, 19 | is_draft: false, 20 | is_submitted: false, 21 | is_cancelled: false, 22 | toolbar: 0, 23 | mindate: moment(), 24 | to_date: function(v) { 25 | return moment(cstr(v), frappe.defaultDateFormat); 26 | }, 27 | from_datetime: function(v) { 28 | v = moment(cstr(v), frappe.defaultDatetimeFormat); 29 | v = v.format(frappe.defaultDateFormat); 30 | return this.to_date(v); 31 | }, 32 | mindate_obj: function() { 33 | return frappe.datetime.moment_to_date_obj(this.mindate); 34 | }, 35 | }; 36 | frm.events.setup_doc(frm); 37 | if (!frm._alert.is_draft) return; 38 | frm.set_query('role', 'for_roles', function(doc, cdt, cdn) { 39 | var qry = {filters: {disabled: 0, desk_access: 1}}; 40 | if (frappe.alerts.$isArrVal(doc.for_roles)) 41 | qry.filters.name = ['notin', frappe.alerts.$map(doc.for_roles, function(v) { return v.role; })]; 42 | return qry; 43 | }); 44 | frm.set_query('user', 'for_users', function(doc, cdt, cdn) { 45 | var qry = {query: frappe.alerts.get_method('search_users')}; 46 | if (frappe.alerts.$isArrVal(doc.for_users)) 47 | qry.filters = {existing: frappe.alerts.$map(doc.for_users, function(v) { return v.user; })}; 48 | return qry; 49 | }); 50 | if (!frm.is_new()) { 51 | if (frappe.alerts.$isStrVal(frm.doc.creation)) 52 | frm._alert.mindate = frm._alert.from_datetime(frm.doc.creation); 53 | else if (frappe.alerts.$isStrVal(frm.doc.from_date)) 54 | frm._alert.mindate = frm._alert.to_date(frm.doc.from_date); 55 | } 56 | var mindate = frm._alert.mindate_obj(), 57 | fields = ['from_date', 'until_date']; 58 | for (var i = 0, f; i < 2; i++) { 59 | f = frm.get_field(fields[i]); 60 | f.df.min_date = mindate; 61 | f.datepicker && f.datepicker.update('minDate', mindate); 62 | } 63 | }, 64 | refresh: function(frm) { 65 | if (!frm._type.toolbar) { 66 | frm.events.toggle_toolbar(frm); 67 | frm._type.toolbar = 1; 68 | } 69 | }, 70 | from_date: function(frm) { 71 | if (frm._alert.ignore) return; 72 | var key = 'from_date', 73 | val = cstr(frm.doc[key]); 74 | if (!val.length || cint(frm._alert.mindate.diff(val, 'days')) > 0) { 75 | frm._alert.ignore++; 76 | frm.set_value(key, frm._alert.mindate.format()); 77 | frm._alert.ignore--; 78 | } 79 | frm.events.until_date(frm); 80 | }, 81 | until_date: function(frm) { 82 | if (frm._alert.ignore) return; 83 | var from = cstr(frm.doc.from_date); 84 | if (!from.length) return frm.events.from_date(frm); 85 | var key = 'until_date', 86 | val = cstr(frm.doc[key]); 87 | if (val === from) return; 88 | if (!val.length || cint(frm._alert.to_date(from).diff(val, 'days')) > 0) { 89 | frm._alert.ignore++; 90 | frm.set_value(key, from); 91 | frm._alert.ignore--; 92 | } 93 | }, 94 | validate: function(frm) { 95 | var errs = []; 96 | if (!frappe.alerts.$isStrVal(frm.doc.title)) 97 | errs.push(__('A valid title is required.')); 98 | if (!frappe.alerts.$isStrVal(frm.doc.alert_type)) 99 | errs.push(__('A valid alert type is required.')); 100 | if (!frappe.alerts.$isStrVal(frm.doc.from_date)) 101 | errs.push(__('A valid from date is required.')); 102 | if (cint(frm._alert.mindate.diff(frm.doc.from_date, 'days')) > 0) 103 | errs.push(__('From date must be later than or equals to "{0}".', [frm._alert.mindate.format()])); 104 | if (!frappe.alerts.$isStrVal(frm.doc.until_date)) 105 | frm.events.until_date(frm); 106 | else if (cint(frm._alert.to_date(frm.doc.from_date).diff(frm.doc.until_date, 'days')) > 0) 107 | errs.push(__('Until date must be later than or equals to "From Date".')); 108 | if (!frappe.alerts.$isStrVal(frm.doc.message)) 109 | errs.push(__('A valid message is required.')); 110 | if ( 111 | !frappe.alerts.$isArrVal(frm.doc.for_roles) 112 | && !frappe.alerts.$isArrVal(frm.doc.for_users) 113 | ) errs.push(__('At least one recipient role or user is required.')); 114 | if (errs.length) { 115 | frappe.alerts.fatal(errs); 116 | return false; 117 | } 118 | }, 119 | after_save: function(frm) { 120 | frm.events.toggle_toolbar(frm); 121 | }, 122 | on_submit: function(frm) { 123 | frm.events.setup_doc(frm); 124 | frm.events.toggle_toolbar(frm); 125 | }, 126 | after_cancel: function(frm) { 127 | frm.events.setup_doc(frm); 128 | frm.events.toggle_toolbar(frm); 129 | }, 130 | setup_doc: function(frm) { 131 | var docstatus = cint(frm.doc.docstatus); 132 | frm._alert.is_draft = docstatus === 0; 133 | frm._alert.is_submitted = docstatus === 1; 134 | frm._alert.is_cancelled = docstatus === 2; 135 | if (frm._alert.is_draft) return; 136 | frappe.alerts.disable_form(frm, __('{0} has been {1}.', [ 137 | cstr(frm.doctype), frm._alert.is_submitted ? __('submitted') : __('cancelled') 138 | ]), frm._alert.is_submitted ? 'green' : 'red'); 139 | frm.set_df_property('seen_by_section', 'hidden', 0); 140 | frappe.alerts.real('alert_seen', function(ret) { 141 | if ( 142 | ret && this.$isArrVal(ret.alerts) 143 | && ret.alerts.includes(cstr(frm.docname)) 144 | ) frm.reload_doc(); 145 | }); 146 | }, 147 | toggle_toolbar: function(frm) { 148 | var label = __('Preview'), 149 | del = frm.is_new() || !frm._alert.is_draft; 150 | if (frm.custom_buttons[label]) { 151 | if (!del) return; 152 | frm.custom_buttons[label].remove(); 153 | delete frm.custom_buttons[label]; 154 | } 155 | if (del || frm.custom_buttons[label]) return; 156 | frm.add_custom_button(label, function() { 157 | frappe.alerts.mock().build(frm.doc); 158 | }); 159 | frm.change_custom_button_type(label, null, 'info'); 160 | }, 161 | }); -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert/alert.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "autoname": "format:ALERT-{YY}{MM}{DD}-{###}", 5 | "creation": "2022-04-04 04:04:04", 6 | "description": "Alert data for Alerts", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "title", 12 | "alert_type", 13 | "main_column", 14 | "from_date", 15 | "until_date", 16 | "message_section", 17 | "message", 18 | "recipients_section", 19 | "for_roles", 20 | "recipients_column", 21 | "for_users", 22 | "control_section", 23 | "is_repeatable", 24 | "control_column", 25 | "number_of_repeats", 26 | "seen_by_section", 27 | "seen_by", 28 | "reached", 29 | "status" 30 | ], 31 | "fields": [ 32 | { 33 | "fieldname": "main_section", 34 | "fieldtype": "Section Break" 35 | }, 36 | { 37 | "fieldname": "title", 38 | "fieldtype": "Data", 39 | "label": "Title", 40 | "reqd": 1, 41 | "bold": 1, 42 | "in_list_view": 1, 43 | "in_preview": 1, 44 | "translatable": 1, 45 | "search_index": 1 46 | }, 47 | { 48 | "fieldname": "alert_type", 49 | "fieldtype": "Link", 50 | "label": "Alert Type", 51 | "options": "Alert Type", 52 | "reqd": 1, 53 | "bold": 1, 54 | "in_list_view": 1, 55 | "in_filter": 1, 56 | "in_standard_filter": 1, 57 | "in_preview": 1, 58 | "search_index": 1, 59 | "ignore_user_permissions": 1 60 | }, 61 | { 62 | "fieldname": "main_column", 63 | "fieldtype": "Column Break" 64 | }, 65 | { 66 | "fieldname": "from_date", 67 | "fieldtype": "Date", 68 | "label": "From Date", 69 | "default": "Today", 70 | "reqd": 1, 71 | "bold": 1, 72 | "in_list_view": 1, 73 | "in_filter": 1, 74 | "in_standard_filter": 1, 75 | "in_preview": 1, 76 | "search_index": 1 77 | }, 78 | { 79 | "fieldname": "until_date", 80 | "fieldtype": "Date", 81 | "label": "Until Date", 82 | "default": "Today", 83 | "read_only_depends_on": "eval:!doc.from_date", 84 | "in_list_view": 1, 85 | "in_filter": 1, 86 | "in_standard_filter": 1, 87 | "in_preview": 1, 88 | "search_index": 1 89 | }, 90 | { 91 | "fieldname": "message_section", 92 | "fieldtype": "Section Break" 93 | }, 94 | { 95 | "fieldname": "message", 96 | "fieldtype": "Text Editor", 97 | "label": "Message", 98 | "reqd": 1, 99 | "bold": 1 100 | }, 101 | { 102 | "fieldname": "recipients_section", 103 | "fieldtype": "Section Break", 104 | "label": "Recipients" 105 | }, 106 | { 107 | "fieldname": "for_roles", 108 | "fieldtype": "Table MultiSelect", 109 | "label": "Recipient Roles", 110 | "options": "Alert For Role" 111 | }, 112 | { 113 | "fieldname": "recipients_column", 114 | "fieldtype": "Column Break" 115 | }, 116 | { 117 | "fieldname": "for_users", 118 | "fieldtype": "Table MultiSelect", 119 | "label": "Recipient Users", 120 | "description": "Selected users will receive the alert regardless of the recipient roles.", 121 | "options": "Alert For User" 122 | }, 123 | { 124 | "fieldname": "control_section", 125 | "fieldtype": "Section Break" 126 | }, 127 | { 128 | "fieldname": "is_repeatable", 129 | "fieldtype": "Check", 130 | "label": "Is Repeatable", 131 | "description": "Display alert more than one time for each recipient", 132 | "in_list_view": 1 133 | }, 134 | { 135 | "fieldname": "control_column", 136 | "fieldtype": "Column Break" 137 | }, 138 | { 139 | "fieldname": "number_of_repeats", 140 | "fieldtype": "Int", 141 | "label": "No. of Repeats", 142 | "default": "1", 143 | "read_only_depends_on": "eval:!doc.is_repeatable", 144 | "non_negative": 1 145 | }, 146 | { 147 | "fieldname": "seen_by_section", 148 | "fieldtype": "Section Break", 149 | "hidden": 1 150 | }, 151 | { 152 | "fieldname": "seen_by", 153 | "fieldtype": "Table", 154 | "label": "Seen By", 155 | "options": "Alert Seen By", 156 | "allow_on_submit": 1 157 | }, 158 | { 159 | "fieldname": "reached", 160 | "fieldtype": "Int", 161 | "label": "Reached", 162 | "default": "0", 163 | "hidden": 1, 164 | "in_list_view": 1, 165 | "allow_on_submit": 1 166 | }, 167 | { 168 | "fieldname": "status", 169 | "fieldtype": "Select", 170 | "label": "Status", 171 | "options": "Draft\nPending\nActive\nFinished\nCancelled", 172 | "default": "Draft", 173 | "read_only": 1, 174 | "hidden": 1, 175 | "search_index": 1, 176 | "allow_on_submit": 1 177 | } 178 | ], 179 | "icon": "fa fa-bell", 180 | "is_submittable": 1, 181 | "modified": "2024-06-12 04:04:04", 182 | "modified_by": "Administrator", 183 | "module": "Alerts", 184 | "name": "Alert", 185 | "naming_rule": "Expression", 186 | "owner": "Administrator", 187 | "permissions": [ 188 | { 189 | "amend": 1, 190 | "cancel": 1, 191 | "create": 1, 192 | "delete": 1, 193 | "email": 1, 194 | "export": 1, 195 | "if_owner": 0, 196 | "import": 1, 197 | "permlevel": 0, 198 | "print": 1, 199 | "read": 1, 200 | "report": 1, 201 | "role": "System Manager", 202 | "set_user_permissions": 1, 203 | "share": 1, 204 | "submit": 1, 205 | "write": 1 206 | }, 207 | { 208 | "amend": 1, 209 | "cancel": 1, 210 | "create": 1, 211 | "delete": 1, 212 | "email": 1, 213 | "export": 1, 214 | "if_owner": 0, 215 | "import": 1, 216 | "permlevel": 0, 217 | "print": 1, 218 | "read": 1, 219 | "report": 1, 220 | "role": "Administrator", 221 | "set_user_permissions": 1, 222 | "share": 1, 223 | "submit": 1, 224 | "write": 1 225 | } 226 | ], 227 | "sort_field": "modified", 228 | "sort_order": "DESC", 229 | "track_changes": 1 230 | } -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert/alert.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 ( 12 | AlertStatus, 13 | clear_doc_cache 14 | ) 15 | 16 | 17 | class Alert(Document): 18 | def before_validate(self): 19 | self._check_app_status() 20 | if self._is_draft: 21 | self._set_defaults() 22 | 23 | 24 | def validate(self): 25 | if self._is_draft: 26 | if not self.title: 27 | self._add_error(_("A valid title is required.")) 28 | if not self.alert_type: 29 | self._add_error(_("A valid alert type is required.")) 30 | self._validate_date() 31 | if not self.message: 32 | self._add_error(_("A valid message is required.")) 33 | if not self.for_roles and not self.for_users: 34 | self._add_error(_("At least one recipient role or user is required.")) 35 | 36 | self._throw_errors() 37 | 38 | 39 | def before_save(self): 40 | clear_doc_cache(self.doctype, self.name) 41 | 42 | 43 | def before_submit(self): 44 | from frappe.utils import getdate 45 | 46 | today = getdate() 47 | if today > getdate(self.until_date): 48 | self._error(_("Alert display from and until dates has already passed.")) 49 | 50 | clear_doc_cache(self.doctype, self.name) 51 | if self.status == AlertStatus.d: 52 | self.status = AlertStatus.p 53 | 54 | if self.status == AlertStatus.p and getdate(self.from_date) <= today: 55 | self.status = AlertStatus.a 56 | self.flags.send_alert = 1 57 | 58 | 59 | def on_update(self): 60 | self._send_alert() 61 | self._clean_flags() 62 | 63 | 64 | def before_update_after_submit(self): 65 | clear_doc_cache(self.doctype, self.name) 66 | if self.has_value_changed("status") and self._is_active: 67 | self.flags.send_alert = 1 68 | 69 | 70 | def on_update_after_submit(self): 71 | self._send_alert() 72 | self._clean_flags() 73 | 74 | 75 | def before_cancel(self): 76 | clear_doc_cache(self.doctype, self.name) 77 | self.status = AlertStatus.c 78 | 79 | 80 | def on_cancel(self): 81 | self._clean_flags() 82 | 83 | 84 | @property 85 | def _is_draft(self): 86 | return cint(self.docstatus) == 0 87 | 88 | 89 | @property 90 | def _is_submitted(self): 91 | return cint(self.docstatus) == 1 92 | 93 | 94 | @property 95 | def _is_pending(self): 96 | return self.status == AlertStatus.p 97 | 98 | 99 | @property 100 | def _is_active(self): 101 | return self.status == AlertStatus.a 102 | 103 | 104 | @property 105 | def _is_repeatable(self): 106 | return cint(self.is_repeatable) > 0 107 | 108 | 109 | @property 110 | def _number_of_repeats(self): 111 | return cint(self.number_of_repeats) 112 | 113 | 114 | def _set_defaults(self): 115 | if not self.from_date: 116 | from frappe.utils import nowdate 117 | 118 | self.from_date = nowdate() 119 | if not self.until_date: 120 | self.until_date = self.from_date 121 | if not self._is_repeatable or self._number_of_repeats < 1: 122 | self.number_of_repeats = 1 123 | if not self.status or self.status != AlertStatus.d: 124 | self.status = AlertStatus.d 125 | 126 | 127 | def _validate_date(self): 128 | if not self.from_date: 129 | self._add_error(_("A valid from date is required.")) 130 | if not self.until_date: 131 | self._add_error(_("A valid until date is required.")) 132 | if ( 133 | self.from_date and self.until_date and ( 134 | self.is_new() or 135 | self.has_value_changed("from_date") or 136 | self.has_value_changed("until_date") 137 | ) 138 | ): 139 | from frappe.utils import getdate 140 | 141 | if getdate(self.from_date) > getdate(self.until_date): 142 | self._add_error(_("Until date must be later than or equals to \"From Date\".")) 143 | 144 | 145 | def _send_alert(self): 146 | if self.flags.get("send_alert", 0): 147 | from alerts.utils import send_alert 148 | 149 | data = { 150 | "name": self.name, 151 | "title": self.title, 152 | "alert_type": self.alert_type, 153 | "message": self.message, 154 | "is_repeatable": 1 if self._is_repeatable else 0, 155 | "number_of_repeats": self._number_of_repeats, 156 | "users": [v.user for v in self.for_users], 157 | "roles": [v.role for v in self.for_roles], 158 | "seen_by": {}, 159 | "seen_today": [] 160 | } 161 | 162 | if self.seen_by: 163 | from frappe.utils import nowdate 164 | 165 | today = nowdate() 166 | for v in self.seen_by: 167 | if v.user not in data["seen_by"]: 168 | data["seen_by"][v.user] = 1 169 | else: 170 | data["seen_by"][v.user] += 1 171 | if v.user not in data["seen_today"] and v.date == today: 172 | data["seen_today"].append(v.user) 173 | 174 | send_alert(data) 175 | 176 | 177 | def _check_app_status(self): 178 | if not self.flags.get("status_checked", 0): 179 | from alerts.utils import check_app_status 180 | 181 | check_app_status() 182 | self.flags.status_checked = 1 183 | 184 | 185 | def _clean_flags(self): 186 | keys = [ 187 | "error_list", 188 | "send_alert", 189 | "status_checked" 190 | ] 191 | for i in range(len(keys)): 192 | self.flags.pop(keys.pop(0), 0) 193 | 194 | 195 | def _add_error(self, msg): 196 | if not self.flags.get("error_list", 0): 197 | self.flags.error_list = [] 198 | self.flags.error_list.append(msg) 199 | 200 | 201 | def _throw_errors(self): 202 | if self.flags.get("error_list", 0): 203 | msg = self.flags.error_list 204 | if len(msg) == 1: 205 | msg = msg.pop(0) 206 | else: 207 | msg = msg.copy() 208 | 209 | self._error(msg) 210 | 211 | 212 | def _error(self, msg): 213 | from alerts.utils import error 214 | 215 | self._clean_flags() 216 | error(msg, _(self.doctype)) -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert/alert_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'] = { 13 | hide_name_column: true, 14 | add_fields: ['status'], 15 | onload: function(list) { 16 | frappe.alerts.on('ready change', function() { this.setup_list(list); }); 17 | }, 18 | get_indicator: function(doc) { 19 | var status = cstr(doc.status), 20 | docstatus = cint(doc.docstatus); 21 | return [ 22 | __(status), 23 | { 24 | 'draft': 'gray', 25 | 'pending': 'orange', 26 | 'active': 'green', 27 | 'finished': 'blue', 28 | 'cancelled': 'red', 29 | }[status.toLowerCase()], 30 | 'status,=,\'' + status + '\'|docstatus,=,' + docstatus 31 | ]; 32 | }, 33 | formatters: { 34 | is_repeatable: function(v) { 35 | return cint(v) > 0 ? __('Yes') : __('No'); 36 | }, 37 | reached: function(v, df, doc) { 38 | return cint(doc.reached); 39 | }, 40 | }, 41 | }; -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_for_role/__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/alert_for_role/alert_for_role.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "autoname": "hash", 5 | "creation": "2022-04-04 04:04:04.119400", 6 | "description": "Alert recipient roles for Alerts", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "role" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "role", 15 | "fieldtype": "Link", 16 | "label": "Role", 17 | "options": "Role", 18 | "reqd": 1, 19 | "bold": 1, 20 | "in_list_view": 1, 21 | "ignore_user_permissions": 1 22 | } 23 | ], 24 | "istable": 1, 25 | "modified": "2024-01-27 04:04:04.119400", 26 | "modified_by": "Administrator", 27 | "module": "Alerts", 28 | "name": "Alert For Role", 29 | "owner": "Administrator" 30 | } -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_for_role/alert_for_role.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.model.document import Document 8 | 9 | 10 | class AlertForRole(Document): 11 | pass -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_for_user/__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/alert_for_user/alert_for_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "autoname": "hash", 5 | "creation": "2022-04-04 04:04:04.119400", 6 | "description": "Alert recipient users for Alerts", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "user" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "user", 15 | "fieldtype": "Link", 16 | "label": "User", 17 | "options": "User", 18 | "reqd": 1, 19 | "bold": 1, 20 | "in_list_view": 1, 21 | "ignore_user_permissions": 1 22 | } 23 | ], 24 | "istable": 1, 25 | "modified": "2024-01-27 04:04:04.119400", 26 | "modified_by": "Administrator", 27 | "module": "Alerts", 28 | "name": "Alert For User", 29 | "owner": "Administrator" 30 | } -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_for_user/alert_for_user.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.model.document import Document 8 | 9 | 10 | class AlertForUser(Document): 11 | pass -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_seen_by/__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/alert_seen_by/alert_seen_by.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "editable_grid": 1, 5 | "autoname": "hash", 6 | "creation": "2022-04-04 04:04:04.119400", 7 | "description": "Alert seen by users for Alerts", 8 | "doctype": "DocType", 9 | "engine": "InnoDB", 10 | "field_order": [ 11 | "user", 12 | "date", 13 | "time" 14 | ], 15 | "fields": [ 16 | { 17 | "fieldname": "user", 18 | "fieldtype": "Link", 19 | "label": "User", 20 | "options": "User", 21 | "reqd": 1, 22 | "bold": 1, 23 | "in_list_view": 1, 24 | "ignore_user_permissions": 1 25 | }, 26 | { 27 | "fieldname": "date", 28 | "fieldtype": "Date", 29 | "label": "Date", 30 | "options": "Today", 31 | "reqd": 1, 32 | "bold": 1, 33 | "in_list_view": 1 34 | }, 35 | { 36 | "fieldname": "time", 37 | "fieldtype": "Time", 38 | "label": "Time", 39 | "options": "Now", 40 | "reqd": 1, 41 | "bold": 1, 42 | "in_list_view": 1 43 | } 44 | ], 45 | "istable": 1, 46 | "modified": "2024-02-08 04:04:04.119400", 47 | "modified_by": "Administrator", 48 | "module": "Alerts", 49 | "name": "Alert Seen By", 50 | "owner": "Administrator" 51 | } -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_seen_by/alert_seen_by.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.model.document import Document 8 | 9 | 10 | class AlertSeenBy(Document): 11 | pass -------------------------------------------------------------------------------- /alerts/alerts/doctype/alert_type/__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/alert_type/alert_type.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('Alert Type', { 10 | onload: function(frm) { 11 | frappe.alerts 12 | .on('ready change', function() { this.setup_form(frm); }) 13 | .on('on_alert', function(d, t) { 14 | frm._type.errs.includes(t) && (d.title = __(frm.doctype)); 15 | }); 16 | frm._type = { 17 | errs: ['fatal', 'error'], 18 | }; 19 | }, 20 | refresh: function(frm) { 21 | if (!frm.is_new() && !frm._type.setup) frm.events.setup_disable_note(frm); 22 | if (!frm._type.toolbar) { 23 | frm.events.toggle_toolbar(frm); 24 | frm._type.toolbar = 1; 25 | } 26 | }, 27 | disabled: function(frm) { 28 | frm.events.toggle_toolbar(frm); 29 | }, 30 | validate: function(frm) { 31 | var errs = []; 32 | if (cstr(frm.doc.display_sound) === 'Custom') { 33 | if (!frappe.alerts.$isStrVal(frm.doc.custom_display_sound)) 34 | errs.push(__('A valid custom display sound is required.')); 35 | if (!(/\.(mp3|wav|ogg)$/i).test(frm.doc.custom_display_sound)) 36 | errs.push(__('Custom display sound must be of a supported format (MP3, WAV, OGG).')); 37 | } 38 | if (errs.length) { 39 | frappe.alerts.fatal(errs); 40 | return false; 41 | } 42 | }, 43 | after_save: function(frm) { 44 | frm.events.toggle_toolbar(frm); 45 | }, 46 | setup_disable_note: function(frm) { 47 | frm._type.setup = 1; 48 | frm.get_field('disable_note').$wrapper.empty().append('\ 49 |

' + __('Note') + ':

\ 50 |

\ 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 | \ 76 | '); 77 | }, 78 | }); -------------------------------------------------------------------------------- /alerts/alerts/doctype/alerts_settings/alerts_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "creation": "2023-04-04 04:04:04", 3 | "description": "Settings for Alerts", 4 | "doctype": "DocType", 5 | "engine": "InnoDB", 6 | "field_order": [ 7 | "general_section", 8 | "is_enabled", 9 | "general_column", 10 | "alerts_section", 11 | "use_fallback_sync", 12 | "alerts_column", 13 | "fallback_sync_delay", 14 | "update_section", 15 | "auto_check_for_update", 16 | "send_update_notification", 17 | "update_notification_sender", 18 | "update_notification_receivers", 19 | "update_column", 20 | "check_for_update", 21 | "update_note", 22 | "current_version", 23 | "latest_version", 24 | "latest_check", 25 | "has_update" 26 | ], 27 | "fields": [ 28 | { 29 | "fieldname": "general_section", 30 | "fieldtype": "Section Break", 31 | "label": "General Settings" 32 | }, 33 | { 34 | "fieldname": "is_enabled", 35 | "fieldtype": "Check", 36 | "label": "Is Enabled", 37 | "default": "1" 38 | }, 39 | { 40 | "fieldname": "general_column", 41 | "fieldtype": "Column Break" 42 | }, 43 | { 44 | "fieldname": "alerts_section", 45 | "fieldtype": "Section Break", 46 | "label": "Alerts Settings" 47 | }, 48 | { 49 | "fieldname": "use_fallback_sync", 50 | "fieldtype": "Check", 51 | "label": "Use Fallback Sync Method", 52 | "description": "Enabled only if realtime events service isn't working", 53 | "read_only_depends_on": "eval:!doc.is_enabled" 54 | }, 55 | { 56 | "fieldname": "alerts_column", 57 | "fieldtype": "Column Break" 58 | }, 59 | { 60 | "fieldname": "fallback_sync_delay", 61 | "fieldtype": "Int", 62 | "label": "Fallback Sync Delay (Minutes)", 63 | "description": "Number of minutes to wait between sync requests", 64 | "default": "5", 65 | "non_negative": 1, 66 | "read_only_depends_on": "eval:!doc.use_fallback_sync" 67 | }, 68 | { 69 | "fieldname": "update_section", 70 | "fieldtype": "Section Break", 71 | "label": "Update Settings" 72 | }, 73 | { 74 | "fieldname": "auto_check_for_update", 75 | "fieldtype": "Check", 76 | "label": "Auto Check For Update", 77 | "default": "1", 78 | "read_only_depends_on": "eval:!doc.is_enabled" 79 | }, 80 | { 81 | "fieldname": "send_update_notification", 82 | "fieldtype": "Check", 83 | "label": "Send Update Notification", 84 | "default": "0", 85 | "read_only_depends_on": "eval:!doc.is_enabled" 86 | }, 87 | { 88 | "fieldname": "update_notification_sender", 89 | "fieldtype": "Link", 90 | "label": "Update Notification Sender", 91 | "options": "User", 92 | "read_only_depends_on": "eval:!doc.is_enabled || !doc.send_update_notification", 93 | "mandatory_depends_on": "eval:doc.is_enabled && doc.send_update_notification", 94 | "ignore_user_permissions": 1 95 | }, 96 | { 97 | "fieldname": "update_notification_receivers", 98 | "fieldtype": "Table MultiSelect", 99 | "label": "Update Notification Receivers", 100 | "options": "Alerts Update Receiver", 101 | "read_only_depends_on": "eval:!doc.is_enabled || !doc.send_update_notification", 102 | "mandatory_depends_on": "eval:doc.is_enabled && doc.send_update_notification" 103 | }, 104 | { 105 | "fieldname": "update_column", 106 | "fieldtype": "Column Break" 107 | }, 108 | { 109 | "fieldname": "check_for_update", 110 | "fieldtype": "Button", 111 | "label": "Check For Update", 112 | "read_only_depends_on": "eval:!doc.is_enabled" 113 | }, 114 | { 115 | "fieldname": "update_note", 116 | "fieldtype": "HTML", 117 | "label": "", 118 | "read_only": 1 119 | }, 120 | { 121 | "fieldname": "current_version", 122 | "fieldtype": "Data", 123 | "label": "Current Version", 124 | "read_only": 1, 125 | "hidden": 1 126 | }, 127 | { 128 | "fieldname": "latest_version", 129 | "fieldtype": "Data", 130 | "label": "Latest Version", 131 | "read_only": 1, 132 | "hidden": 1 133 | }, 134 | { 135 | "fieldname": "latest_check", 136 | "fieldtype": "Data", 137 | "label": "Latest Check", 138 | "read_only": 1, 139 | "hidden": 1 140 | }, 141 | { 142 | "fieldname": "has_update", 143 | "fieldtype": "Check", 144 | "label": "Has Update", 145 | "read_only": 1, 146 | "hidden": 1 147 | } 148 | ], 149 | "icon": "fa fa-cog", 150 | "issingle": 1, 151 | "modified": "2024-06-05 04:04:04", 152 | "modified_by": "Administrator", 153 | "module": "Alerts", 154 | "name": "Alerts Settings", 155 | "owner": "Administrator", 156 | "permissions": [ 157 | { 158 | "amend": 1, 159 | "cancel": 1, 160 | "create": 1, 161 | "delete": 1, 162 | "email": 1, 163 | "export": 1, 164 | "if_owner": 0, 165 | "import": 1, 166 | "permlevel": 0, 167 | "print": 1, 168 | "read": 1, 169 | "report": 1, 170 | "role": "System Manager", 171 | "set_user_permissions": 1, 172 | "share": 1, 173 | "submit": 1, 174 | "write": 1 175 | }, 176 | { 177 | "amend": 1, 178 | "cancel": 1, 179 | "create": 1, 180 | "delete": 1, 181 | "email": 1, 182 | "export": 1, 183 | "if_owner": 0, 184 | "import": 1, 185 | "permlevel": 0, 186 | "print": 1, 187 | "read": 1, 188 | "report": 1, 189 | "role": "Administrator", 190 | "set_user_permissions": 1, 191 | "share": 1, 192 | "submit": 1, 193 | "write": 1 194 | } 195 | ] 196 | } -------------------------------------------------------------------------------- /alerts/alerts/doctype/alerts_settings/alerts_settings.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 | 12 | class AlertsSettings(Document): 13 | def before_validate(self): 14 | if self.update_notification_receivers: 15 | exist = [] 16 | remove = [] 17 | for v in self.update_notification_receivers: 18 | if v.user in exist: 19 | remove.append(v) 20 | else: 21 | exist.append(v.user) 22 | 23 | exist.clear() 24 | if remove: 25 | for i in range(len(remove)): 26 | self.update_notification_receivers.remove(remove.pop(0)) 27 | 28 | 29 | def validate(self): 30 | if self._use_fallback_sync and self._fallback_sync_delay < 1: 31 | self._add_error(_("Fallback sync delay must be greater than or equals to 1 minute.")) 32 | if self._send_update_notification: 33 | if not self.update_notification_sender: 34 | self._add_error(_("A valid update notification sender is required.")) 35 | if not self.update_notification_receivers: 36 | self._add_error(_("At least one valid update notification receiver is required.")) 37 | 38 | self._throw_errors() 39 | 40 | 41 | def before_save(self): 42 | from alerts.utils import clear_doc_cache 43 | 44 | clear_doc_cache(self.doctype) 45 | if self.has_value_changed("is_enabled"): 46 | self.flags.emit_change = 1 47 | 48 | 49 | def after_save(self): 50 | if self.flags.get("emit_change", 0): 51 | from alerts.utils import emit_status_changed 52 | 53 | emit_status_changed({ 54 | "is_enabled": 1 if self._is_enabled else 0, 55 | "use_fallback_sync": 1 if self._use_fallback_sync else 0, 56 | "fallback_sync_delay": self._fallback_sync_delay 57 | }) 58 | 59 | self._clean_flags() 60 | 61 | 62 | @property 63 | def _is_enabled(self): 64 | return cint(self.is_enabled) > 0 65 | 66 | 67 | @property 68 | def _use_fallback_sync(self): 69 | return cint(self.use_fallback_sync) > 0 70 | 71 | 72 | @property 73 | def _fallback_sync_delay(self): 74 | return cint(self.fallback_sync_delay) 75 | 76 | 77 | @property 78 | def _auto_check_for_update(self): 79 | return cint(self.auto_check_for_update) > 0 80 | 81 | 82 | @property 83 | def _send_update_notification(self): 84 | return cint(self.send_update_notification) > 0 85 | 86 | 87 | def _clean_flags(self): 88 | keys = [ 89 | "error_list", 90 | "emit_change" 91 | ] 92 | for i in range(len(keys)): 93 | self.flags.pop(keys.pop(0), 0) 94 | 95 | 96 | def _add_error(self, msg): 97 | if not self.flags.get("error_list", 0): 98 | self.flags.error_list = [] 99 | self.flags.error_list.append(msg) 100 | 101 | 102 | def _throw_errors(self): 103 | if self.flags.get("error_list", 0): 104 | from alerts.utils import error 105 | 106 | msg = self.flags.error_list 107 | if len(msg) == 1: 108 | msg = msg.pop(0) 109 | else: 110 | msg = msg.copy() 111 | 112 | self._clean_flags() 113 | error(msg, _(self.doctype)) -------------------------------------------------------------------------------- /alerts/alerts/doctype/alerts_update_receiver/__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_update_receiver/alerts_update_receiver.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "autoname": "hash", 5 | "creation": "2023-04-04 04:04:04.119400", 6 | "description": "Update receiver for Alerts", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "user" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "user", 15 | "fieldtype": "Link", 16 | "label": "User", 17 | "options": "User", 18 | "reqd": 1, 19 | "bold": 1, 20 | "in_list_view": 1, 21 | "ignore_user_permissions": 1 22 | } 23 | ], 24 | "istable": 1, 25 | "modified": "2024-01-27 04:04:04.119400", 26 | "modified_by": "Administrator", 27 | "module": "Alerts", 28 | "name": "Alerts Update Receiver", 29 | "owner": "Administrator" 30 | } -------------------------------------------------------------------------------- /alerts/alerts/doctype/alerts_update_receiver/alerts_update_receiver.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.model.document import Document 8 | 9 | 10 | class AlertsUpdateReceiver(Document): 11 | pass -------------------------------------------------------------------------------- /alerts/alerts/report/__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/report/alert_report/__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/report/alert_report/alert_report.html: -------------------------------------------------------------------------------- 1 |

{%= __("Alert Report") %}

2 | 3 | {% if (filters.alert_type) { %} 4 |

{%= filters.alert_type %}

5 | {% } %} 6 | 7 | {% if (filters.from_date || filters.until_date) { %} 8 |
9 | {%= __("From") %}: 10 | {% if (filters.from_date) { %} 11 | {%= frappe.datetime.str_to_user(filters.from_date) %} 12 | {% } else { %} 13 | {%= __("Beginning") %} 14 | {% } %} 15 | 16 | {%= __("Until") %}: 17 | {% if (filters.until_date) { %} 18 | {%= frappe.datetime.str_to_user(filters.until_date) %} 19 | {% } else { %} 20 | {%= __("Today") %} 21 | {% } %} 22 |
23 | {% } %} 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% for(var i=0, l=data.length; i 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {% } %} 53 | 54 |
{%= __("Alert") %}{%= __("Title") %}{%= __("Alert Type") %}{%= __("From Date") %}{%= __("Until Date") %}{%= __("Repeatable") %}{%= __("Reached") %}{%= __("Status") %}
{%= frappe.format(data[i].alert, {fieldtype: "Link"}) || " " %}{%= data[i].title || " " %}{%= frappe.format(data[i].alert_type, {fieldtype: "Link"}) || " " %}{%= frappe.datetime.str_to_user(data[i].from_date) %}{%= frappe.datetime.str_to_user(data[i].until_date) %}{%= data[i].is_repeatable || __("No") %}{%= format_number(data[i].reached || 0, null, 0) %}{%= data[i].status || " " %}
55 | 56 |

{%= __("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 = $('