diff --git a/developer/_static/banners/actions.jpg b/developer/_static/banners/actions.jpg new file mode 100644 index 000000000..fb08a2343 Binary files /dev/null and b/developer/_static/banners/actions.jpg differ diff --git a/developer/_static/banners/actions.small.jpg b/developer/_static/banners/actions.small.jpg new file mode 100644 index 000000000..be664dafc Binary files /dev/null and b/developer/_static/banners/actions.small.jpg differ diff --git a/developer/_static/banners/build_a_module.jpg b/developer/_static/banners/build_a_module.jpg new file mode 100644 index 000000000..e885caba7 Binary files /dev/null and b/developer/_static/banners/build_a_module.jpg differ diff --git a/developer/_static/banners/build_a_module.small.jpg b/developer/_static/banners/build_a_module.small.jpg new file mode 100644 index 000000000..272eb7cd6 Binary files /dev/null and b/developer/_static/banners/build_a_module.small.jpg differ diff --git a/developer/_static/banners/build_a_theme.jpg b/developer/_static/banners/build_a_theme.jpg new file mode 100644 index 000000000..ea9a52688 Binary files /dev/null and b/developer/_static/banners/build_a_theme.jpg differ diff --git a/developer/_static/banners/build_a_theme.small.jpg b/developer/_static/banners/build_a_theme.small.jpg new file mode 100644 index 000000000..df27fec5d Binary files /dev/null and b/developer/_static/banners/build_a_theme.small.jpg differ diff --git a/developer/_static/banners/build_a_website.jpg b/developer/_static/banners/build_a_website.jpg new file mode 100644 index 000000000..f302d7e34 Binary files /dev/null and b/developer/_static/banners/build_a_website.jpg differ diff --git a/developer/_static/banners/build_a_website.small.jpg b/developer/_static/banners/build_a_website.small.jpg new file mode 100644 index 000000000..198d4dad9 Binary files /dev/null and b/developer/_static/banners/build_a_website.small.jpg differ diff --git a/developer/_static/banners/build_interface_ext.jpg b/developer/_static/banners/build_interface_ext.jpg new file mode 100644 index 000000000..ab0c93b51 Binary files /dev/null and b/developer/_static/banners/build_interface_ext.jpg differ diff --git a/developer/_static/banners/build_interface_ext.small.jpg b/developer/_static/banners/build_interface_ext.small.jpg new file mode 100644 index 000000000..6c197b394 Binary files /dev/null and b/developer/_static/banners/build_interface_ext.small.jpg differ diff --git a/developer/_static/banners/cdn.jpg b/developer/_static/banners/cdn.jpg new file mode 100644 index 000000000..098541df3 Binary files /dev/null and b/developer/_static/banners/cdn.jpg differ diff --git a/developer/_static/banners/cdn.small.jpg b/developer/_static/banners/cdn.small.jpg new file mode 100644 index 000000000..fb423e0c5 Binary files /dev/null and b/developer/_static/banners/cdn.small.jpg differ diff --git a/developer/_static/banners/cmdline.jpg b/developer/_static/banners/cmdline.jpg new file mode 100644 index 000000000..386bb0fa9 Binary files /dev/null and b/developer/_static/banners/cmdline.jpg differ diff --git a/developer/_static/banners/cmdline.small.jpg b/developer/_static/banners/cmdline.small.jpg new file mode 100644 index 000000000..4727b1063 Binary files /dev/null and b/developer/_static/banners/cmdline.small.jpg differ diff --git a/developer/_static/banners/company.jpg b/developer/_static/banners/company.jpg new file mode 100644 index 000000000..cdecba726 Binary files /dev/null and b/developer/_static/banners/company.jpg differ diff --git a/developer/_static/banners/data_files.jpg b/developer/_static/banners/data_files.jpg new file mode 100644 index 000000000..9218bab9a Binary files /dev/null and b/developer/_static/banners/data_files.jpg differ diff --git a/developer/_static/banners/data_files.small.jpg b/developer/_static/banners/data_files.small.jpg new file mode 100644 index 000000000..957e5ffe4 Binary files /dev/null and b/developer/_static/banners/data_files.small.jpg differ diff --git a/developer/_static/banners/deploying_odoo.jpg b/developer/_static/banners/deploying_odoo.jpg new file mode 100644 index 000000000..a12213599 Binary files /dev/null and b/developer/_static/banners/deploying_odoo.jpg differ diff --git a/developer/_static/banners/deploying_odoo.small.jpg b/developer/_static/banners/deploying_odoo.small.jpg new file mode 100644 index 000000000..84ade466a Binary files /dev/null and b/developer/_static/banners/deploying_odoo.small.jpg differ diff --git a/developer/_static/banners/email_gateway.jpg b/developer/_static/banners/email_gateway.jpg new file mode 100644 index 000000000..b9301e270 Binary files /dev/null and b/developer/_static/banners/email_gateway.jpg differ diff --git a/developer/_static/banners/email_gateway.small.jpg b/developer/_static/banners/email_gateway.small.jpg new file mode 100644 index 000000000..435c312eb Binary files /dev/null and b/developer/_static/banners/email_gateway.small.jpg differ diff --git a/developer/_static/banners/enterprise.jpg b/developer/_static/banners/enterprise.jpg new file mode 100644 index 000000000..7c56721c7 Binary files /dev/null and b/developer/_static/banners/enterprise.jpg differ diff --git a/developer/_static/banners/enterprise.small.jpg b/developer/_static/banners/enterprise.small.jpg new file mode 100644 index 000000000..172fbf5a3 Binary files /dev/null and b/developer/_static/banners/enterprise.small.jpg differ diff --git a/developer/_static/banners/iap.jpg b/developer/_static/banners/iap.jpg new file mode 100644 index 000000000..410d62294 Binary files /dev/null and b/developer/_static/banners/iap.jpg differ diff --git a/developer/_static/banners/iap.small.jpg b/developer/_static/banners/iap.small.jpg new file mode 100644 index 000000000..186c446d8 Binary files /dev/null and b/developer/_static/banners/iap.small.jpg differ diff --git a/developer/_static/banners/index.jpg b/developer/_static/banners/index.jpg new file mode 100644 index 000000000..1edec3005 Binary files /dev/null and b/developer/_static/banners/index.jpg differ diff --git a/developer/_static/banners/installing_odoo.jpg b/developer/_static/banners/installing_odoo.jpg new file mode 100644 index 000000000..1b33e69ee Binary files /dev/null and b/developer/_static/banners/installing_odoo.jpg differ diff --git a/developer/_static/banners/installing_odoo.small.jpg b/developer/_static/banners/installing_odoo.small.jpg new file mode 100644 index 000000000..fdddc8c7a Binary files /dev/null and b/developer/_static/banners/installing_odoo.small.jpg differ diff --git a/developer/_static/banners/iot.jpg b/developer/_static/banners/iot.jpg new file mode 100644 index 000000000..537527d7b Binary files /dev/null and b/developer/_static/banners/iot.jpg differ diff --git a/developer/_static/banners/javascript.jpg b/developer/_static/banners/javascript.jpg new file mode 100644 index 000000000..672a79e03 Binary files /dev/null and b/developer/_static/banners/javascript.jpg differ diff --git a/developer/_static/banners/javascript.small.jpg b/developer/_static/banners/javascript.small.jpg new file mode 100644 index 000000000..f315a888e Binary files /dev/null and b/developer/_static/banners/javascript.small.jpg differ diff --git a/developer/_static/banners/localization.jpg b/developer/_static/banners/localization.jpg new file mode 100644 index 000000000..1939c85de Binary files /dev/null and b/developer/_static/banners/localization.jpg differ diff --git a/developer/_static/banners/m_1.small.jpg b/developer/_static/banners/m_1.small.jpg new file mode 100644 index 000000000..af8f57824 Binary files /dev/null and b/developer/_static/banners/m_1.small.jpg differ diff --git a/developer/_static/banners/m_2.small.jpg b/developer/_static/banners/m_2.small.jpg new file mode 100644 index 000000000..cd3b1dd82 Binary files /dev/null and b/developer/_static/banners/m_2.small.jpg differ diff --git a/developer/_static/banners/m_accounting.small.jpg b/developer/_static/banners/m_accounting.small.jpg new file mode 100644 index 000000000..9e4a0228d Binary files /dev/null and b/developer/_static/banners/m_accounting.small.jpg differ diff --git a/developer/_static/banners/mixins.jpg b/developer/_static/banners/mixins.jpg new file mode 100644 index 000000000..4c9800fe1 Binary files /dev/null and b/developer/_static/banners/mixins.jpg differ diff --git a/developer/_static/banners/mixins.small.jpg b/developer/_static/banners/mixins.small.jpg new file mode 100644 index 000000000..2e5d8eef6 Binary files /dev/null and b/developer/_static/banners/mixins.small.jpg differ diff --git a/developer/_static/banners/mobile.jpg b/developer/_static/banners/mobile.jpg new file mode 100644 index 000000000..f0667cfaf Binary files /dev/null and b/developer/_static/banners/mobile.jpg differ diff --git a/developer/_static/banners/module.jpg b/developer/_static/banners/module.jpg new file mode 100644 index 000000000..2ef413d40 Binary files /dev/null and b/developer/_static/banners/module.jpg differ diff --git a/developer/_static/banners/module.small.jpg b/developer/_static/banners/module.small.jpg new file mode 100644 index 000000000..18db8003a Binary files /dev/null and b/developer/_static/banners/module.small.jpg differ diff --git a/developer/_static/banners/odoo_guideline.jpg b/developer/_static/banners/odoo_guideline.jpg new file mode 100644 index 000000000..322ac65d2 Binary files /dev/null and b/developer/_static/banners/odoo_guideline.jpg differ diff --git a/developer/_static/banners/odoo_guideline.small.jpg b/developer/_static/banners/odoo_guideline.small.jpg new file mode 100644 index 000000000..1d784d87e Binary files /dev/null and b/developer/_static/banners/odoo_guideline.small.jpg differ diff --git a/developer/_static/banners/orm_api.jpg b/developer/_static/banners/orm_api.jpg new file mode 100644 index 000000000..04d51fd91 Binary files /dev/null and b/developer/_static/banners/orm_api.jpg differ diff --git a/developer/_static/banners/orm_api.small.jpg b/developer/_static/banners/orm_api.small.jpg new file mode 100644 index 000000000..770b5ffa9 Binary files /dev/null and b/developer/_static/banners/orm_api.small.jpg differ diff --git a/developer/_static/banners/qweb.jpg b/developer/_static/banners/qweb.jpg new file mode 100644 index 000000000..2c61bf0e9 Binary files /dev/null and b/developer/_static/banners/qweb.jpg differ diff --git a/developer/_static/banners/qweb.small.jpg b/developer/_static/banners/qweb.small.jpg new file mode 100644 index 000000000..8db574c4c Binary files /dev/null and b/developer/_static/banners/qweb.small.jpg differ diff --git a/developer/_static/banners/reports.jpg b/developer/_static/banners/reports.jpg new file mode 100644 index 000000000..b2ff103ef Binary files /dev/null and b/developer/_static/banners/reports.jpg differ diff --git a/developer/_static/banners/reports.small.jpg b/developer/_static/banners/reports.small.jpg new file mode 100644 index 000000000..36bcc3a59 Binary files /dev/null and b/developer/_static/banners/reports.small.jpg differ diff --git a/developer/_static/banners/security.jpg b/developer/_static/banners/security.jpg new file mode 100644 index 000000000..67bcb9f83 Binary files /dev/null and b/developer/_static/banners/security.jpg differ diff --git a/developer/_static/banners/security.small.jpg b/developer/_static/banners/security.small.jpg new file mode 100644 index 000000000..ac6f79db9 Binary files /dev/null and b/developer/_static/banners/security.small.jpg differ diff --git a/developer/_static/banners/testing_modules.jpg b/developer/_static/banners/testing_modules.jpg new file mode 100644 index 000000000..8931371d7 Binary files /dev/null and b/developer/_static/banners/testing_modules.jpg differ diff --git a/developer/_static/banners/testing_modules.small.jpg b/developer/_static/banners/testing_modules.small.jpg new file mode 100644 index 000000000..3ed683f04 Binary files /dev/null and b/developer/_static/banners/testing_modules.small.jpg differ diff --git a/developer/_static/banners/translate.jpg b/developer/_static/banners/translate.jpg new file mode 100644 index 000000000..6795a3aa1 Binary files /dev/null and b/developer/_static/banners/translate.jpg differ diff --git a/developer/_static/banners/translate.small.jpg b/developer/_static/banners/translate.small.jpg new file mode 100644 index 000000000..c225049db Binary files /dev/null and b/developer/_static/banners/translate.small.jpg differ diff --git a/developer/_static/banners/upgrade_api.jpg b/developer/_static/banners/upgrade_api.jpg new file mode 100644 index 000000000..04a9422f9 Binary files /dev/null and b/developer/_static/banners/upgrade_api.jpg differ diff --git a/developer/_static/banners/views.jpg b/developer/_static/banners/views.jpg new file mode 100644 index 000000000..4d9fd0e20 Binary files /dev/null and b/developer/_static/banners/views.jpg differ diff --git a/developer/_static/banners/views.small.jpg b/developer/_static/banners/views.small.jpg new file mode 100644 index 000000000..34aa32d40 Binary files /dev/null and b/developer/_static/banners/views.small.jpg differ diff --git a/developer/_static/banners/web_controllers.jpg b/developer/_static/banners/web_controllers.jpg new file mode 100644 index 000000000..347698b52 Binary files /dev/null and b/developer/_static/banners/web_controllers.jpg differ diff --git a/developer/_static/banners/web_controllers.small.jpg b/developer/_static/banners/web_controllers.small.jpg new file mode 100644 index 000000000..596b28456 Binary files /dev/null and b/developer/_static/banners/web_controllers.small.jpg differ diff --git a/developer/_static/banners/web_service_api.jpg b/developer/_static/banners/web_service_api.jpg new file mode 100644 index 000000000..aa70ba8c5 Binary files /dev/null and b/developer/_static/banners/web_service_api.jpg differ diff --git a/developer/_static/banners/web_service_api.small.jpg b/developer/_static/banners/web_service_api.small.jpg new file mode 100644 index 000000000..63b3adf27 Binary files /dev/null and b/developer/_static/banners/web_service_api.small.jpg differ diff --git a/developer/_static/issue_template.md b/developer/_static/issue_template.md new file mode 100644 index 000000000..7ede783f5 --- /dev/null +++ b/developer/_static/issue_template.md @@ -0,0 +1,19 @@ +Short description of the issue + +***Impacted versions:*** + + - + +***Steps to reproduce:*** + + 1. + 2. + 3. + +***Current behavior:*** + + - + +***Expected behavior:*** + + - diff --git a/developer/_static/odoo.css b/developer/_static/odoo.css new file mode 100644 index 000000000..c370773fd --- /dev/null +++ b/developer/_static/odoo.css @@ -0,0 +1,26 @@ +@import "style.css"; + +p.rubric { + font-family: Lato, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; + color: inherit; + margin-top: 10px; + margin-bottom: 8px; +} + +.section p.rubric { + font-size: 30px; +} +.section .section p.rubric { + font-size: 24px; +} +.section .section .section p.rubric { + font-size: 18px; +} +.section .section .section .section p.rubric { + font-size: 14px; +} +.section .section .section .section .section p.rubric { + font-size: 12px; +} diff --git a/developer/_static/pull-request-version.png b/developer/_static/pull-request-version.png new file mode 100644 index 000000000..dd45388d0 Binary files /dev/null and b/developer/_static/pull-request-version.png differ diff --git a/developer/conf.py b/developer/conf.py new file mode 100644 index 000000000..8cba66544 --- /dev/null +++ b/developer/conf.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +import sys, os +import sphinx + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +DIR = os.path.dirname(__file__) +sys.path.insert(0, + os.path.abspath( + os.path.join(DIR, '_extensions'))) +# put current odoo's source on PYTHONPATH for autodoc +sys.path.insert(0, os.path.abspath(os.path.join(DIR, '../../odoo'))) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = '1.2' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.ifconfig', + 'sphinx.ext.todo', + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.linkcode', + # 'autojsdoc.ext', + 'github_link', + 'odoo_ext', + 'html_domain', + 'exercise_admonition', + 'patchqueue' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'odoo' +copyright = u'Odoo S.A.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '13.0' +# The full version, including alpha/beta/rc tags. +release = '13.0' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# markdown compatibility: make `foo` behave like ``foo``, the rst default is +# title-reference which is never what people are looking for +default_role = 'literal' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'odoo' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'odoo_ext' + +odoo_cover_default = 'banners/installing_odoo.jpg' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ['_extensions'] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +html_add_permalinks = u'' + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# FIXME: no sidebar on index? +html_sidebars = { +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +latex_elements = { + 'papersize': r'a4paper', + 'preamble': u'''\\setcounter{tocdepth}{2} +''', +} + +# default must be set otherwise ifconfig blows up +todo_include_todos = False + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', None), + 'werkzeug': ('http://werkzeug.pocoo.org/docs/', None), +} + +github_user = 'odoo' +github_project = 'odoo' + +# monkeypatch PHP lexer to not require ` + +Start/Stop the Odoo server +========================== + +Odoo uses a client/server architecture in which clients are web browsers +accessing the Odoo server via RPC. + +Business logic and extension is generally performed on the server side, +although supporting client features (e.g. new data representation such as +interactive maps) can be added to the client. + +In order to start the server, simply invoke the command :ref:`odoo-bin +` in the shell, adding the full path to the file if +necessary: + +.. code:: bash + + odoo-bin + +The server is stopped by hitting ``Ctrl-C`` twice from the terminal, or by +killing the corresponding OS process. + +Build an Odoo module +==================== + +Both server and client extensions are packaged as *modules* which are +optionally loaded in a *database*. + +Odoo modules can either add brand new business logic to an Odoo system, or +alter and extend existing business logic: a module can be created to add your +country's accounting rules to Odoo's generic accounting support, while the +next module adds support for real-time visualisation of a bus fleet. + +Everything in Odoo thus starts and ends with modules. + +Composition of a module +----------------------- + +An Odoo module can contain a number of elements: + +Business objects + Declared as Python classes, these resources are automatically persisted + by Odoo based on their configuration + +:ref:`Object views ` + Definition of business objects UI display + +:ref:`Data files ` + XML or CSV files declaring the model metadata : + + * :ref:`views ` or :ref:`reports `, + * configuration data (modules parametrization, :ref:`security rules `), + * demonstration data + * and more + +:ref:`Web controllers ` + Handle requests from web browsers + +Static web data + Images, CSS or javascript files used by the web interface or website + +Module structure +---------------- + +Each module is a directory within a *module directory*. Module directories +are specified by using the :option:`--addons-path ` +option. + +.. tip:: + :class: aphorism + + most command-line options can also be set using :ref:`a configuration + file ` + +An Odoo module is declared by its :ref:`manifest `. + +A module is also a +`Python package `_ +with a ``__init__.py`` file, containing import instructions for various Python +files in the module. + +For instance, if the module has a single ``mymodule.py`` file ``__init__.py`` +might contain:: + + from . import mymodule + +Odoo provides a mechanism to help set up a new module, :ref:`odoo-bin +` has a subcommand :ref:`scaffold +` to create an empty module: + +.. code-block:: console + + $ odoo-bin scaffold + +The command creates a subdirectory for your module, and automatically creates a +bunch of standard files for a module. Most of them simply contain commented code +or XML. The usage of most of those files will be explained along this tutorial. + +.. exercise:: Module creation + + Use the command line above to create an empty module Open Academy, and + install it in Odoo. + + .. only:: solutions + + #. Invoke the command ``odoo-bin scaffold openacademy addons``. + #. Adapt the manifest file to your module. + #. Don't bother about the other files. + + .. patch:: + +Object-Relational Mapping +------------------------- + +A key component of Odoo is the :abbr:`ORM (Object-Relational Mapping)` layer. +This layer avoids having to write most :abbr:`SQL (Structured Query Language)` +by hand and provides extensibility and security services\ [#rawsql]_. + +Business objects are declared as Python classes extending +:class:`~odoo.models.Model` which integrates them into the automated +persistence system. + +Models can be configured by setting a number of attributes at their +definition. The most important attribute is +:attr:`~odoo.models.Model._name` which is required and defines the name for +the model in the Odoo system. Here is a minimally complete definition of a +model:: + + from odoo import models + class MinimalModel(models.Model): + _name = 'test.model' + +Model fields +------------ + +Fields are used to define what the model can store and where. Fields are +defined as attributes on the model class:: + + from odoo import models, fields + + class LessMinimalModel(models.Model): + _name = 'test.model2' + + name = fields.Char() + +Common Attributes +################# + +Much like the model itself, its fields can be configured, by passing +configuration attributes as parameters:: + + name = field.Char(required=True) + +Some attributes are available on all fields, here are the most common ones: + +:attr:`~odoo.fields.Field.string` (``unicode``, default: field's name) + The label of the field in UI (visible by users). +:attr:`~odoo.fields.Field.required` (``bool``, default: ``False``) + If ``True``, the field can not be empty, it must either have a default + value or always be given a value when creating a record. +:attr:`~odoo.fields.Field.help` (``unicode``, default: ``''``) + Long-form, provides a help tooltip to users in the UI. +:attr:`~odoo.fields.Field.index` (``bool``, default: ``False``) + Requests that Odoo create a `database index`_ on the column. + +Simple fields +############# + +There are two broad categories of fields: "simple" fields which are atomic +values stored directly in the model's table and "relational" fields linking +records (of the same model or of different models). + +Example of simple fields are :class:`~odoo.fields.Boolean`, +:class:`~odoo.fields.Date`, :class:`~odoo.fields.Char`. + +Reserved fields +############### + +Odoo creates a few fields in all models\ [#autofields]_. These fields are +managed by the system and shouldn't be written to. They can be read if +useful or necessary: + +:attr:`~odoo.fields.Model.id` (:class:`~odoo.fields.Id`) + The unique identifier for a record in its model. +:attr:`~odoo.fields.Model.create_date` (:class:`~odoo.fields.Datetime`) + Creation date of the record. +:attr:`~odoo.fields.Model.create_uid` (:class:`~odoo.fields.Many2one`) + User who created the record. +:attr:`~odoo.fields.Model.write_date` (:class:`~odoo.fields.Datetime`) + Last modification date of the record. +:attr:`~odoo.fields.Model.write_uid` (:class:`~odoo.fields.Many2one`) + user who last modified the record. + +Special fields +############## + +By default, Odoo also requires a ``name`` field on all models for various +display and search behaviors. The field used for these purposes can be +overridden by setting :attr:`~odoo.models.Model._rec_name`. + +.. exercise:: Define a model + + Define a new data model *Course* in the *openacademy* module. A course + has a title and a description. Courses must have a title. + + .. only:: solutions + + Edit the file ``openacademy/models/models.py`` to include a *Course* class. + + .. patch:: + +Data files +---------- + +Odoo is a highly data driven system. Although behavior is customized using +Python_ code part of a module's value is in the data it sets up when loaded. + +.. tip:: some modules exist solely to add data into Odoo + :class: aphorism + +Module data is declared via :ref:`data files `, XML files with +```` elements. Each ```` element creates or updates a database +record. + +.. code-block:: xml + + + + + {a value} + + + + +* ``model`` is the name of the Odoo model for the record. +* ``id`` is an :term:`external identifier`, it allows referring to the record + (without having to know its in-database identifier). +* ```` elements have a ``name`` which is the name of the field in the + model (e.g. ``description``). Their body is the field's value. + +Data files have to be declared in the manifest file to be loaded, they can +be declared in the ``'data'`` list (always loaded) or in the ``'demo'`` list +(only loaded in demonstration mode). + +.. exercise:: Define demonstration data + + Create demonstration data filling the *Courses* model with a few + demonstration courses. + + .. only:: solutions + + Edit the file ``openacademy/demo/demo.xml`` to include some data. + + .. patch:: + +.. tip:: The content of the data files is only loaded when a module is + installed or updated. + + After making some changes, do not forget to use + :ref:`odoo-bin -u openacademy ` to save the changes + to your database. + +.. _howtos/module/actions: + +Actions and Menus +----------------- + +Actions and menus are regular records in database, usually declared through +data files. Actions can be triggered in three ways: + +#. by clicking on menu items (linked to specific actions) +#. by clicking on buttons in views (if these are connected to actions) +#. as contextual actions on object + +Because menus are somewhat complex to declare there is a ```` +shortcut to declare an ``ir.ui.menu`` and connect it to the corresponding +action more easily. + +.. code-block:: xml + + + Ideas + idea.idea + tree,form + + + +.. danger:: + :class: aphorism + + The action must be declared before its corresponding menu in the XML file. + + Data files are executed sequentially, the action's ``id`` must be present + in the database before the menu can be created. + +.. exercise:: Define new menu entries + + Define new menu entries to access courses under the + OpenAcademy menu entry. A user should be able to : + + - display a list of all the courses + - create/modify courses + + .. only:: solutions + + #. Create ``openacademy/views/openacademy.xml`` with an action and + the menus triggering the action + #. Add it to the ``data`` list of ``openacademy/__manifest__.py`` + + .. patch:: + +Basic views +=========== + +Views define the way the records of a model are displayed. Each type of view +represents a mode of visualization (a list of records, a graph of their +aggregation, …). Views can either be requested generically via their type +(e.g. *a list of partners*) or specifically via their id. For generic +requests, the view with the correct type and the lowest priority will be +used (so the lowest-priority view of each type is the default view for that +type). + +:ref:`View inheritance ` allows altering views +declared elsewhere (adding or removing content). + +Generic view declaration +------------------------ + +A view is declared as a record of the model ``ir.ui.view``. The view type +is implied by the root element of the ``arch`` field: + +.. code-block:: xml + + + view.name + object_name + + + + + + +.. danger:: The view's content is XML. + :class: aphorism + + The ``arch`` field must thus be declared as ``type="xml"`` to be parsed + correctly. + +Tree views +---------- + +Tree views, also called list views, display records in a tabular form. + +Their root element is ````. The simplest form of the tree view simply +lists all the fields to display in the table (each field as a column): + +.. code-block:: xml + + + + + + +.. _howtos/module/views/form: + +Form views +---------- + +Forms are used to create and edit single records. + + +Their root element is ``
``. They are composed of high-level structure +elements (groups, notebooks) and interactive elements (buttons and fields): + +.. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + +
+ +.. exercise:: Customise form view using XML + + Create your own form view for the Course object. Data displayed should be: + the name and the description of the course. + + .. only:: solutions + + .. patch:: + +.. exercise:: Notebooks + + In the Course form view, put the description field under a tab, such that + it will be easier to add other tabs later, containing additional + information. + + .. only:: solutions + + Modify the Course form view as follows: + + .. patch:: + +Form views can also use plain HTML for more flexible layouts: + +.. code-block:: xml + +
+
+
+ +
+
+ + + + +
+
+ +Search views +------------ + +Search views customize the search field associated with the list view (and +other aggregated views). Their root element is ```` and they're +composed of fields defining which fields can be searched on: + +.. code-block:: xml + + + + + + +If no search view exists for the model, Odoo generates one which only allows +searching on the ``name`` field. + +.. exercise:: Search courses + + Allow searching for courses based on their title or their description. + + .. only:: solutions + + .. patch:: + +Relations between models +======================== + +A record from a model may be related to a record from another model. For +instance, a sale order record is related to a client record that contains the +client data; it is also related to its sale order line records. + +.. exercise:: Create a session model + + For the module Open Academy, we consider a model for *sessions*: a session + is an occurrence of a course taught at a given time for a given audience. + + Create a model for *sessions*. A session has a name, a start date, a + duration and a number of seats. Add an action and a menu item to display + them. Make the new model visible via a menu item. + + .. only:: solutions + + #. Create the class *Session* in ``openacademy/models/models.py``. + #. Add access to the session object in ``openacademy/view/openacademy.xml``. + + .. patch:: + + .. note:: ``digits=(6, 2)`` specifies the precision of a float number: + 6 is the total number of digits, while 2 is the number of + digits after the comma. Note that it results in the number + digits before the comma is a maximum 4 + +Relational fields +----------------- + +Relational fields link records, either of the same model (hierarchies) or +between different models. + +Relational field types are: + +:class:`Many2one(other_model, ondelete='set null') ` + A simple link to an other object:: + + print foo.other_id.name + + .. seealso:: `foreign keys `_ + +:class:`One2many(other_model, related_field) ` + A virtual relationship, inverse of a :class:`~odoo.fields.Many2one`. + A :class:`~odoo.fields.One2many` behaves as a container of records, + accessing it results in a (possibly empty) set of records:: + + for other in foo.other_ids: + print other.name + + .. danger:: + + Because a :class:`~odoo.fields.One2many` is a virtual relationship, + there *must* be a :class:`~odoo.fields.Many2one` field in the + :samp:`{other_model}`, and its name *must* be :samp:`{related_field}` + +:class:`Many2many(other_model) ` + Bidirectional multiple relationship, any record on one side can be related + to any number of records on the other side. Behaves as a container of + records, accessing it also results in a possibly empty set of records:: + + for other in foo.other_ids: + print other.name + +.. exercise:: Many2one relations + + Using a many2one, modify the *Course* and *Session* models to reflect their + relation with other models: + + - A course has a *responsible* user; the value of that field is a record of + the built-in model ``res.users``. + - A session has an *instructor*; the value of that field is a record of the + built-in model ``res.partner``. + - A session is related to a *course*; the value of that field is a record + of the model ``openacademy.course`` and is required. + - Adapt the views. + + .. only:: solutions + + #. Add the relevant ``Many2one`` fields to the models, and + #. add them in the views. + + .. patch:: + +.. exercise:: Inverse one2many relations + + Using the inverse relational field one2many, modify the models to reflect + the relation between courses and sessions. + + .. only:: solutions + + #. Modify the ``Course`` class, and + #. add the field in the course form view. + + .. patch:: + +.. exercise:: Multiple many2many relations + + Using the relational field many2many, modify the *Session* model to relate + every session to a set of *attendees*. Attendees will be represented by + partner records, so we will relate to the built-in model ``res.partner``. + Adapt the views accordingly. + + .. only:: solutions + + #. Modify the ``Session`` class, and + #. add the field in the form view. + + .. patch:: + +Inheritance +=========== + +Model inheritance +----------------- + +Odoo provides two *inheritance* mechanisms to extend an existing model in a +modular way. + +The first inheritance mechanism allows a module to modify the behavior of a +model defined in another module: + +- add fields to a model, +- override the definition of fields on a model, +- add constraints to a model, +- add methods to a model, +- override existing methods on a model. + +The second inheritance mechanism (delegation) allows to link every record of a +model to a record in a parent model, and provides transparent access to the +fields of the parent record. + +.. image:: ../images/inheritance_methods.png + :align: center + +.. seealso:: + + * :attr:`~odoo.models.Model._inherit` + * :attr:`~odoo.models.Model._inherits` + +View inheritance +---------------- + +Instead of modifying existing views in place (by overwriting them), Odoo +provides view inheritance where children "extension" views are applied on top of +root views, and can add or remove content from their parent. + +An extension view references its parent using the ``inherit_id`` field, and +instead of a single view its ``arch`` field is composed of any number of +``xpath`` elements selecting and altering the content of their parent view: + +.. code-block:: xml + + + + id.category.list2 + idea.category + + + + + + + + + +``expr`` + An XPath_ expression selecting a single element in the parent view. + Raises an error if it matches no element or more than one +``position`` + Operation to apply to the matched element: + + ``inside`` + appends ``xpath``'s body at the end of the matched element + ``replace`` + replaces the matched element with the ``xpath``'s body, replacing any ``$0`` node occurrence + in the new body with the original element + ``before`` + inserts the ``xpath``'s body as a sibling before the matched element + ``after`` + inserts the ``xpaths``'s body as a sibling after the matched element + ``attributes`` + alters the attributes of the matched element using special + ``attribute`` elements in the ``xpath``'s body + +.. tip:: + + When matching a single element, the ``position`` attribute can be set directly + on the element to be found. Both inheritances below will give the same result. + + .. code-block:: xml + + + + + + + + + + +.. exercise:: Alter existing content + + * Using model inheritance, modify the existing *Partner* model to add an + ``instructor`` boolean field, and a many2many field that corresponds to + the session-partner relation + * Using view inheritance, display this fields in the partner form view + + .. only:: solutions + + .. note:: + + This is the opportunity to introduce the developer mode to + inspect the view, find its external ID and the place to put the + new field. + + #. Create a file ``openacademy/models/partner.py`` and import it in + ``__init__.py`` + #. Create a file ``openacademy/views/partner.xml`` and add it to + ``__manifest__.py`` + + .. patch:: + +Domains +####### + +In Odoo, :ref:`reference/orm/domains` are values that encode conditions on +records. A domain is a list of criteria used to select a subset of a model's +records. Each criteria is a triple with a field name, an operator and a value. + +For instance, when used on the *Product* model the following domain selects +all *services* with a unit price over *1000*:: + + [('product_type', '=', 'service'), ('unit_price', '>', 1000)] + +By default criteria are combined with an implicit AND. The logical operators +``&`` (AND), ``|`` (OR) and ``!`` (NOT) can be used to explicitly combine +criteria. They are used in prefix position (the operator is inserted before +its arguments rather than between). For instance to select products "which are +services *OR* have a unit price which is *NOT* between 1000 and 2000":: + + ['|', + ('product_type', '=', 'service'), + '!', '&', + ('unit_price', '>=', 1000), + ('unit_price', '<', 2000)] + +A ``domain`` parameter can be added to relational fields to limit valid +records for the relation when trying to select records in the client interface. + +.. exercise:: Domains on relational fields + + When selecting the instructor for a *Session*, only instructors (partners + with ``instructor`` set to ``True``) should be visible. + + .. only:: solutions + + .. patch:: + + .. note:: + + A domain declared as a literal list is evaluated server-side and + can't refer to dynamic values on the right-hand side, a domain + declared as a string is evaluated client-side and allows + field names on the right-hand side + +.. exercise:: More complex domains + + Create new partner categories *Teacher / Level 1* and *Teacher / Level 2*. + The instructor for a session can be either an instructor or a teacher + (of any level). + + .. only:: solutions + + #. Modify the *Session* model's domain + #. Modify ``openacademy/view/partner.xml`` to get access to + *Partner categories*: + + .. patch:: + +Computed fields and default values +================================== + +So far fields have been stored directly in and retrieved directly from the +database. Fields can also be *computed*. In that case, the field's value is not +retrieved from the database but computed on-the-fly by calling a method of the +model. + +To create a computed field, create a field and set its attribute +:attr:`~odoo.fields.Field.compute` to the name of a method. The computation +method should simply set the value of the field to compute on every record in +``self``. + +.. danger:: ``self`` is a collection + :class: aphorism + + The object ``self`` is a *recordset*, i.e., an ordered collection of + records. It supports the standard Python operations on collections, like + ``len(self)`` and ``iter(self)``, plus extra set operations like ``recs1 + + recs2``. + + Iterating over ``self`` gives the records one by one, where each record is + itself a collection of size 1. You can access/assign fields on single + records by using the dot notation, like ``record.name``. + +.. code-block:: python + + import random + from odoo import models, fields, api + + class ComputedModel(models.Model): + _name = 'test.computed' + + name = fields.Char(compute='_compute_name') + + def _compute_name(self): + for record in self: + record.name = str(random.randint(1, 1e6)) + + +Dependencies +------------ + +The value of a computed field usually depends on the values of other fields on +the computed record. The ORM expects the developer to specify those dependencies +on the compute method with the decorator :func:`~odoo.api.depends`. +The given dependencies are used by the ORM to trigger the recomputation of the +field whenever some of its dependencies have been modified:: + + from odoo import models, fields, api + + class ComputedModel(models.Model): + _name = 'test.computed' + + name = fields.Char(compute='_compute_name') + value = fields.Integer() + + @api.depends('value') + def _compute_name(self): + for record in self: + record.name = "Record with value %s" % record.value + +.. exercise:: Computed fields + + * Add the percentage of taken seats to the *Session* model + * Display that field in the tree and form views + * Display the field as a progress bar + + .. only:: solutions + + #. Add a computed field to *Session* + #. Show the field in the *Session* view: + + .. patch:: + +Default values +-------------- + +Any field can be given a default value. In the field definition, add the option +``default=X`` where ``X`` is either a Python literal value (boolean, integer, +float, string), or a function taking a recordset and returning a value:: + + name = fields.Char(default="Unknown") + user_id = fields.Many2one('res.users', default=lambda self: self.env.user) + +.. note:: + + The object ``self.env`` gives access to request parameters and other useful + things: + + - ``self.env.cr`` or ``self._cr`` is the database *cursor* object; it is + used for querying the database + - ``self.env.uid`` or ``self._uid`` is the current user's database id + - ``self.env.user`` is the current user's record + - ``self.env.context`` or ``self._context`` is the context dictionary + - ``self.env.ref(xml_id)`` returns the record corresponding to an XML id + - ``self.env[model_name]`` returns an instance of the given model + +.. exercise:: Active objects – Default values + + * Define the start_date default value as today (see + :class:`~odoo.fields.Date`). + * Add a field ``active`` in the class Session, and set sessions as active by + default. + + .. only:: solutions + + .. patch:: + + .. note:: + + Odoo has built-in rules making records with an ``active`` field set + to ``False`` invisible. + +Onchange +======== + +The "onchange" mechanism provides a way for the client interface to update a +form whenever the user has filled in a value in a field, without saving anything +to the database. + +For instance, suppose a model has three fields ``amount``, ``unit_price`` and +``price``, and you want to update the price on the form when any of the other +fields is modified. To achieve this, define a method where ``self`` represents +the record in the form view, and decorate it with :func:`~odoo.api.onchange` +to specify on which field it has to be triggered. Any change you make on +``self`` will be reflected on the form. + +.. code-block:: xml + + + + + + +.. code-block:: python + + # onchange handler + @api.onchange('amount', 'unit_price') + def _onchange_price(self): + # set auto-changing field + self.price = self.amount * self.unit_price + # Can optionally return a warning and domains + return { + 'warning': { + 'title': "Something bad happened", + 'message': "It was very bad indeed", + } + } + +For computed fields, valued ``onchange`` behavior is built-in as can be seen by +playing with the *Session* form: change the number of seats or participants, and +the ``taken_seats`` progressbar is automatically updated. + +.. exercise:: Warning + + Add an explicit onchange to warn about invalid values, like a negative + number of seats, or more participants than seats. + + .. only:: solutions + + .. patch:: + +Model constraints +================= + +Odoo provides two ways to set up automatically verified invariants: +:func:`Python constraints ` and +:attr:`SQL constraints `. + +A Python constraint is defined as a method decorated with +:func:`~odoo.api.constrains`, and invoked on a recordset. The decorator +specifies which fields are involved in the constraint, so that the constraint is +automatically evaluated when one of them is modified. The method is expected to +raise an exception if its invariant is not satisfied:: + + from odoo.exceptions import ValidationError + + @api.constrains('age') + def _check_something(self): + for record in self: + if record.age > 20: + raise ValidationError("Your record is too old: %s" % record.age) + # all records passed the test, don't return anything + +.. exercise:: Add Python constraints + + Add a constraint that checks that the instructor is not present in the + attendees of his/her own session. + + .. only:: solutions + + .. patch:: + +SQL constraints are defined through the model attribute +:attr:`~odoo.models.Model._sql_constraints`. The latter is assigned to a list +of triples of strings ``(name, sql_definition, message)``, where ``name`` is a +valid SQL constraint name, ``sql_definition`` is a table_constraint_ expression, +and ``message`` is the error message. + +.. exercise:: Add SQL constraints + + With the help of `PostgreSQL's documentation`_ , add the following + constraints: + + #. CHECK that the course description and the course title are different + #. Make the Course's name UNIQUE + + .. only:: solutions + + .. patch:: + +.. exercise:: Exercise 6 - Add a duplicate option + + Since we added a constraint for the Course name uniqueness, it is not + possible to use the "duplicate" function anymore (:menuselection:`Form --> + Duplicate`). + + Re-implement your own "copy" method which allows to duplicate the Course + object, changing the original name into "Copy of [original name]". + + .. only:: solutions + + .. patch:: + +Advanced Views +============== + +Tree views +---------- + +Tree views can take supplementary attributes to further customize their +behavior: + +``decoration-{$name}`` + allow changing the style of a row's text based on the corresponding + record's attributes. + + Values are Python expressions. For each record, the expression is evaluated + with the record's attributes as context values and if ``true``, the + corresponding style is applied to the row. Here are some of the other values + available in the context: + + * ``uid``: the id of the current user, + * ``today``: the current local date as a string of the form ``YYYY-MM-DD``, + * ``now``: same as ``today`` with the addition of the current time. + This value is formatted as ``YYYY-MM-DD hh:mm:ss``. + + ``{$name}`` can be ``bf`` (``font-weight: bold``), ``it`` + (``font-style: italic``), or any `bootstrap contextual color + `_ (``danger``, + ``info``, ``muted``, ``primary``, ``success`` or ``warning``). + + .. code-block:: xml + + + + + + +``editable`` + Either ``"top"`` or ``"bottom"``. Makes the tree view editable in-place + (rather than having to go through the form view), the value is the + position where new rows appear. + +.. exercise:: List coloring + + Modify the Session tree view in such a way that sessions lasting less than + 5 days are colored blue, and the ones lasting more than 15 days are + colored red. + + .. only:: solutions + + Modify the session tree view: + + .. patch:: + +Calendars +--------- + +Displays records as calendar events. Their root element is ```` and +their most common attributes are: + +``color`` + The name of the field used for *color segmentation*. Colors are + automatically distributed to events, but events in the same color segment + (records which have the same value for their ``@color`` field) will be + given the same color. +``date_start`` + record's field holding the start date/time for the event +``date_stop`` (optional) +   record's field holding the end date/time for the event +``string`` + record's field to define the label for each calendar event + +.. code-block:: xml + + + + + +.. exercise:: Calendar view + + Add a Calendar view to the *Session* model enabling the user to view the + events associated to the Open Academy. + + .. only:: solutions + + #. Add an ``end_date`` field computed from ``start_date`` and + ``duration`` + + .. tip:: the inverse function makes the field writable, and allows + moving the sessions (via drag and drop) in the calendar view + + #. Add a calendar view to the *Session* model + #. And add the calendar view to the *Session* model's actions + + .. patch:: + +Search views +------------ + +Search view ```` elements can have a ``@filter_domain`` that overrides +the domain generated for searching on the given field. In the given domain, +``self`` represents the value entered by the user. In the example below, it is +used to search on both fields ``name`` and ``description``. + +Search views can also contain ```` elements, which act as toggles for +predefined searches. Filters must have one of the following attributes: + +``domain`` + add the given domain to the current search +``context`` + add some context to the current search; use the key ``group_by`` to group + results on the given field name + +.. code-block:: xml + + + + + + + + + + + + + +To use a non-default search view in an action, it should be linked using the +``search_view_id`` field of the action record. + +The action can also set default values for search fields through its +``context`` field: context keys of the form +:samp:`search_default_{field_name}` will initialize *field_name* with the +provided value. Search filters must have an optional ``@name`` to have a +default and behave as booleans (they can only be enabled by default). + +.. exercise:: Search views + + #. Add a button to filter the courses for which the current user is the + responsible in the course search view. Make it selected by default. + #. Add a button to group courses by responsible user. + + .. only:: solutions + + .. patch:: + +Gantt +----- + +.. warning:: + + The gantt view requires the web_gantt module which is present in + :ref:`the enterprise edition ` version. + +Horizontal bar charts typically used to show project planning and advancement, +their root element is ````. + +.. code-block:: xml + + + +.. exercise:: Gantt charts + + Add a Gantt Chart enabling the user to view the sessions scheduling linked + to the Open Academy module. The sessions should be grouped by instructor. + + .. only:: solutions + + #. Add the gantt view's definition, and add the gantt view to the + *Session* model's action + + .. patch:: + +Graph views +----------- + +Graph views allow aggregated overview and analysis of models, their root +element is ````. + +.. note:: + Pivot views (element ````) a multidimensional table, allows the + selection of filers and dimensions to get the right aggregated dataset + before moving to a more graphical overview. The pivot view shares the same + content definition as graph views. + +Graph views have 4 display modes, the default mode is selected using the +``@type`` attribute. + +Bar (default) + a bar chart, the first dimension is used to define groups on the + horizontal axis, other dimensions define aggregated bars within each group. + + By default bars are side-by-side, they can be stacked by using + ``@stacked="True"`` on the ```` +Line + 2-dimensional line chart +Pie + 2-dimensional pie + +Graph views contain ```` with a mandatory ``@type`` attribute taking +the values: + +``row`` (default) + the field should be aggregated by default +``measure`` + the field should be aggregated rather than grouped on + +.. code-block:: xml + + + + + + +.. warning:: + + Graph views perform aggregations on database values, they do not work + with non-stored computed fields. + +.. exercise:: Graph view + + Add a Graph view in the Session object that displays, for each course, the + number of attendees under the form of a bar chart. + + .. only:: solutions + + #. Add the number of attendees as a stored computed field + #. Then add the relevant view + + .. patch:: + +Kanban +------ + +Used to organize tasks, production processes, etc… their root element is +````. + +A kanban view shows a set of cards possibly grouped in columns. Each card +represents a record, and each column the values of an aggregation field. + +For instance, project tasks may be organized by stage (each column is a +stage), or by responsible (each column is a user), and so on. + +Kanban views define the structure of each card as a mix of form elements +(including basic HTML) and :ref:`reference/qweb`. + +.. exercise:: Kanban view + + Add a Kanban view that displays sessions grouped by course (columns are + thus courses). + + .. only:: solutions + + #. Add an integer ``color`` field to the *Session* model + #. Add the kanban view and update the action + + .. patch:: + +Security +======== + +Access control mechanisms must be configured to achieve a coherent security +policy. + +Group-based access control mechanisms +------------------------------------- + +Groups are created as normal records on the model ``res.groups``, and granted +menu access via menu definitions. However even without a menu, objects may +still be accessible indirectly, so actual object-level permissions (read, +write, create, unlink) must be defined for groups. They are usually inserted +via CSV files inside modules. It is also possible to restrict access to +specific fields on a view or object using the field's groups attribute. + +Access rights +------------- + +Access rights are defined as records of the model ``ir.model.access``. Each +access right is associated to a model, a group (or no group for global +access), and a set of permissions: read, write, create, unlink. Such access +rights are usually created by a CSV file named after its model: +``ir.model.access.csv``. + +.. code-block:: text + + id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink + access_idea_idea,idea.idea,model_idea_idea,base.group_user,1,1,1,0 + access_idea_vote,idea.vote,model_idea_vote,base.group_user,1,1,1,0 + +.. exercise:: Add access control through the Odoo interface + + Create a new user "John Smith". Then create a group + "OpenAcademy / Session Read" with read access to the *Session* model. + + .. only:: solutions + + #. Create a new user *John Smith* through + :menuselection:`Settings --> Users --> Users` + #. Create a new group ``session_read`` through + :menuselection:`Settings --> Users --> Groups`, it should have + read access on the *Session* model + #. Edit *John Smith* to make them a member of ``session_read`` + #. Log in as *John Smith* to check the access rights are correct + +.. exercise:: Add access control through data files in your module + + Using data files, + + * Create a group *OpenAcademy / Manager* with full access to all + OpenAcademy models + * Make *Session* and *Course* readable by all users + + .. only:: solutions + + #. Create a new file ``openacademy/security/security.xml`` to + hold the OpenAcademy Manager group + #. Edit the file ``openacademy/security/ir.model.access.csv`` with + the access rights to the models + #. Finally update ``openacademy/__manifest__.py`` to add the new data + files to it + + .. patch:: + +Record rules +------------ + +A record rule restricts the access rights to a subset of records of the given +model. A rule is a record of the model ``ir.rule``, and is associated to a +model, a number of groups (many2many field), permissions to which the +restriction applies, and a domain. The domain specifies to which records the +access rights are limited. + +Here is an example of a rule that prevents the deletion of leads that are not +in state ``cancel``. Notice that the value of the field ``groups`` must follow +the same convention as the method :meth:`~odoo.models.Model.write` of the ORM. + +.. code-block:: xml + + + Only cancelled leads may be deleted + + + + + + + [('state','=','cancel')] + + +.. exercise:: Record rule + + Add a record rule for the model Course and the group + "OpenAcademy / Manager", that restricts ``write`` and ``unlink`` accesses + to the responsible of a course. If a course has no responsible, all users + of the group must be able to modify it. + + .. only:: solutions + + Create a new rule in ``openacademy/security/security.xml``: + + .. patch:: + +.. _howto/module/wizard: + +Wizards +======= + +Wizards describe interactive sessions with the user (or dialog boxes) through +dynamic forms. A wizard is simply a model that extends the class +:class:`~odoo.models.TransientModel` instead of +:class:`~odoo.models.Model`. The class +:class:`~odoo.models.TransientModel` extends :class:`~odoo.models.Model` +and reuse all its existing mechanisms, with the following particularities: + +- Wizard records are not meant to be persistent; they are automatically deleted + from the database after a certain time. This is why they are called + *transient*. +- Wizard records may refer to regular records or wizard records through relational + fields(many2one or many2many), but regular records *cannot* refer to wizard records through a + many2one field. + +We want to create a wizard that allow users to create attendees for a particular +session, or for a list of sessions at once. + +.. exercise:: Define the wizard + + Create a wizard model with a many2one relationship with the *Session* + model and a many2many relationship with the *Partner* model. + + .. only:: solutions + + Add a new file ``openacademy/wizard.py``: + + .. patch:: + +Launching wizards +----------------- + +Wizards are simply :ref:`window actions ` with a ``target`` +field set to the value ``new``, which opens the view +(usually :ref:`a form `) in a separate dialog. The +action may be triggered via a menu item, but is more generally triggered by a +button. + +An other way to launch wizards is through the :menuselection:`Action` menu of +a tree or form view. This is done through the ``binding_model_id`` field of the +action. Setting this field will make the action appear on the views of the model +the action is "bound" to. + +.. code:: xml + + + Launch the Wizard + wizard.model.name + form + new + + + +.. tip:: + + While wizards use regular views and buttons, normally clicking any button in + a form would first save the form then close the dialog. Because this is + often undesirable in wizards, a special attribute ``special="cancel"`` is + available which immediately closes the wizard without saving the form. + +.. exercise:: Launch the wizard + + #. Define a form view for the wizard. + #. Add the action to launch it in the context of the *Session* model. + #. Define a default value for the session field in the wizard; use the + context parameter ``self._context`` to retrieve the current session. + + .. only:: solutions + + .. patch:: + +.. exercise:: Register attendees + + Add buttons to the wizard, and implement the corresponding method for adding + the attendees to the given session. + + .. only:: solutions + + .. patch:: + +.. exercise:: Register attendees to multiple sessions + + Modify the wizard model so that attendees can be registered to multiple + sessions. + + .. only:: solutions + + .. patch:: + +Internationalization +==================== + +Each module can provide its own translations within the i18n directory, by +having files named LANG.po where LANG is the locale code for the language, or +the language and country combination when they differ (e.g. pt.po or +pt_BR.po). Translations will be loaded automatically by Odoo for all +enabled languages. Developers always use English when creating a module, then +export the module terms using Odoo's gettext POT export feature +(:menuselection:`Settings --> Translations --> Import/Export --> Export +Translation` without specifying a language), to create the module template POT +file, and then derive the translated PO files. Many IDE's have plugins or modes +for editing and merging PO/POT files. + +.. tip:: The Portable Object files generated by Odoo are published on + `Transifex `__, making it + easy to translate the software. + +.. code-block:: text + + |- idea/ # The module directory + |- i18n/ # Translation files + | - idea.pot # Translation Template (exported from Odoo) + | - fr.po # French translation + | - pt_BR.po # Brazilian Portuguese translation + | (...) + +.. tip:: + + By default Odoo's POT export only extracts labels inside XML files or + inside field definitions in Python code, but any Python string can be + translated this way by surrounding it with the function :func:`odoo._` + (e.g. ``_("Label")``) + +.. exercise:: Translate a module + + Choose a second language for your Odoo installation. Translate your + module using the facilities provided by Odoo. + + .. only:: solutions + + #. Create a directory ``openacademy/i18n/`` + #. You will need to activate the developer mode + to access the menus mentioned below ( + :menuselection:`Settings --> Activate the developer mode` + ) + #. Install whichever language you want ( + :menuselection:`Settings --> Translations --> Languages`) + #. Generate the missing terms (:menuselection:`Settings --> + Translations --> Application Terms --> Generate Missing Terms`) + #. Create a template translation file by exporting ( + :menuselection:`Settings --> Translations --> Import/Export + --> Export Translation`) without specifying a language, save in + ``openacademy/i18n/`` + #. Create a translation file by exporting ( + :menuselection:`Settings --> Translations --> Import/Export + --> Export Translation`) and specifying a language. Save it in + ``openacademy/i18n/`` + #. Open the exported translation file (with a basic text editor or a + dedicated PO-file editor e.g. POEdit_ and translate the missing + terms + + #. In ``models.py``, add an import statement for the function + ``odoo._`` and mark missing strings as translatable + + #. Repeat steps 3-6 + + .. patch:: + + .. todo:: do we never reload translations? + + +Reporting +========= + +Printed reports +--------------- + +Odoo uses a report engine based on :ref:`reference/qweb`, +`Twitter Bootstrap`_ and Wkhtmltopdf_. + +A report is a combination two elements: + +* an ``ir.actions.report`` which configures various basic parameters for the + report (default type, whether the report should be saved to the database + after generation,…) + + .. code-block:: xml + + + Invoices + account.invoice + qweb-pdf + account.report_invoice + account.report_invoice + + (object.state in ('open','paid')) and + ('INV'+(object.number or '').replace('/','')+'.pdf') + + report + + + .. tip:: + + Because it largerly a standard action, as with :ref:`howto/module/wizard` + it is generally useful to add the report as a *contextual item* on the + tree and / or form views of the model being reported on via the + ``binding_model_id`` field. + + Here we are also using ``binding_type`` in order for the report to be in + the *report* contextual menu rather than the *action* one. There is no + technical difference but putting elements in the right place helps users. + +* A standard :ref:`QWeb view ` for the actual report: + + .. code-block:: xml + + + + +
+

