[MERGE] Forward-port of branch 12.0 to 13.0

This commit is contained in:
Victor Feyens 2021-05-11 15:13:58 +02:00
commit 3d9afc4c0e
86 changed files with 773 additions and 3211 deletions

View File

@ -102,9 +102,6 @@ extensions = [
'exercise_admonition',
# Build code from git patches
'patchqueue',
# Redirection generator
'redirects',
@ -308,7 +305,7 @@ def _generate_alternate_urls(app, pagename, templatename, context, doctree):
def _build_url(_version=None, _lang=None):
if app.config.is_remote_build:
# Project root like https://odoo.com/documentation/14.0/fr
# Project root like https://www.odoo.com/documentation
_root = app.config.project_root
else:
# Project root like .../documentation/_build/html/14.0/fr

View File

@ -10,3 +10,4 @@ Devices
devices/camera
devices/footswitch
devices/printer
devices/scale

View File

@ -0,0 +1,55 @@
===============
Connect a Scale
===============
When using your **IoT Box** in Odoo, you could need to use a scale. Doing so is easy and convenient
as it can be done in a few steps. Then, you can use it in your **Point of Sale app** to weigh your
products, which is helpful if their price are based on it.
Connection
==========
To link the scale to the **IoT Box**, connect them with a cable.
.. note::
In some cases, a serial to USB adapter may be needed.
If your scale is `compatibale with Odoo IoT Box <https://www.odoo.com/page/iot-hardware>`_, there
is no need to set up anything because it will be automatically detected as soon as it is connected.
.. image:: scale/iot-choice.png
:align: center
:alt: IOT box auto detection.
You may need to restart the box and download your scales drivers from the box in some cases. To do
so, go to the *IoT Box Home Page* and click on *drivers list*. Then, click on load drivers.
.. image:: scale/driver-list.png
:align: center
:alt: View of the IoT box settings and driver list.
Use a Scale in Point of Sale
============================
To use the scale in your *Point of Sale* app, go to :menuselection:`Point of Sale --> Configuration
--> Point of Sale`, open the one you want to configure, then click on *Edit* and enable the *IoT
Box* feature.
.. image:: scale/iot-box-pos.png
:align: center
:alt: View of the IoT box feature inside of the PoS settings.
Now, choose the *IoT Box* in the dropdown menu and check the *Electronic Scale* option. Then, you
hit save.
.. image:: scale/electronic-scale-feature.png
:align: center
:alt: List of the external tools that can be used with PoS and the IoT box.
The scale is now available in all your *PoS* sessions. Then, if a product has a price per weight
set, clicking on it on the *PoS* screen opens the scale screen, where the cashier can weigh the
product and add the correct price to the cart.
.. image:: scale/scale-view.png
:align: center
:alt: Electronic Scale dashboard view when no items are being weighed.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -1,6 +1,4 @@
.. queue:: backend/series
.. _howto/base:
.. _howto/module:
@ -114,14 +112,6 @@ or XML. The usage of most of those files will be explained along this tutorial.
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
-------------------------
@ -216,12 +206,6 @@ overridden by setting :attr:`~odoo.models.Model._rec_name`.
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
----------
@ -260,12 +244,6 @@ be declared in the ``'data'`` list (always loaded) or in the ``'demo'`` list
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.
@ -313,14 +291,6 @@ action more easily.
- 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
===========
@ -413,22 +383,12 @@ elements (groups, notebooks) and interactive elements (buttons and fields):
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
@ -476,10 +436,6 @@ searching on the ``name`` field.
Allow searching for courses based on their title or their description.
.. only:: solutions
.. patch::
Relations between models
========================
@ -496,18 +452,6 @@ client data; it is also related to its sale order line records.
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
-----------------
@ -558,25 +502,11 @@ Relational field types are:
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
@ -584,13 +514,6 @@ Relational field types are:
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
===========
@ -690,21 +613,6 @@ instead of a single view its ``arch`` field is composed of any number of
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
#######
@ -737,31 +645,12 @@ records for the relation when trying to select records in the client interface.
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
==================================
@ -830,13 +719,6 @@ field whenever some of its dependencies have been modified::
* 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
--------------
@ -867,15 +749,6 @@ float, string), or a function taking a recordset and returning a value::
* 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
========
@ -921,10 +794,6 @@ the ``taken_seats`` progressbar is automatically updated.
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
=================
@ -952,10 +821,6 @@ raise an exception if its invariant is not satisfied::
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
@ -970,10 +835,6 @@ and ``message`` is the error message.
#. 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
@ -983,10 +844,6 @@ and ``message`` is the error message.
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
==============
@ -1030,12 +887,6 @@ behavior:
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
---------
@ -1065,19 +916,6 @@ their most common attributes are:
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
------------
@ -1127,10 +965,6 @@ default and behave as booleans (they can only be enabled by default).
responsible in the course search view. Make it selected by default.
#. Add a button to group courses by responsible user.
.. only:: solutions
.. patch::
Gantt
-----
@ -1155,13 +989,6 @@ their root element is ``<gantt>``.
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
-----------
@ -1213,13 +1040,6 @@ the values:
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
------
@ -1240,13 +1060,6 @@ Kanban views define the structure of each card as a mix of form elements
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
========
@ -1283,16 +1096,6 @@ rights are usually created by a CSV file named after its model:
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,
@ -1301,17 +1104,6 @@ rights are usually created by a CSV file named after its model:
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
------------
@ -1345,12 +1137,6 @@ the same convention as the method :meth:`~odoo.models.Model.write` of the ORM.
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::
Wizards
=======
@ -1378,12 +1164,6 @@ session, or for a list of sessions at once.
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
-----------------
@ -1416,28 +1196,16 @@ Wizards use regular views and their buttons may use the attribute
#. 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
====================
@ -1465,7 +1233,7 @@ for editing and merging PO/POT files.
| - pt_BR.po # Brazilian Portuguese translation
| (...)
.. tip::
.. 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
@ -1477,39 +1245,6 @@ for editing and merging PO/POT files.
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
=========
@ -1517,7 +1252,7 @@ Printed reports
---------------
Odoo uses a report engine based on :ref:`reference/qweb`,
`Twitter Bootstrap`_ and Wkhtmltopdf_.
`Twitter Bootstrap`_ and Wkhtmltopdf_.
A report is a combination two elements:
@ -1598,10 +1333,6 @@ http://localhost:8069/report/pdf/account.report_invoice/1.
For each session, it should display session's name, its start and end,
and list the session's attendees.
.. only:: solutions
.. patch::
Dashboards
----------
@ -1613,21 +1344,6 @@ Dashboards
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
===========

View File

@ -1,40 +0,0 @@
# 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 @@
+<odoo>
+
+ <record id="group_manager" model="res.groups">
+ <field name="name">OpenAcademy / Manager</field>
+ </record>
+
+</odoo>

View File

@ -1,27 +0,0 @@
# 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 @@
<record id="group_manager" model="res.groups">
<field name="name">OpenAcademy / Manager</field>
</record>
+
+ <record id="only_responsible_can_modify" model="ir.rule">
+ <field name="name">Only Responsible can modify Course</field>
+ <field name="model_id" ref="model_openacademy_course"/>
+ <field name="groups" eval="[(4, ref('openacademy.group_manager'))]"/>
+ <field name="perm_read" eval="0"/>
+ <field name="perm_write" eval="1"/>
+ <field name="perm_create" eval="0"/>
+ <field name="perm_unlink" eval="1"/>
+ <field name="domain_force">
+ ['|', ('responsible_id','=',False),
+ ('responsible_id','=',user.id)]
+ </field>
+ </record>
</odoo>

View File

@ -1,19 +0,0 @@
# 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 @@
<field name="name">session.tree</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
- <tree string="Session Tree">
+ <tree string="Session Tree" decoration-info="duration&lt;5" decoration-danger="duration&gt;15">
<field name="name"/>
<field name="course_id"/>
+ <field name="duration" invisible="1"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>

View File

@ -1,53 +0,0 @@
# 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+
+ <!-- window action -->
+ <!--
+ The following tag is an action definition for a "window action",
+ that is an action opening a view or a set of views
+ -->
+ <record model="ir.actions.act_window" id="course_list_action">
+ <field name="name">Courses</field>
+ <field name="res_model">openacademy.course</field>
+ <field name="view_mode">tree,form</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">Create the first course
+ </p>
+ </field>
+ </record>
+
+ <!-- top level menu: no parent -->
+ <menuitem id="main_openacademy_menu" name="Open Academy"/>
+ <!-- A first level in the left side menu is needed
+ before using action= attribute -->
+ <menuitem id="openacademy_menu" name="Open Academy"
+ parent="main_openacademy_menu"/>
+ <!-- the following menuitem should appear *after*
+ its parent openacademy_menu and *after* its
+ action course_list_action -->
+ <menuitem id="courses_menu" name="Courses" parent="openacademy_menu"
+ action="course_list_action"/>
+ <!-- Full id location:
+ action="openacademy.course_list_action"
+ It is not required when it is the same module -->
+
+</odoo>

View File

@ -1,77 +0,0 @@
# 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 @@
</field>
</record>
+ <!-- calendar view -->
+ <record model="ir.ui.view" id="session_calendar_view">
+ <field name="name">session.calendar</field>
+ <field name="model">openacademy.session</field>
+ <field name="arch" type="xml">
+ <calendar string="Session Calendar" date_start="start_date" date_stop="end_date" color="instructor_id">
+ <field name="name"/>
+ </calendar>
+ </field>
+ </record>
+
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
- <field name="view_mode">tree,form</field>
+ <field name="view_mode">tree,form,calendar</field>
</record>
<menuitem id="session_menu" name="Sessions"

View File

@ -1,40 +0,0 @@
# HG changeset patch
# Parent a358be0a577b0569831958a8ec1302825c645dee
# Parent 59107edbe5f81bab5e7db172bf2bffa504ce399a
diff --git a/openacademy/models.py b/openacademy/models.py
--- a/openacademy/models.py
+++ b/openacademy/models.py
@@ -30,3 +30,13 @@ class Session(models.Model):
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
+
+ taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
+
+ @api.depends('seats', 'attendee_ids')
+ def _taken_seats(self):
+ for r in self:
+ if not r.seats:
+ r.taken_seats = 0.0
+ else:
+ r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
diff --git a/openacademy/views/openacademy.xml b/openacademy/views/openacademy.xml
--- a/openacademy/views/openacademy.xml
+++ b/openacademy/views/openacademy.xml
@@ -99,6 +99,7 @@
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
+ <field name="taken_seats" widget="progressbar"/>
</group>
</group>
<label for="attendee_ids"/>
@@ -116,6 +117,7 @@
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
+ <field name="taken_seats" widget="progressbar"/>
</tree>
</field>
</record>

View File

@ -1,25 +0,0 @@
# 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")

View File

@ -1,24 +0,0 @@
# 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'

View File

@ -1,27 +0,0 @@
# 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)',

View File

@ -1,152 +0,0 @@
# 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/<model("openacademy.openacademy"):obj>/', 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 @@
+<odoo>
+
+ <!-- -->
+ <!-- <record id="object0" model="openacademy.openacademy"> -->
+ <!-- <field name="name">Object 0</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object1" model="openacademy.openacademy"> -->
+ <!-- <field name="name">Object 1</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object2" model="openacademy.openacademy"> -->
+ <!-- <field name="name">Object 2</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object3" model="openacademy.openacademy"> -->
+ <!-- <field name="name">Object 3</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object4" model="openacademy.openacademy"> -->
+ <!-- <field name="name">Object 4</field> -->
+ <!-- </record> -->
+ <!-- -->
+
+</odoo>
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 @@
+<odoo>
+
+ <!-- <template id="listing"> -->
+ <!-- <ul> -->
+ <!-- <li t-foreach="objects" t-as="object"> -->
+ <!-- <a t-attf-href="{{ root }}/objects/{{ object.id }}"> -->
+ <!-- <t t-esc="object.display_name"/> -->
+ <!-- </a> -->
+ <!-- </li> -->
+ <!-- </ul> -->
+ <!-- </template> -->
+ <!-- <template id="object"> -->
+ <!-- <h1><t t-esc="object.display_name"/></h1> -->
+ <!-- <dl> -->
+ <!-- <t t-foreach="object._fields" t-as="field"> -->
+ <!-- <dt><t t-esc="field"/></dt> -->
+ <!-- <dd><t t-esc="object[field]"/></dd> -->
+ <!-- </t> -->
+ <!-- </dl> -->
+ <!-- </template> -->
+
+</odoo>

View File

