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.
456 lines
16 KiB
456 lines
16 KiB
# -*- encoding: utf-8 -*-
|
|
# #############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# This module copyright (C) 2010 - 2014 Savoir-faire Linux
|
|
# (<http://www.savoirfairelinux.com>).
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
import os
|
|
import re
|
|
import base64
|
|
from datetime import date
|
|
|
|
from collections import namedtuple
|
|
from jinja2 import Environment, FileSystemLoader
|
|
from openerp import models, api, fields
|
|
|
|
YEAR = date.today().year
|
|
|
|
|
|
class ModulePrototyper(models.Model):
|
|
"""Module Prototyper gathers different information from all over the
|
|
database to build a prototype of module.
|
|
We are calling it a prototype as it will most likely need to be reviewed
|
|
by a developer to fix glitch that would sneak it during the generation of
|
|
files but also to add not supported features.
|
|
"""
|
|
_name = "module_prototyper"
|
|
_description = "Module Prototyper"
|
|
|
|
license = fields.Selection(
|
|
[('GPL-2', 'GPL Version 2'),
|
|
('GPL-2 or any later version', 'GPL-2 or later version'),
|
|
('GPL-3', 'GPL Version 3'),
|
|
('GPL-3 or any later version', 'GPL-3 or later version'),
|
|
('AGPL-3', 'Affero GPL-3'),
|
|
('Other OSI approved licence', 'Other OSI Approved Licence'),
|
|
('Other proprietary', 'Other Proprietary')],
|
|
string='License',
|
|
default='AGPL-3',
|
|
)
|
|
name = fields.Char(
|
|
'Technical Name', required=True,
|
|
help=('The technical name will be used to define the name of '
|
|
'the exported module, the name of the model.')
|
|
)
|
|
category_id = fields.Many2one('ir.module.category', 'Category')
|
|
human_name = fields.Char(
|
|
'Module Name', required=True,
|
|
help=('The Module Name will be used as the displayed name of the '
|
|
'exported module.')
|
|
)
|
|
summary = fields.Char('Summary', required=True,
|
|
help=('Enter a summary of your module'))
|
|
description = fields.Text(
|
|
'Description',
|
|
required=True,
|
|
help=('Enter the description of your module, what it does, how to'
|
|
'install, configure and use it, the roadmap or known issues.'
|
|
'The description will be exported in README.rst'),
|
|
default="""
|
|
Module name
|
|
===========
|
|
|
|
This module was written to extend the functionality of ... to support ...
|
|
and allow you to ...
|
|
|
|
Installation
|
|
============
|
|
|
|
To install this module, you need to:
|
|
|
|
* do this ...
|
|
|
|
Configuration
|
|
=============
|
|
|
|
To configure this module, you need to:
|
|
|
|
* go to ...
|
|
|
|
Usage
|
|
=====
|
|
|
|
To use this module, you need to:
|
|
|
|
* go to ...
|
|
|
|
For further information, please visit:
|
|
|
|
* https://www.odoo.com/forum/help-1
|
|
|
|
Known issues / Roadmap
|
|
======================
|
|
|
|
* ...
|
|
|
|
Credits
|
|
=======
|
|
|
|
Contributors
|
|
------------
|
|
|
|
* Firsname Lastname <email.address@example.org>
|
|
|
|
Maintainer
|
|
----------
|
|
|
|
.. image:: http://odoo-community.org/logo.png
|
|
:alt: Odoo Community Association
|
|
:target: http://odoo-community.org
|
|
|
|
This module is maintained by the OCA.
|
|
|
|
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
|
mission is to support the collaborative development of Odoo features and
|
|
promote its widespread use.
|
|
|
|
To contribute to this module, please visit http://odoo-community.org.
|
|
"""
|
|
)
|
|
author = fields.Char('Author', required=True, help=('Enter your name'))
|
|
maintainer = fields.Char(
|
|
'Maintainer',
|
|
help=('Enter the name of the person or organization who will'
|
|
'maintain this module')
|
|
)
|
|
website = fields.Char('Website', help=('Enter the URL of your website'))
|
|
icon_image = fields.Binary(
|
|
'Icon',
|
|
help=('The icon set up here will be used as the icon '
|
|
'for the exported module also')
|
|
)
|
|
version = fields.Char(
|
|
'Version',
|
|
size=3,
|
|
default='0.1',
|
|
help=('Enter the version of your module with 2 digits')
|
|
)
|
|
auto_install = fields.Boolean(
|
|
'Auto Install',
|
|
default=False,
|
|
help='Check if the module should be install by default.'
|
|
)
|
|
application = fields.Boolean(
|
|
'Application',
|
|
default=False,
|
|
help='Check if the module is an Odoo application.'
|
|
)
|
|
# Relations
|
|
dependency_ids = fields.Many2many(
|
|
'ir.module.module', 'module_prototyper_module_rel',
|
|
'module_prototyper_id', 'module_id',
|
|
'Dependencies',
|
|
help=('Enter the list of required modules that need to be installed'
|
|
'for your module to work properly')
|
|
)
|
|
data_ids = fields.Many2many(
|
|
'ir.filters',
|
|
'prototype_data_rel',
|
|
'module_prototyper_id', 'filter_id',
|
|
'Data filters',
|
|
help="The records matching the filters will be added as data."
|
|
)
|
|
demo_ids = fields.Many2many(
|
|
'ir.filters',
|
|
'prototype_demo_rel',
|
|
'module_prototyper_id', 'filter_id',
|
|
'Demo filters',
|
|
help="The records matching the filters will be added as demo data."
|
|
)
|
|
field_ids = fields.Many2many(
|
|
'ir.model.fields', 'prototype_fields_rel',
|
|
'module_prototyper_id', 'field_id', 'Fields',
|
|
help=('Enter the list of fields that you have created or modified'
|
|
'and want to export in this module. New models will be'
|
|
'exported as long as you choose one of his fields.')
|
|
)
|
|
menu_ids = fields.Many2many(
|
|
'ir.ui.menu', 'prototype_menu_rel',
|
|
'module_prototyper_id', 'menu_id', 'Menu Items',
|
|
help=('Enter the list of menu items that you have created and want'
|
|
'to export in this module. Related windows actions will be'
|
|
'exported as well.')
|
|
)
|
|
view_ids = fields.Many2many(
|
|
'ir.ui.view', 'prototype_view_rel',
|
|
'module_prototyper_id', 'view_id', 'Views',
|
|
help=('Enter the list of views that you have created and want to'
|
|
'export in this module.')
|
|
)
|
|
group_ids = fields.Many2many(
|
|
'res.groups', 'prototype_groups_rel',
|
|
'module_prototyper_id', 'group_id', 'Groups',
|
|
help=('Enter the list of groups that you have created and want to'
|
|
'export in this module.')
|
|
)
|
|
right_ids = fields.Many2many(
|
|
'ir.model.access', 'prototype_rights_rel',
|
|
'module_prototyper_id', 'right_id',
|
|
'Access Rights',
|
|
help=('Enter the list of access rights that you have created and'
|
|
'want to export in this module.')
|
|
)
|
|
rule_ids = fields.Many2many(
|
|
'ir.rule', 'prototype_rule_rel',
|
|
'module_prototyper_id', 'rule_id', 'Record Rules',
|
|
help=('Enter the list of record rules that you have created and'
|
|
'want to export in this module.')
|
|
)
|
|
|
|
__data_files = []
|
|
__field_descriptions = {}
|
|
_env = None
|
|
File_details = namedtuple('file_details', ['filename', 'filecontent'])
|
|
template_path = '{}/../templates/'.format(os.path.dirname(__file__))
|
|
|
|
@api.model
|
|
def set_jinja_env(self, api_version):
|
|
"""Set the Jinja2 environment.
|
|
The environment will helps the system to find the templates to render.
|
|
:param api_version: string, odoo api
|
|
:return: jinja2.Environment instance.
|
|
"""
|
|
if self._env is None:
|
|
self._env = Environment(
|
|
loader=FileSystemLoader(
|
|
os.path.join(self.template_path, api_version)
|
|
)
|
|
)
|
|
return self._env
|
|
|
|
def set_field_descriptions(self):
|
|
"""Mock the list of fields into dictionary.
|
|
It allows us to add or change attributes of the fields.
|
|
|
|
:return: None
|
|
"""
|
|
for field in self.field_ids:
|
|
field_description = {}
|
|
# This will mock a field record.
|
|
# the mock will allow us to add data or modify the data
|
|
# of the field (like for the name) with keeping all the
|
|
# attributes of the record.
|
|
field_description.update({
|
|
attr_name: getattr(field, attr_name)
|
|
for attr_name in dir(field)
|
|
if not attr_name[0] == '_'
|
|
})
|
|
# custom fields start with the prefix x_.
|
|
# it has to be removed.
|
|
field_description['name'] = re.sub(r'^x_', '', field.name)
|
|
self.__field_descriptions[field] = field_description
|
|
|
|
@api.model
|
|
def generate_files(self):
|
|
""" Generates the files from the details of the prototype.
|
|
:return: tuple
|
|
"""
|
|
assert self._env is not None, \
|
|
'Run set_env(api_version) before to generate files.'
|
|
|
|
self.set_field_descriptions()
|
|
file_details = []
|
|
file_details.extend(self.generate_models_details())
|
|
file_details.extend(self.generate_views_details())
|
|
file_details.extend(self.generate_menus_details())
|
|
file_details.append(self.generate_module_init_file_details())
|
|
# must be the last as the other generations might add information
|
|
# to put in the __openerp__: additional dependencies, views files, etc.
|
|
file_details.append(self.generate_module_openerp_file_details())
|
|
if self.icon_image:
|
|
file_details.append(self.save_icon())
|
|
|
|
return file_details
|
|
|
|
@api.model
|
|
def save_icon(self):
|
|
"""Save the icon of the prototype as a image.
|
|
The image is used afterwards as the icon of the exported module.
|
|
|
|
:return: FileDetails instance
|
|
"""
|
|
# TODO: The image is not always a jpg.
|
|
# 2 ways to do it:
|
|
# * find a way to detect image type from the data
|
|
# * add document as a dependency.
|
|
# The second options seems to be better, as Document is a base module.
|
|
return self.File_details(
|
|
os.path.join('static', 'description', 'icon.jpg'),
|
|
base64.b64decode(self.icon_image)
|
|
)
|
|
|
|
@api.model
|
|
def generate_module_openerp_file_details(self):
|
|
"""Wrapper to generate the __openerp__.py file of the module."""
|
|
return self.generate_file_details(
|
|
'__openerp__.py',
|
|
'__openerp__.py.template',
|
|
prototype=self,
|
|
data_files=self.__data_files,
|
|
)
|
|
|
|
@api.model
|
|
def generate_module_init_file_details(self):
|
|
"""Wrapper to generate the __init__.py file of the module."""
|
|
return self.generate_file_details(
|
|
'__init__.py',
|
|
'__init__.py.template',
|
|
# no import models if no work of fields in
|
|
# the prototype
|
|
models=bool(self.field_ids)
|
|
)
|
|
|
|
@api.model
|
|
def generate_models_details(self):
|
|
"""Finds the models from the list of fields and generates
|
|
the __init__ file and each models files (one by class).
|
|
"""
|
|
files = []
|
|
# TODO: doesn't work as need to find the module to import
|
|
# and it is not necessary the name of the model the fields
|
|
# belongs to.
|
|
# ie. field.cell_phone is defined in a model inheriting from
|
|
# res.partner.
|
|
# How do we find the module the field was defined in?
|
|
# dependencies = set([dep.id for dep in self.dependencies])
|
|
|
|
relations = {}
|
|
for field in self.__field_descriptions.itervalues():
|
|
model = field.get('model_id')
|
|
relations.setdefault(model, []).append(field)
|
|
# dependencies.add(model.id)
|
|
|
|
# blind update of dependencies.
|
|
# self.write({
|
|
# 'dependencies': [(6, 0, [id_ for id_ in dependencies])]
|
|
# })
|
|
|
|
files.append(self.generate_models_init_details(relations.keys()))
|
|
for model, custom_fields in relations.iteritems():
|
|
files.append(self.generate_model_details(model, custom_fields))
|
|
|
|
return files
|
|
|
|
@api.model
|
|
def generate_models_init_details(self, ir_models):
|
|
"""Wrapper to generate the __init__.py file in models folder."""
|
|
return self.generate_file_details(
|
|
'models/__init__.py',
|
|
'models/__init__.py.template',
|
|
models=[
|
|
self.friendly_name(ir_model.model)
|
|
for ir_model in ir_models
|
|
]
|
|
)
|
|
|
|
@api.model
|
|
def generate_views_details(self):
|
|
"""Wrapper to generate the views files."""
|
|
relations = {}
|
|
for view in self.view_ids:
|
|
relations.setdefault(view.model, []).append(view)
|
|
|
|
views_details = []
|
|
for model, views in relations.iteritems():
|
|
filepath = 'views/{}_view.xml'.format(
|
|
self.friendly_name(model)
|
|
)
|
|
views_details.append(
|
|
self.generate_file_details(
|
|
filepath,
|
|
'views/model_views.xml.template',
|
|
views=views
|
|
)
|
|
)
|
|
self.__data_files.append(filepath)
|
|
|
|
return views_details
|
|
|
|
@api.model
|
|
def generate_menus_details(self):
|
|
"""Wrapper to generate the menus files."""
|
|
relations = {}
|
|
for menu in self.menu_ids:
|
|
relations.setdefault(menu.action.res_model, []).append(menu)
|
|
|
|
menus_details = []
|
|
for model_name, menus in relations.iteritems():
|
|
filepath = 'views/{}_menus.xml'.format(
|
|
self.friendly_name(model_name)
|
|
)
|
|
menus_details.append(
|
|
self.generate_file_details(
|
|
filepath,
|
|
'views/model_menus.xml.template',
|
|
menus=menus,
|
|
)
|
|
)
|
|
self.__data_files.append(filepath)
|
|
|
|
return menus_details
|
|
|
|
@api.model
|
|
def generate_model_details(self, model, field_descriptions):
|
|
"""Wrapper to generate the python file for the model.
|
|
|
|
:param model: ir.model record.
|
|
:param field_descriptions: list of ir.model.fields records.
|
|
:return: FileDetails instance.
|
|
"""
|
|
python_friendly_name = self.friendly_name(model.model)
|
|
return self.generate_file_details(
|
|
'models/{}.py'.format(python_friendly_name),
|
|
'models/model_name.py.template',
|
|
name=python_friendly_name,
|
|
inherit=model.model,
|
|
fields=field_descriptions,
|
|
)
|
|
|
|
@staticmethod
|
|
def friendly_name(name):
|
|
return name.replace('.', '_')
|
|
|
|
@api.model
|
|
def generate_file_details(self, filename, template, **kwargs):
|
|
""" generate file details from jinja2 template.
|
|
:param filename: name of the file the content is related to
|
|
:param template: path to the file to render the content
|
|
:param kwargs: arguments of the template
|
|
:return: File_details instance
|
|
"""
|
|
template = self._env.get_template(template)
|
|
# keywords used in several templates.
|
|
kwargs.update(
|
|
{
|
|
'export_year': YEAR,
|
|
'author': self.author,
|
|
'website': self.website,
|
|
'cr': self._cr,
|
|
}
|
|
)
|
|
return self.File_details(filename, template.render(kwargs))
|