Report title

+
+
+
+
+ + the standard rendering context provides a number of elements, the most + important being: + + ``docs`` + the records for which the report is printed + ``user`` + the user printing the report + +Because reports are standard web pages, they are available through a URL and +output parameters can be manipulated through this URL, for instance the HTML +version of the *Invoice* report is available through +http://localhost:8069/report/html/account.report_invoice/1 (if ``account`` is +installed) and the PDF version through +http://localhost:8069/report/pdf/account.report_invoice/1. + +.. _reference/backend/reporting/printed-reports/pdf-without-styles: + +.. danger:: + + If it appears that your PDF report is missing the styles (i.e. the text + appears but the style/layout is different from the html version), probably + your wkhtmltopdf_ process cannot reach your web server to download them. + + If you check your server logs and see that the CSS styles are not being + downloaded when generating a PDF report, most surely this is the problem. + + The wkhtmltopdf_ process will use the ``web.base.url`` system parameter as + the *root path* to all linked files, but this parameter is automatically + updated each time the Administrator is logged in. If your server resides + behind some kind of proxy, that could not be reachable. You can fix this by + adding one of these system parameters: + + - ``report.url``, pointing to an URL reachable from your server + (probably ``http://localhost:8069`` or something similar). It will be + used for this particular purpose only. + + - ``web.base.url.freeze``, when set to ``True``, will stop the + automatic updates to ``web.base.url``. + +.. exercise:: Create a report for the Session model + + For each session, it should display session's name, its start and end, + and list the session's attendees. + + .. only:: solutions + + .. patch:: + +Dashboards +---------- + +.. exercise:: Define a Dashboard + + Define a dashboard containing the graph view you created, the sessions + calendar view and a list view of the courses (switchable to a form + view). This dashboard should be available through a menuitem in the menu, + and automatically displayed in the web client when the OpenAcademy main + menu is selected. + + .. only:: solutions + + #. Create a file ``openacademy/views/session_board.xml``. It should contain + the board view, the actions referenced in that view, an action to + open the dashboard and a re-definition of the main menu item to add + the dashboard action + + .. note:: Available dashboard styles are ``1``, ``1-1``, ``1-2``, + ``2-1`` and ``1-1-1`` + + #. Update ``openacademy/__manifest__.py`` to reference the new data + file + + .. patch:: + +WebServices +=========== + +The web-service module offer a common interface for all web-services : + +- XML-RPC +- JSON-RPC + +Business objects can also be accessed via the distributed object +mechanism. They can all be modified via the client interface with contextual +views. + +Odoo is accessible through XML-RPC/JSON-RPC interfaces, for which libraries +exist in many languages. + +XML-RPC Library +--------------- + +The following example is a Python 3 program that interacts with an Odoo +server with the library ``xmlrpc.client``:: + + import xmlrpc.client + + root = 'http://%s:%d/xmlrpc/' % (HOST, PORT) + + uid = xmlrpc.client.ServerProxy(root + 'common').login(DB, USER, PASS) + print("Logged in as %s (uid: %d)" % (USER, uid)) + + # Create a new note + sock = xmlrpc.client.ServerProxy(root + 'object') + args = { + 'color' : 8, + 'memo' : 'This is a note', + 'create_uid': uid, + } + note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args) + +.. exercise:: Add a new service to the client + + Write a Python program able to send XML-RPC requests to a PC running + Odoo (yours, or your instructor's). This program should display all + the sessions, and their corresponding number of seats. It should also + create a new session for one of the courses. + + .. only:: solutions + + .. code-block:: python + + import functools + import xmlrpc.client + HOST = 'localhost' + PORT = 8069 + DB = 'openacademy' + USER = 'admin' + PASS = 'admin' + ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT) + + # 1. Login + uid = xmlrpc.client.ServerProxy(ROOT + 'common').login(DB,USER,PASS) + print("Logged in as %s (uid:%d)" % (USER,uid)) + + call = functools.partial( + xmlrpc.client.ServerProxy(ROOT + 'object').execute, + DB, uid, PASS) + + # 2. Read the sessions + sessions = call('openacademy.session','search_read', [], ['name','seats']) + for session in sessions: + print("Session %s (%s seats)" % (session['name'], session['seats'])) + # 3.create a new session + session_id = call('openacademy.session', 'create', { + 'name' : 'My session', + 'course_id' : 2, + }) + + Instead of using a hard-coded course id, the code can look up a course + by name:: + + # 3.create a new session for the "Functional" course + course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0] + session_id = call('openacademy.session', 'create', { + 'name' : 'My session', + 'course_id' : course_id, + }) + +JSON-RPC Library +---------------- + +The following example is a Python 3 program that interacts with an Odoo server +with the standard Python libraries ``urllib.request`` and ``json``. This +example assumes the **Productivity** app (``note``) is installed:: + + import json + import random + import urllib.request + + HOST = 'localhost' + PORT = 8069 + DB = 'openacademy' + USER = 'admin' + PASS = 'admin' + + def json_rpc(url, method, params): + data = { + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": random.randint(0, 1000000000), + } + req = urllib.request.Request(url=url, data=json.dumps(data).encode(), headers={ + "Content-Type":"application/json", + }) + reply = json.loads(urllib.request.urlopen(req).read().decode('UTF-8')) + if reply.get("error"): + raise Exception(reply["error"]) + return reply["result"] + + def call(url, service, method, *args): + return json_rpc(url, "call", {"service": service, "method": method, "args": args}) + + # log in the given database + url = "http://%s:%s/jsonrpc" % (HOST, PORT) + uid = call(url, "common", "login", DB, USER, PASS) + + # create a new note + args = { + 'color': 8, + 'memo': 'This is another note', + 'create_uid': uid, + } + note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args) + +Examples can be easily adapted from XML-RPC to JSON-RPC. + +.. note:: + + There are a number of high-level APIs in various languages to access Odoo + systems without *explicitly* going through XML-RPC or JSON-RPC, such as: + + * https://github.com/akretion/ooor + * https://github.com/OCA/odoorpc + * https://github.com/nicolas-van/openerp-client-lib + * http://pythonhosted.org/OdooRPC + * https://github.com/abhishek-jaiswal/php-openerp-lib + +.. [#autofields] it is possible to :ref:`disable the automatic creation of some + fields ` +.. [#rawsql] writing raw SQL queries is possible, but requires care as it + bypasses all Odoo authentication and security mechanisms. + +.. _database index: + http://use-the-index-luke.com/sql/preface + +.. _POEdit: http://poedit.net + +.. _PostgreSQL's documentation: +.. _table_constraint: + http://www.postgresql.org/docs/9.3/static/ddl-constraints.html + +.. _python: http://python.org + +.. _XPath: http://w3.org/TR/xpath + +.. _twitter bootstrap: http://getbootstrap.com + +.. _wkhtmltopdf: http://wkhtmltopdf.org diff --git a/developer/howtos/backend/exercise-access-rights b/developer/howtos/backend/exercise-access-rights new file mode 100644 index 000000000..465dffea8 --- /dev/null +++ b/developer/howtos/backend/exercise-access-rights @@ -0,0 +1,40 @@ +# HG changeset patch +# Parent 303a5f4f011822dcb42b5833d579eabd3f03f4bf + +Index: addons/openacademy/__manifest__.py +=================================================================== +--- addons.orig/openacademy/__manifest__.py 2014-08-26 17:26:18.143783102 +0200 ++++ addons/openacademy/__manifest__.py 2014-08-26 17:26:18.135783102 +0200 +@@ -25,7 +25,8 @@ + + # always loaded + 'data': [ +- # 'security/ir.model.access.csv', ++ 'security/security.xml', ++ 'security/ir.model.access.csv', + 'templates.xml', + 'views/openacademy.xml', + 'views/partner.xml', +Index: addons/openacademy/security/ir.model.access.csv +=================================================================== +--- addons.orig/openacademy/security/ir.model.access.csv 2014-08-26 17:26:18.143783102 +0200 ++++ addons/openacademy/security/ir.model.access.csv 2014-08-26 17:26:18.135783102 +0200 +@@ -1,2 +1,5 @@ + id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +-access_openacademy_openacademy,openacademy.openacademy,model_openacademy_openacademy,,1,0,0,0 ++course_manager,course manager,model_openacademy_course,group_manager,1,1,1,1 ++session_manager,session manager,model_openacademy_session,group_manager,1,1,1,1 ++course_read_all,course all,model_openacademy_course,,1,0,0,0 ++session_read_all,session all,model_openacademy_session,,1,0,0,0 +Index: addons/openacademy/security/security.xml +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/security/security.xml 2014-08-26 17:26:18.135783102 +0200 +@@ -0,0 +1,7 @@ ++ ++ ++ ++ OpenAcademy / Manager ++ ++ ++ diff --git a/developer/howtos/backend/exercise-access-rules b/developer/howtos/backend/exercise-access-rules new file mode 100644 index 000000000..d6290dfdb --- /dev/null +++ b/developer/howtos/backend/exercise-access-rules @@ -0,0 +1,27 @@ +# HG changeset patch +# Parent 0602022dc2a428f9995c886df33b699b6d3bcb69 + +Index: addons/openacademy/security/security.xml +=================================================================== +--- addons.orig/openacademy/security/security.xml 2014-08-26 17:26:18.971783090 +0200 ++++ addons/openacademy/security/security.xml 2014-08-26 17:26:18.967783090 +0200 +@@ -3,5 +3,19 @@ + + OpenAcademy / Manager + ++ ++ ++ Only Responsible can modify Course ++ ++ ++ ++ ++ ++ ++ ++ ['|', ('responsible_id','=',False), ++ ('responsible_id','=',user.id)] ++ ++ + + diff --git a/developer/howtos/backend/exercise-advanced-treeview b/developer/howtos/backend/exercise-advanced-treeview new file mode 100644 index 000000000..69ddd66cf --- /dev/null +++ b/developer/howtos/backend/exercise-advanced-treeview @@ -0,0 +1,19 @@ +# HG changeset patch +# Parent f8d2422e87b3ff566dc947ad582608db3b15e077 + +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml 2014-08-26 17:26:09.283783234 +0200 ++++ addons/openacademy/views/openacademy.xml 2014-08-26 17:26:09.279783234 +0200 +@@ -115,9 +115,10 @@ + session.tree + openacademy.session + +- ++ + + ++ + + + diff --git a/developer/howtos/backend/exercise-basic-action b/developer/howtos/backend/exercise-basic-action new file mode 100644 index 000000000..65e788b80 --- /dev/null +++ b/developer/howtos/backend/exercise-basic-action @@ -0,0 +1,53 @@ +# HG changeset patch +# Parent 16e4cb131d9f7f3a72a8a1b0bc46c2ce9ac76435 +Index: addons/openacademy/__manifest__.py +=================================================================== +--- addons.orig/openacademy/__manifest__.py 2014-08-26 17:25:53.519783468 +0200 ++++ addons/openacademy/__manifest__.py 2014-08-26 17:25:53.511783468 +0200 +@@ -27,6 +27,7 @@ + 'data': [ + # 'security/ir.model.access.csv', + 'templates.xml', ++ 'views/openacademy.xml', + ], + # only loaded in demonstration mode + 'demo': [ +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/views/openacademy.xml 2014-08-26 17:25:53.511783468 +0200 +@@ -0,0 +1,34 @@ ++ ++ ++ ++ ++ ++ ++ Courses ++ openacademy.course ++ tree,form ++ ++