@ -1,91 +0,0 @@
# 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 @@
+<?xml version="1.0"?>
+<odoo>
+
+ <record model="ir.actions.act_window" id="act_session_graph">
+ <field name="name">Attendees by course</field>
+ <field name="res_model">openacademy.session</field>
+ <field name="view_mode">graph</field>
+ <field name="view_id"
+ ref="openacademy.openacademy_session_graph_view"/>
+ </record>
+ <record model="ir.actions.act_window" id="act_session_calendar">
+ <field name="name">Sessions</field>
+ <field name="res_model">openacademy.session</field>
+ <field name="view_mode">calendar</field>
+ <field name="view_id" ref="openacademy.session_calendar_view"/>
+ </record>
+ <record model="ir.actions.act_window" id="act_course_list">
+ <field name="name">Courses</field>
+ <field name="res_model">openacademy.course</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <record model="ir.ui.view" id="board_session_form">
+ <field name="name">Session Dashboard Form</field>
+ <field name="model">board.board</field>
+ <field name="type">form</field>
+ <field name="arch" type="xml">
+ <form string="Session Dashboard">
+ <board style="2-1">
+ <column>
+ <action
+ string="Attendees by course"
+ name="%(act_session_graph)d"
+ height="150"
+ width="510"/>
+ <action
+ string="Sessions"
+ name="%(act_session_calendar)d"/>
+ </column>
+ <column>
+ <action
+ string="Courses"
+ name="%(act_course_list)d"/>
+ </column>
+ </board>
+ </form>
+ </field>
+ </record>
+ <record model="ir.actions.act_window" id="open_board_session">
+ <field name="name">Session Dashboard</field>
+ <field name="res_model">board.board</field>
+ <field name="view_mode">form</field>
+ <field name="usage">menu</field>
+ <field name="view_id" ref="board_session_form"/>
+ </record>
+
+ <menuitem
+ name="Session Dashboard" parent="base.menu_reporting_dashboard"
+ action="open_board_session"
+ sequence="1"
+ id="menu_board_session"/>
+
+</odoo>

View File

@ -1,28 +0,0 @@
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 @@
<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
+ <field name="active"/>
</group>
<group string="Schedule">
<field name="start_date"/>

View File

@ -1,48 +0,0 @@
# 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 @@
<odoo>
- <!-- -->
- <!-- <record id="object0" model="openacademy.openacademy"> -->
- <!-- <field name="name">Object 0</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object1" model="openacademy.openacademy"> -->
- <!-- <field name="name">Object 1</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object2" model="openacademy.openacademy"> -->
- <!-- <field name="name">Object 2</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object3" model="openacademy.openacademy"> -->
- <!-- <field name="name">Object 3</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object4" model="openacademy.openacademy"> -->
- <!-- <field name="name">Object 4</field> -->
- <!-- </record> -->
- <!-- -->
+ <record model="openacademy.course" id="course0">
+ <field name="name">Course 0</field>
+ <field name="description">Course 0's description
+
+Can have multiple lines
+ </field>
+ </record>
+ <record model="openacademy.course" id="course1">
+ <field name="name">Course 1</field>
+ <!-- no description for this one -->
+ </record>
+ <record model="openacademy.course" id="course2">
+ <field name="name">Course 2</field>
+ <field name="description">Course 2's description</field>
+ </record>
</odoo>

View File

@ -1,42 +0,0 @@
# 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"/>
+ <record model="ir.actions.act_window" id="contact_cat_list_action">
+ <field name="name">Contact Tags</field>
+ <field name="res_model">res.partner.category</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <menuitem id="contact_cat_menu" name="Contact Tags"
+ parent="configuration_menu"
+ action="contact_cat_list_action"/>
+
+ <record model="res.partner.category" id="teacher1">
+ <field name="name">Teacher / Level 1</field>
+ </record>
+ <record model="res.partner.category" id="teacher2">
+ <field name="name">Teacher / Level 2</field>
+ </record>
+
</odoo>

View File

@ -1,17 +0,0 @@
# 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")

View File

@ -1,29 +0,0 @@
# 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 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
+ <record model="ir.ui.view" id="course_form_view">
+ <field name="name">course.form</field>
+ <field name="model">openacademy.course</field>
+ <field name="arch" type="xml">
+ <form string="Course Form">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="description"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
<!-- window action -->
<!--
The following tag is an action definition for a "window action",

View File

@ -1,24 +0,0 @@
# HG changeset patch
# Parent 5508a5440faa7b607d057c4e4ae70af6b6f7cac9
Index: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml 2014-08-26 17:25:55.023783446 +0200
+++ addons/openacademy/views/openacademy.xml 2014-08-26 17:25:55.015783446 +0200
@@ -9,8 +9,15 @@
<sheet>
<group>
<field name="name"/>
- <field name="description"/>
</group>
+ <notebook>
+ <page string="Description">
+ <field name="description"/>
+ </page>
+ <page string="About">
+ This is an example of notebooks
+ </page>
+ </notebook>
</sheet>
</form>
</field>

View File

@ -1,30 +0,0 @@
# HG changeset patch
# Parent dba00a105dd2a82490394b8dec5fea5f1d8847e1
# Parent f4374b6e2e661e0782e396b24c57c1eb97d13288
diff --git a/openacademy/views/openacademy.xml b/openacademy/views/openacademy.xml
--- a/openacademy/views/openacademy.xml
+++ b/openacademy/views/openacademy.xml
@@ -142,10 +142,21 @@
</field>
</record>
+ <record model="ir.ui.view" id="session_gantt_view">
+ <field name="name">session.gantt</field>
+ <field name="model">openacademy.session</field>
+ <field name="arch" type="xml">
+ <gantt string="Session Gantt"
+ date_start="start_date" date_stop="end_date"
+ default_group_by='instructor_id'>
+ </gantt>
+ </field>
+ </record>
+
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
- <field name="view_mode">tree,form,calendar</field>
+ <field name="view_mode">tree,form,calendar,gantt</field>
</record>
<menuitem id="session_menu" name="Sessions"

View File

@ -1,55 +0,0 @@
# HG changeset patch
# Parent a6fe4d3923db1f8f5dff2c39a711a814b0a0f549
# Parent 0687e07f570f363bf5005c6337e0c565a63a6bac
diff --git a/openacademy/models.py b/openacademy/models.py
--- a/openacademy/models.py
+++ b/openacademy/models.py
@@ -60,6 +60,9 @@ class Session(models.Model):
end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')
+ attendees_count = fields.Integer(
+ string="Attendees count", compute='_get_attendees_count', store=True)
+
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
@@ -106,6 +109,11 @@ class Session(models.Model):
# so add one day to get 5 days instead
r.duration = (r.end_date - r.start_date).days + 1
+ @api.depends('attendee_ids')
+ def _get_attendees_count(self):
+ for r in self:
+ r.attendees_count = len(r.attendee_ids)
+
@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
@@ -153,10 +153,21 @@
</field>
</record>
+ <record model="ir.ui.view" id="openacademy_session_graph_view">
+ <field name="name">openacademy.session.graph</field>
+ <field name="model">openacademy.session</field>
+ <field name="arch" type="xml">
+ <graph string="Participations by Courses">
+ <field name="course_id"/>
+ <field name="attendees_count" type="measure"/>
+ </graph>
+ </field>
+ </record>
+
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
- <field name="view_mode">tree,form,calendar,gantt</field>
+ <field name="view_mode">tree,form,calendar,gantt,graph</field>
</record>
<menuitem id="session_menu" name="Sessions"

View File

@ -1,77 +0,0 @@
# HG changeset patch
# Parent 8d66f7620781558d4520f97e4cebc14ed180683e
Index: addons/openacademy/models.py
===================================================================
--- addons.orig/openacademy/models.py
+++ addons/openacademy/models.py
@@ -49,6 +49,7 @@
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
+ color = fields.Integer()
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
@@ -165,10 +165,56 @@
</field>
</record>
+ <record model="ir.ui.view" id="view_openacad_session_kanban">
+ <field name="name">openacademy.session.kanban</field>
+ <field name="model">openacademy.session</field>
+ <field name="arch" type="xml">
+ <kanban default_group_by="course_id">
+ <field name="color"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div
+ t-attf-class="oe_kanban_color_{{kanban_getcolor(record.color.raw_value)}}
+ oe_kanban_global_click_edit oe_semantic_html_override
+ oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
+ <div class="oe_dropdown_kanban">
+ <!-- dropdown menu -->
+ <div class="oe_dropdown_toggle">
+ <i class="fa fa-bars fa-lg" title="Manage" aria-label="Manage"/>
+ <ul class="oe_dropdown_menu">
+ <li>
+ <a type="delete">Delete</a>
+ </li>
+ <li>
+ <ul class="oe_kanban_colorpicker"
+ data-field="color"/>
+ </li>
+ </ul>
+ </div>
+ <div class="oe_clear"></div>
+ </div>
+ <div t-attf-class="oe_kanban_content">
+ <!-- title -->
+ Session name:
+ <field name="name"/>
+ <br/>
+ Start date:
+ <field name="start_date"/>
+ <br/>
+ duration:
+ <field name="duration"/>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
- <field name="view_mode">tree,form,calendar,gantt,graph</field>
+ <field name="view_mode">tree,form,calendar,gantt,graph,kanban</field>
</record>
<menuitem id="session_menu" name="Sessions"

View File

@ -1,22 +0,0 @@
Index: addons/openacademy/models.py
===================================================================
--- addons.orig/openacademy/models.py
+++ addons/openacademy/models.py
@@ -27,3 +27,4 @@
instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
+ attendee_ids = fields.Many2many('res.partner', string="Attendees")
Index: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml
+++ addons/openacademy/views/openacademy.xml
@@ -101,6 +101,8 @@
<field name="seats"/>
</group>
</group>
+ <label for="attendee_ids"/>
+ <field name="attendee_ids"/>
</sheet>
</form>
</field>

View File

@ -1,95 +0,0 @@
# HG changeset patch
# Parent a6e217b1fbbc64111581c269629b1c25c23abb99
Index: addons/openacademy/models.py
===================================================================
--- addons.orig/openacademy/models.py
+++ addons/openacademy/models.py
@@ -9,6 +9,9 @@
name = fields.Char(string="Title", required=True)
description = fields.Text()
+ responsible_id = fields.Many2one('res.users',
+ ondelete='set null', string="Responsible", index=True)
+
class Session(models.Model):
_name = 'openacademy.session'
@@ -18,3 +21,7 @@
start_date = fields.Date()
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")
+ course_id = fields.Many2one('openacademy.course',
+ ondelete='cascade', string="Course", required=True)
Index: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml
+++ addons/openacademy/views/openacademy.xml
@@ -9,6 +9,7 @@
<sheet>
<group>
<field name="name"/>
+ <field name="responsible_id"/>
</group>
<notebook>
<page string="Description">
@@ -34,6 +35,18 @@
</field>
</record>
+ <!-- override the automatically generated list view for courses -->
+ <record model="ir.ui.view" id="course_tree_view">
+ <field name="name">course.tree</field>
+ <field name="model">openacademy.course</field>
+ <field name="arch" type="xml">
+ <tree string="Course Tree">
+ <field name="name"/>
+ <field name="responsible_id"/>
+ </tree>
+ </field>
+ </record>
+
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
@@ -72,16 +85,34 @@
<form string="Session Form">
<sheet>
<group>
- <field name="name"/>
- <field name="start_date"/>
- <field name="duration"/>
- <field name="seats"/>
+ <group string="General">
+ <field name="course_id"/>
+ <field name="name"/>
+ <field name="instructor_id"/>
+ </group>
+ <group string="Schedule">
+ <field name="start_date"/>
+ <field name="duration"/>
+ <field name="seats"/>
+ </group>
</group>
</sheet>
</form>
</field>
</record>
+ <!-- session tree/list view -->
+ <record model="ir.ui.view" id="session_tree_view">
+ <field name="name">session.tree</field>
+ <field name="model">openacademy.session</field>
+ <field name="arch" type="xml">
+ <tree string="Session Tree">
+ <field name="name"/>
+ <field name="course_id"/>
+ </tree>
+ </field>
+ </record>
+
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>

View File

@ -1,19 +0,0 @@
# 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()

View File

@ -1,78 +0,0 @@
# 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+ <odoo>
+
+ <!-- Add instructor field to existing view -->
+ <record model="ir.ui.view" id="partner_instructor_form_view">
+ <field name="name">partner.instructor</field>
+ <field name="model">res.partner</field>
+ <field name="inherit_id" ref="base.view_partner_form"/>
+ <field name="arch" type="xml">
+ <notebook position="inside">
+ <page string="Sessions">
+ <group>
+ <field name="instructor"/>
+ <field name="session_ids"/>
+ </group>
+ </page>
+ </notebook>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="contact_list_action">
+ <field name="name">Contacts</field>
+ <field name="res_model">res.partner</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <menuitem id="configuration_menu" name="Configuration"
+ parent="main_openacademy_menu"/>
+ <menuitem id="contact_menu" name="Contacts"
+ parent="configuration_menu"
+ action="contact_list_action"/>
+
+</odoo>

