├── app_manager ├── __init__.py ├── static │ └── description │ │ └── icon.png ├── models │ ├── __init__.py │ ├── app_manager.py │ ├── res_config_settings.py │ └── ir_module_module.py ├── security │ └── ir.model.access.csv ├── data │ ├── app_manager_data.xml │ ├── ir_cron_module.xml │ └── mail_data.xml ├── views │ ├── menus.xml │ ├── ir_module_module_views.xml │ ├── actions.xml │ ├── res_config_settings_view.xml │ └── app_manager_views.xml └── __manifest__.py ├── .gitignore └── README.md /app_manager/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import models 4 | 5 | -------------------------------------------------------------------------------- /app_manager/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheOdooStore/AppManager/HEAD/app_manager/static/description/icon.png -------------------------------------------------------------------------------- /app_manager/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import ir_module_module 4 | from . import app_manager 5 | from . import res_config_settings 6 | -------------------------------------------------------------------------------- /app_manager/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | access_app_manager,app_manager,model_app_manager,base.group_system,1,1,1,1 -------------------------------------------------------------------------------- /app_manager/data/app_manager_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 0 7 | 8 | 9 | -------------------------------------------------------------------------------- /app_manager/models/app_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import api, fields, models, tools, _ 3 | 4 | 5 | class AppManager(models.Model): 6 | _name = 'app.manager' 7 | _description = 'App Manager management details' 8 | _rec_name = 'kanban_version' 9 | _order = 'id' 10 | 11 | xml_kanban = fields.Text( 12 | string='Kanban code' 13 | ) 14 | 15 | kanban_version = fields.Float( 16 | string='Version' 17 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/usage.statistics.xml 5 | .idea/**/dictionaries 6 | .idea/**/shelf 7 | 8 | # Generated files 9 | .idea/**/contentModel.xml 10 | 11 | # Sensitive or high-churn files 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.local.xml 15 | .idea/**/sqlDataSources.xml 16 | .idea/**/dynamic.xml 17 | .idea/**/uiDesigner.xml 18 | .idea/**/dbnavigator.xml 19 | 20 | # Gradle 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # Mongo Explorer plugin 25 | .idea/**/mongoSettings.xml 26 | 27 | # Editor-based Rest Client 28 | .idea/httpRequests 29 | .idea 30 | *.pyc -------------------------------------------------------------------------------- /app_manager/views/menus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /app_manager/views/ir_module_module_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ir.module.module 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app_manager/models/res_config_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | 6 | class ResConfigSettings(models.TransientModel): 7 | _inherit = 'res.config.settings' 8 | 9 | receive_app_update_mails = fields.Boolean( 10 | string='Receive app update e-mails', 11 | default=True, 12 | config_parameter='app_manager.receive_app_update_mails', 13 | ) 14 | 15 | app_update_mail_template_id = fields.Many2one( 16 | 'mail.template', 17 | string='E-mail template', 18 | default=lambda self: self.env.ref('app_manager.mail_template_app_updates'), 19 | domain=[('model_id.model', '=', 'ir.module.module')], 20 | config_parameter='app_manager.app_update_mail_template_id') 21 | 22 | app_update_user_id = fields.Many2one( 23 | 'res.users', 24 | string='Users', 25 | default=lambda self: self.env.ref('base.user_admin'), 26 | config_parameter='app_manager.app_update_user_id' 27 | ) 28 | 29 | app_storage_location = fields.Char( 30 | string='Location to download apps too', 31 | help='This will be the default location where we store downloaded apps for you to use.', 32 | config_parameter='app_manager.app_storage_location' 33 | ) 34 | -------------------------------------------------------------------------------- /app_manager/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | 'name': "AppManager", 4 | 5 | 'summary': """ 6 | AppManager: the number one way to manage your external apps!""", 7 | 8 | 'description': """ 9 | AppManager is here to help you manage all your external apps. 10 | """, 11 | 12 | 'author': "The Odoo Store", 13 | 'website': "http://www.theodoostore.com", 14 | 15 | # Categories can be used to filter modules in modules listing 16 | # Check https://github.com/odoo/odoo/blob/14.0/odoo/addons/base/data/ir_module_category_data.xml 17 | # for the full list 18 | 'category': 'Uncategorized', 19 | 'version': '0.0.1', 20 | 21 | # any module necessary for this one to work correctly 22 | 'depends': ['product'], 23 | 'license': 'LGPL-3', 24 | 25 | # always loaded 26 | 'data': [ 27 | # Default data 28 | 'data/app_manager_data.xml', 29 | 'data/mail_data.xml', 30 | 31 | # Cron job 32 | 'data/ir_cron_module.xml', 33 | 34 | 'security/ir.model.access.csv', 35 | 'views/ir_module_module_views.xml', 36 | 'views/app_manager_views.xml', 37 | 'views/res_config_settings_view.xml', 38 | 'views/actions.xml', 39 | 'views/menus.xml', 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /app_manager/views/actions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Updates 5 | ir.actions.server 6 | 7 | code 8 | 9 | action = model.check_external_app_updates() 10 | 11 | 12 | 13 | 14 | AppManager apps 15 | ir.module.module 16 | kanban,tree,form 17 | 18 | 19 | {'search_default_store_app': 1} 20 | 21 |

22 | No modules found that are on our appstore. 23 |

24 |
25 |
26 | 27 | 28 | Settings 29 | ir.actions.act_window 30 | res.config.settings 31 | 32 | form 33 | inline 34 | {'module' : 'app_manager'} 35 | 36 |
37 | -------------------------------------------------------------------------------- /app_manager/data/ir_cron_module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppManager: check for remote app updates 6 | 7 | code 8 | model.check_external_app_updates() 9 | 10 | 11 | 13 | 1 14 | days 15 | -1 16 | 17 | 18 | 19 | 20 | AppManager: send app update e-mail 21 | 22 | code 23 | model.send_app_update_email() 24 | 25 | 26 | 28 | 1 29 | weeks 30 | -1 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

AppManager


2 |

3 | 4 | AppManager 5 | 6 |

7 | AppManager is an Odoo app management tool that automatically checks for app updates so you don't have to! 8 | 9 | ## Table of Contents 10 | 11 | - [Introduction](#TLDR) 12 | - [Installation](#installation) 13 | - [Features](#features) 14 | - [Issues](#issues-and-bugs) 15 | 16 | ## TLDR 17 | AppManager automatically checks your database it's apps to see if there are newer app versions available remotely.
18 | We compare the installed versions on your database with the latest available apps on The Odoo Store.
19 | Since The Odoo Store automatically syncs thousands of apps on a daily basis, through a direct Github connector, we always know about updates!
20 | AppManager will auto-check your apps against our platform and if updates are available you will be notified! 21 | 22 | ## Installation 23 | 1. Navigate into your custom addons path and clone this repository 24 | ``` 25 | cd /odoo14/custom/addons 26 | git clone https://github.com/theodoostore/AppManager.git 27 | ``` 28 | 29 | 2. Checkout the right directory depending on the version of your Odoo instance: 30 | ``` 31 | git checkout 14.0 32 | ``` 33 | 34 | 3. Add the Github repository into your Odoo configuration file. 35 | ``` 36 | addons_path=/odoo14/odoo14-server,/some/other/paths/you/have,/odoo14/custom/addons/AppManager 37 | ``` 38 | 39 | 4. Restart your Odoo instance so our new module is available 40 | ``` 41 | sudo service odoo14-server restart 42 | ``` 43 | 44 | 5. Go to Apps and click on "Update Apps List" 45 | 6. Search for the app `app_manager` and install it. 46 | 7. Congrats, that's it! You now see the AppManager app available on your main Odoo screen. 47 | 48 | 49 | ## Features 50 | AppManager overview 51 | 52 | - Quick overview of apps and their statusses 53 | - Quick button to instantly download the latest version from The Odoo Store 54 | - Quick button to instantly download and update your app within the database
Note: this is at your own risk! 55 | - Quick navigation the details about the app on The Odoo Store 56 | - See if apps are certified and up-to-date 57 | - See if apps have security issues (CVE's) 58 | - Get automatically notified about available updates 59 | 60 | 61 | ## Issues and bugs 62 | Have an issue or a bug? Please create a new report under the "Issues" section. 63 | -------------------------------------------------------------------------------- /app_manager/views/res_config_settings_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | res.config.settings.view.form.inherit.app.manager 5 | res.config.settings 6 | 7 | 8 | 9 | 10 |
13 |

App configuration

14 |
15 |
16 |
28 |
29 | 30 |

E-mail notifications

31 |
32 |
33 |
34 | 35 |
36 |
37 |
42 |
43 | 44 |
45 |
54 |
55 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /app_manager/data/mail_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppManager: notify user of app updates 6 | 7 | Your database has app updates! 8 | ${user.email_formatted | safe} 9 | 10 | ${user.lang} 11 | 12 |
13 | % set user = ctx.get('user', user) 14 | % set company = user.company_id 15 | % set module_update_values = object.get_updatable_apps() 16 | AppManager 20 | 21 |

22 | Weekly overview: available app updates 23 |

24 | 25 | % if module_update_values: 26 |

27 | Hi ${user.name},
28 | We've found updates for your apps! Here's an overview of apps that need an update: 29 |

30 | 31 | 33 | 34 | 37 | 40 | 43 | 46 | 49 | 50 |
35 | App icon 36 | 38 | Name 39 | 41 | Installed version 42 | 44 | Available version 45 | 47 | App info 48 |
51 | % for module_values in module_update_values: 52 | 54 | 55 | 60 | 63 | 72 | 75 | 82 | 83 |
56 | App icon 59 | 61 | ${module_values.get('name')} (${module_values.get('technical_name')}) 62 | 64 | ${module_values.get('installed_version')} 65 | % if module_values.get('store_has_security_issue'): 66 |
67 | 68 | Security issue! 69 | 70 | % endif 71 |
73 | ${module_values.get('store_version')} 74 | 76 | 79 | App info 80 | 81 |
84 | % endfor 85 | % endif 86 | % if not module_update_values: 87 |
88 | Good job! Your database is up-to-date and we have not found any app updates! 89 |
90 | % endif 91 |

92 | Happy coding,
93 | The Odoo Store 94 |

95 |
96 |
97 |
98 |
99 |
-------------------------------------------------------------------------------- /app_manager/views/app_manager_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | app.manager.ir.module.search 5 | ir.module.module 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | app.manager.ir.module.tree 30 | ir.module.module 31 | 32 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Store Apps 52 | ir.module.module 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 |
76 |
77 | 78 | Certified app 79 | 80 | 81 | 82 | 83 | Security issue! 84 | 85 | 86 | 87 | 88 | 89 | Needs an update 90 | 91 | 92 |
93 |
94 | 117 | App icon 119 |
120 |

121 | 122 |

123 |
124 |

125 | Installed version: 126 | 127 | Store version: 128 | 129 |

130 | 131 |
132 | 159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | -------------------------------------------------------------------------------- /app_manager/models/ir_module_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | import json 4 | import logging 5 | import tempfile 6 | import os 7 | import io 8 | import shutil 9 | import zipfile 10 | import odoo 11 | from odoo.exceptions import UserError, AccessDenied 12 | from odoo import models, fields, api, _, tools, modules 13 | 14 | _logger = logging.getLogger(__name__) 15 | 16 | 17 | def backup(path, raise_exception=True): 18 | """ 19 | Creates a temp backup of the code in case anything fails. It will be removed again if all went well. 20 | """ 21 | path = os.path.normpath(path) 22 | if not os.path.exists(path): 23 | if not raise_exception: 24 | return None 25 | raise OSError("The path '%s' does not exist on the server." % path) 26 | cnt = 1 27 | while True: 28 | bck = '%s~%d' % (path, cnt) 29 | if not os.path.exists(bck): 30 | shutil.move(path, bck) 31 | return bck 32 | cnt += 1 33 | 34 | 35 | class IrModuleModule(models.Model): 36 | _inherit = 'ir.module.module' 37 | 38 | store_version = fields.Char( 39 | string='Remote version' 40 | ) 41 | 42 | store_url = fields.Char( 43 | string='Store URL' 44 | ) 45 | 46 | store_download_url = fields.Char( 47 | string='Store download URL' 48 | ) 49 | 50 | store_is_free = fields.Boolean( 51 | string='Is free' 52 | ) 53 | 54 | store_app = fields.Boolean( 55 | string='Store app' 56 | ) 57 | 58 | store_update_available = fields.Boolean( 59 | string='Remote update available' 60 | ) 61 | 62 | store_is_certified = fields.Boolean( 63 | string='Certified app' 64 | ) 65 | 66 | store_has_security_issue = fields.Boolean( 67 | string='Has security issue' 68 | ) 69 | 70 | @api.model 71 | def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): 72 | """ 73 | Override of the fields_view_get to inject our custom XML view if the view is "store_manager". 74 | """ 75 | res = super(IrModuleModule, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=False) 76 | if view_type == 'kanban' and "store_manager" in res.get('arch'): 77 | res['arch'] = self.env['app.manager'].sudo().search([], limit=1).xml_kanban 78 | return res 79 | 80 | @api.model 81 | def get_app_store_server(self): 82 | """ 83 | Returns the URL endpoint to The Odoo Store 84 | """ 85 | return 'https://www.theodoostore.com' 86 | 87 | def _download_remote_app_code(self): 88 | """ 89 | Makes a call to the external apps server, requests the zipped app, puts it in our local path and unzips it. 90 | """ 91 | tmp = tempfile.mkdtemp() 92 | download_url = self.store_download_url 93 | 94 | try: 95 | _logger.info('Downloading module `%s` from appstore.', self.name) 96 | response = requests.get(download_url) 97 | response.raise_for_status() 98 | content = response.content 99 | except Exception: 100 | _logger.exception('Failed to fetch module %s from %s', self.name, download_url) 101 | raise UserError( 102 | _('The `%s` module appears to be unavailable at the moment. Please try again later.') % self.name) 103 | else: 104 | zipfile.ZipFile(io.BytesIO(content)).extractall(tmp) 105 | assert os.path.isdir(os.path.join(tmp, self.name)) 106 | 107 | module_path = modules.get_module_path(self.name, downloaded=True, display_warning=False) 108 | 109 | bck = backup(module_path, False) 110 | _logger.info('Copy downloaded module `%s` to `%s`', self.name, module_path) 111 | shutil.move(os.path.join(tmp, self.name), module_path) 112 | if bck: 113 | # All went well - removing the backup 114 | shutil.rmtree(bck) 115 | 116 | self.update_list() 117 | 118 | def _update_app_code(self): 119 | """ 120 | Triggers an actual update of the code and updates the database thanks to the button_immediate_install(). 121 | """ 122 | downloaded = self.search([('name', '=', self.name)]) 123 | installed = self.search([('id', 'in', downloaded.ids), ('state', '=', 'installed')]) 124 | 125 | to_install = self.search([('name', 'in', list(self.name)), ('state', '=', 'uninstalled')]) 126 | post_install_action = to_install.button_immediate_install() 127 | 128 | if installed or to_install: 129 | # in this case, force server restart to reload python code... 130 | self._cr.commit() 131 | odoo.service.server.restart() 132 | return { 133 | 'type': 'ir.actions.client', 134 | 'tag': 'home', 135 | 'params': {'wait': True}, 136 | } 137 | 138 | return post_install_action 139 | 140 | def _validate_configuration_and_access(self): 141 | """ 142 | Checks if the AppManager has been configured and if the user pressing update buttons has enough rights. 143 | """ 144 | # See if the user configured a module path in our configuration view. 145 | app_storage_location = self.env['ir.config_parameter'].sudo().get_param('app_manager.app_storage_location') 146 | if not app_storage_location: 147 | raise UserError(_('We cannot download this app as you haven\'t configured a download path yet.\n' 148 | 'You can do this from AppManager > Settings.')) 149 | 150 | # We'll only allow people with group_system rights to do these kind of operations. 151 | if not self.env.user.has_group('base.group_system'): 152 | raise AccessDenied() 153 | 154 | # Check if the directory exists and if we have enough access rights. 155 | if not os.access(app_storage_location, os.W_OK): 156 | raise UserError(_('The location specified is not accessible by the Odoo user.\n' 157 | 'Please make sure the directory exists and is writable.')) 158 | 159 | def download_remote_app(self): 160 | """ 161 | Main function to handle app downloading logic. Does the following: 162 | 1. Check if user has enough access and if everything is configured well. 163 | 2. Download the remote code (ZIP), unpack it in the specified path and update the apps list 164 | 3. If the button "Download & update app" was triggered we will also do an actual app update for the db. 165 | Notice that this is only for people who really know what they do! 166 | """ 167 | self._validate_configuration_and_access() 168 | self._download_remote_app_code() 169 | if self.env.context.get('update_app'): 170 | # TODO: should we trigger a wizard asking for double verification here? 171 | post_install_action = self._update_app_code() 172 | return post_install_action 173 | 174 | def _check_kanban_update(self, kanban_view_details): 175 | """ 176 | Checks if the database version of our Kanban view is outdated or not. If it is we store the new 177 | (remote) version in our local database. 178 | 179 | Args: 180 | kanban_view_details (dictionary): dictionary with the remote Kanban XML & the version 181 | Returns: 182 | None 183 | """ 184 | remote_kanban_version = float(kanban_view_details.get('version')) 185 | remote_kanban_view = kanban_view_details.get('kanban') 186 | 187 | # We will only keep one record in the db! 188 | app_manager_view = self.env['app.manager'].sudo().search([], limit=1) 189 | 190 | # Updated remote version - saving in our database for rendering! 191 | if app_manager_view.kanban_version < remote_kanban_version: 192 | app_manager_view.write({ 193 | 'xml_kanban': remote_kanban_view, 194 | 'kanban_version': remote_kanban_version 195 | }) 196 | 197 | def _prepare_local_app_details(self): 198 | """ 199 | Gets all locally installed modules and stores them into a dictionary along with the Odoo version. 200 | This is used to compare local app details with remote details. 201 | 202 | returns: 203 | app_dictionary (dictionary): dict with the most important details of the installed apps along with the db 204 | it's version 205 | """ 206 | installed_modules = self.env['ir.module.module'].search_read( 207 | [('state', '=', 'installed')], ["id", "name", "installed_version", "latest_version", "state", "author"] 208 | ) 209 | 210 | # Don't worry we are not going to do anything unethical with this. In fact, right now we do not do anything 211 | # with it. We're just including this for possible feature services related to your specific database. 212 | database_uuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid') 213 | 214 | app_dictionary = json.dumps({ 215 | 'modules': installed_modules, 216 | 'version': odoo.service.common.exp_version(), 217 | 'database_uuid': database_uuid 218 | }) 219 | 220 | return app_dictionary 221 | 222 | def check_external_app_updates(self): 223 | """ 224 | Main function which will get all details about apps from The Odoo Store. 225 | 1. Gets The Odoo Store URL 226 | 2. Stores the details about installed apps on the customer database in JSON format 227 | 3. Posts the data and gets back details about these apps from The Odoo Store server 228 | This is details such as the certification, if there is a remote update, security issues, ... 229 | 4. Stores the details in the local db for showing a visual UI to the end user about all apps 230 | """ 231 | app_store_server_url = self.get_app_store_server() 232 | 233 | # Prepares all local app details and converts them into a JSON 234 | app_dictionary = self._prepare_local_app_details() 235 | 236 | headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} 237 | response = requests.post( 238 | url=app_store_server_url + '/api/get_apps_to_update', data=app_dictionary, headers=headers 239 | ) 240 | 241 | # Failsafe for a down server, maintenance, ... Either way The Odoo Store API is not reachable. 242 | if response.status_code != 200: 243 | _logger.critical("We could not connect to '%s'. Falling back to old view/values." % app_store_server_url) 244 | return self.env.ref('app_manager.open_store_module_action').read()[0] 245 | else: 246 | _logger.info( 247 | "We could connect to the API's from '%s'. Fetching possible view updates & app details." % 248 | app_store_server_url 249 | ) 250 | 251 | # We should have a good JSON load now - let's parse it 252 | json_response = json.loads(response.text).get('result') 253 | 254 | # Return the actual view (which now has updated database values for our apps & the (possible) new Kanban view 255 | 256 | # Failsafe in case we do not get any good response 257 | # or because we do not yet have an XML architecture tested/supported for this Odoo version. 258 | if not json_response or json_response.get('version_not_supported'): 259 | # For sure no new view or changes - let's fall back to our already stored view. 260 | return self.env.ref('app_manager.open_store_module_action').read()[0] 261 | 262 | # Checks if our remote server has a new XML update for the Kanban design or not 263 | self._check_kanban_update(json_response.get('views')) 264 | 265 | apps_to_update = json_response.get('modules') 266 | to_update_apps = [] 267 | if apps_to_update: 268 | for app_to_update in apps_to_update: 269 | app = self.browse(int(app_to_update.get('id'))) 270 | app.update({ 271 | 'store_app': True, 272 | 'store_is_certified': app_to_update.get('is_certified'), 273 | 'store_is_free': True if app_to_update.get('app_type') == 'Free' else False, 274 | 'store_url': app_to_update.get('store_url'), 275 | 'store_download_url': app_to_update.get('download_url'), 276 | 'store_version': app_to_update.get('remote_version'), 277 | 'store_update_available': True if app_to_update.get('remote_version') != app.installed_version else False, 278 | 'store_has_security_issue': True if app_to_update.get('has_security_issue') else False, 279 | }) 280 | to_update_apps += app 281 | self.env['ir.module.module'].write(to_update_apps) 282 | 283 | # We (might have had) a view update - let's call the action. 284 | # This has to be done after our remote sync so we 'get' the possible new remote view. 285 | action = self.env.ref('app_manager.open_store_module_action').read()[0] 286 | return action 287 | 288 | def get_updatable_apps(self): 289 | """ 290 | Fetches details about all apps that have updates available on The Odoo Store and passes the details 291 | along as a list. This is used for the email template. 292 | """ 293 | module_values = [] 294 | # There is a reason why we search apps & then convert the values we need in a dictionary! 295 | # Odoo is unable to fetch fields on the base model in a new module within Jinja2. This is the easiest workaround 296 | modules = self.search([ 297 | ('store_update_available', '=', True) 298 | ]) 299 | 300 | for module in modules: 301 | module_values.append({ 302 | 'name': module.shortdesc, 303 | 'technical_name': module.name, 304 | 'app_icon': module.icon, 305 | 'installed_version': module.installed_version, 306 | 'store_version': module.store_version, 307 | 'store_url': module.store_url, 308 | 'store_has_security_issue': module.store_has_security_issue 309 | }) 310 | return module_values 311 | 312 | def send_app_update_email(self): 313 | """ 314 | Automatically sends an e-mail to the user which is configured under AppManager > Settings. 315 | """ 316 | # Check if this database should send an e-mail or not 317 | notify_by_email = self.env['ir.config_parameter'].sudo().get_param('app_manager.receive_app_update_mails') 318 | 319 | if notify_by_email: 320 | # Get all the details that we need for sending out the email 321 | app_update_email_template = self.env['ir.config_parameter'].sudo().get_param( 322 | 'app_manager.app_update_mail_template_id') 323 | user_id = self.env['ir.config_parameter'].sudo().get_param('app_manager.app_update_user_id') 324 | user_email = self.env['res.users'].browse(int(user_id)).partner_id.email 325 | # Just a placeholder for generating an unique id 326 | app_manager = self.env['app.manager'].search([], limit=1).id 327 | 328 | mail_template = self.env['mail.template'].sudo().browse(int(app_update_email_template)) 329 | mail_template['email_to'] = user_email 330 | mail_template.send_mail(app_manager, force_send=True) 331 | --------------------------------------------------------------------------------