Create the first course ++

++
++
++ ++ ++ ++ ++ ++ ++ ++ ++ ++
diff --git a/developer/howtos/backend/exercise-calendar b/developer/howtos/backend/exercise-calendar new file mode 100644 index 000000000..56689b022 --- /dev/null +++ b/developer/howtos/backend/exercise-calendar @@ -0,0 +1,77 @@ +# HG changeset patch +# Parent 85a8d7317b9e13480f39ad739955442d15144451 +# Parent 16fcdc4c6462a7872636f3c19550c16879af5281 + +diff --git a/openacademy/models.py b/openacademy/models.py +--- a/openacademy/models.py ++++ b/openacademy/models.py +@@ -1,5 +1,6 @@ + # -*- coding: utf-8 -*- + ++from datetime import timedelta + from odoo import models, fields, api, exceptions + + class Course(models.Model): +@@ -57,6 +58,8 @@ class Session(models.Model): + attendee_ids = fields.Many2many('res.partner', string="Attendees") + + taken_seats = fields.Float(string="Taken seats", compute='_taken_seats') ++ end_date = fields.Date(string="End Date", store=True, ++ compute='_get_end_date', inverse='_set_end_date') + + @api.depends('seats', 'attendee_ids') + def _taken_seats(self): +@@ -83,6 +86,27 @@ class Session(models.Model): + }, + } + ++ @api.depends('start_date', 'duration') ++ def _get_end_date(self): ++ for r in self: ++ if not (r.start_date and r.duration): ++ r.end_date = r.start_date ++ continue ++ ++ # Add duration to start_date, but: Monday + 5 days = Saturday, so ++ # subtract one second to get on Friday instead ++ duration = timedelta(days=r.duration, seconds=-1) ++ r.end_date = r.start_date + duration ++ ++ def _set_end_date(self): ++ for r in self: ++ if not (r.start_date and r.end_date): ++ continue ++ ++ # Compute the difference between dates, but: Friday - Monday = 4 days, ++ # so add one day to get 5 days instead ++ r.duration = (r.end_date - r.start_date).days + 1 ++ + @api.constrains('instructor_id', 'attendee_ids') + def _check_instructor_not_in_attendees(self): + for r in self: +diff --git a/openacademy/views/openacademy.xml b/openacademy/views/openacademy.xml +--- a/openacademy/views/openacademy.xml ++++ b/openacademy/views/openacademy.xml +@@ -124,10 +124,21 @@ +
+
+ ++ ++ ++ session.calendar ++ openacademy.session ++ ++ ++ ++ ++ ++ ++ + + Sessions + openacademy.session +- tree,form ++ tree,form,calendar + + + + + ++ + + + +
diff --git a/developer/howtos/backend/exercise-constraint-python b/developer/howtos/backend/exercise-constraint-python new file mode 100644 index 000000000..248507a24 --- /dev/null +++ b/developer/howtos/backend/exercise-constraint-python @@ -0,0 +1,25 @@ +# HG changeset patch +# Parent 7a7d003fe38426a405ce0657a627a139133ec4dd +# Parent 52f54b46487c8224a5aade4b921be77360ed3eae + +diff --git a/openacademy/models.py b/openacademy/models.py +--- a/openacademy/models.py ++++ b/openacademy/models.py +@@ -1,6 +1,6 @@ + # -*- coding: utf-8 -*- + +-from odoo import models, fields, api ++from odoo import models, fields, api, exceptions + + class Course(models.Model): + _name = 'openacademy.course' +@@ -58,3 +58,9 @@ class Session(models.Model): + 'message': "Increase seats or remove excess attendees", + }, + } ++ ++ @api.constrains('instructor_id', 'attendee_ids') ++ def _check_instructor_not_in_attendees(self): ++ for r in self: ++ if r.instructor_id and r.instructor_id in r.attendee_ids: ++ raise exceptions.ValidationError("A session's instructor can't be an attendee") diff --git a/developer/howtos/backend/exercise-constraint-sql b/developer/howtos/backend/exercise-constraint-sql new file mode 100644 index 000000000..5595a6db3 --- /dev/null +++ b/developer/howtos/backend/exercise-constraint-sql @@ -0,0 +1,24 @@ +# HG changeset patch +# Parent 121bbfe120be3007f5e04611dbc27038abafcce8 + +Index: addons/openacademy/models.py +=================================================================== +--- addons.orig/openacademy/models.py ++++ addons/openacademy/models.py +@@ -14,6 +14,16 @@ + session_ids = fields.One2many( + 'openacademy.session', 'course_id', string="Sessions") + ++ _sql_constraints = [ ++ ('name_description_check', ++ 'CHECK(name != description)', ++ "The title of the course should not be the description"), ++ ++ ('name_unique', ++ 'UNIQUE(name)', ++ "The course title must be unique"), ++ ] ++ + + class Session(models.Model): + _name = 'openacademy.session' diff --git a/developer/howtos/backend/exercise-copy-override b/developer/howtos/backend/exercise-copy-override new file mode 100644 index 000000000..119ed8ca2 --- /dev/null +++ b/developer/howtos/backend/exercise-copy-override @@ -0,0 +1,27 @@ +# HG changeset patch +# Parent 7d14b75cdfd4c7a272a13572947de5d47f3e851f +# Parent f400352a70963801f0b4732d33a0183e4f6800ff + +diff --git a/openacademy/models.py b/openacademy/models.py +--- a/openacademy/models.py ++++ b/openacademy/models.py +@@ -14,6 +14,19 @@ class Course(models.Model): + session_ids = fields.One2many( + 'openacademy.session', 'course_id', string="Sessions") + ++ def copy(self, default=None): ++ default = dict(default or {}) ++ ++ copied_count = self.search_count( ++ [('name', '=like', u"Copy of {}%".format(self.name))]) ++ if not copied_count: ++ new_name = u"Copy of {}".format(self.name) ++ else: ++ new_name = u"Copy of {} ({})".format(self.name, copied_count) ++ ++ default['name'] = new_name ++ return super(Course, self).copy(default) ++ + _sql_constraints = [ + ('name_description_check', + 'CHECK(name != description)', diff --git a/developer/howtos/backend/exercise-creation b/developer/howtos/backend/exercise-creation new file mode 100644 index 000000000..52de611b6 --- /dev/null +++ b/developer/howtos/backend/exercise-creation @@ -0,0 +1,152 @@ +# HG changeset patch +# Parent 0000000000000000000000000000000000000000 +Index: addons/openacademy/__manifest__.py +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/__manifest__.py 2014-08-26 17:25:49.787783523 +0200 +@@ -0,0 +1,35 @@ ++# -*- coding: utf-8 -*- ++{ ++ 'name': "Open Academy", ++ ++ 'summary': """Manage trainings""", ++ ++ 'description': """ ++ Open Academy module for managing trainings: ++ - training courses ++ - training sessions ++ - attendees registration ++ """, ++ ++ 'author': "My Company", ++ 'website': "http://www.yourcompany.com", ++ ++ # Categories can be used to filter modules in modules listing ++ # Check https://github.com/odoo/odoo/blob/13.0/odoo/addons/base/data/ir_module_category_data.xml ++ # for the full list ++ 'category': 'Test', ++ 'version': '0.1', ++ ++ # any module necessary for this one to work correctly ++ 'depends': ['base'], ++ ++ # always loaded ++ 'data': [ ++ # 'security/ir.model.access.csv', ++ 'templates.xml', ++ ], ++ # only loaded in demonstration mode ++ 'demo': [ ++ 'demo.xml', ++ ], ++} +Index: addons/openacademy/__init__.py +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/__init__.py 2014-08-26 17:25:49.791783523 +0200 +@@ -0,0 +1,3 @@ ++# -*- coding: utf-8 -*- ++from . import controllers ++from . import models +Index: addons/openacademy/controllers.py +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/controllers.py 2014-08-26 17:25:49.791783523 +0200 +@@ -0,0 +1,20 @@ ++# -*- coding: utf-8 -*- ++from odoo import http ++ ++# class Openacademy(http.Controller): ++# @http.route('/openacademy/openacademy/', auth='public') ++# def index(self, **kw): ++# return "Hello, world" ++ ++# @http.route('/openacademy/openacademy/objects/', auth='public') ++# def list(self, **kw): ++# return http.request.render('openacademy.listing', { ++# 'root': '/openacademy/openacademy', ++# 'objects': http.request.env['openacademy.openacademy'].search([]), ++# }) ++ ++# @http.route('/openacademy/openacademy/objects//', auth='public') ++# def object(self, obj, **kw): ++# return http.request.render('openacademy.object', { ++# 'object': obj ++# }) +Index: addons/openacademy/demo.xml +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/demo.xml 2014-08-26 17:25:49.791783523 +0200 +@@ -0,0 +1,25 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +Index: addons/openacademy/models.py +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/models.py 2014-08-26 17:25:49.791783523 +0200 +@@ -0,0 +1,8 @@ ++# -*- coding: utf-8 -*- ++ ++from odoo import models, fields, api ++ ++# class openacademy(models.Model): ++# _name = 'openacademy.openacademy' ++ ++# name = fields.Char() +Index: addons/openacademy/security/ir.model.access.csv +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/security/ir.model.access.csv 2014-08-26 17:25:49.791783523 +0200 +@@ -0,0 +1,2 @@ ++id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink ++access_openacademy_openacademy,openacademy.openacademy,model_openacademy_openacademy,,1,0,0,0 +Index: addons/openacademy/templates.xml +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/templates.xml 2014-08-26 17:25:49.791783523 +0200 +@@ -0,0 +1,22 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ diff --git a/developer/howtos/backend/exercise-dashboard b/developer/howtos/backend/exercise-dashboard new file mode 100644 index 000000000..9e5eb41b7 --- /dev/null +++ b/developer/howtos/backend/exercise-dashboard @@ -0,0 +1,91 @@ +# HG changeset patch +# Parent 643813940cbea07bec792f9e1c60022a9292fa90 + +Index: addons/openacademy/__manifest__.py +=================================================================== +--- addons.orig/openacademy/__manifest__.py 2014-08-26 17:26:21.535783052 +0200 ++++ addons/openacademy/__manifest__.py 2014-08-26 17:26:21.531783052 +0200 +@@ -21,7 +21,7 @@ + 'version': '0.1', + + # any module necessary for this one to work correctly +- 'depends': ['base'], ++ 'depends': ['base', 'board'], + + # always loaded + 'data': [ +@@ -30,6 +30,7 @@ + 'templates.xml', + 'views/openacademy.xml', + 'views/partner.xml', ++ 'views/session_board.xml', + 'reports.xml', + ], + # only loaded in demonstration mode +Index: addons/openacademy/views/session_board.xml +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/views/session_board.xml 2014-08-26 17:26:21.531783052 +0200 +@@ -0,0 +1,62 @@ ++ ++ ++ ++ ++ Attendees by course ++ openacademy.session ++ graph ++ ++ ++ ++ Sessions ++ openacademy.session ++ calendar ++ ++ ++ ++ Courses ++ openacademy.course ++ tree,form ++ ++ ++ Session Dashboard Form ++ board.board ++ form ++ ++
++ ++ ++ ++ ++ ++ ++ ++ ++ ++
++
++
++ ++ Session Dashboard ++ board.board ++ form ++ menu ++ ++ ++ ++ ++ ++
diff --git a/developer/howtos/backend/exercise-defaults b/developer/howtos/backend/exercise-defaults new file mode 100644 index 000000000..6365f4417 --- /dev/null +++ b/developer/howtos/backend/exercise-defaults @@ -0,0 +1,28 @@ +Index: addons/openacademy/models.py +=================================================================== +--- addons.orig/openacademy/models.py ++++ addons/openacademy/models.py +@@ -20,9 +20,10 @@ + _description = "OpenAcademy Sessions" + + name = fields.Char(required=True) +- start_date = fields.Date() ++ start_date = fields.Date(default=fields.Date.today) + duration = fields.Float(digits=(6, 2), help="Duration in days") + seats = fields.Integer(string="Number of seats") ++ active = fields.Boolean(default=True) + + instructor_id = fields.Many2one('res.partner', string="Instructor", + domain=['|', ('instructor', '=', True), +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml ++++ addons/openacademy/views/openacademy.xml +@@ -94,6 +94,7 @@ + + + ++ + + + diff --git a/developer/howtos/backend/exercise-demo b/developer/howtos/backend/exercise-demo new file mode 100644 index 000000000..50a956442 --- /dev/null +++ b/developer/howtos/backend/exercise-demo @@ -0,0 +1,48 @@ +# HG changeset patch +# Parent 84e2b0b43fc61fd0bcbb44c1929755d44ee58ae5 + +Index: addons/openacademy/demo.xml +=================================================================== +--- addons.orig/openacademy/demo.xml 2014-08-26 17:25:52.683783480 +0200 ++++ addons/openacademy/demo.xml 2014-08-26 17:25:52.679783480 +0200 +@@ -1,25 +1,19 @@ + + +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- ++ ++ Course 0 ++ Course 0's description ++ ++Can have multiple lines ++ ++ ++ ++ Course 1 ++ ++ ++ ++ Course 2 ++ Course 2's description ++ + + diff --git a/developer/howtos/backend/exercise-domain-advanced b/developer/howtos/backend/exercise-domain-advanced new file mode 100644 index 000000000..9b96a4459 --- /dev/null +++ b/developer/howtos/backend/exercise-domain-advanced @@ -0,0 +1,42 @@ +# HG changeset patch +# Parent 69d1f2d359eb8ef304a9d99f17790c78b35eda1a + +Index: addons/openacademy/models.py +=================================================================== +--- addons.orig/openacademy/models.py ++++ addons/openacademy/models.py +@@ -25,7 +25,8 @@ + seats = fields.Integer(string="Number of seats") + + instructor_id = fields.Many2one('res.partner', string="Instructor", +- domain=[('instructor', '=', True)]) ++ domain=['|', ('instructor', '=', True), ++ ('category_id.name', 'ilike', "Teacher")]) + course_id = fields.Many2one('openacademy.course', + ondelete='cascade', string="Course", required=True) + attendee_ids = fields.Many2many('res.partner', string="Attendees") +Index: addons/openacademy/views/partner.xml +=================================================================== +--- addons.orig/openacademy/views/partner.xml ++++ addons/openacademy/views/partner.xml +@@ -29,4 +29,20 @@ + parent="configuration_menu" + action="contact_list_action"/> + ++ ++ Contact Tags ++ res.partner.category ++ tree,form ++ ++ ++ ++ ++ Teacher / Level 1 ++ ++ ++ Teacher / Level 2 ++ ++ + diff --git a/developer/howtos/backend/exercise-domain-basic b/developer/howtos/backend/exercise-domain-basic new file mode 100644 index 000000000..71ee608cf --- /dev/null +++ b/developer/howtos/backend/exercise-domain-basic @@ -0,0 +1,17 @@ +# HG changeset patch +# Parent 142c5065ff1b7266d944d4ef5239e814ae22f0df + +Index: addons/openacademy/models.py +=================================================================== +--- addons.orig/openacademy/models.py ++++ addons/openacademy/models.py +@@ -24,7 +24,8 @@ + duration = fields.Float(digits=(6, 2), help="Duration in days") + seats = fields.Integer(string="Number of seats") + +- instructor_id = fields.Many2one('res.partner', string="Instructor") ++ instructor_id = fields.Many2one('res.partner', string="Instructor", ++ domain=[('instructor', '=', True)]) + course_id = fields.Many2one('openacademy.course', + ondelete='cascade', string="Course", required=True) + attendee_ids = fields.Many2many('res.partner', string="Attendees") diff --git a/developer/howtos/backend/exercise-formview b/developer/howtos/backend/exercise-formview new file mode 100644 index 000000000..9f80bc2f7 --- /dev/null +++ b/developer/howtos/backend/exercise-formview @@ -0,0 +1,29 @@ +# HG changeset patch +# Parent 4a0db1d29257764f4df5cb1ee0be7e59e8c8d0d8 + +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml 2014-08-26 17:25:54.291783456 +0200 ++++ addons/openacademy/views/openacademy.xml 2014-08-26 17:25:54.283783457 +0200 +@@ -1,6 +1,21 @@ + + + ++ ++ course.form ++ openacademy.course ++ ++
++ ++ ++ ++ ++ ++ ++
++
++
++ + + ++
++ ++
    ++
  • ++ Delete ++
  • ++
  • ++
      ++ ++
    ++
++
++ ++
++ ++ Session name: ++ ++
++ Start date: ++ ++
++ duration: ++ ++
++ ++ ++ ++ ++
++ ++ + + Sessions + openacademy.session +- tree,form,calendar,gantt,graph ++ tree,form,calendar,gantt,graph,kanban + + + +
+ ++
+ + ++ ++ ++ course.tree ++ openacademy.course ++ ++ ++ ++ ++ ++ ++ ++ + + ++ ++ session.tree ++ openacademy.session ++ ++ ++ ++ ++ ++ ++ ++ + + Sessions + openacademy.session diff --git a/developer/howtos/backend/exercise-model b/developer/howtos/backend/exercise-model new file mode 100644 index 000000000..6180794d1 --- /dev/null +++ b/developer/howtos/backend/exercise-model @@ -0,0 +1,19 @@ +# HG changeset patch +# Parent e3bb12713a6d38c28f50d46e8c1bab74ac40c1be +Index: addons/openacademy/models.py +=================================================================== +--- addons.orig/openacademy/models.py ++++ addons/openacademy/models.py +@@ -2,7 +2,9 @@ + + from odoo import models, fields, api + +-# class openacademy(models.Model): +-# _name = 'openacademy.openacademy' ++class Course(models.Model): ++ _name = 'openacademy.course' ++ _description = "OpenAcademy Courses" + +-# name = fields.Char() ++ name = fields.Char(string="Title", required=True) ++ description = fields.Text() diff --git a/developer/howtos/backend/exercise-model-inheritance b/developer/howtos/backend/exercise-model-inheritance new file mode 100644 index 000000000..073d706c8 --- /dev/null +++ b/developer/howtos/backend/exercise-model-inheritance @@ -0,0 +1,78 @@ +# HG changeset patch +# Parent d903c828fb10f2b38e5f43e9ceaeae0a9db7f858 + +Index: addons/openacademy/__init__.py +=================================================================== +--- addons.orig/openacademy/__init__.py 2014-08-26 17:26:01.227783353 +0200 ++++ addons/openacademy/__init__.py 2014-08-26 17:26:01.219783354 +0200 +@@ -1,3 +1,4 @@ + # -*- coding: utf-8 -*- + from . import controllers + from . import models ++from . import partner +Index: addons/openacademy/__manifest__.py +=================================================================== +--- addons.orig/openacademy/__manifest__.py 2014-08-26 17:26:01.227783353 +0200 ++++ addons/openacademy/__manifest__.py 2014-08-26 17:26:01.223783354 +0200 +@@ -28,6 +28,7 @@ + # 'security/ir.model.access.csv', + 'templates.xml', + 'views/openacademy.xml', ++ 'views/partner.xml', + ], + # only loaded in demonstration mode + 'demo': [ +Index: addons/openacademy/partner.py +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/partner.py 2014-08-26 17:26:01.223783354 +0200 +@@ -0,0 +1,12 @@ ++# -*- coding: utf-8 -*- ++from odoo import fields, models ++ ++class Partner(models.Model): ++ _inherit = 'res.partner' ++ ++ # Add a new column to the res.partner model, by default partners are not ++ # instructors ++ instructor = fields.Boolean("Instructor", default=False) ++ ++ session_ids = fields.Many2many('openacademy.session', ++ string="Attended Sessions", readonly=True) +Index: addons/openacademy/views/partner.xml +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ addons/openacademy/views/partner.xml 2014-08-26 17:26:01.223783354 +0200 +@@ -0,0 +1,32 @@ ++ ++ ++ ++ ++ ++ partner.instructor ++ res.partner ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Contacts ++ res.partner ++ tree,form ++ ++ ++ ++ ++ diff --git a/developer/howtos/backend/exercise-onchange b/developer/howtos/backend/exercise-onchange new file mode 100644 index 000000000..355e5ad70 --- /dev/null +++ b/developer/howtos/backend/exercise-onchange @@ -0,0 +1,28 @@ +# HG changeset patch +# Parent 8d5573b704b2867788dd6895503f1871c2976a29 +# Parent 9eb163e5da677a0d09e01a354ba56697b576a4bc + +diff --git a/openacademy/models.py b/openacademy/models.py +--- a/openacademy/models.py ++++ b/openacademy/models.py +@@ -41,3 +41,20 @@ class Session(models.Model): + r.taken_seats = 0.0 + else: + r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats ++ ++ @api.onchange('seats', 'attendee_ids') ++ def _verify_valid_seats(self): ++ if self.seats < 0: ++ return { ++ 'warning': { ++ 'title': "Incorrect 'seats' value", ++ 'message': "The number of available seats may not be negative", ++ }, ++ } ++ if self.seats < len(self.attendee_ids): ++ return { ++ 'warning': { ++ 'title': "Too many attendees", ++ 'message': "Increase seats or remove excess attendees", ++ }, ++ } diff --git a/developer/howtos/backend/exercise-one2many b/developer/howtos/backend/exercise-one2many new file mode 100644 index 000000000..67ded32da --- /dev/null +++ b/developer/howtos/backend/exercise-one2many @@ -0,0 +1,36 @@ +# HG changeset patch +# Parent cb05882d4fe73e97b9d34a69190ced14d1a50c24 + +Index: addons/openacademy/models.py +=================================================================== +--- addons.orig/openacademy/models.py ++++ addons/openacademy/models.py +@@ -11,6 +11,8 @@ + + responsible_id = fields.Many2one('res.users', + ondelete='set null', string="Responsible", index=True) ++ session_ids = fields.One2many( ++ 'openacademy.session', 'course_id', string="Sessions") + + + class Session(models.Model): +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml ++++ addons/openacademy/views/openacademy.xml +@@ -15,8 +15,13 @@ + + + +- +- This is an example of notebooks ++ ++ ++ ++ ++ ++ ++ + + + diff --git a/developer/howtos/backend/exercise-report b/developer/howtos/backend/exercise-report new file mode 100644 index 000000000..9cf3e990f --- /dev/null +++ b/developer/howtos/backend/exercise-report @@ -0,0 +1,52 @@ +# HG changeset patch +# Parent c140f0a861a08881d8737bca0ffb83904a2059a3 + +Index: addons/openacademy/__manifest__.py +=================================================================== +--- addons.orig/openacademy/__manifest__.py 2014-08-29 08:39:43.975536806 +0200 ++++ addons/openacademy/__manifest__.py 2014-08-29 08:39:52.000000000 +0200 +@@ -30,6 +30,7 @@ + 'templates.xml', + 'views/openacademy.xml', + 'views/partner.xml', ++ 'reports.xml', + ], + # only loaded in demonstration mode + 'demo': [ +Index: foo/openacademy/reports.xml +=================================================================== +--- /dev/null ++++ foo/openacademy/reports.xml +@@ -0,0 +1,32 @@ ++ ++ ++ ++ Session Report ++ openacademy.session ++ qweb-pdf ++ openacademy.report_session_view ++ openacademy.report_session ++ ++ report ++ ++ ++ ++ ++ diff --git a/developer/howtos/backend/exercise-searchview b/developer/howtos/backend/exercise-searchview new file mode 100644 index 000000000..1f7ef805c --- /dev/null +++ b/developer/howtos/backend/exercise-searchview @@ -0,0 +1,28 @@ +# HG changeset patch +# Parent 93a45ab8dd0a76c131cb5eeca6e44b71dca9f100 + +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:01:45.299033618 +0200 ++++ addons/openacademy/views/openacademy.xml 2014-08-28 14:18:58.847018275 +0200 +@@ -36,6 +36,12 @@ + + + ++ ++ ++ ++ + +
+ +@@ -61,6 +67,7 @@ + Courses + openacademy.course + tree,form ++ + +

Create the first course +

diff --git a/developer/howtos/backend/exercise-searchview-basic b/developer/howtos/backend/exercise-searchview-basic new file mode 100644 index 000000000..a04813351 --- /dev/null +++ b/developer/howtos/backend/exercise-searchview-basic @@ -0,0 +1,24 @@ +# HG changeset patch +# Parent b9bfc8929e0ffc3eb153641e14952fe5d99eb908 +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml 2014-08-26 17:25:55.807783434 +0200 ++++ addons/openacademy/views/openacademy.xml 2014-08-26 17:25:55.799783434 +0200 +@@ -23,6 +23,17 @@ +
+ + ++ ++ course.search ++ openacademy.course ++ ++ ++ ++ ++ ++ ++ ++ + + + ++ ++ ++ session.form ++ openacademy.session ++ ++
++ ++ ++ ++ ++ ++ ++ ++ ++
++
++
++ ++ ++ Sessions ++ openacademy.session ++ tree,form ++ ++ ++ ++ + diff --git a/developer/howtos/backend/exercise-translations b/developer/howtos/backend/exercise-translations new file mode 100644 index 000000000..02b656efe --- /dev/null +++ b/developer/howtos/backend/exercise-translations @@ -0,0 +1,56 @@ +# HG changeset patch +# Parent 7c95aad3b60e4c2006c5f706bd157e8e05318bfa + +diff --git a/openacademy/models.py b/openacademy/models.py +--- a/openacademy/models.py ++++ b/openacademy/models.py +@@ -1,7 +1,7 @@ + # -*- coding: utf-8 -*- + + from datetime import timedelta +-from odoo import models, fields, api, exceptions ++from odoo import models, fields, api, exceptions, _ + + class Course(models.Model): + _name = 'openacademy.course' +@@ -20,11 +20,11 @@ class Course(models.Model): + default = dict(default or {}) + + copied_count = self.search_count( +- [('name', '=like', u"Copy of {}%".format(self.name))]) ++ [('name', '=like', _(u"Copy of {}%").format(self.name))]) + if not copied_count: +- new_name = u"Copy of {}".format(self.name) ++ new_name = _(u"Copy of {}").format(self.name) + else: +- new_name = u"Copy of {} ({})".format(self.name, copied_count) ++ new_name = _(u"Copy of {} ({})").format(self.name, copied_count) + + default['name'] = new_name + return super(Course, self).copy(default) +@@ -81,15 +81,15 @@ class Session(models.Model): + if self.seats < 0: + return { + 'warning': { +- 'title': "Incorrect 'seats' value", +- 'message': "The number of available seats may not be negative", ++ 'title': _("Incorrect 'seats' value"), ++ 'message': _("The number of available seats may not be negative"), + }, + } + if self.seats < len(self.attendee_ids): + return { + 'warning': { +- 'title': "Too many attendees", +- 'message': "Increase seats or remove excess attendees", ++ 'title': _("Too many attendees"), ++ 'message': _("Increase seats or remove excess attendees"), + }, + } + +@@ -114,4 +114,4 @@ class Session(models.Model): + def _check_instructor_not_in_attendees(self): + for r in self: + if r.instructor_id and r.instructor_id in r.attendee_ids: +- raise exceptions.ValidationError("A session's instructor can't be an attendee") ++ raise exceptions.ValidationError(_("A session's instructor can't be an attendee")) diff --git a/developer/howtos/backend/exercise-wizard b/developer/howtos/backend/exercise-wizard new file mode 100644 index 000000000..9c44d71ca --- /dev/null +++ b/developer/howtos/backend/exercise-wizard @@ -0,0 +1,36 @@ +Index: addons/openacademy/__init__.py +=================================================================== +--- addons.orig/openacademy/__init__.py ++++ addons/openacademy/__init__.py +@@ -2,3 +2,4 @@ + from . import controllers + from . import models + from . import partner ++from . import wizard +Index: addons/openacademy/wizard.py +=================================================================== +--- /dev/null ++++ addons/openacademy/wizard.py +@@ -0,0 +1,11 @@ ++# -*- coding: utf-8 -*- ++ ++from odoo import models, fields, api ++ ++class Wizard(models.TransientModel): ++ _name = 'openacademy.wizard' ++ _description = "Wizard: Quick Registration of Attendees to Sessions" ++ ++ session_id = fields.Many2one('openacademy.session', ++ string="Session", required=True) ++ attendee_ids = fields.Many2many('res.partner', string="Attendees") +Index: foo/openacademy/security/ir.model.access.csv +=================================================================== +--- foo.orig/openacademy/security/ir.model.access.csv ++++ foo/openacademy/security/ir.model.access.csv +@@ -1,5 +1,6 @@ + id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink + course_manager,course manager,model_openacademy_course,group_manager,1,1,1,1 + session_manager,session manager,model_openacademy_session,group_manager,1,1,1,1 ++wizard_manager,wizard session manager,model_openacademy_wizard,group_manager,1,1,1,1 + course_read_all,course all,model_openacademy_course,,1,0,0,0 + session_read_all,session all,model_openacademy_session,,1,0,0,0 diff --git a/developer/howtos/backend/exercise-wizard-action b/developer/howtos/backend/exercise-wizard-action new file mode 100644 index 000000000..305b5859b --- /dev/null +++ b/developer/howtos/backend/exercise-wizard-action @@ -0,0 +1,29 @@ +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml ++++ addons/openacademy/views/openacademy.xml +@@ -232,6 +232,12 @@ + + + ++
++
+ +
+ +Index: addons/openacademy/wizard.py +=================================================================== +--- addons.orig/openacademy/wizard.py ++++ addons/openacademy/wizard.py +@@ -12,3 +12,7 @@ + session_id = fields.Many2one('openacademy.session', + string="Session", required=True, default=_default_session) + attendee_ids = fields.Many2many('res.partner', string="Attendees") ++ ++ def subscribe(self): ++ self.session_id.attendee_ids |= self.attendee_ids ++ return {} diff --git a/developer/howtos/backend/exercise-wizard-launch b/developer/howtos/backend/exercise-wizard-launch new file mode 100644 index 000000000..65b7be845 --- /dev/null +++ b/developer/howtos/backend/exercise-wizard-launch @@ -0,0 +1,45 @@ +Index: addons/openacademy/wizard.py +=================================================================== +--- addons.orig/openacademy/wizard.py ++++ addons/openacademy/wizard.py +@@ -6,6 +6,9 @@ class Wizard(models.TransientModel): + _name = 'openacademy.wizard' + _description = "Wizard: Quick Registration of Attendees to Sessions" + ++ def _default_session(self): ++ return self.env['openacademy.session'].browse(self._context.get('active_id')) ++ + session_id = fields.Many2one('openacademy.session', +- string="Session", required=True) ++ string="Session", required=True, default=_default_session) + attendee_ids = fields.Many2many('res.partner', string="Attendees") +Index: foo/openacademy/views/openacademy.xml +=================================================================== +--- foo.orig/openacademy/views/openacademy.xml ++++ foo/openacademy/views/openacademy.xml +@@ -220,4 +220,25 @@ + parent="openacademy_menu" + action="session_list_action"/> + ++ ++ wizard.form ++ openacademy.wizard ++ ++
++ ++ ++ ++ ++
++
++
++ ++ ++ Add Attendees ++ openacademy.wizard ++ form ++ new ++ ++ ++ + diff --git a/developer/howtos/backend/exercise-wizard-multi b/developer/howtos/backend/exercise-wizard-multi new file mode 100644 index 000000000..8e8805e52 --- /dev/null +++ b/developer/howtos/backend/exercise-wizard-multi @@ -0,0 +1,37 @@ +Index: addons/openacademy/views/openacademy.xml +=================================================================== +--- addons.orig/openacademy/views/openacademy.xml ++++ addons/openacademy/views/openacademy.xml +@@ -229,7 +229,7 @@ + +
+ +- ++ + + +