View File

@ -1,28 +0,0 @@
# 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",
+ },
+ }

View File

@ -1,36 +0,0 @@
# 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 @@
<page string="Description">
<field name="description"/>
</page>
- <page string="About">
- This is an example of notebooks
+ <page string="Sessions">
+ <field name="session_ids">
+ <tree string="Registered sessions">
+ <field name="name"/>
+ <field name="instructor_id"/>
+ </tree>
+ </field>
</page>
</notebook>
</sheet>

View File

@ -1,50 +0,0 @@
# 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: addons/openacademy/reports.xml
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ addons/openacademy/reports.xml 2014-08-29 08:41:12.663540061 +0200
@@ -0,0 +1,30 @@
+<odoo>
+
+ <report
+ id="report_session"
+ model="openacademy.session"
+ string="Session Report"
+ name="openacademy.report_session_view"
+ file="openacademy.report_session"
+ report_type="qweb-pdf" />
+
+ <template id="report_session_view">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="doc">
+ <t t-call="web.external_layout">
+ <div class="page">
+ <h2 t-field="doc.name"/>
+ <p>From <span t-field="doc.start_date"/> to <span t-field="doc.end_date"/></p>
+ <h3>Attendees:</h3>
+ <ul>
+ <t t-foreach="doc.attendee_ids" t-as="attendee">
+ <li><span t-field="attendee.name"/></li>
+ </t>
+ </ul>
+ </div>
+ </t>
+ </t>
+ </t>
+ </template>
+
+</odoo>

View File

@ -1,28 +0,0 @@
# 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 @@
<search>
<field name="name"/>
<field name="description"/>
+ <filter name="my_courses" string="My Courses"
+ domain="[('responsible_id', '=', uid)]"/>
+ <group string="Group By">
+ <filter name="by_responsible" string="Responsible"
+ context="{'group_by': 'responsible_id'}"/>
+ </group>
</search>
</field>
</record>
@@ -61,6 +67,7 @@
<field name="name">Courses</field>
<field name="res_model">openacademy.course</field>
<field name="view_mode">tree,form</field>
+ <field name="context" eval="{'search_default_my_courses': 1}"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">Create the first course
</p>

View File

@ -1,24 +0,0 @@
# 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 @@
</field>
</record>
+ <record model="ir.ui.view" id="course_search_view">
+ <field name="name">course.search</field>
+ <field name="model">openacademy.course</field>
+ <field name="arch" type="xml">
+ <search>
+ <field name="name"/>
+ <field name="description"/>
+ </search>
+ </field>
+ </record>
+
<!-- window action -->
<!--
The following tag is an action definition for a "window action",

View File

@ -1,57 +0,0 @@
# HG changeset patch
# Parent 22f8d180a7f9ad209d7e98cf7d1bd0fee1f05350
Index: addons/openacademy/models.py
===================================================================
--- addons.orig/openacademy/models.py
+++ addons/openacademy/models.py
@@ -8,3 +8,13 @@
name = fields.Char(string="Title", required=True)
description = fields.Text()
+
+
+class Session(models.Model):
+ _name = 'openacademy.session'
+ _description = "OpenAcademy Sessions"
+
+ name = fields.Char(required=True)
+ start_date = fields.Date()
+ duration = fields.Float(digits=(6, 2), help="Duration in days")
+ seats = fields.Integer(string="Number of seats")
Index: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml
+++ addons/openacademy/views/openacademy.xml
@@ -64,4 +64,32 @@
action="openacademy.course_list_action"
It is not required when it is the same module -->
+ <!-- session form view -->
+ <record model="ir.ui.view" id="session_form_view">
+ <field name="name">session.form</field>
+ <field name="model">openacademy.session</field>
+ <field name="arch" type="xml">
+ <form string="Session Form">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="start_date"/>
+ <field name="duration"/>
+ <field name="seats"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="session_list_action">
+ <field name="name">Sessions</field>
+ <field name="res_model">openacademy.session</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem id="session_menu" name="Sessions"
+ parent="openacademy_menu"
+ action="session_list_action"/>
+
</odoo>

View File

@ -1,56 +0,0 @@
# 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"))

View File

@ -1,25 +0,0 @@
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")

View File

@ -1,29 +0,0 @@
Index: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml
+++ addons/openacademy/views/openacademy.xml
@@ -232,6 +232,12 @@
<field name="session_id"/>
<field name="attendee_ids"/>
</group>
+ <footer>
+ <button name="subscribe" type="object"
+ string="Subscribe" class="oe_highlight"/>
+ or
+ <button special="cancel" string="Cancel"/>
+ </footer>
</form>
</field>
</record>
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 {}

View File

@ -1,44 +0,0 @@
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: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml
+++ addons/openacademy/views/openacademy.xml
@@ -223,4 +223,24 @@
parent="openacademy_menu"
action="session_list_action"/>
+ <record model="ir.ui.view" id="wizard_form_view">
+ <field name="name">wizard.form</field>
+ <field name="model">openacademy.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Add Attendees">
+ <group>
+ <field name="session_id"/>
+ <field name="attendee_ids"/>
+ </group>
+ </form>
+ </field>
+ </record>
+
+ <act_window id="launch_session_wizard"
+ name="Add Attendees"
+ binding_model="openacademy.session"
+ res_model="openacademy.wizard"
+ view_mode="form"
+ target="new"/>
+
</odoo>

View File

@ -1,37 +0,0 @@
Index: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml
+++ addons/openacademy/views/openacademy.xml
@@ -229,7 +229,7 @@
<field name="arch" type="xml">
<form string="Add Attendees">
<group>
- <field name="session_id"/>
+ <field name="session_ids"/>
<field name="attendee_ids"/>
</group>
<footer>
Index: addons/openacademy/wizard.py
===================================================================
--- addons.orig/openacademy/wizard.py
+++ addons/openacademy/wizard.py
@@ -6,13 +6,14 @@ 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'))
+ def _default_sessions(self):
+ return self.env['openacademy.session'].browse(self._context.get('active_ids'))
- session_id = fields.Many2one('openacademy.session',
- string="Session", required=True, default=_default_session)
+ session_ids = fields.Many2many('openacademy.session',
+ string="Sessions", required=True, default=_default_sessions)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
def subscribe(self):
- self.session_id.attendee_ids |= self.attendee_ids
+ for session in self.session_ids:
+ session.attendee_ids |= self.attendee_ids
return {}

View File

@ -1,35 +0,0 @@
exercise-creation
exercise-model
exercise-demo
exercise-basic-action
exercise-formview
exercise-formview-notebooks
exercise-searchview-basic
exercise-session
exercise-many2one
exercise-one2many
exercise-many2many
exercise-model-inheritance
exercise-domain-basic
exercise-domain-advanced
exercise-computed
exercise-defaults
exercise-onchange
exercise-constraint-python
exercise-constraint-sql
exercise-copy-override
exercise-advanced-treeview
exercise-calendar
exercise-searchview
exercise-gantt
exercise-graph
exercise-kanban
exercise-access-rights
exercise-access-rules
exercise-wizard
exercise-wizard-launch
exercise-wizard-action
exercise-wizard-multi
exercise-translations
exercise-report
exercise-dashboard

View File

