diff --git a/.gitignore b/.gitignore
index b6e47617de1..966147abab3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# mac os garbage
+.DS_Store
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000000..277f7c9898f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.languageServer": "None"
+}
diff --git a/1_learn_owl.md b/1_learn_owl.md
new file mode 100644
index 00000000000..9354066240a
--- /dev/null
+++ b/1_learn_owl.md
@@ -0,0 +1,560 @@
+# Module 1: Learn Owl 🦉
+
+This chapter introduces the Owl framework, a tailor-made component system for
+Odoo. The main building blocks of Owl are components and templates. In Owl,
+every part of the user interface is managed by a component: they hold the logic
+and define the templates that are used to render the user interface. In
+practice, a component is represented by a small JavaScript class subclassing the
+Component class.
+
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+
+Start your development environment with a new database, on the `master` branch,
+and make sure to add this repository in the addons path. Then, install the
+`awesome_owl` addon. Once it is done, you can open the `/awesome_owl` route
+(typically on `localhost:8069/awesome_owl`). If you see the `hello world`
+message, you are ready to start!
+
+The `awesome_owl addon` provides a simplified environment that only contains Owl
+and a few other files. The goal is to learn Owl itself, without relying on Odoo
+web client code.
+
+## Content
+
+- [Resources](#resources)
+- [Example: a Counter component](#example-a-counter-component)
+- [1. Displaying a Counter](#1-displaying-a-counter)
+- [2. Extract Counter in a sub component](#2-extract-counter-in-a-sub-component)
+- [3. A simple Card component](#3-a-simple-card-component)
+- [4. Using markup to display html](#4-using-markup-to-display-html)
+- [5. Props validation](#5-props-validation)
+- [6. The sum of two Counter](#6-the-sum-of-two-counter)
+- [6B. Bonus Project](#6b-bonus-project)
+- [7. A todo list](#7-a-todo-list)
+- [8. Use dynamic attributes](#8-use-dynamic-attributes)
+- [9. Adding a todo](#9-adding-a-todo)
+- [10. Focusing the input](#10-focusing-the-input)
+- [11. Toggling todos](#11-toggling-todos)
+- [12. Deleting todos](#12-deleting-todos)
+- [13. Improved state management](#13-improved-state-management)
+- [13B. Bonus Project: Todo class](#13b-bonus-project-todo-class)
+- [14. Generic Card with slots](#14-generic-card-with-slots)
+- [15. Minimizing card content](#15-minimizing-card-content)
+
+## Resources
+
+- [Owl repository](https://github.com/odoo/owl)
+- [Owl documentation](https://github.com/odoo/owl#documentation)
+
+## Example: a Counter component
+
+First, let us have a look at a simple example. The Counter component shown below
+is a component that maintains an internal number value, displays it, and updates
+it whenever the user clicks on the button.
+
+```js
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "my_module.Counter";
+
+ setup() {
+ this.state = useState({ value: 0 });
+ }
+
+ increment() {
+ this.state.value++;
+ }
+}
+```
+
+The Counter component specifies the name of a template that represents its html.
+It is written in XML using the QWeb language:
+
+```xml
+
+
+
Counter:
+
+
+
+```
+
+## 1. Displaying a counter
+
+
+
+As a first exercise, let us modify the `Playground` component located in
+`awesome_owl/static/src/` to turn it into a counter. To see the result, you can
+go to the `/awesome_owl` route with your browser.
+
+1. Modify `playground.js` so that it acts as a counter like in the example
+ above. Keep `Playground` for the class name. You will need to use the
+ `useState` hook so that the component is updated whenever the button is
+ clicked.
+2. In the same component, create an increment method.
+3. Modify the template in `playground.xml` so that it displays your counter
+ variable. Use
+ [`t-esc`](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#outputting-data)
+ to output the data.
+4. Add a button in the template and specify a
+ [`t-on-click`](https://github.com/odoo/owl/blob/master/doc/reference/event_handling.md#event-handling)
+ attribute in the button to trigger the increment method whenever the button
+ is clicked.
+
+This exercise showcases an important feature of Owl: the reactivity system. The
+`useState` function wraps a value in a proxy so Owl can keep track of which
+component needs which part of the state, so it can be updated whenever a value
+has been changed. Try removing the `useState` function and see what happens.
+
+**Tip:** the Odoo JavaScript files downloaded by the browser are minified. For
+debugging purpose, it’s easier when the files are not minified. Switch to debug
+mode with assets so that the files are not minified.
+
+## 2. Extract Counter in a sub component
+
+For now we have the logic of a counter in the `Playground` component, but it is
+not reusable. Let us see how to create a
+[sub-component](https://github.com/odoo/owl/blob/master/doc/reference/component.md#sub-components)
+from it:
+
+1. Extract the counter code from the `Playground` component into a new `Counter`
+ component.
+2. You can do it in the same file first, but once it’s done, update your code to
+ move the `Counter` in its own folder and file. Import it relatively from
+ ./counter/counter.
+3. Make sure the template is in its own file, with the same name.
+4. Use in the template of the Playground component to add two
+ counters in your playground.
+
+
+
+**Tip:** by convention, most components code, template and css should have the
+same snake-cased name as the component. For example, if we have a `TodoList`
+component, its code should be in `todo_list.js`, `todo_list.xml` and if
+necessary, `todo_list.scss`
+
+## 3. A simple Card component
+
+Components are really the most natural way to divide a complicated user
+interface into multiple reusable pieces. But to make them truly useful, it is
+necessary to be able to communicate some information between them. Let us see
+how a parent component can provide information to a sub component by using
+attributes (most commonly known as
+[props](https://github.com/odoo/owl/blob/master/doc/reference/props.md)).
+
+The goal of this exercise is to create a `Card` component, that takes two props:
+`title` and `content`. For example, here is how it could be used:
+
+```xml
+
+```
+
+The above example should produce some html using bootstrap that look like this:
+
+```html
+
+
+
my title
+
some content
+
+
+```
+
+1. Create a `Card` component
+2. Import it in `Playground` and display a few cards in its template
+
+
+
+## 4. Using markup to display html
+
+If you used `t-esc` in the previous exercise, then you may have noticed that Owl
+automatically escapes its content. For example, if you try to display some html
+like this: `` with
+`this.html = "
some content
""`, the resulting output will simply
+display the html as a string.
+
+In this case, since the `Card` component may be used to display any kind of
+content, it makes sense to allow the user to display some html. This is done
+with the
+[`t-out`](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#outputting-data)
+directive.
+
+However, displaying arbitrary content as html is dangerous, it could be used to
+inject malicious code, so by default, Owl will always escape a string unless it
+has been explicitely marked as safe with the `markup` function.
+
+1. Update `Card` to use `t-out`,
+2. Update `Playground` to import `markup`, and use it on some html values
+3. Make sure that you see that normal strings are always escaped, unlike
+ markuped strings.
+
+
+
+**Note:** the `t-esc` directive can still be used in Owl templates. It is
+slightly faster than `t-out`.
+
+## 5. Props validation
+
+The `Card` component has an implicit API. It expects to receive two strings in
+its props object: the `title` and the `content`. Let us make that API more
+explicit. We can add a props definition that will let Owl perform a validation
+step in
+[dev mode](https://github.com/odoo/owl/blob/master/doc/reference/app.md#dev-mode).
+You can activate the dev mode in the
+[App configuration](https://github.com/odoo/owl/blob/master/doc/reference/app.md#configuration)
+(but it is activated by default on the `awesome_owl` playground).
+
+It is a good practice to do props validation for every component.
+
+1. Add
+ [props validation](https://github.com/odoo/owl/blob/master/doc/reference/props.md#props-validation)
+ to the Card component.
+2. Rename the `title` props into something else in the playground template, then
+ check in the Console tab of your browser’s dev tools that you can see an
+ error.
+
+## 6. The sum of two Counter
+
+We saw in a previous exercise that `props` can be used to provide information
+from a parent to a child component. Now, let us see how we can communicate
+information in the opposite direction: in this exercise, we want to display two
+`Counter` components, and below them, the sum of their values. So, the parent
+component (`Playground`) need to be informed whenever one of the `Counter` value
+is changed.
+
+This can be done by using a
+[callback prop](https://github.com/odoo/owl/blob/master/doc/reference/props.md#binding-function-props):
+a prop that is a function meant to be called back. The child component can
+choose to call that function with any argument. In our case, we will simply add
+an optional `onChange` prop that will be called whenever the `Counter` component
+is incremented.
+
+1. Add prop validation to the `Counter` component: it should accept an optional
+ `onChange` function prop.
+2. Update the `Counter` component to call the `onChange` prop (if it exists)
+ whenever it is incremented.
+3. Modify the `Playground` component to maintain a local state value (`sum`),
+ initially set to 2, and display it in its template
+4. Implement an `incrementSum` method in `Playground`
+5. Give that method as a prop to two (or more!) sub `Counter` components.
+
+
+
+**Important:** there is a subtlety with callback props: they usually should be
+defined with the .bind suffix. See the
+[documentation](https://github.com/odoo/owl/blob/master/doc/reference/props.md#binding-function-props).
+
+## 6B. Bonus Project
+
+The code for the previous exercise is designed from a pedagogical perspective,
+but the design is actually somewhat strange/fragile. This is because we are in a
+situation where a parent component need to compute some value that are actually
+owned by its children. So, we end up with a fragile design, where we use events
+to coordinate components.
+
+A better solution would be to reorganize the code so that the playground hold a
+list of values, and give them to each `Counter`.
+
+1. Move the state from each `Counter` component to the `Playground` component
+2. Use a getter to define the sum of each value as a derived state
+
+## 7. A todo list
+
+Let us now discover various features of Owl by creating a todo list. We need two
+components: a `TodoList` component that will display a list of `TodoItem`
+components. The list of todos is a state that should be maintained by the
+`TodoList`.
+
+For this tutorial, a `todo` is an object that contains three values:
+
+- an `id` (number),
+- a `description` (string),
+- and a flag `isCompleted` (boolean):
+
+```js
+{ id: 3, description: "buy milk", isCompleted: false }
+```
+
+1. Create a `TodoList` and a `TodoItem` components.
+2. The `TodoItem` component should receive a `todo` as a prop, and display its
+ `id` and `description` in a `div`.
+3. For now, hardcode the list of todos:
+
+ ```js
+ // in TodoList
+ this.todos = useState([
+ { id: 3, description: "buy milk", isCompleted: false },
+ ]);
+ ```
+
+4. Use
+ [t-foreach](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#loops)
+ to display each todo in a `TodoItem`.
+5. Display a `TodoList` in the playground.
+6. Add props validation to `TodoItem`.
+
+
+
+**Tip:** since the `TodoList` and `TodoItem` components are so tightly coupled,
+it makes sense to put them in the same folder.
+
+**Note:** the `t-foreach` directive is not exactly the same in Owl as the QWeb
+python implementation: it requires a `t-key` unique value, so that Owl can
+properly reconcile each element.
+
+## 8. Use dynamic attributes
+
+For now, the `TodoItem` component does not visually show if the todo is
+completed. Let us do that by using a
+[dynamic attributes](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#dynamic-attributes).
+
+1. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on
+ the `TodoItem` root element if it is completed.
+2. Change the hardcoded `this.todos` value to check that it is properly
+ displayed.
+
+Even though the directive is named `t-att` (for attribute), it can be used to
+set a class value (and other html properties such as the value of an input).
+
+
+
+**Tip:** Owl let you combine static class values with dynamic values. The
+following example will work as expected:
+
+```xml
+
+```
+
+See also:
+[Owl: Dynamic class attributes](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#dynamic-class-attribute)
+
+## 9. Adding a todo
+
+So far, the todos in our list are hard-coded. Let us make it more useful by
+allowing the user to add a todo to the list.
+
+
+
+1. Remove the hardcoded values in the `TodoList` component:
+
+```js
+this.todos = useState([]);
+```
+
+2. Add an input above the task list with placeholder _Enter a new task_.
+3. Add an
+ [event handler](https://github.com/odoo/owl/blob/master/doc/reference/event_handling.md)
+ on the keyup event named addTodo.
+4. Implement `addTodo` to check if enter was pressed (`ev.keyCode === 13`), and
+ in that case, create a new todo with the current content of the input as the
+ description and clear the input of all content.
+5. Make sure the todo has a unique id. It can be just a counter that increments
+ at each todo.
+6. Bonus point: don’t do anything if the input is empty.
+
+See also:
+[Owl reactivity](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md)
+
+## 10. Focusing the input
+
+Let’s see how we can access the DOM with
+[t-ref](https://github.com/odoo/owl/blob/master/doc/reference/refs.md) and
+[useRef](https://github.com/odoo/owl/blob/master/doc/reference/refs.md). The
+main idea is that you need to mark the target element in the component template
+with a `t-ref`:
+
+```xml
+
hello
+```
+
+Then you can access it in the JS with the
+[useRef](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useref)
+hook. However, there is a problem if you think about it: the actual html element
+for a component does not exist when the component is created. It only exists
+when the component is mounted. But hooks have to be called in the setup method.
+So, `useRef` returns an object that contains a `el` (for element) key that is
+only defined when the component is mounted.
+
+```js
+setup() {
+ this.myRef = useRef('some_name');
+ onMounted(() => {
+ console.log(this.myRef.el);
+ });
+}
+```
+
+1. Focus the `input` from the previous exercise. This should be done from the
+ `TodoList` component (note that there is a `focus` method on the input html
+ element).
+2. Bonus point: extract the code into a specialized hook `useAutofocus` in a new
+ `utils.js` file.
+
+
+
+**Tip:** Refs are usually suffixed by `Ref` to make it obvious that they are
+special objects:
+
+```js
+this.inputRef = useRef("input");
+```
+
+## 11. Toggling todos
+
+Now, let’s add a new feature: mark a todo as completed. This is actually
+trickier than one might think. The owner of the state is not the same as the
+component that displays it. So, the `TodoItem` component needs to communicate to
+its parent that the todo state needs to be toggled. One classic way to do this
+is by adding a
+[callback prop](https://github.com/odoo/owl/blob/master/doc/reference/props.md#binding-function-props)
+`toggleState`.
+
+1. Add an input with the attribute `type="checkbox"` before the id of the task,
+ which must be checked if the state `isCompleted` is true.
+2. Add a callback props `toggleState` to `TodoItem`.
+3. Add a `change` event handler on the input in the `TodoItem` component and
+ make sure it calls the `toggleState` function with the todo `id`.
+4. Make it work!
+
+
+
+**Tip:** Owl does not create attributes computed with the `t-att` directive if
+its expression evaluates to a falsy value.
+
+## 12. Deleting todos
+
+The final touch is to let the user delete a todo.
+
+1. Add a new callback prop `removeTodo` in `TodoItem`.
+2. Insert `` in the template of the
+ `TodoItem` component.
+3. Whenever the user clicks on it, it should call the `removeTodo` method.
+4. Make it work!
+
+
+
+**Tip:** If you’re using an array to store your todo list, you can use the
+JavaScript `splice` function to remove a todo from it.
+
+```js
+// find the index of the element to delete
+const index = list.findIndex((elem) => elem.id === elemId);
+if (index >= 0) {
+ // remove the element at index from list
+ list.splice(index, 1);
+}
+```
+
+## 13. Improved state management
+
+Note: this exercise (and the next one) is more advanced. Feel free to skip it.
+
+So far, the `TodoList` has a simple architecture: a parent component `TodoList`
+that holds the state and the update methods (add/remove/toggle), and a child
+component `TodoItem`. This is fine for most situations, but at some point, if we
+expect more and more complex features to be implemented, it is useful to
+separate the _state management_ (or _model_) code from the UI code.
+
+1. Define a `TodoModel` class. It should have a list of todos, and four methods:
+ `getTodo`, `add`, `remove`, `toggle`.
+2. Move all state related code from `TodoList` to `TodoModel`
+3. Modify `TodoList` to instantiate a `TodoModel` in a `useState`:
+
+ ```js
+ // useState is here to make the model reactive
+ this.model = useState(new TodoModel());
+ ```
+
+4. Update `TodoItem` to take 2 props: `model` and `id`
+5. Make it work!
+
+Congratulation, your state management code is now separate from your UI code!
+
+**Tip:** it is often useful to use a `t-set` directive in a template to compute
+once an important value. For example, in the template for `TodoItem`, we can do
+this:
+
+```xml
+
+```
+
+## 13B. Bonus Project: Todo class
+
+The previous exercise successfully refactored the code to make it easier to
+maintain. However, there is still something quite awkward: the `TodoItem`
+receive two props, the model and the id for the todo. It is necessary to allow
+the `TodoItem` to toggle and to remove the todo.
+
+It would be nicer if we could package the state and its update function in one
+convenient object. That's what OOP is for! It turns out that using simple
+classes work well with Owl and the reactivity system (as long as you stay away
+from private fields).
+
+1. In `todo_model.js`, define the following `Todo` class:
+
+ ```js
+ export class Todo {
+ static nextId = 1;
+
+ constructor(model, description) {
+ this._model = model;
+ this.id = Todo.nextId++;
+ this.description = description;
+ this.isCompleted = false;
+ }
+
+ toggle() {
+ this.isCompleted = !this.isCompleted;
+ }
+
+ remove() {
+ this._model.remove(this.id);
+ }
+ }
+ ```
+
+2. Adapt `TodoModel` to use it
+3. Remove `toggle` method from `TodoModel` (no longer necessary)
+4. adapt `TodoItem` component to only receive a `Todo` instance as props
+5. make it work!
+
+This is a very useful pattern when working with complicated objects.
+
+## 14. Generic Card with slots
+
+In a previous exercise, we built a simple `Card` component. But it is honestly
+quite limited. What if we want to display some arbitrary content inside a card,
+such as a sub-component? Well, it does not work, since the content of the card
+is described by a string. It would however be very convenient if we could
+describe the content as a piece of template.
+
+This is exactly what Owl’s
+[slot](https://github.com/odoo/owl/blob/master/doc/reference/slots.md) system is
+designed for: allowing to write generic components.
+
+Let us modify the `Card` component to use slots:
+
+1. Remove the `content` prop.
+2. Use the default slot to define the body.
+3. Insert a few cards with arbitrary content, such as a `Counter` component.
+4. (bonus) Add prop validation.
+
+
+
+**See also:**
+[Bootstrap: documentation on cards](https://getbootstrap.com/docs/5.2/components/card/)
+
+## 15. Minimizing card content
+
+Finally, let’s add a feature to the `Card` component, to make it more
+interesting: we want a button to toggle its content (show it or hide it).
+
+1. Add a state to the `Card` component to track if it is open (the default) or
+ not
+2. Add a `t-if` in the template to conditionally render the content
+3. Add a button in the header, and modify the code to flip the state when the
+ button is clicked
+
+
diff --git a/2_make_a_dashboard.md b/2_make_a_dashboard.md
new file mode 100644
index 00000000000..b6650ba8655
--- /dev/null
+++ b/2_make_a_dashboard.md
@@ -0,0 +1,400 @@
+# Module 2: Make a Dashboard
+
+It is now time to learn about the Odoo JavaScript framework in its entirety, as
+used by the web client. This document is a complete standalone project, in which
+we will implement a dashboard client action. It is mostly an excuse to discover
+and practice many features of the odoo web framework!
+
+
+
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+Start your odoo server with this repository in the addons path, then install the
+`awesome_dashboard` addon.
+
+For this project, we will start from the empty dashboard provided by the
+`awesome_dashboard` addon. Then, we will progressively add features to it using
+the Odoo JavaScript framework.
+
+Note that a lot of theory is covered in the slides for this event. Also, don't
+hesitate to ask questions!
+
+## Content
+
+- [1. A Common Layout](#1-a-common-layout)
+- [2. Add some buttons for quick navigation](#2-add-some-buttons-for-quick-navigation)
+- [3. Add a dashboard item](#3-add-a-dashboard-item)
+- [4. Call the server, add some statistics](#4-call-the-server-add-some-statistics)
+- [5. Cache network calls, create a service](#5-cache-network-calls-create-a-service)
+- [6. Display a pie chart](#6-display-a-pie-chart)
+- [7. Periodic Updates](#7-periodic-updates)
+- [8. Lazy loading the dashboard](#8-lazy-loading-the-dashboard)
+- [9. Making our dashboard generic](#9-making-our-dashboard-generic)
+- [10. Making our dashboard extensible](#10-making-our-dashboard-extensible)
+- [11. Add and remove dashboard items](#11-add-and-remove-dashboard-items)
+- [12. Going further](#12-going-further)
+
+## 1. A Common Layout
+
+Most screens in the Odoo web client uses a common layout: a control panel on
+top, with some buttons, and a main content zone just below. This is done using
+the `Layout` component, available in `@web/search/layout`.
+
+1. Update the `AwesomeDashboard` component located in
+ `awesome_dashboard/static/src/` to use the `Layout` component. You can use
+ `{controlPanel: {} }` for the display props of the `Layout` component.
+2. Add a className prop to `Layout`: `className="'o_dashboard h-100'"`
+3. Add a `dashboard.scss` file in which you set the `background-color` of
+ .`o_dashboard` to gray (or your favorite color)
+4. Open http://localhost:8069/odoo, then open the Awesome Dashboard app, and see
+ the result.
+
+
+
+#### See also
+
+- Example:
+ [use of Layout in client action](https://github.com/odoo/odoo/blob/master/addons/web/static/src/webclient/actions/reports/report_action.js)
+ and
+ [template](https://github.com/odoo/odoo/blob/master/addons/web/static/src/webclient/actions/reports/report_action.xml)
+- Example:
+ [use of Layout in kanban views](https://github.com/odoo/odoo/blob/master/addons/web/static/src/views/kanban/kanban_controller.xml)
+
+## 2. Add some buttons for quick navigation
+
+One important service provided by Odoo is the `action` service: it can execute
+all kind of standard actions defined by Odoo. For example, here is how one
+component could execute an action by its xml id:
+
+```js
+import { useService } from "@web/core/utils/hooks";
+...
+setup() {
+ this.action = useService("action");
+}
+openSettings() {
+ this.action.doAction("base_setup.action_general_configuration");
+}
+...
+```
+
+Let us now add two buttons to our control panel:
+
+1. A button `Customers`, which opens a kanban view with all customers (this
+ action already exists, so you should use its
+ [xml id](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/odoo/addons/base/views/res_partner_views.xml#L510)).
+2. A button `Leads`, which opens a dynamic action on the `crm.lead` model with a
+ list and a form view. Follow the example of
+ [this use of the action service](https://github.com/odoo/odoo/blob/ef424a9dc22a5abbe7b0a6eff61cf113826f04c0/addons/account/static/src/components/journal_dashboard_activity/journal_dashboard_activity.js#L28-L35).
+
+
+
+**See also:**
+[code: action service](https://github.com/odoo/odoo/blob/master/addons/web/static/src/webclient/actions/action_service.js)
+
+## 3. Add a dashboard item
+
+Let us now improve the content of this dashboard.
+
+1. Create a generic `DashboardItem` component that display its default slot in a
+ nice card layout. It should take an optional `size` number props, that
+ default to 1. The width should be hardcoded to `(18*size)rem`.
+2. Add two cards to the dashboard. One with no size, and the other with a size
+ of 2.
+
+
+
+**See also:**
+[Owl slot system](https://github.com/odoo/owl/blob/master/doc/reference/slots.md)
+
+## 4. Call the server, add some statistics
+
+Let’s improve the dashboard by adding a few dashboard items to display real
+business data. The `awesome_dashboard` addon provides a
+`/awesome_dashboard/statistics` route that is meant to return some interesting
+information.
+
+To call a specific controller, we need to use the `rpc` function. This function
+`rpc(route, params, settings)` is the low level communication code that will
+create a network request to the server in jsonrpc, then will return a promise
+with the result. A basic request could look like this:
+
+```js
+import { rpc } from "@web/core/network/rpc";
+...
+setup() {
+ onWillStart(async () => {
+ const result = await rpc("/my/controller", {a: 1, b: 2});
+ // ...
+ });
+}
+...
+```
+
+1. Update Dashboard so that it uses the `rpc` function.
+2. Call the statistics route `/awesome_dashboard/statistics` in the
+ `onWillStart` hook.
+3. Display a few cards in the dashboard containing:
+ - Number of new orders this month
+ - Total amount of new orders this month
+ - Average amount of t-shirt by order this month
+ - Number of cancelled orders this month
+ - Average time for an order to go from ‘new’ to ‘sent’ or ‘cancelled’
+
+
+
+## 5. Cache network calls, create a service
+
+If you open the Network tab of your browser’s dev tools, you will see that the
+call to `/awesome_dashboard/statistics` is done every time the client action is
+displayed. This is because the `onWillStart` hook is called each time the
+`Dashboard` component is mounted. But in this case, we would prefer to do it
+only the first time, so we actually need to maintain some state outside of the
+`Dashboard` component. This is a nice use case for a service!
+
+1. Register and import a `new awesome_dashboard.statistics` service.
+2. It should provide a function `loadStatistics` that, once called, performs the
+ actual rpc, and always return the same information.
+3. Use the `memoize` utility function from `@web/core/utils/functions` that
+ allows caching the statistics.
+4. Use this service in the `Dashboard` component.
+5. Check that it works as expected.
+
+#### See also
+
+- [Example: simple service](https://github.com/odoo/odoo/blob/master/addons/web/static/src/core/network/http_service.js)
+- [Example: service with a dependency](https://github.com/odoo/odoo/blob/master/addons/web/static/src/core/network/http_service.js)
+
+## 6. Display a pie chart
+
+Everyone likes charts (!), so let us add a pie chart in our dashboard. It will
+display the proportions of t-shirts sold for each size: S/M/L/XL/XXL.
+
+For this exercise, we will use [Chart.js](https://www.chartjs.org/). It is the
+chart library used by the graph view. However, it is not loaded by default, so
+we will need to either add it to our assets bundle, or lazy load it. Lazy
+loading is usually better since our users will not have to load the chartjs code
+every time if they don’t need it.
+
+1. Create a `PieChart` component.
+2. In its `onWillStart` method, load chartjs, you can use the
+ [loadJs function](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/core/assets.js#L23)
+ to load `/web/static/lib/Chart/Chart.js`.
+3. Use the `PieChart` component in a `DashboardItem` to display a
+ [pie chart](https://www.chartjs.org/docs/2.8.0/charts/doughnut.html) that
+ shows the quantity for each sold t-shirts in each size (that information is
+ available in the `/statistics` route). Note that you can use the size
+ property to make it look larger.
+4. The `PieChart` component will need to render a canvas, and draw on it using
+ chart.js. You can use this code to create the pie chart:
+
+ ```js
+ import { getColor } from "@web/core/colors/colors";
+ ...
+ renderChart() {
+ const labels = Object.keys(this.props.data);
+ const data = Object.values(this.props.data);
+ const color = labels.map((_, index) => getColor(index));
+ this.chart = new Chart(this.canvasRef.el, {
+ type: "pie",
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: this.props.label,
+ data: data,
+ backgroundColor: color,
+ },
+ ],
+ },
+ });
+ }
+ ```
+
+5. Make it work!
+
+
+
+#### See also
+
+- Example:
+ [lazy loading a js file](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/views/graph/graph_renderer.js#L57)
+- Example:
+ [rendering a chart in a component](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/views/graph/graph_renderer.js#L618)
+
+## 7. Periodic Updates
+
+Since we moved the data loading in a cache, it never updates. But let us say
+that we are looking at fast moving data, so we want to periodically (for
+example, every 10min) reload fresh data.
+
+This is quite simple to implement, with a `setTimeout` or `setInterval` in the
+statistics service. However, here is the tricky part: if the dashboard is
+currently being displayed, it should be updated immediately.
+
+To do that, one can use a `reactive` object: it is just like the proxy returned
+by `useState`, but not linked to any component. A component can then do a
+`useState` on it to subscribe to its changes.
+
+1. Update the statistics service to reload data every 10 minutes (to test it,
+ use 10s instead!)
+2. Modify it to return a
+ [reactive](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md#reactive)
+ object. Reloading data should update the reactive object in place.
+3. Update the `Dashboard` component to wrap the reactive object in a `useState`
+
+#### See also
+
+- [Documentation on reactivity](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md)
+- [Example: Use of reactive in a service](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/core/debug/profiling/profiling_service.js#L30)
+
+## 8. Lazy loading the dashboard
+
+Let us imagine that our dashboard is getting quite big, and is only of interest
+to some of our users. In that case, it could make sense to lazy load our
+dashboard, and all related assets, so we only pay the cost of loading the code
+when we actually want to look at it.
+
+One way to do this is to use `LazyComponent` (from @web/core/assets) as an
+intermediate that will load an asset bundle before displaying our component.
+
+For example, it can be used like this:
+
+```js
+export class ExampleComponentLoader extends Component {
+ static components = { LazyComponent };
+ static template = xml`
+
+ `;
+}
+
+registry
+ .category("actions")
+ .add("example_module.example_action", ExampleComponentLoader);
+```
+
+1. Move all dashboard assets into a sub folder `/dashboard` to make it easier to
+ add to a bundle.
+2. Create a `awesome_dashboard.dashboard` assets bundle containing all content
+ of the `/dashboard` folder.
+3. Modify `dashboard.js` to register itself to the `lazy_components` registry
+ instead of `actions`.
+4. In `src/dashboard_action.js`, create an intermediate component that uses
+ `LazyComponent` and register it to the `actions` registry.
+
+## 9. Making our dashboard generic
+
+So far, we have a nice working dashboard. But it is currently hardcoded in the
+dashboard template. What if we want to customize our dashboard? Maybe some users
+have different needs and want to see other data.
+
+So, the next step is to make our dashboard generic: instead of hard-coding its
+content in the template, it can just iterate over a list of dashboard items. But
+then, many questions come up: how to represent a dashboard item, how to register
+it, what data should it receive, and so on. There are many different ways to
+design such a system, with different trade-offs.
+
+For this tutorial, we will say that a dashboard item is an object with the
+following structure:
+
+```js
+const item = {
+ id: "average_quantity",
+ description: "Average amount of t-shirt",
+ Component: StandardItem,
+ // size and props are optionals
+ size: 3,
+ props: (data) => ({
+ title: "Average amount of t-shirt by order this month",
+ value: data.average_quantity,
+ }),
+};
+```
+
+The `description` value will be useful in a later exercise to show the name of
+items that the user can add to their dashboard. The `size` number is optional,
+and simply describes the size of the dashboard item that will be displayed.
+Finally, the `props` function is optional. If not given, we will simply give the
+`statistics` object as data. But if it is defined, it will be used to compute
+specific props for the component.
+
+The goal is to replace the content of the dashboard with something that look
+like the following snippet:
+
+```xml
+
+
+
+
+
+
+```
+
+Note that the above example features two advanced features of Owl: dynamic
+components and dynamic props.
+
+We currently have two kinds of item components: number cards with a title and a
+number, and pie cards with some label and a pie chart.
+
+1. Create and implement two components: `NumberCard` and `PieChartCard`, with
+ the corresponding props.
+2. Create a file `dashboard_items.js` in which you define and export a list of
+ items, using `NumberCard` and `PieChartCard` to recreate our current
+ dashboard.
+3. Import that list of items in our `Dashboard` component, add it to the
+ component, and update the template to use a `t-foreach` like shown above.
+
+ ```js
+ setup() {
+ this.items = items;
+ }
+ ```
+
+And now, our dashboard template is generic!
+
+## 10. Making our dashboard extensible
+
+However, the content of our item list is still hardcoded. Let us fix that by
+using a registry:
+
+1. Instead of exporting a list, register all dashboard items in a
+ `awesome_dashboard` registry
+2. Import all the items of the `awesome_dashboard` registry in the `Dashboard`
+ component
+
+The dashboard is now easily extensible. Any other Odoo addon that wants to
+register a new item to the dashboard can just add it to the registry.
+
+## 11. Add and remove dashboard items
+
+Let us see how we can make our dashboard customizable. To make it simple, we
+will save the user dashboard configuration in the local storage, so that we
+don’t have to deal with the server for now.
+
+For this exercise, the dashboard configuration will be saved as a list of
+removed item ids.
+
+1. Add a button in the control panel with a gear icon to indicate that it is a
+ settings button.
+2. Clicking on that button should open a dialog.
+3. In that dialog, we want to see a list of all existing dashboard items, each
+ with a checkbox.
+4. There should be a Apply button in the footer. Clicking on it will build a
+ list of all item ids that are unchecked.
+5. We want to store that value in the local storage.
+6. And modify the Dashboard component to filter the current items by removing
+ the ids of items from the configuration.
+
+
+
+## 12. Going further
+
+Here is a list of some small (and not so small) improvements you could try to do
+if you have the time:
+
+- Make sure your application can be translated (with `_t`).
+- Clicking on a section of the pie chart should open a list view of all orders
+ that have the corresponding size.
+- Save the content of the dashboard in a user setting on the server!
+- Make it responsive: in mobile mode, each card should take 100% of the width.
+- Update the dashboard in real time by using the bus (hard)
diff --git a/3_customize_fields_views.md b/3_customize_fields_views.md
new file mode 100644
index 00000000000..0cda469b6e0
--- /dev/null
+++ b/3_customize_fields_views.md
@@ -0,0 +1,114 @@
+# Module 3: Customize Fields and Views
+
+This project is a sequence of (mostly) independant exercises designed to teach
+how to work with fields and views in Odoo.
+
+The setting for this project is an addon that manages a shelter and various kind
+of animals that are adopted. All the code is located in the `awesome_shelter`
+addon.
+
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+Start your odoo server with this repository in the addons path, then install the
+`awesome_shelter` addon.
+
+The `awesome_shelter` addon introduces a few useful models:
+
+- `awesome_shelter.animal` represents a specific animal that has been rescued
+- `awesome_shelter.animal_race` is a specific animal race, such as german
+ sheperd.
+- `awesome_shelter.animal_type` is a small model that help us categorize
+ animals, such as "dogs" or "cats"
+
+## Content
+
+- [1. Add a ribbon](#1-add-a-ribbon)
+- [2. Display a custom view banner](#2-display-a-custom-view-banner)
+- [3. Subclass a char field](#3-subclass-a-char-field)
+- [4. Customize a status bar widget](#4-customize-a-status-bar-widget)
+- [5. Display pictogram and type in list view](#5-display-pictogram-and-type-in-list-view)
+- [6. Extend a view](#6-extend-a-view)
+
+## 1. Add a ribbon
+
+In the kanban and form view for an animal, it would be cool to have a visual
+feedback showing that the animal is adopted. Let us do that by adding a ribbon!
+
+This can be done by using an existing widget named `web_ribbon`
+
+1. modify the form view to add a `web_ribbon` widget, which should be visible
+ only when the state is `adopted`
+2. do the same for the kanban view
+
+
+
+## 2. Display a custom view banner
+
+The previous exercise showed how to use a widget. In this exercise, we will
+implement a new widget from scratch.
+
+In the form view for the animal model, we want to display a banner with a
+message when the animal has been adopted for more than 6 months.
+
+1. create a new component named `LongStayBanner`
+2. register it in the `view_widgets` registry
+3. Use it in the form view:
+
+ ```xml
+
+ ```
+
+
+
+## 3. Subclass a char field
+
+Let us now see how to specialize a field.
+
+1. Subclass the charfield component from `@web/views/fields/char/char_field`
+2. register it in the fields registry, and use the field in the form view
+3. create a new template to add a button when the name is not set (this can be
+ done with `xpaths` or using `t-call`)
+4. when the button is clicked, choose a name from a hardcoded list of pet names,
+ and set the value of the field
+
+
+
+
+## 4. Customize a status bar widget
+
+Let us now customize the status bar widget to display a more festive effect when
+an animal is adopted, which usually happens when the shelter staff clicks on the
+`adopted` status.
+
+1. subclass the status bar field
+2. register it in the fields registry, and use the field in the form view
+3. override the `selectItem` to display a celebratory rainbow man when the
+ animal is adopted
+
+
+
+## 5. Display pictogram and type in list view
+
+Animals have a type (cat, dog, etc). Those types can have a pictogram. We want
+an extension of a one2many field widget to display the name and the pictogram a
+little bit like the one2many_avatar, as a single element instead of two columns
+in list view.
+
+1. subclass the many2one field
+2. use it in the list view
+3. modify your field to display a pictogram if it is defined
+
+
+
+## 6. Extend a view
+
+Finally, it sometimes happens that we need to work with views instead of fields.
+
+For this exercise, we want to modify the kanban view to reload its data every 10
+seconds or so. This is because the shelter staff want to display it on a small
+external screen as a visual monitoring tool.
+
+1. Subclass the kanban view
+2. register your kanban view in the `views` registry, and use it (with
+ `js_class` attribute) in the animal kanban view
+3. Modify the code to reload every 10s
diff --git a/4_build_a_clicker_game.md b/4_build_a_clicker_game.md
new file mode 100644
index 00000000000..87960a76c63
--- /dev/null
+++ b/4_build_a_clicker_game.md
@@ -0,0 +1,449 @@
+# Module 4: Build a Clicker Game
+
+For this project, we will build together a
+[clicker game](https://en.wikipedia.org/wiki/Incremental_game), completely
+integrated with Odoo. In this game, the goal is to accumulate a large number of
+clicks, and to automate the system. The interesting part is that we will use the
+Odoo user interface as our playground. For example, we will hide bonuses in some
+random parts of the web client.
+
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+Start your odoo server with this repository in the addons path, then install the
+`awesome_clicker` addon.
+
+The code for this addon is currently empty, but we will progressively add
+features to it.
+
+
+
+## Content
+
+- [1. Create a systray item](#1-create-a-systray-item)
+- [2. Count external clicks](#2-count-external-clicks)
+- [3. Create a client action](#3-create-a-client-action)
+- [4. Move the state to a service](#4-move-the-state-to-a-service)
+- [5. Use a custom hook](#5-use-a-custom-hook)
+- [6. Humanize the displayed value](#6-humanize-the-displayed-value)
+- [7. Add a tooltip in ClickValue component](#7-add-a-tooltip-in-clickvalue-component)
+- [8. Buy ClickBots](#8-buy-clickbots)
+- [9. Refactor to a class model](#9-refactor-to-a-class-model)
+- [10. Notify when a milestone is reached](#10-notify-when-a-milestone-is-reached)
+- [11. Add BigBots](#11-add-bigbots)
+- [12. Add a new type of resource: power](#12-add-a-new-type-of-resource-power)
+- [13. Define some random rewards](#13-define-some-random-rewards)
+- [14. Provide a reward when opening a form view](#14-provide-a-reward-when-opening-a-form-view)
+- [15. Add commands in command palette](#15-add-commands-in-command-palette)
+- [16. Add yet another resource: trees](#16-add-yet-another-resource-trees)
+- [17. Use a dropdown menu for the systray item](#17-use-a-dropdown-menu-for-the-systray-item)
+- [18. Use a Notebook component](#18-use-a-notebook-component)
+- [19. Persist the game state](#19-persist-the-game-state)
+
+## 1. Create a systray item
+
+To get started, we want to display a counter in the systray.
+
+1. Create a `clicker_systray_item.js` (and `xml`) file with a hello world Owl
+ component.
+2. Register it to the systray registry, and make sure it is visible.
+3. Update the content of the item so that it displays the following string:
+ `Clicks: 0`, and add a button on the right to increment the value.
+
+
+
+And voila, we have a completely working clicker game!
+
+#### See also
+
+- [Documentation on the systray registry](https://www.odoo.com/documentation/master/developer/reference/frontend/registries.html#frontend-registries-systray)
+- [Example: adding a systray item to the registry](https://github.com/odoo/odoo/blob/c4fb9c92d7826ddbc183d38b867ca4446b2fb709/addons/web/static/src/webclient/user_menu/user_menu.js#L41-L42)
+
+## 2. Count external clicks
+
+Well, to be honest, it is not much fun yet. So let us add a new feature: we want
+all clicks in the user interface to count, so the user is incentivized to use
+Odoo as much as possible! But obviously, the intentional clicks on the main
+counter should still count more.
+
+1. Use `useExternalListener` to listen on all clicks on `document.body`.
+2. Each of these clicks should increase the counter value by 1.
+3. Modify the code so that each click on the counter increased the value by 10
+4. Make sure that a click on the counter does not increase the value by 11!
+5. Also additional challenge: make sure the external listener capture the
+ events, so we don’t miss any clicks.
+
+#### See also
+
+- [Owl documentation on useExternalListener](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useexternallistener)
+- [MDN page on event capture](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_capture)
+
+## 3. Create a client action
+
+Currently, the current user interface is quite small: it is just a systray item.
+We certainly need more room to display more of our game. To do that, let us
+create a client action. A client action is a main action, managed by the web
+client, that displays a component.
+
+1. Create a `client_action.js` (and `xml`) file, with a hello world component.
+2. Register that client action in the action registry under the name
+ `awesome_clicker.client_action`
+3. Add a button on the systray item with the text "Open". Clicking on it should
+ open the client action `awesome_clicker.client_action` (use the action
+ service to do that).
+4. To avoid disrupting employees workflow, we prefer the client action to open
+ within a popover rather than in fullscreen mode. Modify the `doAction` call
+ to open it in a popover.
+
+You can use target: `"new"` in the doAction to open the action in a popover:
+
+```js
+{
+ type: "ir.actions.client",
+ tag: "awesome_clicker.client_action",
+ target: "new",
+ name: "Clicker"
+}
+```
+
+
+
+**See also:**
+[How to create a client action](https://www.odoo.com/documentation/master/developer/howtos/javascript_client_action.html#howtos-javascript-client-action)
+
+## 4. Move the state to a service
+
+For now, our client action is just a hello world component. We want it to
+display our game state, but that state is currently only available in the
+systray item. So it means that we need to change the location of our state to
+make it available for all our components. This is a perfect use case for
+services.
+
+1. Create a `clicker_service.js` file with the corresponding service.
+2. This service should export a reactive value (the number of clicks) and a few
+ functions to update it:
+ ```js
+ const state = reactive({ clicks: 0 });
+ ...
+ return {
+ state,
+ increment(inc) {
+ state.clicks += inc
+ }
+ };
+ ```
+3. Access the state in both the systray item and the client action (don’t forget
+ to `useState` it). Modify the systray item to remove its own local state and
+ use it. Also, you can remove the +10 clicks button.
+4. Display the state in the client action, and add a +10 clicks button in it.
+
+
+
+**See also:**
+[Short explanation on services](https://www.odoo.com/documentation/master/developer/tutorials/discover_js_framework/02_build_a_dashboard.html#tutorials-discover-js-framework-services)
+
+## 5. Use a custom hook
+
+Right now, every part of the code that will need to use our clicker service will
+have to import `useService` and `useState`. Since it is quite common, let us use
+a custom hook. It is also useful to put more emphasis on the `clicker` part, and
+less emphasis on the `service` part.
+
+1. Create and export a `useClicker` hook that encapsulate the `useService` and
+ `useState` in a simple function.
+2. Update all current uses of the clicker service to the new hook:
+
+ ```js
+ this.clicker = useClicker();
+ ```
+
+**See also:**
+[Documentation on hooks](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md)
+
+## 6. Humanize the displayed value
+
+We will in the future display large numbers, so let us get ready for that. There
+is a `humanNumber` function that format numbers in a easier to comprehend way:
+for example, `1234` could be formatted as `1.2k`
+
+1. Use it to display our counters (both in the systray item and the client
+ action).
+2. Create a `ClickValue` component that displays the value.
+
+Note that Owl allows component that contains just text nodes!
+
+
+
+**See also:**
+[definition of humanNumber function](https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/utils/numbers.js#L119)
+
+## 7. Add a tooltip in ClickValue component
+
+With the `humanNumber` function, we actually lost some precision on our
+interface. Let us display the real number as a tooltip.
+
+1. The tooltip needs an html element. Change the `ClickValue` to wrap its value
+ in a `` element
+2. Add a dynamic `data-tooltip` attribute to display the exact value.
+
+
+
+**See also:**
+[Documentation in the tooltip service](https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/tooltip/tooltip_service.js#L17)
+
+## 8. Buy ClickBots
+
+Let us make our game even more interesting: once a player get to 1000 clicks for
+the first time, the game should unlock a new feature: the player can buy robots
+for 1000 clicks. These robots will generate 10 clicks every 10 seconds.
+
+1. Add a `level` number to our state. This is a number that will be incremented
+ at some milestones, and unlock new features
+2. Add a `clickBots` number to our state. It represents the number of robots
+ that have been purchased.
+3. Modify the client action to display the number of click bots (only if
+ `level >= 1`), with a `Buy` button that is enabled if `clicks >= 1000`. The
+ `Buy` button should increment the number of clickbots by 1.
+4. Set a 10s interval in the service that will increment the number of clicks by
+ `10*clickBots`.
+5. Make sure the `Buy` button is disabled if the player does not have enough
+ clicks.
+
+
+
+# 9. Refactor to a class model
+
+The current code is written in a somewhat functional style. But to do so, we
+have to export the state and all its update functions in our clicker object. As
+this project grows, this may become more and more complex. To make it simpler,
+let us split our business logic out of our service and into a class.
+
+1. Create a `clicker_model` file that exports a reactive class. Move all the
+ state and update functions from the service into the model.
+2. Rewrite the clicker service to instantiate and export the clicker model
+ class.
+
+To create the `ClickerModel`, you can extend the `Reactive` class from
+`@web/core/utils/reactive`. The `Reactive` class wrap the model into a reactive
+proxy.
+
+**See also:**
+[Example of subclassing Reactive](https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/model/relational_model/datapoint.js#L32)
+
+# 10. Notify when a milestone is reached
+
+There is not much feedback that something changed when we reached 1k clicks. Let
+us use the `effect` service to communicate that information clearly. The problem
+is that our click model does not have access to services. Also, we want to keep
+as much as possible the UI concern out of the model. So, we can explore a new
+strategy for communication: event buses.
+
+1. Update the clicker model to instantiate a bus, and to trigger a
+ `MILESTONE_1k` event when we reach 1000 clicks for the first time.
+2. Change the clicker service to listen to the same event on the model bus.
+3. When that happens, use the effect service to display a rainbow man. 4 Add
+ some text to explain that the user can now buy clickbots.
+
+
+
+#### See also
+
+- [Owl documentation on event bus](https://github.com/odoo/owl/blob/master/doc/reference/utils.md#eventbus)
+- [Documentation on effect service](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html#frontend-services-effect)
+
+# 11. Add BigBots
+
+Clearly, we need a way to provide the player with more choices. Let us add a new
+type of clickbot: `BigBots`, which are just more powerful: they provide with 100
+clicks each 10s, but they cost 5000 clicks.
+
+1. increment `level` when it gets to 5k (so it should be 2)
+2. Update the state to keep track of `bigbots`
+3. `bigbots` should be available at `level >= 2`
+4. Display the corresponding information in the client action
+
+**Tip** If you need to use `<` or `>` in a template as a javascript expression,
+be careful since it might clash with the xml parser. To solve that, you can use
+one of the special aliases: `gt`, `gte`, `lt` or `lte`. See the
+[Owl documentation page on template expressions](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#expression-evaluation).
+
+
+
+# 12. Add a new type of resource: power
+
+Now, to add another scaling point, let us add a new type of resource: a power
+multiplier. This is a number that can be increased at level >= 3, and multiplies
+the action of the bots (so, instead of providing `N` click, clickbots now
+provide us with `power*N` clicks).
+
+1. increment level when the number of clicks gets to 100k (so it should be 3).
+2. update the state to keep track of the power (initial value is 1).
+3. change bots to use that number as a multiplier.
+4. Update the user interface to display and let the user purchase a new power
+ level (costs: 50k).
+
+## 13. Define some random rewards
+
+We want the user to obtain sometimes bonuses, to reward using Odoo. A reward is
+an object with:
+
+- a `description` string.
+- an `apply` function that take the game state in argument and can modify it.
+- a `minLevel` number (optional) that describes at which unlock level the bonus
+ is available.
+- a `maxLevel` number (optional) that describes at which unlock level a bonus is
+ no longer available.
+
+1. Define a list of rewards in a new file `click_rewards.js`. For example:
+
+ ```js
+ export const rewards = [
+ {
+ description: "Get 1 click bot",
+ apply(clicker) {
+ clicker.increment(1);
+ },
+ maxLevel: 3,
+ },
+ {
+ description: "Get 10 click bot",
+ apply(clicker) {
+ clicker.increment(10);
+ },
+ minLevel: 3,
+ maxLevel: 4,
+ },
+ {
+ description: "Increase bot power!",
+ apply(clicker) {
+ clicker.multipler += 1;
+ },
+ minLevel: 3,
+ },
+ ];
+ ```
+
+ You can add whatever you want to that list!
+
+2. Define a function `getReward` that will select a random reward from the list
+ of rewards that matches the current unlock level.
+3. Extract the code that choose randomly in an array in a function `choose` that
+ you can move to another `utils.js` file.
+
+# 14. Provide a reward when opening a form view
+
+1. Patch the form controller. Each time a form controller is created, it should
+ randomly decides (1% chance) if a reward should be given.
+2. If the answer is yes, call a method `getReward` on the model.
+3. That method should choose a reward, send a sticky notification, with a button
+ `Collect` that will then apply the reward, and finally, it should open the
+ `clicker` client action.
+
+
+
+#### See also
+
+- [Documentation on patching a class](https://www.odoo.com/documentation/master/developer/reference/frontend/patching_code.html#frontend-patching-class)
+- [Definition of patch function](https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/utils/patch.js#L71)
+- [Example of patching a class](https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/utils/patch.js#L71)
+
+## 15. Add commands in command palette
+
+Let us now provide more interesting ways to interact with our game.
+
+1. Add a command `Open Clicker Game` to the command palette.
+2. Add another command: `Buy 1 click bot`.
+
+
+
+**See also:**
+[Example of use of command provider registry](https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/debug/debug_providers.js#L10)
+
+## 16. Add yet another resource: trees
+
+It is now time to introduce a completely new type of resources. Here is one that
+should not be too controversial: trees. We will now allow the user to plant
+(collect?) fruit trees. A tree costs 1 million clicks, but it will provide us
+with fruits (either pears or cherries).
+
+1. Update the state to keep track of various types of trees: pear/cherries, and
+ their fruits.
+2. Add a function that computes the total number of trees and fruits.
+3. Define a new unlock level at `clicks >= 1 000 000`.
+4. Update the client user interface to display the number of trees and fruits,
+ and also, to buy trees.
+5. Increment the fruit number by 1 for each tree every 30s.
+
+## 17. Use a dropdown menu for the systray item
+
+Our game starts to become interesting. But for now, the systray only displays
+the total number of clicks. We want to see more information: the total number of
+trees and fruits. Also, it would be useful to have a quick access to some
+commands and some more information. Let us use a dropdown menu!
+
+1. Replace the systray item by a dropdown menu.
+2. It should display the numbers of clicks, trees, and fruits, each with a nice
+ icon.
+3. Clicking on it should open a dropdown menu that displays more detailed
+ information: each types of trees and fruits.
+4. Also, a few dropdown items with some commands: open the clicker game, buy a
+ clickbot, ...
+
+
+
+## 18. Use a Notebook component
+
+We now keep track of a lot more information. Let us improve our client interface
+by organizing the information and features in various tabs, with the `Notebook`
+component:
+
+1. Use the `Notebook` component.
+2. All `click` content should be displayed in one tab.
+3. All tree/fruits content should be displayed in another tab.
+
+
+
+#### See also
+
+- [Odoo: Documentation on Notebook component](https://www.odoo.com/documentation/master/developer/reference/frontend/owl_components.html#frontend-owl-notebook)
+- [Owl: Documentation on slots](https://github.com/odoo/owl/blob/master/doc/reference/slots.md)
+- [Tests of Notebook component](https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/tests/core/notebook_tests.js#L27)
+
+## 19. Persist the game state
+
+You certainly noticed a big flaw in our game: it is transient. The game state is
+lost each time the user closes the browser tab. Let us fix that. We will use the
+local storage to persist the state.
+
+1. Import `browser` from `@web/core/browser/browser` to access the localstorage.
+2. Serialize the state every 10s (in the same interval code) and store it on the
+ local storage.
+3. When the clicker service is started, it should load the state from the local
+ storage (if any), or initialize itself otherwise.
+
+## 20. Introduce state migration system
+
+Once you persist state somewhere, a new problem arises: what happens when you
+update your code, so the shape of the state changes, and the user opens its
+browser with a state that was created with an old version? Welcome to the world
+of migration issues!
+
+It is probably wise to tackle the problem early. What we will do here is add a
+version number to the state, and introduce a system to automatically update the
+states if it is not up to date.
+
+1. Add a version number to the state.
+2. Define an (empty) list of migrations. A migration is an object with a
+ `fromVersion` number, a `toVersion` number, and a `apply` function.
+3. Whenever the code loads the state from the local storage, it should check the
+ version number. If the state is not uptodate, it should apply all necessary
+ migrations.
+
+## 21. Add another type of trees
+
+To test our migration system, let us add a new type of trees: peaches.
+
+1. Add `peach` trees in the model and in the UI.
+2. Increment the state version number.
+3. Define a migration.
+
+
diff --git a/README.md b/README.md
index 0c6667bb6f2..11670ba1eec 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,20 @@
-# Odoo tutorials
+
-This repository hosts the code for the bases and solutions of the
-[official Odoo tutorials](https://www.odoo.com/documentation/16.0/developer/howtos.html).
+# Odoo : the Javascript Framework Training
-It has 2 branches for each Odoo version: one for the bases and one for
-the solutions. For example, `16.0` and `16.0-solutions`. The first
-contains the code of the modules that serve as base for the tutorials,
-and the second contains the code of the same modules with the complete
-solution.
\ No newline at end of file
+This branch contains the exercises and the supporting code for the O doo Javascript
+Framework Traning. There are four addons, each corresponding to a
+standalone project that can be done independently from the others.
+
+To get started, clone this repository, checkout this branch, and start an odoo
+server with this folder in the addons path.
+
+Note that a lot of theory is covered in the slides for this event. Also, don't
+hesitate to ask questions!
+
+| Title | Content |
+| ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
+| [Module 1: Learn Owl 🦉](1_learn_owl.md) | Owl, components, hooks, reactivity, state management, ... |
+| [Module 2: Make a Dashboard](2_make_a_dashboard.md) | assets, basics of odoo framework, rpcs, systray, services, registries, ... |
+| [Module 3: Customize Fields and Views](3_customize_fields_views.md) | fields, views |
+| [Module 4: Build a Clicker Game](4_build_a_clicker_game.md) | advanced framework, state management, ... |
diff --git a/_images/add_ribbon.png b/_images/add_ribbon.png
new file mode 100644
index 00000000000..533811369af
Binary files /dev/null and b/_images/add_ribbon.png differ
diff --git a/_images/autofocus.png b/_images/autofocus.png
new file mode 100644
index 00000000000..eb13a35b67c
Binary files /dev/null and b/_images/autofocus.png differ
diff --git a/_images/banner.png b/_images/banner.png
new file mode 100644
index 00000000000..e5f5246c981
Binary files /dev/null and b/_images/banner.png differ
diff --git a/_images/bigbot.png b/_images/bigbot.png
new file mode 100644
index 00000000000..77ae8dc00ef
Binary files /dev/null and b/_images/bigbot.png differ
diff --git a/_images/clickbot.png b/_images/clickbot.png
new file mode 100644
index 00000000000..fc721748587
Binary files /dev/null and b/_images/clickbot.png differ
diff --git a/_images/client_action.png b/_images/client_action.png
new file mode 100644
index 00000000000..434a19213df
Binary files /dev/null and b/_images/client_action.png differ
diff --git a/_images/command_palette.png b/_images/command_palette.png
new file mode 100644
index 00000000000..31c3a9ea37e
Binary files /dev/null and b/_images/command_palette.png differ
diff --git a/_images/counter.png b/_images/counter.png
new file mode 100644
index 00000000000..5e2ac1a8f6d
Binary files /dev/null and b/_images/counter.png differ
diff --git a/_images/create_todo.png b/_images/create_todo.png
new file mode 100644
index 00000000000..83ec5c4c532
Binary files /dev/null and b/_images/create_todo.png differ
diff --git a/_images/dashboard_item.png b/_images/dashboard_item.png
new file mode 100644
index 00000000000..1524ca5b0e2
Binary files /dev/null and b/_images/dashboard_item.png differ
diff --git a/_images/delete_todo.png b/_images/delete_todo.png
new file mode 100644
index 00000000000..cc9ff9a85ed
Binary files /dev/null and b/_images/delete_todo.png differ
diff --git a/_images/double_counter.png b/_images/double_counter.png
new file mode 100644
index 00000000000..99e2b88d48e
Binary files /dev/null and b/_images/double_counter.png differ
diff --git a/_images/dropdown.png b/_images/dropdown.png
new file mode 100644
index 00000000000..1cdeddc40cc
Binary files /dev/null and b/_images/dropdown.png differ
diff --git a/_images/final.png b/_images/final.png
new file mode 100644
index 00000000000..f40107a7fc3
Binary files /dev/null and b/_images/final.png differ
diff --git a/_images/generic_card.png b/_images/generic_card.png
new file mode 100644
index 00000000000..ad079e95dde
Binary files /dev/null and b/_images/generic_card.png differ
diff --git a/_images/humanized_number.png b/_images/humanized_number.png
new file mode 100644
index 00000000000..f51cf9c9938
Binary files /dev/null and b/_images/humanized_number.png differ
diff --git a/_images/humanized_tooltip.png b/_images/humanized_tooltip.png
new file mode 100644
index 00000000000..b11e5d52269
Binary files /dev/null and b/_images/humanized_tooltip.png differ
diff --git a/_images/increment_button.png b/_images/increment_button.png
new file mode 100644
index 00000000000..1d96e7aa0a3
Binary files /dev/null and b/_images/increment_button.png differ
diff --git a/_images/items_configuration.png b/_images/items_configuration.png
new file mode 100644
index 00000000000..a828a187b54
Binary files /dev/null and b/_images/items_configuration.png differ
diff --git a/_images/markup.png b/_images/markup.png
new file mode 100644
index 00000000000..bd8fea06f1a
Binary files /dev/null and b/_images/markup.png differ
diff --git a/_images/milestone1.png b/_images/milestone1.png
new file mode 100644
index 00000000000..cc11eda3624
Binary files /dev/null and b/_images/milestone1.png differ
diff --git a/_images/muted_todo.png b/_images/muted_todo.png
new file mode 100644
index 00000000000..e732e4cfae2
Binary files /dev/null and b/_images/muted_todo.png differ
diff --git a/_images/navigation_buttons.png b/_images/navigation_buttons.png
new file mode 100644
index 00000000000..79d0221d217
Binary files /dev/null and b/_images/navigation_buttons.png differ
diff --git a/_images/new_layout.png b/_images/new_layout.png
new file mode 100644
index 00000000000..7bfd8128770
Binary files /dev/null and b/_images/new_layout.png differ
diff --git a/_images/notebook.png b/_images/notebook.png
new file mode 100644
index 00000000000..101be3bd3fd
Binary files /dev/null and b/_images/notebook.png differ
diff --git a/_images/odoo_logo.png b/_images/odoo_logo.png
new file mode 100644
index 00000000000..474f9ff5d66
Binary files /dev/null and b/_images/odoo_logo.png differ
diff --git a/_images/overview_02.png b/_images/overview_02.png
new file mode 100644
index 00000000000..bd618027cee
Binary files /dev/null and b/_images/overview_02.png differ
diff --git a/_images/peach_tree.png b/_images/peach_tree.png
new file mode 100644
index 00000000000..7ccbe51a7f5
Binary files /dev/null and b/_images/peach_tree.png differ
diff --git a/_images/pictogram.png b/_images/pictogram.png
new file mode 100644
index 00000000000..37f1a1b2fb9
Binary files /dev/null and b/_images/pictogram.png differ
diff --git a/_images/pie_chart.png b/_images/pie_chart.png
new file mode 100644
index 00000000000..ba56ded24b8
Binary files /dev/null and b/_images/pie_chart.png differ
diff --git a/_images/rainbowadopted.png b/_images/rainbowadopted.png
new file mode 100644
index 00000000000..52803dbf2b5
Binary files /dev/null and b/_images/rainbowadopted.png differ
diff --git a/_images/reward.png b/_images/reward.png
new file mode 100644
index 00000000000..bdd85e24175
Binary files /dev/null and b/_images/reward.png differ
diff --git a/_images/simple_card.png b/_images/simple_card.png
new file mode 100644
index 00000000000..8cff4e90d01
Binary files /dev/null and b/_images/simple_card.png differ
diff --git a/_images/statistics1.png b/_images/statistics1.png
new file mode 100644
index 00000000000..d05fa928f92
Binary files /dev/null and b/_images/statistics1.png differ
diff --git a/_images/subcharfield.png b/_images/subcharfield.png
new file mode 100644
index 00000000000..e4b321652be
Binary files /dev/null and b/_images/subcharfield.png differ
diff --git a/_images/sum_counter.png b/_images/sum_counter.png
new file mode 100644
index 00000000000..9948016ca23
Binary files /dev/null and b/_images/sum_counter.png differ
diff --git a/_images/systray.png b/_images/systray.png
new file mode 100644
index 00000000000..06b40b440bd
Binary files /dev/null and b/_images/systray.png differ
diff --git a/_images/todo_list.png b/_images/todo_list.png
new file mode 100644
index 00000000000..6d77ee395a5
Binary files /dev/null and b/_images/todo_list.png differ
diff --git a/_images/toggle_card.png b/_images/toggle_card.png
new file mode 100644
index 00000000000..6048b38c738
Binary files /dev/null and b/_images/toggle_card.png differ
diff --git a/_images/toggle_todo.png b/_images/toggle_todo.png
new file mode 100644
index 00000000000..594b6c8aef6
Binary files /dev/null and b/_images/toggle_todo.png differ
diff --git a/_images/trees.png b/_images/trees.png
new file mode 100644
index 00000000000..35b39ce1619
Binary files /dev/null and b/_images/trees.png differ
diff --git a/awesome_clicker/__init__.py b/awesome_clicker/__init__.py
new file mode 100644
index 00000000000..40a96afc6ff
--- /dev/null
+++ b/awesome_clicker/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py
new file mode 100644
index 00000000000..f2a57dd1b46
--- /dev/null
+++ b/awesome_clicker/__manifest__.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Awesome Clicker",
+
+ 'summary': """
+ Companion addon for the Odoo JS Framework Training
+ """,
+
+ 'description': """
+ Companion addon for the Odoo JS Framework Training
+ """,
+
+ 'author': "Odoo",
+ 'website': "https://www.odoo.com/",
+ 'category': 'Tutorials',
+ 'version': '0.1',
+ 'application': True,
+ 'installable': True,
+ 'depends': ['base', 'web'],
+
+ 'data': [],
+ 'assets': {
+ 'web.assets_backend': [
+ 'awesome_clicker/static/src/**/*',
+ ],
+
+ },
+ 'license': 'AGPL-3'
+}
diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py
new file mode 100644
index 00000000000..b0f26a9a602
--- /dev/null
+++ b/awesome_dashboard/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
new file mode 100644
index 00000000000..b575bf4f1be
--- /dev/null
+++ b/awesome_dashboard/__manifest__.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Awesome Dashboard",
+
+ 'summary': """
+ Companion addon for the Odoo JS Framework Training
+ """,
+
+ 'description': """
+ Companion addon for the Odoo JS Framework Training
+ """,
+
+ 'author': "Odoo",
+ 'website': "https://www.odoo.com/",
+ 'category': 'Tutorials',
+ 'version': '0.1',
+ 'application': True,
+ 'installable': True,
+ 'depends': ['base', 'web', 'mail', 'crm'],
+
+ 'data': [
+ 'views/views.xml',
+ ],
+ 'assets': {
+ 'web.assets_backend': [
+ 'awesome_dashboard/static/src/**/*',
+ ],
+ },
+ 'license': 'AGPL-3'
+}
diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py
new file mode 100644
index 00000000000..457bae27e11
--- /dev/null
+++ b/awesome_dashboard/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
\ No newline at end of file
diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py
new file mode 100644
index 00000000000..c46cfc63bfa
--- /dev/null
+++ b/awesome_dashboard/controllers/controllers.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+import logging
+import random
+
+from odoo import http
+from odoo.http import request
+
+logger = logging.getLogger(__name__)
+
+class AwesomeDashboard(http.Controller):
+ @http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user')
+ def get_statistics(self):
+ """
+ Returns a dict of statistics about the orders:
+ 'average_quantity': the average number of t-shirts by order
+ 'average_time': the average time (in hours) elapsed between the
+ moment an order is created, and the moment is it sent
+ 'nb_cancelled_orders': the number of cancelled orders, this month
+ 'nb_new_orders': the number of new orders, this month
+ 'total_amount': the total amount of orders, this month
+ """
+
+ return {
+ 'average_quantity': random.randint(4, 12),
+ 'average_time': random.randint(4, 123),
+ 'nb_cancelled_orders': random.randint(0, 50),
+ 'nb_new_orders': random.randint(10, 200),
+ 'orders_by_size': {
+ 'm': random.randint(0, 150),
+ 's': random.randint(0, 150),
+ 'xl': random.randint(0, 150),
+ },
+ 'total_amount': random.randint(100, 1000)
+ }
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js
new file mode 100644
index 00000000000..c4fb245621b
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard.js
@@ -0,0 +1,8 @@
+import { Component } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml
new file mode 100644
index 00000000000..1a2ac9a2fed
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ hello dashboard
+
+
+
diff --git a/awesome_dashboard/views/views.xml b/awesome_dashboard/views/views.xml
new file mode 100644
index 00000000000..47fb2b6f258
--- /dev/null
+++ b/awesome_dashboard/views/views.xml
@@ -0,0 +1,11 @@
+
+
+
+ Dashboard
+ awesome_dashboard.dashboard
+
+
+
+
+
+
diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py
new file mode 100644
index 00000000000..457bae27e11
--- /dev/null
+++ b/awesome_owl/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
\ No newline at end of file
diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py
new file mode 100644
index 00000000000..242e53ddeb7
--- /dev/null
+++ b/awesome_owl/__manifest__.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Awesome Owl",
+
+ 'summary': """
+ Companion addon for the Odoo JS Framework Training
+ """,
+
+ 'description': """
+ Companion addon for the Odoo JS Framework Training
+ """,
+
+ 'author': "Odoo",
+ 'website': "https://www.odoo.com",
+
+ # Categories can be used to filter modules in modules listing
+ # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
+ # for the full list
+ 'category': 'Tutorials',
+ 'version': '0.2',
+
+ # any module necessary for this one to work correctly
+ 'depends': ['base', 'web'],
+ 'application': True,
+ 'installable': True,
+ 'data': [
+ 'views/templates.xml',
+ ],
+ 'assets': {
+ 'awesome_owl.assets_playground': [
+ ('include', 'web._assets_helpers'),
+ ('include', 'web._assets_backend_helpers'),
+ 'web/static/src/scss/pre_variables.scss',
+ 'web/static/lib/bootstrap/scss/_variables.scss',
+ 'web/static/lib/bootstrap/scss/_maps.scss',
+ ('include', 'web._assets_bootstrap'),
+ ('include', 'web._assets_core'),
+ 'web/static/src/libs/fontawesome/css/font-awesome.css',
+ 'awesome_owl/static/src/**/*',
+ ],
+ },
+ 'license': 'AGPL-3'
+}
diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py
new file mode 100644
index 00000000000..457bae27e11
--- /dev/null
+++ b/awesome_owl/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
\ No newline at end of file
diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py
new file mode 100644
index 00000000000..bccfd6fe283
--- /dev/null
+++ b/awesome_owl/controllers/controllers.py
@@ -0,0 +1,10 @@
+from odoo import http
+from odoo.http import request, route
+
+class OwlPlayground(http.Controller):
+ @http.route(['/awesome_owl'], type='http', auth='public')
+ def show_playground(self):
+ """
+ Renders the owl playground page
+ """
+ return request.render('awesome_owl.playground')
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..cd72da2c009
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,14 @@
+import { Component } from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.Card";
+ static props = {
+ title: String,
+ slots: {
+ type: Object,
+ shape: {
+ default: true
+ },
+ }
+ };
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..5c284e81757
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,13 @@
+
+
+
+