You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

339 lines
12 KiB

  1. # -*- encoding: utf-8 -*-
  2. # #############################################################################
  3. #
  4. # OpenERP, Open Source Management Solution
  5. # This module copyright (C) 2010 - 2014 Savoir-faire Linux
  6. # (<http://www.savoirfairelinux.com>).
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Affero General Public License as
  10. # published by the Free Software Foundation, either version 3 of the
  11. # License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Affero General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Affero General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. ##############################################################################
  22. import os
  23. import re
  24. import base64
  25. from datetime import date
  26. YEAR = date.today().year
  27. from collections import namedtuple
  28. from jinja2 import Environment, FileSystemLoader
  29. from openerp import models, api, fields
  30. class Prototype(models.Model):
  31. _name = "prototype"
  32. _description = "Prototype"
  33. licence = fields.Char(
  34. 'Licence',
  35. default='AGPL-3',
  36. )
  37. name = fields.Char(
  38. 'Technical Name', required=True,
  39. help=('The technical name will be used to define the name of '
  40. 'the exported module, the name of the model.')
  41. )
  42. category_id = fields.Many2one('ir.module.category', 'Category')
  43. human_name = fields.Char(
  44. 'Module Name', required=True,
  45. help=('The Module Name will be used as the displayed name of the '
  46. 'exported module.')
  47. )
  48. summary = fields.Char('Summary', required=True)
  49. description = fields.Text('Description', required=True)
  50. author = fields.Char('Author', required=True)
  51. maintainer = fields.Char('Maintainer')
  52. website = fields.Char('Website')
  53. icon_image = fields.Binary(
  54. 'Icon',
  55. help=('The icon set up here will be used as the icon '
  56. 'for the exported module also')
  57. )
  58. version = fields.Char('Version', size=3, default='0.1')
  59. auto_install = fields.Boolean(
  60. 'Auto Install',
  61. default=False,
  62. help='Check if the module should be install by default.'
  63. )
  64. application = fields.Boolean(
  65. 'Application',
  66. default=False,
  67. help='Check if the module is an Odoo application.'
  68. )
  69. # Relations
  70. dependency_ids = fields.Many2many(
  71. 'ir.module.module', 'prototype_module_rel',
  72. 'prototype_id', 'module_id',
  73. 'Dependencies'
  74. )
  75. data_ids = fields.Many2many(
  76. 'ir.filters',
  77. 'prototype_data_rel',
  78. 'prototype_id', 'filter_id',
  79. 'Data filters',
  80. help="The records matching the filters will be added as data."
  81. )
  82. demo_ids = fields.Many2many(
  83. 'ir.filters',
  84. 'prototype_demo_rel',
  85. 'prototype_id', 'filter_id',
  86. 'Demo filters',
  87. help="The records matching the filters will be added as demo data."
  88. )
  89. field_ids = fields.Many2many(
  90. 'ir.model.fields', 'prototype_fields_rel',
  91. 'prototype_id', 'field_id', 'Fields'
  92. )
  93. menu_ids = fields.Many2many(
  94. 'ir.ui.menu', 'prototype_menu_rel',
  95. 'prototype_id', 'menu_id', 'Menu Items'
  96. )
  97. view_ids = fields.Many2many(
  98. 'ir.ui.view', 'prototype_view_rel',
  99. 'prototype_id', 'view_id', 'Views'
  100. )
  101. group_ids = fields.Many2many(
  102. 'res.groups', 'prototype_groups_rel',
  103. 'prototype_id', 'group_id', 'Groups'
  104. )
  105. right_ids = fields.Many2many(
  106. 'ir.model.access', 'prototype_rights_rel',
  107. 'prototype_id', 'right_id',
  108. 'Access Rights'
  109. )
  110. rule_ids = fields.Many2many(
  111. 'ir.rule', 'prototype_rule_rel',
  112. 'prototype_id', 'rule_id', 'Record Rules'
  113. )
  114. __data_files = []
  115. __field_descriptions = {}
  116. _env = None
  117. File_details = namedtuple('file_details', ['filename', 'filecontent'])
  118. template_path = '{}/../templates/'.format(os.path.dirname(__file__))
  119. @api.model
  120. def set_jinja_env(self, api_version):
  121. """Set the Jinja2 environment.
  122. The environment will helps the system to find the templates to render.
  123. :param api_version: string, odoo api
  124. :return: jinja2.Environment instance.
  125. """
  126. if self._env is None:
  127. self._env = Environment(
  128. loader=FileSystemLoader(
  129. os.path.join(self.template_path, api_version)
  130. )
  131. )
  132. return self._env
  133. def set_field_descriptions(self):
  134. """Mock the list of fields into dictionary.
  135. It allows us to add or change attributes of the fields.
  136. :return: None
  137. """
  138. for field in self.field_ids:
  139. field_description = {}
  140. # This will mock a field record.
  141. # the mock will allow us to add data or modify the data
  142. # of the field (like for the name) with keeping all the
  143. # attributes of the record.
  144. field_description.update({
  145. attr_name: getattr(field, attr_name)
  146. for attr_name in dir(field)
  147. if not attr_name[0] == '_'
  148. })
  149. # custom fields start with the prefix x_.
  150. # it has to be removed.
  151. field_description['name'] = re.sub(r'^x_', '', field.name)
  152. self.__field_descriptions[field] = field_description
  153. @api.model
  154. def generate_files(self):
  155. """ Generates the files from the details of the prototype.
  156. :return: tuple
  157. """
  158. assert self._env is not None, \
  159. 'Run set_env(api_version) before to generate files.'
  160. self.set_field_descriptions()
  161. file_details = []
  162. file_details.extend(self.generate_models_details())
  163. file_details.extend(self.generate_views_details())
  164. file_details.extend(self.generate_menus_details())
  165. file_details.append(self.generate_module_init_file_details())
  166. # must be the last as the other generations might add information
  167. # to put in the __openerp__: additional dependencies, views files, etc.
  168. file_details.append(self.generate_module_openerp_file_details())
  169. file_details.append(self.save_icon())
  170. return file_details
  171. @api.model
  172. def save_icon(self):
  173. return self.File_details(
  174. os.path.join('static', 'description', 'icon.jpg'),
  175. base64.b64decode(self.icon_image)
  176. )
  177. @api.model
  178. def generate_module_openerp_file_details(self):
  179. """Wrapper to generate the __openerp__.py file of the module."""
  180. return self.generate_file_details(
  181. '__openerp__.py',
  182. '__openerp__.py.template',
  183. prototype=self,
  184. data_files=self.__data_files,
  185. )
  186. @api.model
  187. def generate_module_init_file_details(self):
  188. """Wrapper to generate the __init__.py file of the module."""
  189. return self.generate_file_details(
  190. '__init__.py',
  191. '__init__.py.template',
  192. # no import models if no work of fields in
  193. # the prototype
  194. models=bool(self.field_ids)
  195. )
  196. @api.model
  197. def generate_models_details(self):
  198. """Finds the models from the list of fields and generates
  199. the __init__ file and each models files (one by class).
  200. """
  201. files = []
  202. # TODO: doesn't work as need to find the module to import
  203. # and it is not necessary the name of the model the fields
  204. # belongs to.
  205. # ie. field.cell_phone is defined in a model inheriting from
  206. # res.partner.
  207. # How do we find the module the field was defined in?
  208. # dependencies = set([dep.id for dep in self.dependencies])
  209. relations = {}
  210. for field in self.__field_descriptions.itervalues():
  211. model = field.get('model_id')
  212. relations.setdefault(model, []).append(field)
  213. # dependencies.add(model.id)
  214. # blind update of dependencies.
  215. # self.write({
  216. # 'dependencies': [(6, 0, [id_ for id_ in dependencies])]
  217. # })
  218. files.append(self.generate_models_init_details(relations.keys()))
  219. for model, fields in relations.iteritems():
  220. files.append(self.generate_model_details(model, fields))
  221. return files
  222. @api.model
  223. def generate_models_init_details(self, ir_models):
  224. """Wrapper to generate the __init__.py file in models folder."""
  225. return self.generate_file_details(
  226. 'models/__init__.py',
  227. 'models/__init__.py.template',
  228. models=[
  229. self.friendly_name(ir_model.model)
  230. for ir_model in ir_models
  231. ]
  232. )
  233. @api.model
  234. def generate_views_details(self):
  235. """Wrapper to generate the views files."""
  236. relations = {}
  237. for view in self.view_ids:
  238. relations.setdefault(view.model, []).append(view)
  239. views_details = []
  240. for model, views in relations.iteritems():
  241. filepath = 'views/{}_view.xml'.format(
  242. self.friendly_name(model)
  243. )
  244. views_details.append(
  245. self.generate_file_details(
  246. filepath,
  247. 'views/model_views.xml.template',
  248. views=views
  249. )
  250. )
  251. self.__data_files.append(filepath)
  252. return views_details
  253. @api.model
  254. def generate_menus_details(self):
  255. """Wrapper to generate the menus files."""
  256. relations = {}
  257. for menu in self.menu_ids:
  258. relations.setdefault(menu.action.res_model, []).append(menu)
  259. menus_details = []
  260. for model_name, menus in relations.iteritems():
  261. filepath = 'views/{}_menus.xml'.format(
  262. self.friendly_name(model_name)
  263. )
  264. menus_details.append(
  265. self.generate_file_details(
  266. filepath,
  267. 'views/model_menus.xml.template',
  268. menus=menus,
  269. )
  270. )
  271. self.__data_files.append(filepath)
  272. return menus_details
  273. @api.model
  274. def generate_model_details(self, model, field_descriptions):
  275. """Wrapper to generate the python file for the model.
  276. :param model: ir.model record.
  277. :param field_descriptions: list of ir.model.fields records.
  278. :return: FileDetails instance.
  279. """
  280. python_friendly_name = self.friendly_name(model.model)
  281. return self.generate_file_details(
  282. 'models/{}.py'.format(python_friendly_name),
  283. 'models/model_name.py.template',
  284. name=python_friendly_name,
  285. inherit=model.model,
  286. fields=field_descriptions,
  287. )
  288. @staticmethod
  289. def friendly_name(name):
  290. return name.replace('.', '_')
  291. @api.model
  292. def generate_file_details(self, filename, template, **kwargs):
  293. """ generate file details from jinja2 template.
  294. :param filename: name of the file the content is related to
  295. :param template: path to the file to render the content
  296. :param kwargs: arguments of the template
  297. :return: File_details instance
  298. """
  299. template = self._env.get_template(template)
  300. # keywords used in several templates.
  301. kwargs.update(
  302. {
  303. 'export_year': YEAR,
  304. 'author': self.author,
  305. 'website': self.website,
  306. 'cr': self._cr,
  307. }
  308. )
  309. return self.File_details(filename, template.render(kwargs))