@ -1,6 +1,3 @@
.. queue:: website/series
==================
Building a Website
==================
@ -31,9 +28,6 @@ This will automatically create a ``my-modules`` *module directory* with an
``academy`` module inside. The directory can be an existing module directory
if you want, but the module name must be unique within the directory.
.. patch::
:hidden:
A demonstration module
======================
@ -63,7 +57,17 @@ send data back.
Add a simple controller and ensure it is imported by ``__init__.py`` (so
Odoo can find it):
.. patch::
.. code-block:: python
:caption: ``academy/controllers.py``
# -*- coding: utf-8 -*-
from odoo import http
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
return "Hello, world"
Shut down your server (:kbd:`^C`) then restart it:
@ -89,7 +93,30 @@ features.
Create a template and ensure the template file is registered in the
``__manifest__.py`` manifest, and alter the controller to use our template:
.. patch::
.. code-block:: python
:caption: ``academy/controllers.py``
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
return http.request.render('academy.index', {
'teachers': ["Diana Padilla", "Jody Caroll", "Lester Vaughn"],
})
.. code-block:: xml
:caption: ``academy/templates.xml``
<odoo>
<template id="index">
<title>Academy</title>
<t t-foreach="teachers" t-as="teacher">
<p><t t-esc="teacher"/></p>
</t>
</template>
</odoo>
The templates iterates (``t-foreach``) on all the teachers (passed through the
*template context*), and prints each teacher in its own paragraph.
@ -126,12 +153,33 @@ Defining the data model
Define a teacher model, and ensure it is imported from ``__init__.py`` so it
is correctly loaded:
.. patch::
.. code-block:: python
:caption: ``academy/models.py``
from odoo import models, fields, api
class Teachers(models.Model):
_name = 'academy.teachers'
name = fields.Char()
Then setup :ref:`basic access control <reference/security/acl>` for the model
and add them to the manifest:
.. patch::
.. code-block:: python
:caption: ``academy/__manifest__.py``
# always loaded
'data': [
'security/ir.model.access.csv',
'templates.xml',
],
.. code-block:: csv
:caption: ``academy/security/ir.model.access.csv``
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
this simply gives read access (``perm_read``) to all users (``group_id:id``
left empty).
@ -154,7 +202,22 @@ The second step is to add some demonstration data to the system so it's
possible to test it easily. This is done by adding a ``demo``
:ref:`data file <reference/data>`, which must be linked from the manifest:
.. patch::
.. code-block:: xml
:caption: ``academy/demo.xml``
<odoo>
<record id="padilla" model="academy.teachers">
<field name="name">Diana Padilla</field>
</record>
<record id="carroll" model="academy.teachers">
<field name="name">Jody Carroll</field>
</record>
<record id="vaughn" model="academy.teachers">
<field name="name">Lester Vaughn</field>
</record>
</odoo>
.. tip::
@ -177,7 +240,31 @@ The last step is to alter model and template to use our demonstration data:
matching the filter ("all records" here), alter the template to print each
teacher's ``name``
.. patch::
.. code-block:: python
:caption: ``academy/controllers.py``
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
Teachers = http.request.env['academy.teachers']
return http.request.render('academy.index', {
'teachers': Teachers.search([])
})
.. code-block:: xml
:caption: ``academy/templates.xml``
<odoo>
<template id="index">
<title>Academy</title>
<t t-foreach="teachers" t-as="teacher">
<p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
</t>
</template>
</odoo>
Restart the server and update the module (in order to update the manifest
and templates and load the demo file) then navigate to
@ -200,7 +287,48 @@ integration and a few other services (e.g. default styling, theming) via the
allows using the website layout in our template
#. use the website layout in the template
.. patch::
.. code-block:: python
:caption: ``academy/__manifest__.py``
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['website'],
# always loaded
'data': [
.. code-block:: python
:caption: ``academy/controllers.py``
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public', website=True)
def index(self, **kw):
Teachers = http.request.env['academy.teachers']
return http.request.render('academy.index', {
'teachers': Teachers.search([])
})
.. code-block:: xml
:caption: ``academy/templates.xml``
<odoo>
<template id="index">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure">
<div class="container">
<t t-foreach="teachers" t-as="teacher">
<p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
</t>
</div>
</div>
</t>
</template>
</odoo>
After restarting the server while updating the module (in order to update the
manifest and template) access http://localhost:8069/academy/academy/ should
@ -238,7 +366,13 @@ but routing strings can also use `converter patterns`_ which match bits
of URLs and make those available as local variables. For instance we can
create a new controller method which takes a bit of URL and prints it out:
.. patch::
.. code-block:: python
:caption: ``academy/controllers.py``
# New route
@http.route('/academy/<name>/', auth='public', website=True)
def teacher(self, name):
return '<h1>{}</h1>'.format(name)
restart Odoo, access http://localhost:8069/academy/Alice/ and
http://localhost:8069/academy/Bob/ and see the difference.
@ -247,7 +381,12 @@ As the name indicates, `converter patterns`_ don't just do extraction, they
also do *validation* and *conversion*, so we can change the new controller
to only accept integers:
.. patch::
.. code-block:: python
:caption: ``academy/controllers.py``
@http.route('/academy/<int:id>/', auth='public', website=True)
def teacher(self, id):
return '<h1>{} ({})</h1>'.format(id, type(id).__name__)
Restart Odoo, access http://localhost:8069/academy/2, note how the old value
was a string, but the new one was converted to an integers. Try accessing
@ -259,11 +398,52 @@ Odoo provides an additional converter called ``model`` which provides records
directly when given their id. Let's use this to create a generic page for
teacher biographies:
.. patch::
.. code-block:: python
:caption: ``academy/controllers.py``
@http.route('/academy/<model("academy.teachers"):teacher>/', auth='public', website=True)
def teacher(self, teacher):
return http.request.render('academy.biography', {
'person': teacher
})
.. code-block:: xml
:caption: ``academy/templates.xml``
<template id="biography">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure"/>
<div class="oe_structure">
<div class="container">
<h3><t t-esc="person.name"/></h3>
</div>
</div>
<div class="oe_structure"/>
</t>
</template>
then change the list of model to link to our new controller:
.. patch::
.. code-block:: xml
:caption: ``academy/templates.xml``
<template id="index">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure">
<div class="container">
<t t-foreach="teachers" t-as="teacher">
<p>
<a t-attf-href="/academy/{{ slug(teacher) }}">
<t t-esc="teacher.name"/></a>
</p>
</t>
</div>
</div>
</t>
</template>
Restart Odoo and upgrade the module, then you can visit each teacher's page.
As an exercise, try adding blocks to a teacher's page to write a biography,
@ -278,7 +458,31 @@ Field editing
Data which is specific to a record should be saved on that record, so let us
add a new biography field to our teachers:
.. patch::
.. code-block:: python
:caption: ``academy/models.py``
class Teachers(models.Model):
_name = 'academy.teachers'
name = fields.Char()
biography = fields.Html()
.. code-block:: xml
:caption: ``academy/templates.xml``
<template id="biography">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure"/>
<div class="oe_structure">
<div class="container">
<h3><t t-esc="person.name"/></h3>
<div><t t-esc="person.biography"/></div>
</div>
</div>
<div class="oe_structure"/>
</t>
</template>
Restart Odoo and update the views, reload the teacher's page and… the field
is invisible since it contains nothing.
@ -290,7 +494,15 @@ For record fields, templates can use a special ``t-field`` directive which
allows editing the field content from the website using field-specific
interfaces. Change the *person* template to use ``t-field``:
.. patch::
.. code-block:: xml
:caption: ``academy/templates.xml``
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<div t-field="person.biography"/>
</div>
</div>
Restart Odoo and upgrade the module, there is now a placeholder under the
teacher's name and a new zone for blocks in :guilabel:`Edit` mode. Content
@ -303,16 +515,43 @@ the index page.
``t-field`` can also take formatting options which depend on the exact field.
For instance if we display the modification date for a teacher's record:
.. patch::
.. code-block:: xml
:caption: ``academy/templates.xml``
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<p>Last modified: <i t-field="person.write_date"/></p>
<div t-field="person.biography"/>
</div>
</div>
it is displayed in a very "computery" manner and hard to read, but we could
ask for a human-readable version:
.. patch::
.. code-block:: xml
:caption: ``academy/templates.xml``
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<p>Last modified: <i t-field="person.write_date" t-options='{"format": "long"}'/></p>
<div t-field="person.biography"/>
</div>
</div>
or a relative display:
.. patch::
.. code-block:: xml
:caption: ``academy/templates.xml``
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<p>Last modified: <i t-field="person.write_date" t-options='{"widget": "relative"}'/></p>
<div t-field="person.biography"/>
</div>
</div>
Administration and ERP integration
==================================
@ -344,7 +583,32 @@ reachable, generally through a menu.
Let's create a menu for our model:
.. patch::
.. code-block:: python
:caption: ``academy/__manifest__.py``
# always loaded
'data': [
'security/ir.model.access.csv',
'templates.xml',
'views.xml',
],
.. code-block:: xml
:caption: ``academy/views.xml``
<odoo>
<record id="action_academy_teachers" model="ir.actions.act_window">
<field name="name">Academy teachers</field>
<field name="res_model">academy.teachers</field>
</record>
<menuitem sequence="0" id="menu_academy" name="Academy"/>
<menuitem id="menu_academy_content" parent="menu_academy"
name="Academy Content"/>
<menuitem id="menu_academy_content_teachers"
parent="menu_academy_content"
action="action_academy_teachers"/>
</odoo>
then accessing http://localhost:8069/web/ in the top left should be a menu
:guilabel:`Academy`, which is selected by default, as it is the first menu,
@ -360,7 +624,21 @@ displayed side-by-side with the ``name`` field and not given enough space.
Let's define a custom form view to make viewing and editing teacher records
a better experience:
.. patch::
.. code-block:: xml
:caption: ``academy/views.xml``
<record id="academy_teacher_form" model="ir.ui.view">
<field name="name">Academy teachers: form</field>
<field name="model">academy.teachers</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="biography"/>
</sheet>
</form>
</field>
</record>
Relations between models
------------------------
@ -375,18 +653,117 @@ For demonstration, let's create a *courses* model. Each course should have a
``teacher`` field, linking to a single teacher record, but each teacher can
teach many courses:
.. patch::
.. code-block:: python
:caption: ``academy/models.py``
class Courses(models.Model):
_name = 'academy.courses'
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
.. code-block:: csv
:caption: ``academy/security/ir.model.access.csv``
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0
let's also add views so we can see and edit a course's teacher:
.. patch::
.. code-block:: xml
:caption: ``academy/views.xml``
<record id="action_academy_courses" model="ir.actions.act_window">
<field name="name">Academy courses</field>
<field name="res_model">academy.courses</field>
</record>
<record id="academy_course_search" model="ir.ui.view">
<field name="name">Academy courses: search</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="teacher_id"/>
</search>
</field>
</record>
<record id="academy_course_list" model="ir.ui.view">
<field name="name">Academy courses: list</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<tree string="Courses">
<field name="name"/>
<field name="teacher_id"/>
</tree>
</field>
</record>
<record id="academy_course_form" model="ir.ui.view">
<field name="name">Academy courses: form</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="teacher_id"/>
</sheet>
</form>
</field>
</record>
<menuitem sequence="0" id="menu_academy" name="Academy"/>
<menuitem id="menu_academy_content" parent="menu_academy"
name="Academy Content"/>
<menuitem id="menu_academy_content_courses"
parent="menu_academy_content"
action="action_academy_courses"/>
<menuitem id="menu_academy_content_teachers"
parent="menu_academy_content"
action="action_academy_teachers"/>
It should also be possible to create new courses directly from a teacher's
page, or to see all the courses they teach, so add
:class:`the inverse relationship <odoo.fields.One2many>` to the *teachers*
model:
.. patch::
.. code-block:: python
:caption: ``academy/models.py``
class Teachers(models.Model):
_name = 'academy.teachers'
name = fields.Char()
biography = fields.Html()
course_ids = fields.One2many('academy.courses', 'teacher_id', string="Courses")
class Courses(models.Model):
_name = 'academy.courses'
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
.. code-block:: xml
:caption: ``academy/views.xml``
<record id="academy_teacher_form" model="ir.ui.view">
<field name="name">Academy teachers: form</field>
<field name="model">academy.teachers</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="biography"/>
<field name="course_ids">
<tree Sstring="Courses" editable="bottom">
<field name="name"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
Discussions and notifications
-----------------------------
@ -404,7 +781,47 @@ the discussion thread. Discussion threads are per-record.
For our academy, it makes sense to allow discussing courses to handle e.g.
scheduling changes or discussions between teachers and assistants:
.. patch::
.. code-block:: python
:caption: ``academy/__manifest__.py``
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['website', 'mail'],
# always loaded
'data': [
.. code-block:: python
:caption: ``academy/models.py``
class Courses(models.Model):
_name = 'academy.courses'
_inherit = 'mail.thread'
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
.. code-block:: xml
:caption: ``academy/views.xml``
<record id="academy_course_form" model="ir.ui.view">
<field name="name">Academy courses: form</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="teacher_id"/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
At the bottom of each course form, there is now a discussion thread and the
possibility for users of the system to leave messages and follow or unfollow
@ -426,7 +843,16 @@ add anything we need to it).
First of all we need to add a dependency on ``website_sale`` so we get both
products (via ``sale``) and the ecommerce interface:
.. patch::
.. code-block:: python
:caption: ``academy/__manifest__.py``
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['mail', 'website_sale'],
# always loaded
'data': [
restart Odoo, update your module, there is now a :guilabel:`Shop` section in
the website, listing a number of pre-filled (via demonstration data) products.
@ -434,7 +860,65 @@ the website, listing a number of pre-filled (via demonstration data) products.
The second step is to replace the *courses* model by ``product.template``,
and add a new category of product for courses:
.. patch::
.. code-block:: python
:caption: ``academy/__manifest__.py``
'security/ir.model.access.csv',
'templates.xml',
'views.xml',
'data.xml',
],
# only loaded in demonstration mode
'demo': [
.. code-block:: xml
:caption: ``academy/data.xml``
<odoo>
<record model="product.public.category" id="category_courses">
<field name="name">Courses</field>
<field name="parent_id" ref="website_sale.categ_others"/>
</record>
</odoo>
.. code-block:: xml
:caption: ``academy/demo.xml``
<record id="course0" model="product.template">
<field name="name">Course 0</field>
<field name="teacher_id" ref="padilla"/>
<field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
<field name="website_published">True</field>
<field name="list_price" type="float">0</field>
<field name="type">service</field>
</record>
<record id="course1" model="product.template">
<field name="name">Course 1</field>
<field name="teacher_id" ref="padilla"/>
<field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
<field name="website_published">True</field>
<field name="list_price" type="float">0</field>
<field name="type">service</field>
</record>
<record id="course2" model="product.template">
<field name="name">Course 2</field>
<field name="teacher_id" ref="vaughn"/>
<field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
<field name="website_published">True</field>
<field name="list_price" type="float">0</field>
<field name="type">service</field>
</record>
.. code-block:: python
:caption: ``academy/models.py``
class Courses(models.Model):
_name = 'academy.courses'
_inherit = ['mail.thread', 'product.template']
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
With this installed, a few courses are now available in the :guilabel:`Shop`,
though they may have to be looked for.
@ -482,7 +966,14 @@ Altering view architectures is done in 3 steps:
#. In the architecture, use the ``xpath`` tag to select and alter elements
from the modified view
.. patch::
.. code-block:: xml
:caption: ``academy/templates.xml``
<template id="product_item_hide_no_price" inherit_id="website_sale.products_item">
<xpath expr="//div[hasclass('product_price')]/b" position="attributes">
<attribute name="t-if">product.price &gt; 0</attribute>
</xpath>
</template>
The second thing we will change is making the product categories sidebar
visible by default: :menuselection:`Customize --> Product Categories` lets
@ -498,7 +989,12 @@ menu with a check box, allowing administrators to activate or disable them
We simply need to modify the *Product Categories* record and set its default
to *active="True"*:
.. patch::
.. code-block:: xml
:caption: ``academy/templates.xml``
<record id="website_sale.products_categories" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
With this, the *Product Categories* sidebar will automatically be enabled when
the *Academy* module is installed.

View File

@ -1,23 +0,0 @@
# HG changeset patch
# Parent 91c0cc5b319e7bf240d359817b8bd044769a4f5c
# Parent 0a9ec16d98785205f25868bc23485569a1444cf3
diff --git a/academy/__manifest__.py b/academy/__manifest__.py
--- a/academy/__manifest__.py
+++ b/academy/__manifest__.py
@@ -24,7 +24,7 @@
# always loaded
'data': [
- # 'security/ir.model.access.csv',
+ 'security/ir.model.access.csv',
'templates.xml',
],
# only loaded in demonstration mode
diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv
--- a/academy/security/ir.model.access.csv
+++ b/academy/security/ir.model.access.csv
@@ -1,2 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_academy_academy,academy.academy,model_academy_academy,,1,0,0,0
+access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0

View File

@ -1,22 +0,0 @@
# HG changeset patch
# Parent 78b4476f35cbef86ee4f858daf843e3b932fb9fa
# Parent 0a4f5e3206a0738201a40d2d1a88f41fdbbc98bf
diff --git a/academy/controllers.py b/academy/controllers.py
--- a/academy/controllers.py
+++ b/academy/controllers.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
from odoo import http
-# class Academy(http.Controller):
-# @http.route('/academy/academy/', auth='public')
-# def index(self, **kw):
-# return "Hello, world"
+class Academy(http.Controller):
+ @http.route('/academy/academy/', auth='public')
+ def index(self, **kw):
+ return "Hello, world"
# @http.route('/academy/academy/objects/', auth='public')
# def list(self, **kw):

View File

@ -1,18 +0,0 @@
# HG changeset patch
# Parent 3e800beb7db0f9f05bfd4db275c86e8903d4f127
# Parent 4082801768b28c2871920e41d23b7879763e763e
diff --git a/academy/models.py b/academy/models.py
--- a/academy/models.py
+++ b/academy/models.py
@@ -2,7 +2,7 @@
from odoo import models, fields, api
-# class academy(models.Model):
-# _name = 'academy.academy'
+class Teachers(models.Model):
+ _name = 'academy.teachers'
-# name = fields.Char()
+ name = fields.Char()

View File

@ -1,23 +0,0 @@
# HG changeset patch
# Parent 4814709fe1c52515f5108623c2c8f0bce57ffac0
# Parent 926b3fdd1bba025d9543bb85ac40159b392ba13c
diff --git a/academy/models.py b/academy/models.py
--- a/academy/models.py
+++ b/academy/models.py
@@ -6,3 +6,4 @@ class Teachers(models.Model):
_name = 'academy.teachers'
name = fields.Char()
+ biography = fields.Html()
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -21,6 +21,7 @@
<div class="oe_structure">
<div class="container">
<h3><t t-esc="person.name"/></h3>
+ <div><t t-esc="person.biography"/></div>
</div>
</div>
<div class="oe_structure"/>

View File

@ -1,18 +0,0 @@
# HG changeset patch
# Parent 216904cc7636679adbf1a5a211070e739c59c3a8
# Parent edeaf1ebff99c9b3d7cbe1925ca3d2ece519ba52
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -20,8 +20,8 @@
<div class="oe_structure"/>
<div class="oe_structure">
<div class="container">
- <h3><t t-esc="person.name"/></h3>
- <div><t t-esc="person.biography"/></div>
+ <h3 t-field="person.name"/>
+ <div t-field="person.biography"/>
</div>
</div>
<div class="oe_structure"/>

View File

@ -1,29 +0,0 @@
# HG changeset patch
# Parent 957395d27f63dfdb2ea0dacdbe72bc35e724ddcc
# Parent 7156498d3cffb6128e73da2e351f2b5af5f7be63
diff --git a/academy/models.py b/academy/models.py
--- a/academy/models.py
+++ b/academy/models.py
@@ -12,6 +12,7 @@ class Teachers(models.Model):
class Courses(models.Model):
_name = 'academy.courses'
+ _inherit = 'mail.thread'
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
diff --git a/academy/views.xml b/academy/views.xml
--- a/academy/views.xml
+++ b/academy/views.xml
@@ -61,6 +61,10 @@
<label for="teacher_id"/>
<field name="teacher_id"/>
</sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
</form>
</field>
</record>

View File

@ -1,44 +0,0 @@
# HG changeset patch
# Parent 122340f4360287bbc53f54e53ac609a8e7237796
# Parent 1f9194514fa81c49b9bc7922a010513d816a0c2e
diff --git a/academy/controllers.py b/academy/controllers.py
--- a/academy/controllers.py
+++ b/academy/controllers.py
@@ -9,9 +9,11 @@ class Academy(http.Controller):
'teachers': Teachers.search([])
})
- @http.route('/academy/<int:id>/', auth='public', website=True)
- def teacher(self, id):
- return '<h1>{} ({})</h1>'.format(id, type(id).__name__)
+ @http.route('/academy/<model("academy.teachers"):teacher>/', auth='public', website=True)
+ def teacher(self, teacher):
+ return http.request.render('academy.biography', {
+ 'person': teacher
+ })
# @http.route('/academy/academy/objects/', auth='public')
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -12,6 +12,18 @@
</div>
</t>
</template>
+ <template id="biography">
+ <t t-call="website.layout">
+ <t t-set="title">Academy</t>
+ <div class="oe_structure"/>
+ <div class="oe_structure">
+ <div class="container">
+ <p><t t-esc="person.id"/> <t t-esc="person.name"/></p>
+ </div>
+ </div>
+ <div class="oe_structure"/>
+ </t>
+ </template>
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->

View File

@ -1,24 +0,0 @@
# HG changeset patch
# Parent c3705f93fcea86a279f2c31204cc0c2914e784d8
# Parent 8f2d190d879cdf6464bd61f7994b8b582befcafd
diff --git a/academy/models.py b/academy/models.py
--- a/academy/models.py
+++ b/academy/models.py
@@ -7,3 +7,9 @@ class Teachers(models.Model):
name = fields.Char()
biography = fields.Html()
+
+class Courses(models.Model):
+ _name = 'academy.courses'
+
+ name = fields.Char()
+ teacher_id = fields.Many2one('academy.teachers', string="Teacher")
diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv
--- a/academy/security/ir.model.access.csv
+++ b/academy/security/ir.model.access.csv
@@ -1,2 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
+access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0

View File

@ -1,21 +0,0 @@
# HG changeset patch
# Parent 1708c59c9731207e06ca84529c839c571e622481
# Parent ac472417df8770ce9b6922968aa86779a86be35c
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -28,6 +28,13 @@
<div class="oe_structure"/>
</t>
</template>
+
+ <template id="product_item_hide_no_price" inherit_id="website_sale.products_item">
+ <xpath expr="//div[hasclass('product_price')]/b" position="attributes">
+ <attribute name="t-if">product.price &gt; 0</attribute>
+ </xpath>
+ </template>
+
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->

View File

@ -1,33 +0,0 @@
# HG changeset patch
# Parent c3619e60d819e746dcaae49a99d7ca1f4c58593c
diff -r c3619e60d819 -r 957395d27f63 academy/models.py
--- a/academy/models.py Wed Aug 13 14:29:45 2014 +0200
+++ b/academy/models.py Wed Aug 13 15:06:30 2014 +0200
@@ -8,6 +8,8 @@ class Teachers(models.Model):
name = fields.Char()
biography = fields.Html()
+ course_ids = fields.One2many('academy.courses', 'teacher_id', string="Courses")
+
class Courses(models.Model):
_name = 'academy.courses'
diff -r c3619e60d819 -r 957395d27f63 academy/views.xml
--- a/academy/views.xml Wed Aug 13 14:29:45 2014 +0200
+++ b/academy/views.xml Wed Aug 13 15:06:30 2014 +0200
@@ -12,8 +12,15 @@
<form>
<sheet>
<label for="name"/> <field name="name"/>
+
<label for="biography"/>
<field name="biography"/>
+
+ <field name="course_ids">
+ <tree string="Courses" editable="bottom">
+ <field name="name"/>
+ </tree>
+ </field>
</sheet>
</form>
</field>

View File

@ -1,142 +0,0 @@
# HG changeset patch
# Parent 2ee7212e5de4702dff08d9b5e4268e7dc261d038
# Parent 4ca7b848bb60c3573de7ed41a28b4b65186a5725
diff --git a/academy/__manifest__.py b/academy/__manifest__.py
--- a/academy/__manifest__.py
+++ b/academy/__manifest__.py
@@ -27,6 +27,7 @@
'security/ir.model.access.csv',
'templates.xml',
'views.xml',
+ 'data.xml',
],
# only loaded in demonstration mode
'demo': [
diff --git a/academy/data.xml b/academy/data.xml
new file mode 100644
--- /dev/null
+++ b/academy/data.xml
@@ -0,0 +1,6 @@
+<odoo>
+ <record model="product.public.category" id="category_courses">
+ <field name="name">Courses</field>
+ <field name="parent_id" ref="website_sale.categ_others"/>
+ </record>
+</odoo>
diff --git a/academy/demo.xml b/academy/demo.xml
--- a/academy/demo.xml
+++ b/academy/demo.xml
@@ -10,4 +10,29 @@
<field name="name">Lester Vaughn</field>
</record>
+ <record id="course0" model="product.template">
+ <field name="name">Course 0</field>
+ <field name="teacher_id" ref="padilla"/>
+ <field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
+ <field name="is_published">True</field>
+ <field name="list_price" type="float">0</field>
+ <field name="type">service</field>
+ </record>
+ <record id="course1" model="product.template">
+ <field name="name">Course 1</field>
+ <field name="teacher_id" ref="padilla"/>
+ <field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
+ <field name="is_published">True</field>
+ <field name="list_price" type="float">0</field>
+ <field name="type">service</field>
+ </record>
+ <record id="course2" model="product.template">
+ <field name="name">Course 2</field>
+ <field name="teacher_id" ref="vaughn"/>
+ <field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
+ <field name="is_published">True</field>
+ <field name="list_price" type="float">0</field>
+ <field name="type">service</field>
+ </record>
+
</odoo>
diff --git a/academy/models.py b/academy/models.py
--- a/academy/models.py
+++ b/academy/models.py
@@ -8,11 +8,9 @@ class Teachers(models.Model):
name = fields.Char()
biography = fields.Html()
- course_ids = fields.One2many('academy.courses', 'teacher_id', string="Courses")
+ course_ids = fields.One2many('product.template', 'teacher_id', string="Courses")
class Courses(models.Model):
- _name = 'academy.courses'
- _inherit = 'mail.thread'
+ _inherit = 'product.template'
- name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv
--- a/academy/security/ir.model.access.csv
+++ b/academy/security/ir.model.access.csv
@@ -1,3 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
-access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0
diff --git a/academy/views.xml b/academy/views.xml
--- a/academy/views.xml
+++ b/academy/views.xml
@@ -26,55 +26,9 @@
</field>
</record>
- <record id="action_academy_courses" model="ir.actions.act_window">
- <field name="name">Academy courses</field>
- <field name="res_model">academy.courses</field>
- </record>
- <record id="academy_course_search" model="ir.ui.view">
- <field name="name">Academy courses: search</field>
- <field name="model">academy.courses</field>
- <field name="arch" type="xml">
- <search>
- <field name="name"/>
- <field name="teacher_id"/>
- </search>
- </field>
- </record>
- <record id="academy_course_list" model="ir.ui.view">
- <field name="name">Academy courses: list</field>
- <field name="model">academy.courses</field>
- <field name="arch" type="xml">
- <tree string="Courses">
- <field name="name"/>
- <field name="teacher_id"/>
- </tree>
- </field>
- </record>
- <record id="academy_course_form" model="ir.ui.view">
- <field name="name">Academy courses: form</field>
- <field name="model">academy.courses</field>
- <field name="arch" type="xml">
- <form>
- <sheet>
- <label for="name"/>
- <field name="name"/>
- <label for="teacher_id"/>
- <field name="teacher_id"/>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
- </form>
- </field>
- </record>
-
<menuitem sequence="0" id="menu_academy" name="Academy"/>
<menuitem id="menu_academy_content" parent="menu_academy"
name="Academy Content"/>
- <menuitem id="menu_academy_content_courses"
- parent="menu_academy_content"
- action="action_academy_courses"/>
<menuitem id="menu_academy_content_teachers"
parent="menu_academy_content"
action="action_academy_teachers"/>

View File

@ -1,18 +0,0 @@
# HG changeset patch
# Parent c706c2ec28328a65a8aee6a7ba7d06896468c1e3
# Parent 053262442acdbe3c803c29ce35002ec69452c5fd
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -35,6 +35,10 @@
</xpath>
</template>
+ <record id="website_sale.products_categories" model="ir.ui.view">
+ <field name="active" eval="True"/>
+ </record>
+
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->

View File

@ -1,57 +0,0 @@
# HG changeset patch
# Parent c687289c105fe855f799a1e5b498382edbd80cb3
diff -r c687289c105f -r 1012f1736bb6 academy/views.xml
--- a/academy/views.xml Thu Aug 14 12:58:50 2014 +0200
+++ b/academy/views.xml Thu Aug 14 13:00:19 2014 +0200
@@ -19,9 +19,51 @@
</field>
</record>
+ <record id="action_academy_courses" model="ir.actions.act_window">
+ <field name="name">Academy courses</field>
+ <field name="res_model">academy.courses</field>
+ </record>
+ <record id="academy_course_search" model="ir.ui.view">
+ <field name="name">Academy courses: search</field>
+ <field name="model">academy.courses</field>
+ <field name="arch" type="xml">
+ <search>
+ <field name="name"/>
+ <field name="teacher_id"/>
+ </search>
+ </field>
+ </record>
+ <record id="academy_course_list" model="ir.ui.view">
+ <field name="name">Academy courses: list</field>
+ <field name="model">academy.courses</field>
+ <field name="arch" type="xml">
+ <tree string="Courses">
+ <field name="name"/>
+ <field name="teacher_id"/>
+ </tree>
+ </field>
+ </record>
+ <record id="academy_course_form" model="ir.ui.view">
+ <field name="name">Academy courses: form</field>
+ <field name="model">academy.courses</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <label for="name"/>
+ <field name="name"/>
+ <label for="teacher_id"/>
+ <field name="teacher_id"/>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
<menuitem sequence="0" id="menu_academy" name="Academy"/>
<menuitem id="menu_academy_content" parent="menu_academy"
name="Academy Content"/>
+ <menuitem id="menu_academy_content_courses"
+ parent="menu_academy_content"
+ action="action_academy_courses"/>
<menuitem id="menu_academy_content_teachers"
parent="menu_academy_content"
action="action_academy_teachers"/>

View File

@ -1,30 +0,0 @@
# HG changeset patch
# Parent f0a3629abb17ca9f4dddd8885321d9ca5de14b55
# Parent 4c1c3d77ba8ad35dc759405a0db39a6eae943b0f
diff --git a/academy/controllers.py b/academy/controllers.py
--- a/academy/controllers.py
+++ b/academy/controllers.py
@@ -4,8 +4,9 @@ from odoo import http
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
+ Teachers = http.request.env['academy.teachers']
return http.request.render('academy.index', {
- 'teachers': ["Diana Padilla", "Jody Caroll", "Lester Vaughn"],
+ 'teachers': Teachers.search([])
})
# @http.route('/academy/academy/objects/', auth='public')
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -3,7 +3,7 @@
<template id="index">
<title>Academy</title>
<t t-foreach="teachers" t-as="teacher">
- <p><t t-esc="teacher"/></p>
+ <p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
</t>
</template>
<!-- <template id="object"> -->

View File

@ -1,42 +0,0 @@
# HG changeset patch
# Parent c77aa2c3341b9ab3a4e9ed05874e8a278fe69453
# Parent f416ab6c60f9cb3a803d30ff4dbff880eaa44a65
diff --git a/academy/demo.xml b/academy/demo.xml
--- a/academy/demo.xml
+++ b/academy/demo.xml
@@ -1,25 +1,13 @@
<odoo>
- <!-- -->
- <!-- <record id="object0" model="academy.academy"> -->
- <!-- <field name="name">Object 0</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object1" model="academy.academy"> -->
- <!-- <field name="name">Object 1</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object2" model="academy.academy"> -->
- <!-- <field name="name">Object 2</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object3" model="academy.academy"> -->
- <!-- <field name="name">Object 3</field> -->
- <!-- </record> -->
- <!-- -->
- <!-- <record id="object4" model="academy.academy"> -->
- <!-- <field name="name">Object 4</field> -->
- <!-- </record> -->
- <!-- -->
+ <record id="padilla" model="academy.teachers">
+ <field name="name">Diana Padilla</field>
+ </record>
+ <record id="carroll" model="academy.teachers">
+ <field name="name">Jody Carroll</field>
+ </record>
+ <record id="vaughn" model="academy.teachers">
+ <field name="name">Lester Vaughn</field>
+ </record>
</odoo>

View File

@ -1,16 +0,0 @@
# HG changeset patch
# Parent f3ad3f919b49bc70c87e1500ec0d39f741243b3d
# Parent 07a68419287382d7dd9d600c8b813c3c37a88864
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -21,7 +21,7 @@
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
- <p>Last modified: <i t-field="person.write_date"/></p>
+ <p>Last modified: <i t-field="person.write_date" t-options='{"format": "long"}'/></p>
<div t-field="person.biography"/>
</div>
</div>

View File

@ -1,15 +0,0 @@
# HG changeset patch
# Parent d5b8a2de3e35c26c29f85ce500d41ccd2373c508
# Parent 65d27383542ef9214ff9e2c308aa54b042cff2d9
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -21,6 +21,7 @@
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
+ <p>Last modified: <i t-field="person.write_date"/></p>
<div t-field="person.biography"/>
</div>
</div>

View File

@ -1,16 +0,0 @@
# HG changeset patch
# Parent c3d04f578b762612422e09777e0ac5b3863d6ea6
# Parent 0bc39fe6579c4e48b61e77bc970da2e1784f1a67
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -21,7 +21,7 @@
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
- <p>Last modified: <i t-field="person.write_date" t-options='{"format": "long"}'/></p>
+ <p>Last modified: <i t-field="person.write_date" t-options='{"widget": "relative"}'/></p>
<div t-field="person.biography"/>
</div>
</div>

View File

@ -1,153 +0,0 @@
# HG changeset patch
# Parent 0000000000000000000000000000000000000000
# Parent 0000000000000000000000000000000000000000
diff --git a/academy/__init__.py b/academy/__init__.py
new file mode 100644
--- /dev/null
+++ b/academy/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import controllers
+import models
diff --git a/academy/__manifest__.py b/academy/__manifest__.py
new file mode 100644
--- /dev/null
+++ b/academy/__manifest__.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Academy",
+
+ 'summary': """
+ Short (1 phrase/line) summary of the module's purpose, used as
+ subtitle on modules listing or apps.odoo.com""",
+
+ 'description': """
+ Long description of module's purpose
+ """,
+
+ 'author': "My Company",
+ 'website': "https://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': 'Uncategorized',
+ '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',
+ ],
+}
diff --git a/academy/controllers.py b/academy/controllers.py
new file mode 100644
--- /dev/null
+++ b/academy/controllers.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from odoo import http
+
+# class Academy(http.Controller):
+# @http.route('/academy/academy/', auth='public')
+# def index(self, **kw):
+# return "Hello, world"
+
+# @http.route('/academy/academy/objects/', auth='public')
+# def list(self, **kw):
+# return http.request.render('academy.listing', {
+# 'root': '/academy/academy',
+# 'objects': http.request.env['academy.academy'].search([]),
+# })
+
+# @http.route('/academy/academy/objects/<model("academy.academy"):obj>/', auth='public')
+# def object(self, obj, **kw):
+# return http.request.render('academy.object', {
+# 'object': obj
+# })
diff --git a/academy/demo.xml b/academy/demo.xml
new file mode 100644
--- /dev/null
+++ b/academy/demo.xml
@@ -0,0 +1,25 @@
+<odoo>
+
+ <!-- -->
+ <!-- <record id="object0" model="academy.academy"> -->
+ <!-- <field name="name">Object 0</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object1" model="academy.academy"> -->
+ <!-- <field name="name">Object 1</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object2" model="academy.academy"> -->
+ <!-- <field name="name">Object 2</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object3" model="academy.academy"> -->
+ <!-- <field name="name">Object 3</field> -->
+ <!-- </record> -->
+ <!-- -->
+ <!-- <record id="object4" model="academy.academy"> -->
+ <!-- <field name="name">Object 4</field> -->
+ <!-- </record> -->
+ <!-- -->
+
+</odoo>
diff --git a/academy/models.py b/academy/models.py
new file mode 100644
--- /dev/null
+++ b/academy/models.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, fields, api
+
+# class academy(models.Model):
+# _name = 'academy.academy'
+
+# name = fields.Char()
diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv
new file mode 100644
--- /dev/null
+++ b/academy/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_academy_academy,academy.academy,model_academy_academy,,1,0,0,0
diff --git a/academy/templates.xml b/academy/templates.xml
new file mode 100644
--- /dev/null
+++ b/academy/templates.xml
@@ -0,0 +1,22 @@
+<odoo>
+
+ <!-- <template id="listing"> -->
+ <!-- <ul> -->
+ <!-- <li t-foreach="objects" t-as="object"> -->
+ <!-- <a t-attf-href="#{ root }/objects/#{ object.id }"> -->
+ <!-- <t t-esc="object.display_name"/> -->
+ <!-- </a> -->
+ <!-- </li> -->
+ <!-- </ul> -->
+ <!-- </template> -->
+ <!-- <template id="object"> -->
+ <!-- <h1><t t-esc="object.display_name"/></h1> -->
+ <!-- <dl> -->
+ <!-- <t t-foreach="object._fields" t-as="field"> -->
+ <!-- <dt><t t-esc="field"/></dt> -->
+ <!-- <dd><t t-esc="object[field]"/></dd> -->
+ <!-- </t> -->
+ <!-- </dl> -->
+ <!-- </template> -->
+
+</odoo>

View File

@ -1,16 +0,0 @@
# HG changeset patch
# Parent 6106208621f01d5aea67727f09adf5c5b2a38642
# Parent ae97cfb61fd0e1a47727a9d8ed4a702116254eff
diff --git a/academy/__manifest__.py b/academy/__manifest__.py
--- a/academy/__manifest__.py
+++ b/academy/__manifest__.py
@@ -20,7 +20,7 @@
'version': '0.1',
# any module necessary for this one to work correctly
- 'depends': ['website'],
+ 'depends': ['website_sale'],
# always loaded
'data': [

View File

@ -1,19 +0,0 @@
# HG changeset patch
# Parent 0795247fa6cbb63238941f3dd26a8b6144fa71ee
# Parent f66ff336f8ed20e022660bd74a43af1b44a1f275
diff --git a/academy/controllers.py b/academy/controllers.py
--- a/academy/controllers.py
+++ b/academy/controllers.py
@@ -9,6 +9,11 @@ class Academy(http.Controller):
'teachers': Teachers.search([])
})
+ @http.route('/academy/<name>/', auth='public', website=True)
+ def teacher(self, name):
+ return '<h1>{}</h1>'.format(name)
+
+
# @http.route('/academy/academy/objects/', auth='public')
# def list(self, **kw):
# return http.request.render('academy.listing', {

View File

@ -1,20 +0,0 @@
# HG changeset patch
# Parent e9d9740fa1852c80541c6b5b94280c8ec74cc4bb
# Parent cf501446e2f8d7a059ef7bd16866ed03c8f2a04f
diff --git a/academy/controllers.py b/academy/controllers.py
--- a/academy/controllers.py
+++ b/academy/controllers.py
@@ -9,9 +9,9 @@ class Academy(http.Controller):
'teachers': Teachers.search([])
})
- @http.route('/academy/<name>/', auth='public', website=True)
- def teacher(self, name):
- return '<h1>{}</h1>'.format(name)
+ @http.route('/academy/<int:id>/', auth='public', website=True)
+ def teacher(self, id):
+ return '<h1>{} ({})</h1>'.format(id, type(id).__name__)
# @http.route('/academy/academy/objects/', auth='public')

View File

@ -1,27 +0,0 @@
module-empty
basic-controller
template
basic-model
basic-acl
demo-data
data-retrieval-orm
website-support
routing-basic
routing-converter
converter-model
teacher-links
biography-esc
biography-field
field-raw
field-format
field-widget
teachers-menu
teachers-formview
course-m2o
course-views
course-o2m
chatter-backend
product-dependency
course-product
course-no-price
course-products-sidebar

View File

@ -1,27 +0,0 @@
# HG changeset patch
# Parent 91ffd360f3ffd44acb605c09753498b3d5f3f210
# Parent 976a92eff9104d355cb86a0b41e883a19e75e923
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -6,7 +6,9 @@
<div class="oe_structure">
<div class="container">
<t t-foreach="teachers" t-as="teacher">
- <p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
+ <p><a t-attf-href="/academy/{{ slug(teacher) }}">
+ <t t-esc="teacher.name"/></a>
+ </p>
</t>
</div>
</div>
@@ -18,7 +20,7 @@
<div class="oe_structure"/>
<div class="oe_structure">
<div class="container">
- <p><t t-esc="person.id"/> <t t-esc="person.name"/></p>
+ <h3><t t-esc="person.name"/></h3>
</div>
</div>
<div class="oe_structure"/>

View File

@ -1,26 +0,0 @@
# HG changeset patch
# Parent 0201053e995dff675aac7499eaa88eaf68bd2882
diff -r 0201053e995d -r c3705f93fcea academy/views.xml
--- a/academy/views.xml Wed Aug 13 11:04:49 2014 +0200
+++ b/academy/views.xml Wed Aug 13 11:04:55 2014 +0200
@@ -5,6 +5,20 @@
<field name="res_model">academy.teachers</field>
</record>
+ <record id="academy_teacher_form" model="ir.ui.view">
+ <field name="name">Academy teachers: form</field>
+ <field name="model">academy.teachers</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <label for="name"/> <field name="name"/>
+ <label for="biography"/>
+ <field name="biography"/>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
<menuitem sequence="0" id="menu_academy" name="Academy"/>
<menuitem id="menu_academy_content" parent="menu_academy"
name="Academy Content"/>

View File

@ -1,35 +0,0 @@
# HG changeset patch
# Parent c8cc9e66a2701dad6589ff1d950e1cb7738d0854
# Parent 44a09b8865d418cda8b7c46dfb70fadc86e22184
diff --git a/academy/__manifest__.py b/academy/__manifest__.py
--- a/academy/__manifest__.py
+++ b/academy/__manifest__.py
@@ -26,6 +26,7 @@
'data': [
'security/ir.model.access.csv',
'templates.xml',
+ 'views.xml',
],
# only loaded in demonstration mode
'demo': [
diff --git a/academy/views.xml b/academy/views.xml
new file mode 100644
--- /dev/null
+++ b/academy/views.xml
@@ -0,0 +1,13 @@
+<odoo>
+
+ <record id="action_academy_teachers" model="ir.actions.act_window">
+ <field name="name">Academy teachers</field>
+ <field name="res_model">academy.teachers</field>
+ </record>
+
+ <menuitem sequence="0" id="menu_academy" name="Academy"/>
+ <menuitem id="menu_academy_content" parent="menu_academy"
+ name="Academy Content"/>
+ <menuitem id="menu_academy_content_teachers"
+ parent="menu_academy_content"
+ action="action_academy_teachers"/>
+
+</odoo>

View File

@ -1,42 +0,0 @@
# HG changeset patch
# Parent d5ad2ad84a5db0668e4e08fefb6e3f628c2e14d1
# Parent e65c9826721cb2421131399bc9223e03b482d38f
diff --git a/academy/controllers.py b/academy/controllers.py
--- a/academy/controllers.py
+++ b/academy/controllers.py
@@ -4,7 +4,9 @@ from odoo import http
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
- return "Hello, world"
+ return http.request.render('academy.index', {
+ 'teachers': ["Diana Padilla", "Jody Caroll", "Lester Vaughn"],
+ })
# @http.route('/academy/academy/objects/', auth='public')
# def list(self, **kw):
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -1,14 +1,11 @@
<odoo>
- <!-- <template id="listing"> -->
- <!-- <ul> -->
- <!-- <li t-foreach="objects" t-as="object"> -->
- <!-- <a t-attf-href="#{ root }/objects/#{ object.id }"> -->
- <!-- <t t-esc="object.display_name"/> -->
- <!-- </a> -->
- <!-- </li> -->
- <!-- </ul> -->
- <!-- </template> -->
+ <template id="index">
+ <title>Academy</title>
+ <t t-foreach="teachers" t-as="teacher">
+ <p><t t-esc="teacher"/></p>
+ </t>
+ </template>
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->

View File

@ -1,50 +0,0 @@
# HG changeset patch
# Parent 7ae0db4c2ddf5d0f7f58db2af8976fcec905cc4e
# Parent 06f959831b14dd5d2e0a57d3e14bf919e18f8e3a
diff --git a/academy/__manifest__.py b/academy/__manifest__.py
--- a/academy/__manifest__.py
+++ b/academy/__manifest__.py
@@ -20,7 +20,7 @@
'version': '0.1',
# any module necessary for this one to work correctly
- 'depends': ['base'],
+ 'depends': ['website'],
# always loaded
'data': [
diff --git a/academy/controllers.py b/academy/controllers.py
--- a/academy/controllers.py
+++ b/academy/controllers.py
@@ -2,7 +2,7 @@
from odoo import http
class Academy(http.Controller):
- @http.route('/academy/academy/', auth='public')
+ @http.route('/academy/academy/', auth='public', website=True)
def index(self, **kw):
Teachers = http.request.env['academy.teachers']
return http.request.render('academy.index', {
diff --git a/academy/templates.xml b/academy/templates.xml
--- a/academy/templates.xml
+++ b/academy/templates.xml
@@ -1,9 +1,15 @@
<odoo>
<template id="index">
- <title>Academy</title>
- <t t-foreach="teachers" t-as="teacher">
- <p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
+ <t t-call="website.layout">
+ <t t-set="title">Academy</t>
+ <div class="oe_structure">
+ <div class="container">
+ <t t-foreach="teachers" t-as="teacher">
+ <p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
+ </t>
+ </div>
+ </div>
</t>
</template>
<!-- <template id="object"> -->

View File

@ -1,40 +1,6 @@
.. _webservices/iap:
.. using sphinx-patchqueue:
* the "queue" directive selects a *series* file which lists the patches in
the patch queue, in order of application (from top to bottom). The
corresponding patch files should be in the same directory.
* the "patch" directive steps to the next patch in the queue, applies it
and reifies its content (depending on the extension's configuration, by
default it shows the changed files post-diff application, slicing to
only display sections affecte by the file)
.. while it's technically possible to apply and update patches by hand, it's
finnicky work and easy to break.
.. the easiest way is to install quilt (http://savannah.nongnu.org/projects/quilt),
go to the directory where you want to reify the addon, then create a
"patches" symlink to the patches directory (the iap/ folder next to this
file) or set QUILT_PATCHES to that folder.
.. at that point you have a "primed" queue with no patch applied, and you can
move within the queue with "quilt push" and "quilt pop".
* "quilt new" creates a new empty patch at the top of the stack
* "quilt add" tells quilt to start tracking the file, quilt add *works per
patch*, it must be called *every time you want to alter a file within a
patch*: quilt is not a full VCS (since it's intended to sit on top of
an existing source) and does not do permanent tracking of files
* "quilt edit" is a shorthand to "quilt add" then open the file in your
editor, I suggest you use that rather than open the edited module
normally, it avoids forgetting to "quilt add" before doing your
modifications (at which point your modifications are untracked,
invisible and depending on your editor may be a PITA to revert & redo)
* "quilt refresh" updates the current patch to include pending changes
.. see "man quilt" for the rest of the subcommands. FWIW I could not get
"quilt setup" to do anything useful.
===============
In-App Purchase
===============
@ -152,8 +118,6 @@ For this example, the service we will provide is ~~mining dogecoins~~ burning
Register the service on Odoo
----------------------------
.. queue:: iap_service/series
.. todo:: complete this part with screenshots
The first step is to register your service on the IAP endpoint (production
@ -258,8 +222,6 @@ A credit pack is essentially a product with five characteristics:
Odoo App
--------
.. queue:: iap/series
.. todo:: does this actually require apps?
The second step is to develop an `Odoo App`_ which clients can install in their
@ -271,13 +233,55 @@ First, we will create an *odoo module* depending on ``iap``. IAP is a standard
V11 module and the dependency ensures a local account is properly set up and
we will have access to some necessary views and useful helpers.
.. patch::
.. code-block:: python
:emphasize-lines: 1-5
:caption: `coalroller/__manifest__.py`
{
'name': "Coal Roller",
'category': 'Tools',
'depends': ['iap'],
}
Second, the "local" side of the integration. Here we will only be adding an
action button to the partners view, but you can of course provide significant
local value via your application and additional parts via a remote service.
.. patch::
.. code-block:: python
:emphasize-lines: 5-7
:caption: `coalroller/__manifest__.py`
{
'name': "Coal Roller",
'category': 'Tools',
'depends': ['iap'],
'data': [
'views/res_partner_views.xml',
],
}
.. code-block:: xml
:emphasize-lines: 1-17
:caption: `coalroller/views/res_partner_views.xml`
<odoo>
<record model="ir.ui.view" id="partner_form_coalroll">
<field name="name">partner.form.coalroll</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']">
<button type="object" name="action_partner_coalroll"
class="oe_stat_button" icon="fa-gears">
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Roll Coal</span>
</div>
</button>
</xpath>
</field>
</record>
</odoo>
.. image:: images/button.png
:align: center
@ -301,7 +305,31 @@ In that call, we will need to provide:
where :class:`service_name <ServiceName>` is the name of the service registered
on IAP endpoint.
.. patch::
.. code-block:: python
:emphasize-lines: 1-21
:caption: `coalroller/models/res_partner.py`
from odoo import api, models
from odoo.addons.iap import jsonrpc, InsufficientCreditError
# whichever URL you deploy the service at, here we will run the remote
# service in a local Odoo bound to the port 8070
DEFAULT_ENDPOINT = 'http://localhost:8070'
class Partner(models.Model):
_inherit = 'res.partner'
def action_partner_coalroll(self):
# fetch the user's token for our service
user_token = self.env['iap.account'].get('coalroller')
params = {
# we don't have any parameter to provide
'account_token': user_token.account_token
}
# ir.config_parameter allows locally overriding the endpoint
# for testing & al
endpoint = self.env['ir.config_parameter'].sudo().get_param('coalroller.endpoint', DEFAULT_ENDPOINT)
jsonrpc(endpoint + '/roll', params=params)
return True
.. note::
@ -325,20 +353,56 @@ In that call, we will need to provide:
Service
-------
.. queue:: iap_service/series
Though that is not *required*, since ``iap`` provides both a client helper
for JSON-RPC2_ calls (:func:`~odoo.addons.iap.jsonrpc`) and a service helper
for transactions (:class:`~odoo.addons.iap.models.iap.charge`) we will also be
implementing the service side as an Odoo module:
.. patch::
.. code-block:: python
:emphasize-lines: 1-5
:caption: `coalroller_service/__manifest__.py`
{
'name': "Coal Roller Service",
'category': 'Tools',
'depends': ['iap'],
}
Since the query from the client comes as JSON-RPC2_ we will need the
corresponding controller which can call :class:`~odoo.addons.iap.models.iap.charge` and
perform the service within:
.. patch::
.. code-block:: python
:emphasize-lines: 1-27
:caption: `coalroller_service/controllers/main.py`
from passlib import pwd, hash
from odoo import http
from odoo.addons.iap import charge
class CoalBurnerController(http.Controller):
@http.route('/roll', type='json', auth='none', csrf='false')
def roll(self, account_token):
# the service key *is a secret*, it should not be committed in
# the source
service_key = http.request.env['ir.config_parameter'].sudo().get_param('coalroller.service_key')
# we charge 1 credit for 10 seconds of CPU
cost = 1
# TODO: allow the user to specify how many (tens of seconds) of CPU they want to use
with charge(http.request.env, service_key, account_token, cost):
# 10 seconds of CPU per credit
end = time.time() (10 * cost)
while time.time() < end:
# we will use CPU doing useful things: generating and
# hashing passphrases
p = pwd.genphrase()
h = hash.pbkdf2_sha512.hash(p)
# here we don't have anything useful to the client, an error
# will be raised & transmitted in case of issue, if no error
# is raised we did the job
.. todo:: for the actual IAP will the "portal" page be on odoo.com or iap.odoo.com?
@ -382,8 +446,66 @@ parameters we can use to make things clearer to the end-user.
service provider is requesting, its purpose is to tell your users why
they should be interested in your IAP offers.
.. patch::
.. code-block:: python
:emphasize-lines: 5-7
:caption: `coalroller_service/__manifest__.py`
{
'name': "Coal Roller Service",
'category': 'Tools',
'depends': ['iap'],
'data': [
'views/no-credit.xml',
],
}
.. code-block:: python
:emphasize-lines: 10-12
:caption: `coalroller_service/controllers/main.py`
@http.route('/roll', type='json', auth='none', csrf='false')
def roll(self, account_token):
# the service key *is a secret*, it should not be committed in
# the source
service_key = http.request.env['ir.config_parameter'].sudo().get_param('coalroller.service_key')
# we charge 1 credit for 10 seconds of CPU
cost = 1
# TODO: allow the user to specify how many (tens of seconds) of CPU they want to use
with charge(http.request.env, service_key, account_token, cost,
description="We're just obeying orders",
credit_template='coalroller_service.no_credit'):
# 10 seconds of CPU per credit
end = time.time() (10 * cost)
while time.time() < end:
# we will use CPU doing useful things: generating and
# hashing passphrases
p = pwd.genphrase()
h = hash.pbkdf2_sha512.hash(p)
.. code-block:: xml
:emphasize-lines: 1-18
:caption: `coalroller_service/views/no-credit.xml`
<odoo>
<template id="no_credit" name="No credit warning">
<div>
<div class="container-fluid">
<div class="row">
<div class="col-md-7 offset-lg-1 mt32 mb32">
<h2>Consume electricity doing nothing useful!</h2>
<ul>
<li>Heat our state of the art data center for no reason</li>
<li>Use multiple watts for only 0.1€</li>
<li>Roll coal without going outside</li>
</ul>
</div>
</div>
</div>
</div>
</template>
</odoo>
.. TODO:: how do you test your service?

View File

@ -1,16 +0,0 @@
Index: coalroller/coalroller/__manifest__.py
===================================================================
--- /dev/null
+++ coalroller/coalroller/__manifest__.py
@@ -0,0 +1,5 @@
+{
+ 'name': "Coal Roller",
+ 'category': 'Tools',
+ 'depends': ['iap'],
+}
Index: coalroller/coalroller/__init__.py
===================================================================
--- /dev/null
+++ coalroller/coalroller/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-

View File

@ -1,34 +0,0 @@
Index: coalroller/coalroller/__manifest__.py
===================================================================
--- coalroller.orig/coalroller/__manifest__.py
+++ coalroller/coalroller/__manifest__.py
@@ -2,4 +2,7 @@
'name': "Coal Roller",
'category': 'Tools',
'depends': ['iap'],
+ 'data': [
+ 'views/views.xml',
+ ],
}
Index: coalroller/coalroller/views/views.xml
===================================================================
--- /dev/null
+++ coalroller/coalroller/views/views.xml
@@ -0,0 +1,17 @@
+<odoo>
+ <record model="ir.ui.view" id="partner_form_coalroll">
+ <field name="name">partner.form.coalroll</field>
+ <field name="model">res.partner</field>
+ <field name="inherit_id" ref="base.view_partner_form" />
+ <field name="arch" type="xml">
+ <xpath expr="//div[@name='button_box']">
+ <button type="object" name="action_partner_coalroll"
+ class="oe_stat_button" icon="fa-gears">
+ <div class="o_form_field o_stat_info">
+ <span class="o_stat_text">Roll Coal</span>
+ </div>
+ </button>
+ </xpath>
+ </field>
+ </record>
+</odoo>

View File

@ -1,39 +0,0 @@
Index: coalroller/coalroller/__init__.py
===================================================================
--- coalroller.orig/coalroller/__init__.py
+++ coalroller/coalroller/__init__.py
@@ -1 +1,2 @@
# -*- coding: utf-8 -*-
+from . import models
Index: coalroller/coalroller/models/__init__.py
===================================================================
--- /dev/null
+++ coalroller/coalroller/models/__init__.py
@@ -0,0 +1 @@
+from . import res_partner
Index: coalroller/coalroller/models/res_partner.py
===================================================================
--- /dev/null
+++ coalroller/coalroller/models/res_partner.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from odoo import api, models
+from odoo.addons.iap import jsonrpc, InsufficientCreditError
+
+# whichever URL you deploy the service at, here we will run the remote
+# service in a local Odoo bound to the port 8070
+DEFAULT_ENDPOINT = 'http://localhost:8070'
+class Partner(models.Model):
+ _inherit = 'res.partner'
+ def action_partner_coalroll(self):
+ # fetch the user's token for our service
+ user_token = self.env['iap.account'].get('coalroller')
+ params = {
+ # we don't have any parameter to provide
+ 'account_token': user_token.account_token
+ }
+ # ir.config_parameter allows locally overriding the endpoint
+ # for testing & al
+ endpoint = self.env['ir.config_parameter'].sudo().get_param('coalroller.endpoint', DEFAULT_ENDPOINT)
+ jsonrpc(endpoint + '/roll', params=params)
+ return True

View File

@ -1,3 +0,0 @@
01-init
02-button
03-callback

View File

@ -1,16 +0,0 @@
Index: coalroller_service/coalroller_service/__init__.py
===================================================================
--- /dev/null
+++ coalroller_service/coalroller_service/__init__.py
@@ -0,0 +1 @@
+# -*- encoding: utf-8 -*-
Index: coalroller_service/coalroller_service/__manifest__.py
===================================================================
--- /dev/null
+++ coalroller_service/coalroller_service/__manifest__.py
@@ -0,0 +1,5 @@
+{
+ 'name': "Coal Roller Service",
+ 'category': 'Tools',
+ 'depends': ['iap'],
+}

View File

@ -1,48 +0,0 @@
Index: coalroller_service/coalroller_service/controllers/main.py
===================================================================
--- /dev/null
+++ coalroller_service/coalroller_service/controllers/main.py
@@ -0,0 +1,29 @@
+import time
+
+from passlib import pwd, hash
+
+from odoo import http
+from odoo.addons.iap import charge
+
+class CoalBurnerController(http.Controller):
+ @http.route('/roll', type='json', auth='none', csrf='false')
+ def roll(self, account_token):
+ # the service key *is a secret*, it should not be committed in
+ # the source
+ service_key = self.env['ir.config_parameter'].sudo().get_param('coalroller.service_key')
+
+ # we charge 1 credit for 10 seconds of CPU
+ cost = 1
+ # TODO: allow the user to specify how many (tens of seconds) of CPU they want to use
+ with charge(http.request.env, service_key, account_token, cost):
+
+ # 10 seconds of CPU per credit
+ end = time.time() + (10 * cost)
+ while time.time() < end:
+ # we will use CPU doing useful things: generating and
+ # hashing passphrases
+ p = pwd.genphrase()
+ h = hash.pbkdf2_sha512.hash(p)
+ # here we don't have anything useful to the client, an error
+ # will be raised & transmitted in case of issue, if no error
+ # is raised we did the job
Index: coalroller_service/coalroller_service/controllers/__init__.py
===================================================================
--- /dev/null
+++ coalroller_service/coalroller_service/controllers/__init__.py
@@ -0,0 +1,2 @@
+# -*- encoding: utf-8 -*-
+from . import main
Index: coalroller_service/coalroller_service/__init__.py
===================================================================
--- coalroller_service.orig/coalroller_service/__init__.py
+++ coalroller_service/coalroller_service/__init__.py
@@ -1 +1,2 @@
# -*- encoding: utf-8 -*-
+from . import controllers

View File

@ -1,56 +0,0 @@
Index: coalroller_service/coalroller_service/controllers/main.py
===================================================================
--- coalroller_service.orig/coalroller_service/controllers/main.py
+++ coalroller_service/coalroller_service/controllers/main.py
@@ -10,12 +10,14 @@ class CoalBurnerController(http.Controll
def roll(self, account_token):
# the service key *is a secret*, it should not be committed in
# the source
- service_key = self.env['ir.config_parameter'].sudo().get_param('coalroller.service_key')
+ service_key = http.request.env['ir.config_parameter'].sudo().get_param('coalroller.service_key')
# we charge 1 credit for 10 seconds of CPU
cost = 1
# TODO: allow the user to specify how many (tens of seconds) of CPU they want to use
- with charge(http.request.env, service_key, account_token, cost):
+ with charge(http.request.env, service_key, account_token, cost,
+ description="We're just obeying orders",
+ credit_template='coalroller_service.no_credit'):
# 10 seconds of CPU per credit
end = time.time() + (10 * cost)
Index: coalroller_service/coalroller_service/views/no-credit.xml
===================================================================
--- /dev/null
+++ coalroller_service/coalroller_service/views/no-credit.xml
@@ -0,0 +1,18 @@
+<odoo>
+ <template id="no_credit" name="No credit warning">
+ <div>
+ <div class="container-fluid">
+ <div class="row">
+ <div class="col-md-7 offset-lg-1 mt32 mb32">
+ <h2>Consume electricity doing nothing useful!</h2>
+ <ul>
+ <li>Heat our state of the art data center for no reason</li>
+ <li>Use multiple watts for only 0.1€</li>
+ <li>Roll coal without going outside</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+</odoo>
Index: coalroller_service/coalroller_service/__manifest__.py
===================================================================
--- coalroller_service.orig/coalroller_service/__manifest__.py
+++ coalroller_service/coalroller_service/__manifest__.py
@@ -2,4 +2,7 @@
'name': "Coal Roller Service",
'category': 'Tools',
'depends': ['iap'],
+ 'data': [
+ 'views/no-credit.xml',
+ ],
}

View File

@ -1,3 +0,0 @@
01-init
02-tnx
03-template

View File

@ -31,7 +31,7 @@ Terms and Conditions
.. |download_enterprise_en| image:: legal/img/pdf.svg
:alt: Download Odoo Enterprise Agreement
:target: odoo_enterprise_agreement.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_enterprise_agreement.pdf
.. |view_partnership_en| image:: legal/img/txt.svg
:alt: View Odoo Partnership Agreement
@ -39,7 +39,7 @@ Terms and Conditions
.. |download_partnership_en| image:: legal/img/pdf.svg
:alt: Download Odoo Partnership Agreement
:target: odoo_partnership_agreement.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_partnership_agreement.pdf
.. |view_terms_of_sale_en| image:: legal/img/txt.svg
:alt: View Terms of Sale
@ -51,7 +51,7 @@ Terms and Conditions
.. |download_enterprise_fr| image:: legal/img/pdf.svg
:alt: Download Odoo Enterprise Agreement (FR)
:target: odoo_enterprise_agreement_fr.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_enterprise_agreement_fr.pdf
.. |view_enterprise_fr| image:: legal/img/txt.svg
:alt: View Odoo Enterprise Agreement (FR)
@ -59,7 +59,7 @@ Terms and Conditions
.. |download_partnership_fr| image:: legal/img/pdf.svg
:alt: Download Odoo Partnership Agreement (FR)
:target: odoo_partnership_agreement_fr.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_partnership_agreement_fr.pdf
.. |view_enterprise_nl| image:: legal/img/txt.svg
:alt: View Odoo Enterprise Agreement (NL)
@ -71,7 +71,7 @@ Terms and Conditions
.. |download_terms_of_sale_fr| image:: legal/img/pdf.svg
:alt: Download Odoo Terms of Sale (FR)
:target: terms_of_sale_fr.pdf
:target: https://www.odoo.com/documentation/12.0/terms_of_sale_fr.pdf
.. |view_terms_of_sale_fr| image:: legal/img/txt.svg
:alt: View Terms of Sale (FR)
@ -79,7 +79,7 @@ Terms and Conditions
.. |download_enterprise_nl| image:: legal/img/pdf.svg
:alt: Download Odoo Enterprise Agreement (NL)
:target: odoo_enterprise_agreement_nl.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_enterprise_agreement_nl.pdf
.. |download_partnership_nl| image:: legal/img/pdf.svg
:alt: Download Odoo Partnership Agreement (NL)
@ -87,7 +87,7 @@ Terms and Conditions
.. |download_enterprise_de| image:: legal/img/pdf.svg
:alt: Download Odoo Enterprise Agreement (DE)
:target: odoo_enterprise_agreement_de.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_enterprise_agreement_de.pdf
.. |view_enterprise_de| image:: legal/img/txt.svg
:alt: View Odoo Enterprise Agreement (DE)
@ -95,7 +95,7 @@ Terms and Conditions
.. |download_partnership_de| image:: legal/img/pdf.svg
:alt: Download Odoo Partnership Agreement (DE)
:target: odoo_partnership_agreement_de.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_partnership_agreement_de.pdf
.. |view_partnership_de| image:: legal/img/txt.svg
:alt: View Odoo Partnership Agreement (DE)
@ -103,7 +103,7 @@ Terms and Conditions
.. |download_enterprise_es| image:: legal/img/pdf.svg
:alt: Download Odoo Enterprise Agreement (ES)
:target: odoo_enterprise_agreement_es.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_enterprise_agreement_es.pdf
.. |view_enterprise_es| image:: legal/img/txt.svg
:alt: View Odoo Partnership Agreement (ES)
@ -111,7 +111,7 @@ Terms and Conditions
.. |download_partnership_es| image:: legal/img/pdf.svg
:alt: Download Odoo Partnership Agreement (ES)
:target: odoo_partnership_agreement_es.pdf
:target: https://www.odoo.com/documentation/12.0/odoo_partnership_agreement_es.pdf
.. |view_partnership_es| image:: legal/img/txt.svg
:alt: View Odoo Partnership Agreement (ES)

View File

@ -402,7 +402,6 @@ header.o_main_header{
}
}
p, h5 {
color: $gray;
}
@ -451,30 +450,9 @@ header.o_main_header{
}
}
// "in between" pages (Applications / Contributing)
// pages with full width: Legal
&.o_fullwidth_page {
.toctree-wrapper {
display: none;
/* margin-top: 1.5rem;
> ul {
display: flex;
flex-wrap: wrap;
> li.toctree-l1 {
@include media-breakpoint-up(lg) {
width: 33.3%;
}
padding-bottom: 3rem;
padding-right: 3rem;
}
}
ul {
list-style: none;
padding-left:0;
} */
}
.toctree-l1 > a {
display: block;
color: $gray-darker;
@ -485,6 +463,8 @@ header.o_main_header{
}
}
// pages with column for code on the right
&.o_has_code_column {
article.doc-body {
@ -569,6 +549,11 @@ header.o_main_header{
article.doc-body {
position: relative;
// hide ugly toctree on "in between" pages (e.g.: /applications.html, /administration.html, ...)
.toctree-wrapper {
display: none;
}
.o_git_link {
@include font-size($font-size-secondary);
@include o-position-absolute($top: 0px, $right: 1rem);

View File

@ -4,4 +4,3 @@ pygments-csv-lexer~=0.1
pysass~=0.1.0
sphinx~=3.0
werkzeug==0.14.1
sphinx-patchqueue>=1.0