69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | awesome_tshirt.orders.list
77 | awesome_tshirt.order
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | awesome_tshirt.orders.gallery
92 | awesome_tshirt.order
93 |
94 |
95 |
96 |
97 |
98 |
99 | awesome_tshirt.orders.search
100 | awesome_tshirt.order
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | awesome_tshirt.res.partner.form
110 | res.partner
111 |
112 |
113 |
114 | customer_form_view
115 |
116 |
117 |
118 |
119 |
120 |
121 | T-shirt Orders
122 | awesome_tshirt.order
123 | kanban,tree,form,gallery
124 |
125 |
126 |
127 | Dashboard
128 | awesome_tshirt.dashboard
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/exercises_1_owl.md:
--------------------------------------------------------------------------------
1 | # Part 1: Owl Framework 🦉
2 |
3 | Components are the basic UI building blocks in Odoo. Odoo components are made
4 | with the Owl framework, which is a component system custom made for Odoo.
5 |
6 | Let us take some time to get used to Owl itself. The exercises in this section
7 | may be artificial, but their purpose is to understand and practice the basic
8 | notions of Owl quickly
9 |
10 | 
11 |
12 | ## 1.1 A `Counter` Component
13 |
14 | Let us see first how to create a sub component.
15 |
16 | - Extract the `counter` code from the `AwesomeDashboard` component into a new
17 | component `Counter`.
18 | - You can do it in the same file first, but once it's done, update your code to
19 | move the `Counter` in its own file. (don't forget the `/** @odoo-module **/`)
20 | - also, make sure the template is in its own file, with the same name.
21 |
22 |
23 | Preview
24 |
25 | 
26 |
27 |
28 |
29 |
30 | Resources
31 |
32 | - [owl: github repository](https://github.com/odoo/owl)
33 | - [owl: documentation](https://github.com/odoo/owl#documentation)
34 | - [owl: using sub components](https://github.com/odoo/owl/blob/master/doc/reference/component.md#sub-components)
35 | - [odoo: documentation on assets](https://www.odoo.com/documentation/master/developer/reference/frontend/assets.html)
36 |
37 |
38 |
39 | ## 1.2 A `Todo` Component
40 |
41 | We will modify the `AwesomeDashboard` component to keep track of a list of todos.
42 | This will be done incrementally in multiple exercises, that will introduce
43 | various concepts.
44 |
45 | First, let's create a `Todo` component that display a task, which is described by an id (number), a description (string), and a status `done` (boolean). For
46 | example:
47 |
48 | ```js
49 | { id: 3, description: "buy milk", done: false }
50 | ```
51 |
52 | - create a `Todo` component that receive a `todo` in props, and display it: it
53 | should show something like `3. buy milk`
54 | - also, add the bootstrap classes `text-muted` and `text-decoration-line-through` on the task if it is done
55 | - modify `AwesomeDashboard` to display a `Todo` component, with some hardcoded
56 | props to test it first. For example:
57 | ```js
58 | setup() {
59 | ...
60 | this.todo = { id: 3, description: "buy milk", done: false };
61 | }
62 | ```
63 |
64 |
65 | Preview
66 |
67 | 
68 |
69 |
70 |
71 |
72 | Resources
73 |
74 | - [owl: props](https://github.com/odoo/owl/blob/master/doc/reference/props.md)
75 | - [owl: Dynamic attributes](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#dynamic-attributes)
76 | - [owl: Dynamic class attributes](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#dynamic-class-attribute)
77 |
78 |
79 |
80 | ## 1.3 Props Validation
81 |
82 | The `Todo` component has an implicit API: it expects to receive in its props the
83 | description of a todo in a specified format: `id`, `description` and `done`.
84 | Let us make that API more explicit: we can add a props definition that will let
85 | Owl perform a validation step in dev mode. It is a good practice to do that for
86 | every component.
87 |
88 | - Add props validation to Todo
89 | - make sure it fails in dev mode
90 |
91 |
92 | Resources
93 |
94 | - [owl: props validation](https://github.com/odoo/owl/blob/master/doc/reference/props.md#props-validation)
95 | - [odoo: debug mode](https://www.odoo.com/documentation/master/developer/reference/frontend/framework_overview.html#debug-mode)
96 | - [odoo: activate debug mode](https://www.odoo.com/documentation/master/applications/general/developer_mode.html#developer-mode)
97 |
98 |
99 |
100 | ## 1.4 A List of todos
101 |
102 | Now, let us display a list of todos instead of just one todo. For now, we can
103 | still hardcode the list.
104 |
105 | - Change the code to display a list of todos, instead of just one, and use
106 | `t-foreach` in the template
107 | - think about how it should be keyed
108 |
109 |
110 | Preview
111 |
112 | 
113 |
114 |
115 |
116 |
117 | Resources
118 |
119 | - [owl: t-foreach](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#loops)
120 |
121 |
122 |
123 | ## 1.5 Adding a todo
124 |
125 | So far, the todos in our list are hardcoded. Let us make it more useful by allowing
126 | the user to add a todo to the list.
127 |
128 | - add input above the task list with placeholder `Enter a new task`
129 | - add an event handler on the `keyup` event named `addTodo`
130 | - implement `addTodo` to check if enter was pressed (`ev.keyCode === 13`), and
131 | in that case, create a new todo with the current content of the input as description
132 | - make sure it has a unique id (can be just a counter)
133 | - then clear the input of all content
134 | - bonus point: don't do anything if input is empty
135 |
136 | Notice that nothing updates in the UI: this is because Owl does not know that it
137 | should update the UI. This can be fixed by wrapping the todo list in a `useState`:
138 |
139 | ```js
140 | this.todos = useState([]);
141 | ```
142 |
143 |
144 | Preview
145 |
146 | 
147 |
148 |
149 |
150 |
151 | Resources
152 |
153 | - [owl: reactivity](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md)
154 |
155 |
156 |
157 | ## 1.6 Focusing the input
158 |
159 | Let's see how we can access the DOM with `t-ref`. For this exercise, we want to
160 | focus the `input` from the previous exercise whenever the dashboard is mounted.
161 |
162 | Bonus point: extract the code into a specialized hook `useAutofocus`
163 |
164 |
165 | Resources
166 |
167 | - [owl: component lifecycle](https://github.com/odoo/owl/blob/master/doc/reference/component.md#lifecycle)
168 | - [owl: hooks](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md)
169 | - [owl: useRef](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useref)
170 |
171 |
172 |
173 | ## 1.7 Toggling todos
174 |
175 | Now, let's add a new feature: mark a todo as completed. This is actually
176 | trickier than one might think: the owner of the state is not the same as the
177 | component that displays it. So, the `Todo` component need to communicate to its
178 | parent that the todo state needs to be toggled. One classic way to do this is
179 | by using a callback prop `toggleState`
180 |
181 | - add an input of type="checkbox" before the id of the task, which is checked if
182 | the `done` state is true,
183 | - add a callback props `toggleState`
184 | - add a `click` event handler on the input in `Todo`, and make sure it calls
185 | the `toggleState` function with the todo id.
186 | - make it work!
187 |
188 |
189 | Preview
190 |
191 | 
192 |
193 |
194 |
195 |
196 | Resources
197 |
198 | - [owl: binding function props](https://github.com/odoo/owl/blob/master/doc/reference/props.md#binding-function-props)
199 |
200 |
201 |
202 | ## 1.8 Deleting todos
203 |
204 | The final touch is to let the user delete a todo.
205 |
206 | - add a new callback prop `removeTodo`
207 | - add a `` in the Todo component
208 | - whenever the user clicks on it, it should call the `removeTodo` method
209 | - make it work as expected
210 |
211 |
212 | Preview
213 |
214 | 
215 |
216 |
217 |
218 | ## 1.9 Generic components with Slots
219 |
220 | Owl has a powerful slot system to allow you to write generic components. This is
221 | useful to factorize common layout between different parts of the interface.
222 |
223 | - write a `Card` component, using the following bootstrap html structure:
224 |
225 | ```html
226 |
227 |
228 |
229 |
Card title
230 |
231 | Some quick example text to build on the card title and make up the bulk
232 | of the card's content.
233 |
237 | ```
238 |
239 | - this component should have two slots: one slot for the title, and one for
240 | the content (the default slot). For example, here is how one could use it:
241 |
242 | ```xml
243 |
244 | Card title
245 |
Some quick example text...
246 | Go somewhere
247 |
248 |
249 | ```
250 |
251 | - bonus point: if the `title` slot is not given, the `h5` should not be
252 | rendered at all
253 |
254 |
255 | Preview
256 |
257 | 
258 |
259 |
260 |
261 |
262 | Resources
263 |
264 | - [owl: slots](https://github.com/odoo/owl/blob/master/doc/reference/slots.md)
265 | - [owl: slot props](https://github.com/odoo/owl/blob/master/doc/reference/slots.md#slots-and-props)
266 | - [bootstrap: documentation on cards](https://getbootstrap.com/docs/5.2/components/card/)
267 |
268 |
269 |
270 | ## 1.10 Miscellaneous small tasks
271 |
272 | - add prop validation on the Card component
273 | - try to express in the prop validation system that it requires a `default`
274 | slot, and an optional `title` slot
275 |
276 |
277 | Resources
278 |
279 | - [owl: props validation](https://github.com/odoo/owl/blob/master/doc/reference/props.md#props-validation)
280 |
281 |
282 |
--------------------------------------------------------------------------------
/exercises_2_web_framework.md:
--------------------------------------------------------------------------------
1 | # Part 2: Odoo web Framework
2 |
3 | We will now learn to use the Odoo javascript framework. In this module, we will
4 | improve our Awesome dashboard. This will be a good opportunity to discover many useful features.
5 |
6 | 
7 |
8 | ## 2.1 A new Layout
9 |
10 | Most screens in the Odoo web client uses a common layout: a control panel on top,
11 | with some buttons, and a main content zone just below. This is done using a
12 | `Layout` component, available in `@web/search/layout`.
13 |
14 | - update the `AwesomeDashboard` component to use the `Layout` component
15 |
16 | Note that the `Layout` component has been primarily designed with the current
17 | views in mind. It is kind of awkward to use in another context, so it is highly
18 | suggested to have a look at how it is done in the link provided in resources.
19 |
20 |
21 | Preview
22 |
23 | 
24 |
25 |
26 |
27 |
28 | Resources
29 |
30 | - [example: use of Layout in client action](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/webclient/actions/reports/report_action.js) and [template](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/webclient/actions/reports/report_action.xml)
31 | - [example: use of Layout in kanban view](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/views/kanban/kanban_controller.xml)
32 | - [code: Layout component](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/search/layout.js)
33 |
34 |
35 |
36 | ## 2.2 Add some buttons for quick navigation
37 |
38 | Bafien Ckinpaers want buttons for easy access to common views in Odoo. Let us
39 | add three buttons in the control panel bottom left zone:
40 |
41 | - a button `Customers`, which opens a kanban view with all customers (this action already exists, so you should use its xml id)
42 | - a button `New Orders`, which opens a list view with all orders created in the last 7 days
43 | - a button `Cancelled Order`, which opens a list of all orders created in the last 7 days, but already cancelled
44 |
45 |
46 | Preview
47 |
48 | 
49 |
50 |
51 |
52 |
53 | Resources
54 |
55 | - [odoo: page on services](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html)
56 | - [example: doaction use](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/account/static/src/components/journal_dashboard_activity/journal_dashboard_activity.js#L35)
57 | - [data: action displaying res.partner](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/odoo/addons/base/views/res_partner_views.xml#L511)
58 | - [code: action service](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/webclient/actions/action_service.js#L1456)
59 |
60 |
61 |
62 | ## 2.3 Call the server, add some statistics
63 |
64 | Let's improve the dashboard by adding a few cards (see the `Card` component
65 | made in the Owl training) containing a few statistics. There is a route
66 | `/awesome_tshirt/statistics` that will perform some computations and return an
67 | object containing some useful informations.
68 |
69 | - change `Dashboard` so that it uses the `rpc` service
70 | - call the statistics route in the `onWillStart` hook
71 | - display a few cards in the dashboard containing:
72 | - number of new orders this month
73 | - total amount of new orders this month
74 | - average amount of t-shirt by order this month
75 | - number of cancelled orders this month
76 | - average time for an order to go from 'new' to 'sent' or 'cancelled'
77 |
78 |
79 | Preview
80 |
81 | 
82 |
83 |
84 |
85 |
86 | Resources
87 |
88 | - [odoo: rpc service](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html#rpc-service)
89 | - [code: rpc service](https://github.com/odoo/odoo/blob/master/addons/web/static/src/core/network/rpc_service.js)
90 | - [example: calling a route in willStart](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/lunch/static/src/views/search_model.js#L21)
91 |
92 |
93 |
94 | ## 2.4 Cache network calls, create a service
95 |
96 | If you open your browser dev tools, in the network tabs, you will probably see
97 | that the call to `/awesome_tshirt/statistics` is done every time the client
98 | action is displayed. This is because the `onWillStart` hook is called each
99 | time the `Dashboard` component is mounted. But in this case, we probably would
100 | prefer to do it only the first time, so we actually need to maintain some state
101 | outside of the `Dashboard` component. This is a nice use case for a service!
102 |
103 | - implements a new `awesome_tshirt.statistics` service
104 | - it should provide a function `loadStatistics` that, once called, performs the
105 | actual rpc, and always return the same information
106 | - maybe use the `memoize` utility function from `@web/core/utils/functions`
107 | - use it in the `Dashboard` component
108 | - check that it works as expected
109 |
110 |
111 | Resources
112 |
113 | - [example: simple service](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/core/network/http_service.js)
114 | - [example: service with a dependency](https://github.com/odoo/odoo/blob/baecd946a09b5744f9cb60318563a9720c5475f9/addons/web/static/src/core/user_service.js)
115 | - [code: memoize function](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/core/utils/functions.js#L11)
116 |
117 |
118 |
119 | ## 2.5 Display a pie chart
120 |
121 | Everyone likes charts (!), so let us add a pie chart in our dashboard, which
122 | displays the proportions of t-shirts sold for each size: S/M/L/XL/XXL
123 |
124 | For this exercise, we will use Chart.js. It is the chart library used by the
125 | graph view. However, it is not loaded by default, so we will need to either add
126 | it to our assets bundle, or lazy load it (usually better, since our users will not have
127 | to load the chartjs code every time if they don't need it).
128 |
129 | - load chartjs
130 | - in a `Card` (from previous exercises), display a pie chart in the dashboard that displays the correct quantity for each
131 | sold tshirts in each size (that information is available in the statistics route)
132 |
133 |
134 | Preview
135 |
136 | 
137 |
138 |
139 |
140 |
141 | Resources
142 |
143 | - [Chart.js website](https://www.chartjs.org/)
144 | - [Chart.js documentation on pie chart](https://www.chartjs.org/docs/latest/samples/other-charts/pie.html)
145 | - [example: lazy loading a js file](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/views/graph/graph_renderer.js#L53)
146 | - [code: loadJs function](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/core/assets.js#L23)
147 | - [example: rendering a chart in a component](https://github.com/odoo/odoo/blob/3eb1660e7bee4c5b2fe63f82daad5f4acbea2dd2/addons/web/static/src/views/graph/graph_renderer.js#L630)
148 |
149 |
150 |
151 | ## 2.6 Misc
152 |
153 | Here is a list of some small improvements you could try to do if you have the
154 | time:
155 |
156 | - make sure your application can be translated (with `env._t`)
157 | - clicking on a section of the pie chart should open a list view of all orders
158 | which have the corresponding size
159 | - add a scss file and see if you can change the background color of the dashboard action
160 |
161 |
162 | Preview
163 |
164 | 
165 |
166 |
167 |
168 |
169 | Resources
170 |
171 | - [odoo: translating modules (slightly outdated)](https://www.odoo.com/documentation/master/developer/howtos/translations.html)
172 | - [example: use of env.\_t function](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/account/static/src/components/bills_upload/bills_upload.js#L64)
173 | - [code: translation code in web/](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/core/l10n/translation.js#L16)
174 |
175 |
176 |
--------------------------------------------------------------------------------
/exercises_3_fields_views.md:
--------------------------------------------------------------------------------
1 | # Part 3: Fields and Views
2 |
3 | Fields and views are among the most important concepts in the Odoo user interface.
4 | They are key to many important user interactions, and should therefore work
5 | perfectly.
6 |
7 | ## 3.1 An `image_preview` field
8 |
9 | Each new order on the website will be created as an `awesome_tshirt.order`. This
10 | model has a `image_url` field (of type char), which is currently only visible as
11 | a string. We want to be able to see it in the form view.
12 |
13 | For this task, we need to create a new field component `image_preview`. This
14 | component is specified as follows:
15 |
16 | in readonly mode, it is only an image tag with the correct src if field is set.
17 | In edit mode, it also behaves like classical char fields (you can use the `CharField`
18 | in your template by passing it props): an `input` should be displayed with the
19 | text value of the field, so it can be edited
20 |
21 | - register your field in the proper registry
22 | - update the arch of the form view to use your new field.
23 |
24 | Note: it is possible to solve this exercise by inheriting `CharField`, but the
25 | goal of this exercise is to create a field from scratch.
26 |
27 |
28 | Resources
29 |
30 | - [code: CharField](https://github.com/odoo/odoo/blob/baecd946a09b5744f9cb60318563a9720c5475f9/addons/web/static/src/views/fields/char/char_field.js)
31 | - [owl: `t-props` directive](https://github.com/odoo/owl/blob/master/doc/reference/props.md#dynamic-props)
32 |
33 |
34 |
35 | ## 3.2 Improving the image_preview field
36 |
37 | We want to improve the widget of the previous task to help the staff recognize
38 | orders for which some action should be done. In particular, we want to display a warning 'MISSING TSHIRT DESIGN' in red, if there is no image url
39 | specified on the order.
40 |
41 | ## 3.3 Customizing a field component
42 |
43 | Let's see how to use inheritance to extend an existing component.
44 |
45 | There is a `is_late`, readonly, boolean field on the task model. That would be
46 | a useful information to see on the list/kanban/view. Then, let us say that
47 | we want to add a red word `Late!` next to it whenever it is set to true.
48 |
49 | - create a new field `late_order_boolean` inheriting from `BooleanField`
50 | - use it in the list/kanban/form view
51 | - modify it to add a red `Late` next to it, as requested
52 |
53 |
54 | Preview
55 |
56 | 
57 |
58 |
59 |
60 |
61 | Resources
62 |
63 | - [example: field inheriting another (js)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/account/static/src/components/account_type_selection/account_type_selection.js)
64 | - [example: field inheriting another (xml)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/account/static/src/components/account_type_selection/account_type_selection.xml)
65 | - [odoo: doc on xpaths](https://www.odoo.com/documentation/master/developer/reference/backend/views.html#inheritance-specs)
66 |
67 |
68 |
69 | ## 3.4 Message for some customers
70 |
71 | Odoo form views support a `widget` api, which is like a field, but more generic.
72 | It is useful to insert arbitrary components in the form view. Let us see how we
73 | can use it.
74 |
75 | For a super efficient workflow, we would like to display a message/warning box
76 | with some information in the form view, with specific messages depending on some
77 | conditions:
78 |
79 | - if the image_url field is not set, it should display "No image"
80 | - if the amount of the order is higher than 100 euros, it should display "Add promotional material"
81 |
82 | Make sure that your widget is updated in real time.
83 |
84 | Note: extra challenge for this task: the feature is not documented.
85 |
86 |
87 | Resources
88 |
89 | - [example: using tag in a form view](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/calendar/views/calendar_views.xml#L197)
90 | - [example: implementation of widget (js)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/web/static/src/views/widgets/week_days/week_days.js)
91 | - [example: implementation of widget (xml)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/web/static/src/views/widgets/week_days/week_days.xml)
92 |
93 |
94 |
95 | ## 3.5 Use `markup`
96 |
97 | Let's see how we can display raw html in a template. Before, there was a `t-raw`
98 | directive that would just output anything as html. This was unsafe, and has been
99 | replaced by a `t-out` directive, that acts like a `t-esc` unless the data has
100 | been marked explicitely with a `markup` function.
101 |
102 | - modify the previous exercise to put the `image` and `material` words in bold
103 | - the warnings should be markuped, and the template should be modified to use `t-out`
104 |
105 | This is an example of a safe use of `t-out`, since the string is static.
106 |
107 |
108 | Preview
109 |
110 | 
111 |
112 |
113 |
114 |
115 | Resources
116 |
117 | - [owl: doc on `t-out`](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#outputting-data)
118 |
119 |
120 |
121 | ## 3.6 Add buttons in control panel
122 |
123 | In practice, once the t-shirt order is printed, we need to print a label to put
124 | on the package. To help with that, let us add a button in the order form view control panel:
125 |
126 | - create a customized form view extending the web form view and register it as `awesome_tshirt.order_form_view`
127 | - add a `js_class` attribute to the arch of the form view so Odoo will load it
128 | - create a new template inheriting from the form controller template to add a button after the create button
129 | - add a button, clicking on this button should call the method `print_label` from the model
130 | `awesome_tshirt.order`, with the proper id (note: `print_label` is a mock method, it only display a message in the logs)
131 | - it should be disabled if the current order is in `create` mode (i.e., it does not exist yet)
132 | - it should be displayed as a primary button if the customer is properly set and if the task stage is `printed`. Otherwise, it is only displayed as a secondary button.
133 |
134 | Note: you can use the `orm` service instead of the `rpc` service. It provides a
135 | higher level interface when interacting with models.
136 |
137 | Bonus point: clicking twice on the button should not trigger 2 rpcs
138 |
139 |
140 | Preview
141 |
142 | 
143 |
144 |
145 |
146 |
147 | Resources
148 |
149 | - [example: extending a view (js)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/mass_mailing/static/src/views/mailing_contact_view_list.js)
150 | - [example: extending a view (xml)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/mass_mailing/static/src/views/mass_mailing_views.xml)
151 | - [example: using a `js_class` attribute](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/mass_mailing/views/mailing_contact_views.xml#L44)
152 | - [code: orm service](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/web/static/src/core/orm_service.js)
153 | - [example: using the orm service](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/account/static/src/components/open_move_widget/open_move_widget.js)
154 |
155 |
156 |
157 | ## 3.7 Auto reload the kanban view
158 |
159 | Bafien is upset: he wants to see the kanban view of the tshirt orders on his
160 | external monitor, but it needs to be up to date. He is tired of clicking on
161 | the `refresh` icon every 30s, so he tasked you to find a way to do it automatically.
162 |
163 | Just like the previous exercise, that kind of customization requires creating a
164 | new javascript view.
165 |
166 | - extend the kanban view/controller to reload its data every minute
167 | - register it in the view registry, under the `awesome_tshirt.autoreloadkanban`
168 | - use it in the arch of the kanban view (with the `js_class` attribute)
169 |
170 | Note: make sure that if you use a `setInterval`, or something similar, that it is
171 | properly cancelled when your component is unmounted. Otherwise, you would introduce
172 | a memory leak.
173 |
--------------------------------------------------------------------------------
/exercises_4_misc.md:
--------------------------------------------------------------------------------
1 | # Part 4: Miscenalleous tasks
2 |
3 | 
4 |
5 | ## 4.1 Interacting with the notification system
6 |
7 | Note: this task depends on the previous exercise.
8 |
9 | After using the `Print Label` for some t-shirt tasks, it is apparent that there
10 | should be some feedback that the `print_label` action is completed (or failed,
11 | for example, the printer is not connected or ran out of paper).
12 |
13 | - display a notification message when the action is completed succesfully, and a
14 | warning if it failed
15 | - if it failed, the notification should be permanent.
16 |
17 |
18 | Preview
19 |
20 | 
21 |
22 |
23 |
24 |
25 | Resources
26 |
27 | - [odoo: notification service](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html#notification-service)
28 | - [example of code using the notification service](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/web/static/src/views/fields/image_url/image_url_field.js)
29 |
30 |
31 |
32 | ## 4.2 Add a systray item
33 |
34 | Our beloved leader want to keep a close eyes on new orders. He want to see
35 | the number of new, unprocessed orders at all time. Let's do that with a systray
36 | item.
37 |
38 | - create a systray component that connects to the statistics service we made
39 | previously
40 | - use it to display the number of new orders
41 | - clicking on it should open a list view with all of these orders
42 | - bonus point: avoid doing the initial rpc by adding the information to the
43 | session info
44 |
45 |
46 | Preview
47 |
48 | 
49 |
50 |
51 |
52 |
53 | Resources
54 |
55 | - [odoo: systray registry](https://www.odoo.com/documentation/master/developer/reference/frontend/registries.html#systray-registry)
56 | - [example: systray item](https://github.com/odoo/odoo/blob/cbdea4010ede6203f5f49d08d5a3bc44f2ff89e8/addons/web/static/src/webclient/user_menu/user_menu.js)
57 | - [example: adding some information to the "session info"](https://github.com/odoo/odoo/blob/cbdea4010ede6203f5f49d08d5a3bc44f2ff89e8/addons/barcodes/models/ir_http.py)
58 | - [example: reading the session information](https://github.com/odoo/odoo/blob/cbdea4010ede6203f5f49d08d5a3bc44f2ff89e8/addons/barcodes/static/src/barcode_service.js#L5)
59 |
60 |
61 |
62 | ## 4.3 Real life update
63 |
64 | So far, the systray item from above does not update unless the user refreshes
65 | the browser. Let us do that by calling periodically (for example, every minute)
66 | the server to reload the information
67 |
68 | - modify the systray item code to get its data from the tshirt service
69 | - the tshirt service should periodically reload its data
70 |
71 | Now the question arises: how is the systray item notified that it should rerender
72 | itself? It can be done in various ways, but for this training, we will chose to
73 | use the most _declarative_ approach:
74 |
75 | - modify the tshirt service to return a reactive object (see resources). Reloading
76 | data should update the reactive object in place
77 | - the systray item can then perform a `useState` on the service return value
78 | - this is not really necessary, but you can also _package_ the calls to `useService` and `useState` in a custom hook `useStatistics`
79 |
80 |
81 | Resources
82 |
83 | - [owl: page on reactivity](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md)
84 | - [owl: documentation on `reactive`](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md#reactive)
85 | - [example: use of reactive in a service](https://github.com/odoo/odoo/blob/3eb1660e7bee4c5b2fe63f82daad5f4acbea2dd2/addons/web/static/src/core/debug/profiling/profiling_service.js#L30)
86 |
87 |
88 |
89 | ## 4.4 Add a command to the command palette
90 |
91 | Now, let us see how we can interact with the command palette. Our staff sometimes
92 | need to work on the image from a tshirt order. Let us modify the image preview
93 | field (from a previous exercise) to add a command to the command palette to
94 | open the image in a new browser tab (or window)
95 |
96 | Make sure that the command is only active whenever a field preview is visible
97 | in the screen.
98 |
99 |
100 | Preview
101 |
102 | 
103 |
104 |
105 |
106 | Resources
107 |
108 | - [example: using the `useCommand` hook](https://github.com/odoo/odoo/blob/cbdea4010ede6203f5f49d08d5a3bc44f2ff89e8/addons/web/static/src/core/debug/debug_menu.js#L15)
109 | - [code: command service](https://github.com/odoo/odoo/blob/master/addons/web/static/src/core/commands/command_service.js)
110 |
111 |
112 |
113 | ## 4.5 Monkey patching a component
114 |
115 | Often, it is possible to do what we want by using existing extension points that allow
116 | customization, such as registering something in a registry. But it happens that
117 | we want to modify something that has no such mechanism. In that case, we have to
118 | fall back on a less safe form of customization: monkey patching. Almost everything
119 | in odoo can be monkey patched.
120 |
121 | Bafien, our beloved leader, heard that employees perform better if they are
122 | constantly being watched. Since he is not able to be there in person for each
123 | and every one of his employees, he tasked you with the following: update the
124 | Odoo user interface to add a blinking red eye in the control panel. Clicking on
125 | that eye should open a dialog with the following message: `Bafien is watching you. This interaction is recorded and may be used in legal proceedings if necessary. Do you agree to these terms?`.
126 |
127 | - create `control_panel_patch.js` (and css/xml) files
128 | - patch the ControlPanel template to add some icon next to the breadcrumbs
129 | (ou may want to use the `fa-eye` or `fa-eyes` icon).
130 |
131 | Note that there are two ways to inherit a template with a xpath: `t-inherit-mode="primary"` (creating a new independant template with the modification), and `t-inherit-mode="extension"` (modifying in place the template)
132 |
133 |
134 | Maybe use this css
135 |
136 | ```css
137 | .blink {
138 | animation: blink-animation 1s steps(5, start) infinite;
139 | -webkit-animation: blink-animation 1s steps(5, start) infinite;
140 | }
141 | @keyframes blink-animation {
142 | to {
143 | visibility: hidden;
144 | }
145 | }
146 | @-webkit-keyframes blink-animation {
147 | to {
148 | visibility: hidden;
149 | }
150 | }
151 | ```
152 |
153 |
154 |
155 | Make sure it is visible in all views!
156 |
157 | - import the `ControlPanel` component and the `patch` function
158 | - update the code to display the message on click by using the dialog service
159 | (you can use the `ConfirmationDialog`)
160 |
161 |
162 | Preview
163 |
164 | 
165 | 
166 |
167 |
168 |
169 |
170 | Resources
171 |
172 | - [odoo: patching code](https://www.odoo.com/documentation/master/developer/reference/frontend/patching_code.html)
173 | - [code: patch function](https://github.com/odoo/odoo/blob/f42110cbcd9edbbf827e5d36d6cd4f693452c747/addons/web/static/src/core/utils/patch.js#L16)
174 | - [code: ControlPanel component](https://github.com/odoo/odoo/blob/f42110cbcd9edbbf827e5d36d6cd4f693452c747/addons/web/static/src/search/control_panel/control_panel.js)
175 | - [font awesome website](https://fontawesome.com/)
176 | - [code: dialog service](https://github.com/odoo/odoo/blob/f42110cbcd9edbbf827e5d36d6cd4f693452c747/addons/web/static/src/core/dialog/dialog_service.js)
177 | - [code: ConfirmationDialog](https://github.com/odoo/odoo/blob/f42110cbcd9edbbf827e5d36d6cd4f693452c747/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js)
178 | - [example: using the dialog service](https://github.com/odoo/odoo/blob/f42110cbcd9edbbf827e5d36d6cd4f693452c747/addons/board/static/src/board_controller.js#L88)
179 | - [example: xpath with `t-inherit-mode="primary"`](https://github.com/odoo/odoo/blob/3eb1660e7bee4c5b2fe63f82daad5f4acbea2dd2/addons/account/static/src/components/account_move_form/account_move_form_notebook.xml#L4)
180 | - [example: xpath with `t-inherit-mode="extension"`](https://github.com/odoo/odoo/blob/16.0/addons/calendar/static/src/components/activity/activity.xml#L4)
181 |
182 |
183 |
184 | ## 4.6 Fetching orders from a customer
185 |
186 | Let's see how to use some standard components to build a powerful feature,
187 | combining autocomplete, fetching data, fuzzy lookup. We will add an input
188 | in our dashboard to easily search all orders from a given customer.
189 |
190 | - update the `tshirt_service` to add a method `loadCustomers`, which returns
191 | a promise that returns the list of all customers (and only performs the call
192 | once)
193 | - import the `Autocomplete` component from `@web/core/autocomplete/autocomplete`
194 | - add it to the dashboard, next to the buttons in the controlpanel.
195 | - update the code to fetch the list of customers with the tshirt_service, and display it in the
196 | autocomplete component, filtered by the fuzzyLookup method.
197 |
198 |
199 | Preview
200 |
201 | 
202 |
203 |
204 |
205 | ## 4.7 Reintroduce Kitten Mode
206 |
207 | Let us add a special mode to Odoo: whenever the url contains `kitten=1`, we will
208 | display a kitten in the background of odoo, because we like kittens.
209 |
210 | - create a `kitten_mode.js` file
211 | - create a `kitten` service, which should check the content of the active url
212 | hash (with the help of the `router` service)
213 | - if `kitten` is set, we are in kitten mode. This should add a class `.o-kitten-mode` on
214 | document body
215 | - add the following css in `kitten_mode.css`:
216 |
217 | ```css
218 | .o-kitten-mode {
219 | background-image: url(https://upload.wikimedia.org/wikipedia/commons/5/58/Mellow_kitten_%28Unsplash%29.jpg);
220 | background-size: cover;
221 | background-attachment: fixed;
222 | }
223 |
224 | .o-kitten-mode > * {
225 | opacity: 0.9;
226 | }
227 | ```
228 |
229 | - add a command to the command palette to toggle kitten mode. Toggling the
230 | kitten mode should toggle the `.o-kitten-mode` class and update the current
231 | url accordingly
232 |
233 |
234 | Preview
235 |
236 | 
237 |
238 |
239 |
240 |
241 | Resources
242 |
243 | - [odoo: router service](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html#router-service)
244 |
245 |
246 |
247 | ## 4.8 Lazy loading our dashboard
248 |
249 | This is not really necessary, but the exercise is interesting. Imagine that
250 | our awesome dashboard is a large application, with potentially multiple external
251 | libraries, lots of code/styles/templates. Also, suppose that the dashboard is
252 | only used by some users in some business flows, so we want to lazy load it, in
253 | order to speed up the loading of the web client in most cases.
254 |
255 | So, let us do that!
256 |
257 | - modify the manifest to create a new bundle `awesome_tshirt.dashboard`
258 | - add the `AwesomeDashboard` code to this bundle
259 | - remove it from the `web.assets_backend` bundle (so it is not loaded twice!)
260 |
261 | So far, we removed the dashboard from the main bundle, but it should now be
262 | lazily loaded. Right now, there is not client action registered in the action
263 | registry.
264 |
265 | - create a new file `dashboard_loader.js`
266 | - copy the code registering the awesomedashboard to the dashboard loader
267 | - register the awesomedashboard as a lazy_component
268 | - modify the code in dashboard_loader to use the LazyComponent
269 |
270 |
271 | Resources
272 |
273 | - [odoo: documentation on assets](https://www.odoo.com/documentation/master/developer/reference/frontend/assets.html)
274 | - [code: LazyComponent](https://github.com/odoo/odoo/blob/2971dc0a98bd263f06f79702700d32e5c1b87a17/addons/web/static/src/core/assets.js#L255)
275 |
276 |
277 |
--------------------------------------------------------------------------------
/exercises_5_custom_kanban_view.md:
--------------------------------------------------------------------------------
1 | # Part 5: Custom Kanban View (hard)
2 |
3 | This is a more complicated project that will showcase some non trivial aspects
4 | of the framework. The goal is to practice composing views, coordinating various
5 | aspects of the UI and doing it in a maintainable way.
6 |
7 | Bafien had the greatest idea ever (after the freeze!): a mix of a Kanban View
8 | and a list view would be perfect for your needs! In a nutshell, he wants a list
9 | of customers on the left of the task kanban view. When you click on a customer
10 | on the left sidebar, the kanban view on the right is filtered to only display orders
11 | linked to that customer.
12 |
13 | 
14 |
15 | ## 5.1 Create a new kanban view
16 |
17 | Since we are customizing the kanban view, let us start by extending it and using
18 | our extension in the kanban view for the tshirt orders
19 |
20 | - extend the kanban view
21 | - register it under the `awesome_tshirt.kanbanview_with_customers`
22 | - use it in the `js_class`
23 |
24 | ## 5.2 Create a CustomerList component
25 |
26 | We will need to display a list of customers, so we might as well create the
27 | component.
28 |
29 | - create a `CustomerList` component (which just display a div with some text for now)
30 | - it should have a `selectCustomer` prop
31 | - create a new template extending (xpath) the kanban controller template to add
32 | the `CustomerList` next to the kanban renderer, give it an empty function as `selectCustomer` for now
33 | - subclass the kanban controller to add `CustomerList` in its sub components
34 | - make sure you see your component in the kanban view
35 |
36 |
37 | Preview
38 |
39 | 
40 |
41 |
42 |
43 | ## 5.3 Load and display data
44 |
45 | - modify the `CustomerList` component to fetch a list of all customers in its willStart
46 | - display it in the template in a `t-foreach`
47 | - add an event handler on click
48 | - whenever a customer is selected, call the `selectCustomer` function prop
49 |
50 |
51 | Preview
52 |
53 | 
54 |
55 |
56 |
57 | ## 5.4 Update the main kanban view
58 |
59 | - implement `selectCustomer` in the kanban controller to add the proper domain
60 | - modify the template to give the real function to the CustomerList `selectCustomer` prop
61 |
62 | Since it is not trivial to interact with the search view, here is a quick snippet to
63 | help:
64 |
65 | ```js
66 | selectCustomer(customer_id, customer_name) {
67 | this.env.searchModel.setDomainParts({
68 | customer: {
69 | domain: [["customer_id", "=", customer_id]],
70 | facetLabel: customer_name,
71 | },
72 | });
73 | }
74 | ```
75 |
76 |
77 | Preview
78 |
79 | 
80 |
81 |
82 |
83 | ## 5.5 Only display customers which have an active order
84 |
85 | There is a `has_active_order` field on `res.partner`. Let us allow the user to
86 | filter results on customers with an active order.
87 |
88 | - add an input of type checkbox in the `CustomerList` component, with a label `Active customers` next to it
89 | - changing the value of the checkbox should filter the list on customers with an
90 | active order
91 |
92 | ## 5.6 Add a search bar to Customer List
93 |
94 | Add an input above the customer list that allows the user to enter a string and
95 | to filter the displayed customers, according to their name. Note that you can
96 | use the `fuzzyLookup` function to perform the filter.
97 |
98 |
99 | Preview
100 |
101 | 
102 |
103 |
104 |
105 |
106 | Resources
107 |
108 | - [code: fuzzylookup function](https://github.com/odoo/odoo/blob/cbdea4010ede6203f5f49d08d5a3bc44f2ff89e8/addons/web/static/src/core/utils/search.js#L43)
109 | - [example: using fuzzyLookup](https://github.com/odoo/odoo/blob/cbdea4010ede6203f5f49d08d5a3bc44f2ff89e8/addons/web/static/tests/core/utils/search_test.js#L17)
110 |
111 |
112 |
113 | ## 5.7 Refactor the code to use `t-model`
114 |
115 | To solve the previous two exercises, it is likely that you used an event listener
116 | on the inputs. Let us see how we could do it in a more declarative way, with the
117 | `t-model` directive.
118 |
119 | - make sure you have a reactive object that represents the fact that the filter is active (so, something like `this.state = useState({ displayActiveCustomers: false, searchString: ''})`)
120 | - modify the code to add a getter `displayedCustomers` which returns the currently
121 | active list of customers
122 | - modify the template to use `t-model`
123 |
124 |
125 | Resources
126 |
127 | - [owl: documentation on `t-model`](https://github.com/odoo/owl/blob/master/doc/reference/input_bindings.md)
128 |
129 |
130 |
131 | ## 5.8 Paginate customers!
132 |
133 | - Add a `Pager` in the `CustomerList`, and only load/render the first 20 customers
134 | - whenever the pager is changed, the customer list should update accordingly.
135 |
136 | This is actually pretty hard, in particular in combination with the filtering
137 | done in the previous exercise. There are many edge cases to take into account.
138 |
139 |
140 | Preview
141 |
142 | 
143 |
144 |
145 |
146 |
147 | Resources
148 |
149 | - [odoo: Pager](https://www.odoo.com/documentation/master/developer/reference/frontend/owl_components.html#pager)
150 |
151 |
152 |
--------------------------------------------------------------------------------
/exercises_6_creating_views.md:
--------------------------------------------------------------------------------
1 | # Part 6: Making a view from scratch
2 |
3 | Let us see how one can create a new view, completely from scratch. In a way, it
4 | is not very difficult to do so, but there are no really good resources on how to
5 | do it. Note that most situations should be solved by either a customized
6 | existing view, or a client action.
7 |
8 | For this exercise, let's assume that we want to create a `gallery` view, which is
9 | a view that let us represent a set of records with a image field. In our
10 | Awesome Tshirt scenario, we would like to be able to see a set of t-shirts images.
11 |
12 | The problem could certainly be solved with a kanban view, but this means that it
13 | is not possible to have in the same action our normal kanban view and the gallery
14 | view.
15 |
16 | Let us make a gallery view. Each gallery view will be defined by a `image_field`
17 | attribute in its arch:
18 |
19 | ```xml
20 |
21 | ```
22 |
23 | 
24 |
25 | ## Setup
26 |
27 | Simply install the `awesome_gallery` addon. It contains the few server files
28 | necessary to add a new view.
29 |
30 | ## 6.1 Make a hello world view
31 |
32 | First step is to create a javascript implementation with a simple component.
33 |
34 | - create the `gallery_view.js`,`gallery_controller.js` and `gallery_controller.xml` files in `static/src`
35 | - in `gallery_controller.js`, implement a simple hello world component
36 | - in `gallery_view.js`, import the controller, create a view object, and register it
37 | in the view registry under the name `gallery`
38 | - add `gallery` as one of the view type in the orders action
39 | - make sure that you can see your hello world component when switching to the
40 | gallery view
41 |
42 |
43 | Preview
44 |
45 | 
46 | 
47 |
48 |
49 |
50 |
51 | Resources
52 |
53 | - [notes on view architecture](notes_views.md)
54 |
55 |
56 |
57 | ## 6.2 Use the Layout component
58 |
59 | So far, our gallery view does not look like a standard view. Let use the `Layout`
60 | component to have the standard features like other views.
61 |
62 | - import the `Layout` component and add it to the `components` of `GalleryController`
63 | - update the template to use `Layout` (it needs a `display` prop, which can be found in `props.display`),
64 |
65 |
66 | Preview
67 |
68 | 
69 |
70 |
71 |
72 | ## 6.3 Parse the arch
73 |
74 | For now, our gallery view does not do much. Let's start by reading the information
75 | contained in the arch of the view.
76 |
77 | - create a `ArchParser` file and class, it can inherit from `XMLParser` in `@web/core/utils/xml`
78 | - use it to read the `image_field` information,
79 | - update the `gallery` view code to add it to the props received by the controller
80 |
81 | Note that it is probably a little overkill to do it like that, since we basically
82 | only need to read one attribute from the arch, but it is a design that is used by
83 | every other odoo views, since it let us extract some upfront processing out of
84 | the controller.
85 |
86 |
87 | Resources
88 |
89 | - [example: graph arch parser](https://github.com/odoo/odoo/blob/master/addons/web/static/src/views/graph/graph_arch_parser.js)
90 |
91 |
92 |
93 | ## 6.4 Load some data
94 |
95 | Let us now get some real data.
96 |
97 | - add a `loadImages(domain) {...} ` method to the `GalleryController`. It should
98 | perform a `webSearchRead` call to fetch records corresponding to the domain,
99 | and use the `imageField` received in props
100 | - modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps`
101 | hooks
102 | - modify the template to display the data inside the default slot of the `Layout` component
103 |
104 | Note that the code loading data will be moved into a proper model in the next
105 | exercise.
106 |
107 |
108 | Preview
109 |
110 | 
111 |
112 |
113 |
114 | ## 6.5 Reorganize code
115 |
116 | Real views are a little bit more organized. This may be overkill in this example,
117 | but it is intended to learn how to structure code in Odoo. Also, this will scale
118 | better with changing requirements.
119 |
120 | - move all the model code in its own class: `GalleryModel`,
121 | - move all the rendering code in a `GalleryRenderer` component
122 | - adapt the `GalleryController` and the `gallery_view` to make it work
123 |
124 | ## 6.6 Display images
125 |
126 | Update the renderer to display images in a nice way (if the field is set). If
127 | the image_field is empty, display an empty box instead.
128 |
129 |
130 | Preview
131 |
132 | 
133 |
134 |
135 |
136 | ## 6.7 Switch to form view on click
137 |
138 | Update the renderer to react to a click on an image and switch to a form view
139 |
140 |
141 | Resources
142 |
143 | - [code: switchView function](https://github.com/odoo/odoo/blob/master/addons/web/static/src/webclient/actions/action_service.js#L1329)
144 |
145 |
146 |
147 | ## 6.8 Add an optional tooltip
148 |
149 | It is useful to have some additional information on mouse hover.
150 |
151 | - update the code to allow an optional additional attribute on the arch:
152 | ```xml
153 |
154 | ```
155 | - on mouse hover, display the content of the tooltip field (note that it should
156 | work if the field is a char field, a number field or a many2one field)
157 | - update the orders gallery view to add the customer as tooltip field.
158 |
159 |
160 | Preview
161 |
162 | 
163 |
164 |
165 |
166 |
167 | Resources
168 |
169 | - [code: tooltip hook](https://github.com/odoo/odoo/blob/master/addons/web/static/src/core/tooltip/tooltip_hook.js)
170 |
171 |
172 |
173 | ## 6.9 Add pagination
174 |
175 | Let's add a pager on the control panel, and manage all the pagination like
176 | a normal odoo view. Note that it is surprisingly difficult.
177 |
178 |
179 | Preview
180 |
181 | 
182 |
183 |
184 |
185 |
186 | Resources
187 |
188 | - [code: usePager hook](https://github.com/odoo/odoo/blob/master/addons/web/static/src/search/pager_hook.js)
189 |
190 |
191 |
192 | ## 6.10 Validating views
193 |
194 | We have a nice and useful view so far. But in real life, we may have issue with
195 | users incorrectly encoding the `arch` of their Gallery view: it is currently
196 | only an unstructured piece of xml.
197 |
198 | Let us add some validation! XML document in Odoo can be described with a rng
199 | file (relax ng), and then validated.
200 |
201 | - add a rng file that describes the current grammar:
202 | - a mandatory attribute `image_field`
203 | - an optional attribute: `tooltip_field`
204 | - add some code to make sure all views are validated against this rng file
205 | - while we are at it, let us make sure that `image_field` and `tooltip_field` are
206 | fields from the current model.
207 |
208 | Since validating rng file is not trivial, here is a snippet to help:
209 |
210 | ```python
211 | # -*- coding: utf-8 -*-
212 | import logging
213 | import os
214 |
215 | from lxml import etree
216 |
217 | from odoo.loglevels import ustr
218 | from odoo.tools import misc, view_validation
219 |
220 | _logger = logging.getLogger(__name__)
221 |
222 | _viewname_validator = None
223 |
224 | @view_validation.validate('viewname')
225 | def schema_viewname(arch, **kwargs):
226 | """ Check the gallery view against its schema
227 |
228 | :type arch: etree._Element
229 | """
230 | global _viewname_validator
231 |
232 | if _viewname_validator is None:
233 | with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f:
234 | _viewname_validator = etree.RelaxNG(etree.parse(f))
235 |
236 | if _viewname_validator.validate(arch):
237 | return True
238 |
239 | for error in _viewname_validator.error_log:
240 | _logger.error(ustr(error))
241 | return False
242 |
243 | ```
244 |
245 |
246 | Resources
247 |
248 | - [example: graph view rng file](https://github.com/odoo/odoo/blob/master/odoo/addons/base/rng/graph_view.rng)
249 |
250 |
251 |
--------------------------------------------------------------------------------
/exercises_7_testing.md:
--------------------------------------------------------------------------------
1 | # Part 7: Testing
2 |
3 | Automatically testing code is important when working on a codebase, to ensure that
4 | we don't introduce (too many) bugs or regressions. Let us see how to test our
5 | code.
6 |
7 | ## 7.1 Integration testing
8 |
9 | To make sure our application works as expected, we can test a business flow with
10 | a tour: this is a sequence of steps that we can execute. Each step wait until
11 | some desired DOM state is reached, then performs an action. If at some point, it
12 | is unable to go to the next step for a long time, the tour fails.
13 |
14 | Let's write a tour to ensure that it is possible to perform an tshirt order,
15 | from our public route
16 |
17 | - add a `/static/tests/tours` folder
18 | - add a `/static/tests/tours/order_flow.js` file
19 | - add a tour that performs the following steps:
20 | - open the `/awesome_tshirt/order` route
21 | - fill the order form
22 | - validate it
23 | - navigate to our webclient
24 | - open the list view for the the t-shirt order
25 | - check that our order can be found in the list
26 | - run the tour manually
27 | - add a python test to run it also
28 | - run the tour from the terminal
29 |
30 |
31 | Resources
32 |
33 | - [odoo: integration testing](https://www.odoo.com/documentation/15.0/developer/reference/backend/testing.html#integration-testing)
34 |
35 |
36 |
37 | ## 7.2 Unit testing a Component
38 |
39 | It is also useful to test independantly a component or a piece of code. Unit
40 | tests are useful to quickly locate an issue.
41 |
42 | - add a `static/tests/counter_tests.js` file
43 | - add a QUnit test that instantiate a counter, clicks on it, and make sure it is
44 | incremented
45 |
46 |
47 | Preview
48 |
49 | 
50 |
51 |
52 |
53 |
54 | Resources
55 |
56 | - [odoo: QUnit test suite](https://www.odoo.com/documentation/15.0/developer/reference/backend/testing.html#qunit-test-suite)
57 | - [example of testing an owl component](https://github.com/odoo/odoo/blob/master/addons/web/static/tests/core/checkbox_tests.js)
58 |
59 |
60 |
61 | ## 7.3 Unit testing our gallery view
62 |
63 | Note that this depends on our Gallery View from before.
64 |
65 | Many components needs more setup to be tested. In particular, we often need to
66 | mock some demo data. Let us see how to do that.
67 |
68 | - add a `/static/tests/gallery_view_tests.js` file
69 | - add a test that instantiate the gallery view with some demo data
70 | - add another test that check that when the user clicks on an image, it is switched
71 | to the form view of the corresponding order.
72 |
73 |
74 | Preview
75 |
76 | 
77 |
78 |
79 |
80 |
81 | Resources
82 |
83 | - [example of testing a list view](https://github.com/odoo/odoo/blob/master/addons/web/static/tests/views/list_view_tests.js)
84 |
85 |
86 |
--------------------------------------------------------------------------------
/images/1.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.0.png
--------------------------------------------------------------------------------
/images/1.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.1.png
--------------------------------------------------------------------------------
/images/1.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.2.png
--------------------------------------------------------------------------------
/images/1.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.4.png
--------------------------------------------------------------------------------
/images/1.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.5.png
--------------------------------------------------------------------------------
/images/1.7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.7.png
--------------------------------------------------------------------------------
/images/1.8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.8.png
--------------------------------------------------------------------------------
/images/1.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.9.png
--------------------------------------------------------------------------------
/images/2.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.0.png
--------------------------------------------------------------------------------
/images/2.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.1.png
--------------------------------------------------------------------------------
/images/2.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.2.png
--------------------------------------------------------------------------------
/images/2.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.3.png
--------------------------------------------------------------------------------
/images/2.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.5.png
--------------------------------------------------------------------------------
/images/2.6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.6.png
--------------------------------------------------------------------------------
/images/3.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/3.3.png
--------------------------------------------------------------------------------
/images/3.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/3.5.png
--------------------------------------------------------------------------------
/images/3.6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/3.6.png
--------------------------------------------------------------------------------
/images/4.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.1.png
--------------------------------------------------------------------------------
/images/4.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.2.png
--------------------------------------------------------------------------------
/images/4.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.4.png
--------------------------------------------------------------------------------
/images/4.5.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.5.1.png
--------------------------------------------------------------------------------
/images/4.5.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.5.2.png
--------------------------------------------------------------------------------
/images/4.6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.6.png
--------------------------------------------------------------------------------
/images/4.7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.7.png
--------------------------------------------------------------------------------
/images/5.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.0.png
--------------------------------------------------------------------------------
/images/5.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.2.png
--------------------------------------------------------------------------------
/images/5.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.3.png
--------------------------------------------------------------------------------
/images/5.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.4.png
--------------------------------------------------------------------------------
/images/5.6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.6.png
--------------------------------------------------------------------------------
/images/5.7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.7.png
--------------------------------------------------------------------------------
/images/6.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.0.png
--------------------------------------------------------------------------------
/images/6.1.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.1.1.png
--------------------------------------------------------------------------------
/images/6.1.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.1.2.png
--------------------------------------------------------------------------------
/images/6.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.2.png
--------------------------------------------------------------------------------
/images/6.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.4.png
--------------------------------------------------------------------------------
/images/6.6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.6.png
--------------------------------------------------------------------------------
/images/6.8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.8.png
--------------------------------------------------------------------------------
/images/6.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.9.png
--------------------------------------------------------------------------------
/images/7.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/7.2.png
--------------------------------------------------------------------------------
/images/7.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/7.3.png
--------------------------------------------------------------------------------
/notes_architecture.md:
--------------------------------------------------------------------------------
1 | # Notes: Architecture
2 |
3 | Let us discuss here how Odoo javascript code is designed. Roughly speaking,
4 | all features are made with a combination of components, services, registries,
5 | hooks or helper/utility functions.
6 |
7 | ```mermaid
8 | graph TD
9 | A[Components]
10 | B[Services]
11 | C[Registries]
12 | D[Hooks]
13 | ```
14 |
15 | ## Component Tree
16 |
17 | From a very high level stand point, the javascript code defines a (dynamic) tree
18 | of components. For example, with an active list view, it might look like this:
19 |
20 | ```mermaid
21 | graph TD
22 |
23 | A[WebClient]
24 | B[ActionController]
25 | C[NavBar]
26 |
27 | subgraph "Current action (list view)"
28 | E[ListController]
29 | F[Field]
30 | G[Field]
31 | H[...]
32 | end
33 | S[Systray]
34 | T[Systray Item]
35 | V[...]
36 | U[UserMenu]
37 |
38 | A --- B
39 | A --- C
40 | B --- E
41 | E --- F
42 | E --- G
43 | E --- H
44 | C --- S
45 | S --- T
46 | S --- V
47 | S --- U
48 | ```
49 |
50 | In this case, if the user clicks on a record, it may open a form view, and the
51 | content of the current action would be replaced with a form view. The, switching
52 | a notebook tab would destroy the content of the previous tab, and render the
53 | new content.
54 |
55 | This is how Owl applications work: the visible components are displayed, updated,
56 | and replaced with other components, depending on the user actions.
57 |
58 | ## Services
59 |
60 | Documentation: [services](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html)
61 |
62 | In practice, every component (except the root component) may be destroyed at
63 | any time and replaced (or not) with another component. This means that each
64 | component internal state is not persistent. This is fine in many cases, but there
65 | certainly are situations where we want to keep some data around. For example,
66 | all discuss messages, or the current menu.
67 |
68 | Also, it may happen that we need to write some code that is not a component.
69 | Maybe something that process all barcodes, or that manages the user configuration
70 | (context, ...).
71 |
72 | The Odoo framework defines the notion of `service`, which is a persistent piece
73 | of code that exports state and/or functions. Each service can depend on other
74 | services, and components can import a service.
75 |
76 | The following example registers a simple service that displays a notification every 5 seconds:
77 |
78 | ```js
79 | import { registry } from "@web/core/registry";
80 |
81 | const myService = {
82 | dependencies: ["notification"],
83 | start(env, { notification }) {
84 | let counter = 1;
85 | setInterval(() => {
86 | notification.add(`Tick Tock ${counter++}`);
87 | }, 5000);
88 | },
89 | };
90 |
91 | registry.category("services").add("myService", myService);
92 | ```
93 |
94 | Note that services are registered in a `registry`. See below for more on that.
95 |
96 | Services can be accessed by any component. Imagine that we have a service to
97 | maintain some shared state:
98 |
99 | ```js
100 | import { registry } from "@web/core/registry";
101 |
102 | const sharedStateService = {
103 | start(env) {
104 | let state = {};
105 |
106 | return {
107 | getValue(key) {
108 | return state[key];
109 | },
110 | setValue(key, value) {
111 | state[key] = value;
112 | },
113 | };
114 | },
115 | };
116 |
117 | registry.category("services").add("shared_state", sharedStateService);
118 | ```
119 |
120 | Then, any component can do this:
121 |
122 | ```js
123 | import { useService } from "@web/core/utils/hooks";
124 |
125 | setup() {
126 | this.sharedState = useService("shared_state");
127 | const value = this.sharedState.getValue("somekey");
128 | // do something with value
129 | }
130 | ```
131 |
132 | ## Registries
133 |
134 | Documentation: [registries](https://www.odoo.com/documentation/master/developer/reference/frontend/registries.html)
135 |
136 | Registries are central to the code architecture: they maintain a collection of
137 | key/value pairs, that are used in many places to read some information. This is
138 | the main way to extend or customize the web client.
139 |
140 | For example, a common usecase is to register a field or a view in a registry,
141 | then add the information in a view arch xml, so the web client will know what
142 | it should use.
143 |
144 | But fields and views are only two usecases. There are many situations where we
145 | decides to go with a registry, because it makes it easy to extend. For example,
146 |
147 | - service registry
148 | - field registry
149 | - user menu registry
150 | - effect registry
151 | - systray registry
152 | - ...
153 |
154 | ## Extending/Customizing Odoo JS Code
155 |
156 | As seen above, registries are really the most robust extension point of Odoo JS
157 | code. They provide an official API, and are designed to be used. So, one can
158 | do a lot of things with just registries, by adding and/or replacing values.
159 |
160 | Another less robust way of customizing the web client is by monkey patching a
161 | component and/or class.
162 |
163 | Documentation: [patching code](https://www.odoo.com/documentation/master/developer/reference/frontend/patching_code.html)
164 |
165 | This is totally okay if there are no other way to do it, but you should be aware
166 | that this is less robust: any change in the patched code may break the customizing,
167 | even if it is just a function rename!
168 |
169 | ## Example: the main component registry
170 |
171 | A very common need is to add some components as a direct child of the root component.
172 | This is how some features are done:
173 |
174 | - notifications: we need a container component to render all active notifications
175 | - discuss: need a container component to add all discuss chat window
176 | - voip: need the possibility to open/close a dialing panel on top of the UI
177 |
178 | To make it easy, the web client is actually looking into a special registry, `main_components`,
179 | to determine which component should be rendered. This is done by the `MainComponentsContainer`
180 | component, which basically performs a `t-foreach` on each key in that registry.
181 |
182 | ```mermaid
183 | graph TD
184 | A[WebClient]
185 | B[Navbar]
186 | C[ActionContainer]
187 | A --- B
188 | A --- C
189 | A --- D
190 | subgraph from main_components registry
191 | D[MainComponentsContainer]
192 | D --- E[NotificationContainer]
193 | D --- F[DialogContainer]
194 | D --- G[VOIPPanel]
195 | D --- H[...]
196 | end
197 | ```
198 |
199 | Adding a component to that list is as simple as subscribing to the `main_components`
200 | registry. Also, remember that the template of a component can look like this:
201 |
202 | ```xml
203 |
204 |
205 | some content here
206 |
207 |
208 | ```
209 |
210 | So, your component may be empty until some condition happens.
211 |
212 | ## Example: the notification system
213 |
214 | Often, we can think of a feature as a combination of the blocks above. Let us
215 | see for example how the notification system can be designed. We have:
216 |
217 | - a `Notification` component, which receive some props and renders a notification
218 | - a `notification` service, which exports a reactive list of active notifications, and
219 | a few methods to manipulate them (add/close)
220 | - a `NotificationContainer`, which subscribe to the `notification` service, and
221 | render them with a `t-foreach` loop.
222 |
223 | With that system, code anywhere in Odoo can uses the `notification` service to
224 | add/close a notification. This will cause an update to the internal list of
225 | notification, which will in turn trigger a render by the `NotificationContainer`.
226 |
--------------------------------------------------------------------------------
/notes_concurrency.md:
--------------------------------------------------------------------------------
1 | # Notes: Concurrency
2 |
3 | Concurrency is a huge topic in web application: a network request is asynchronous,
4 | so there are a lot of issues/situations that can arises. One need to be careful
5 | when writing asynchronous code.
6 |
7 | Roughly speaking, there are two things that we should consider:
8 |
9 | - writing efficient code, meaning that we want to parallelize as much as possible.
10 | Doing 3 requests sequentially takes longer than doing them in parallel
11 | - writing robust code: at each moment, the internal state of the application
12 | should be consistent, and the resulting UI should match the user expectation,
13 | regardless of the order in which requests returned (remember that a request
14 | can take an arbitrary amount of time to return)
15 |
16 | ## Parallel versus sequential
17 |
18 | Let's talk about efficiency. Assume that we need to load from the server two
19 | (independant) pieces of information. We can do it in two different ways:
20 |
21 | ```js
22 | async sequentialLoadData() {
23 | const data1 = await loadData1();
24 | const data2 = await loadData2();
25 | // ...
26 | }
27 |
28 | async parallelLoadData() {
29 | const [data1, data2] = await Promise.all([loadData1(), loadData2()]);
30 | // ...
31 | }
32 |
33 | ```
34 |
35 | The difference will be visible in the network tab: either the requests are fired
36 | sequentially, or in parallel. Obviously, if the two requests are independant,
37 | it is better to make them in parallel. If they are dependant, then they have to
38 | be done sequentially:
39 |
40 | ```js
41 | async sequentialDependantLoadData() {
42 | const data1 = await loadData1();
43 | const data2 = await loadData2(data1);
44 | // ...
45 | }
46 | ```
47 |
48 | Note that this has implications for the way we design (asynchronous) components:
49 | each component can load its data with an asynchronous `onWillStart` method. But
50 | since a child component is only rendered once its parent component is ready, this
51 | means that all `onWillStart` will run sequentially. As such, there should ideally
52 | only ever be one or two levels of components that load data in such a way. Otherwise,
53 | you end up with a loading cascade, which can be slow.
54 |
55 | A way to solve these issues may be to write a controller or a python model method to
56 | gather all the data directly, so it can be loaded in a single round-trip to the
57 | server.
58 |
59 | ## Avoiding corrupted state
60 |
61 | A common concurrency issue is to update the internal state in a non atomic way.
62 | This results in a period of time during which the component is inconsistent, and
63 | may misbehave or crash if rendered. For example:
64 |
65 | ```js
66 | async incorrectUpdate(id) {
67 | this.id = id;
68 | this.data = await loadRecord(id);
69 | }
70 | ```
71 |
72 | In the above example, the internal state of the component is inconsistent while
73 | the load request is ongoing: it has the new `id`, but the `data` is from the
74 | previous record. It should be fixed by updating the state atomically:
75 |
76 | ```js
77 | async correctUpdate(id) {
78 | this.data = await loadRecord(id);
79 | this.id = id;
80 | }
81 | ```
82 |
83 | ## Mutex
84 |
85 | As we have seen, some operations have to be sequential. But in practice, actual
86 | code is often hard to coordinate properly. An UI is active all the time, and various
87 | updates can be done (almost) simultaneously, or at any time. In that case, it can become difficult to maintain integrity.
88 |
89 | Let us discuss a simple example: imagine a `Model` that maintains the state of
90 | a record. The user can perform various actions:
91 |
92 | - update a field, which triggers a call to the server to apply computed fields (`onchange`),
93 | - save the record, which is a call to the server `save` method,
94 | - go to the next record
95 |
96 | So, what happens if the user update a field, then clicks on save while the onchange is ongoing?
97 | We obviously want to save the record with the updated value, so the code that
98 | perform the save operation need to wait for the return of the onchange.
99 |
100 | Another similar scenario: the user save the record, then go to the next record.
101 | In that case, we also need to wait for the save operation to be completed before
102 | loading the next record.
103 |
104 | If you think about it, it becomes quickly difficult to coordinate all these
105 | operations. Even more so when you add additional transformations (such as updating
106 | relational fields, loading other data, grouping data, drilling down in some groups,
107 | folding columns in kanban view, ...)
108 |
109 | Many of these interactions can be coordinated with the help of a `Mutex`: it is
110 | basically a queue which wait for the previous _job_ to be complete before executing
111 | the new one. So, here is how the above example could be modelled (in pseudo-code):
112 |
113 | ```js
114 | import { Mutex } from "@web/core/utils/concurrency";
115 |
116 | class Model {
117 | constructor() {
118 | this.mutex = new Mutex();
119 | }
120 | update(newValue) {
121 | this.mutex.exec(async () => {
122 | this.state = await this.applyOnchange(newValue);
123 | });
124 | }
125 | save() {
126 | this.mutex.exec(() => {
127 | this._save();
128 | });
129 | }
130 | _save() {
131 | // actual save code
132 | }
133 | openRecord(id) {
134 | this.mutex.exec(() => {
135 | this.state = await this.loadRecord(id)
136 | });
137 | }
138 | }
139 | ```
140 |
141 | ## KeepLast
142 |
143 | As seen above, many user interactions need to be properly coordinated. Let us
144 | imagine the following scenario: the user selects a menu in the Odoo web client.
145 | Just after, the user changes her/his mind and select another menu. What should
146 | happen?
147 |
148 | If we don't do anything, there is a risk that the web client displays either of
149 | the action, then switch immediately to the other, depending on the order in which
150 | the requests ends.
151 |
152 | We can solve this by using a mutex:
153 |
154 | ```js
155 | // in web client
156 |
157 | selectMenu(id) {
158 | this.mutex.exec(() => this.loadMenu(id));
159 | }
160 | ```
161 |
162 | This will make it determinist: each action from the user will be executed, then
163 | the next action will take place. However, this is not optimal: we
164 | probably want to stop (as much as possible) the first action, and start immediately
165 | the new action, so the web client will only display the second action, and will
166 | do it as fast as possible.
167 |
168 | This can be done by using the `KeepLast` primitive from Odoo: it is basically
169 | like a Mutex, except that it cancels the current action, if any (not really
170 | cancelling, but keeping the promise pending, without resolving it). So, the
171 | code above could be written like this:
172 |
173 | ```js
174 | import { KeepLast } from "@web/core/utils/concurrency";
175 | // in web client
176 |
177 | class WebClient {
178 | constructor() {
179 | this.keepLast = new KeepLast();
180 | }
181 |
182 | selectMenu(id) {
183 | this.keepLast.add(() => this.loadMenu(id));
184 | }
185 | }
186 | ```
187 |
--------------------------------------------------------------------------------
/notes_fields.md:
--------------------------------------------------------------------------------
1 | # Notes: Fields
2 |
3 | In the context of the javascript framework, fields are components specialized for
4 | visualizing/editing a specific field for a given record.
5 |
6 | For example, a (python) model may define a boolean field, which will be represented
7 | by a field component `BooleanField`.
8 |
9 | Usually, fields can display data in `readonly` or in `edit` mode. Also, they are
10 | often specific to a field type: `boolean`, `float`, `many2one`, ...
11 |
12 | Fields have to be registered in the `fields` registry. Once it's done, they can
13 | be used in some views (namely: `form`, `list`, `kanban`) by using the `widget`
14 | attribute:
15 |
16 | ```xml
17 |
18 | ```
19 |
20 | Note that fields may in some case be used outside the context of a view.
21 |
22 | ## Generic Field Component
23 |
24 | Just like concrete views are designed to be created by a generic `View` component,
25 | concrete fields are also designed to be created by a generic component, `Field`.
26 |
27 | For example:
28 |
29 | ```xml
30 |
36 | ```
37 |
38 | This example show some of the props accepted by the `Field` component. Then, it
39 | will make sure it loads the correct component from the `fields` registry, prepare
40 | the base props, and create its child. Note that the `Field` component is _dom less_:
41 | it only exists as a wrapper for the concrete field instance.
42 |
43 | Here is what it look like for the form view:
44 |
45 | ```mermaid
46 | graph TD
47 | A[FormRenderer]
48 | B[Field] --- C[BooleanField]
49 | D[Field] --- E[Many2OneField]
50 |
51 | A --- B
52 | A --- D
53 | A --- F[...]
54 |
55 | ```
56 |
57 | ## Defining a field component
58 |
59 | A field component is basically just a component registered in the `fields` registry.
60 | It may define some additional static keys (metadata), such as `displayName` or `supportedTypes`,
61 | and the most important one: `extractProps`, which prepare the base props received
62 | by the `CharField`.
63 |
64 | Let us discuss a (simplified) implementation of a `CharField`:
65 |
66 | First, here is the template:
67 |
68 | ```xml
69 |
70 |
71 |
72 |
73 |
74 |
80 |
81 |
82 | ```
83 |
84 | It features a readonly mode, an edit mode, which is an input with a few attributes.
85 | Now, here is the code:
86 |
87 | ```js
88 | export class CharField extends Component {
89 | get formattedValue() {
90 | return formatChar(this.props.value, { isPassword: this.props.isPassword });
91 | }
92 |
93 | updateValue(ev) {
94 | let value = ev.target.value;
95 | if (this.props.shouldTrim) {
96 | value = value.trim();
97 | }
98 | this.props.update(value);
99 | }
100 | }
101 |
102 | CharField.template = "web.CharField";
103 | CharField.displayName = _lt("Text");
104 | CharField.supportedTypes = ["char"];
105 |
106 | CharField.extractProps = ({ attrs, field }) => {
107 | return {
108 | shouldTrim: field.trim && !archParseBoolean(attrs.password),
109 | maxLength: field.size,
110 | isPassword: archParseBoolean(attrs.password),
111 | placeholder: attrs.placeholder,
112 | };
113 | };
114 |
115 | registry.category("fields").add("char", CharField);
116 | ```
117 |
118 | There are a few important things to notice:
119 |
120 | - the `CharField` receives its (raw) value in props. It needs to format it before displaying it
121 | - it receives a `update` function in its props, which is used by the field to notify
122 | the owner of the state that the value of this field has been changed. Note that
123 | the field does not (and should not) maintain a local state with its value. Whenever
124 | the change has been applied, it will come back (possibly after an onchange) by the
125 | way of the props.
126 | - it defines an `extractProps` function. This is a step that translates generic
127 | standard props, specific to a view, to specialized props, useful to the component.
128 | This allows the component to have a better API, and may make it so that it is
129 | reusable.
130 |
131 | Note that the exact API for fields is not really documented anywhere.
132 |
--------------------------------------------------------------------------------
/notes_network_requests.md:
--------------------------------------------------------------------------------
1 | # Notes: Network Requests
2 |
3 | A web app such as the Odoo web client would not be very useful if it was unable
4 | to talk to the server. Loading data and calling model methods from the browser
5 | is a very common need.
6 |
7 | Roughly speaking, there are two different kind of requests:
8 |
9 | - calling a controller (an arbitrary route)
10 | - calling a method on a model (`/web/dataset/call_kw/some_model/some_method`). This
11 | will call the python code from the corresponding method, and return the result.
12 |
13 | In odoo these two kind of requests are done with `XmlHTTPRequest`s, in `jsonrpc`.
14 |
15 | ## Calling a method on a model (orm service)
16 |
17 | Let us first see the most common request: calling a method on a model. This is
18 | usually what we need to do.
19 |
20 | There is a service dedicated to do just that: `orm_service`, located in `core/orm_service.js`
21 | It provides a way to call common model methods, as well as a generic `call` method:
22 |
23 | ```js
24 | setup() {
25 | this.orm = useService("orm");
26 | onWillStart(async () => {
27 | // will read the fields 'id' and 'descr' from the record with id=3 of my.model
28 | const data = await this.orm.read("my.model", [3], ["id", "descr"]);
29 | // ...
30 | });
31 | }
32 | ```
33 |
34 | Here is a list of its various methods:
35 |
36 | - `create(model, records, kwargs)`
37 | - `nameGet(model, ids, kwargs)`
38 | - `read(model, ids, fields, kwargs)`
39 | - `readGroup(model, domain, fields, groupby, kwargs)`
40 | - `search(model, domain, kwargs)`
41 | - `searchRead(model, domain, fields, kwargs)`
42 | - `searchCount(model, domain, kwargs)`
43 | - `unlink(model, ids, kwargs)`
44 | - `webReadGroup(model, domain, fields, groupby, kwargs)`
45 | - `webSearchRead(model, domain, fields, kwargs)`
46 | - `write(model, ids, data, kwargs)`
47 |
48 | Also, in case one needs to call an arbitrary method on a model, there is:
49 |
50 | - `call(model, method, args, kwargs)`
51 |
52 | Note that the specific methods should be preferred, since they can perform some
53 | light validation on the shape of their arguments.
54 |
55 | ## Calling a controller (rpc service)
56 |
57 | Whenever we need to call a specific controller, we need to use the (low level)
58 | `rpc` service. It only exports a single function that perform the request:
59 |
60 | ```
61 | rpc(route, params, settings)
62 | ```
63 |
64 | Here is a short explanation on the various arguments:
65 |
66 | - `route` is the target route, as a string. For example `/myroute/`
67 | - `params`, optional, is an object that contains all data that will be given to the controller
68 | - `settings`, optional, for some advance control on the request (make it silent, or
69 | using a specific xhr instance)
70 |
71 | For example, a basic request could look like this:
72 |
73 | ```js
74 | setup() {
75 | this.rpc = useService("rpc");
76 | onWillStart(async () => {
77 | const result = await this.rpc("/my/controller", {a: 1, b: 2});
78 | // ...
79 | });
80 | }
81 | ```
82 |
--------------------------------------------------------------------------------
/notes_odoo_js_ecosystem.md:
--------------------------------------------------------------------------------
1 | # Notes: The Odoo Javascript Ecosystem
2 |
3 | A quick overview
4 |
5 | ## Historical context
6 |
7 | First web client was in Odoo v6.1 (port of a gtk application). Back then,
8 | not many large web applications, so Odoo (then openERP) was built with jquery
9 | and a custom framework (mostly a Class and a Widget implementation). Remember
10 | that it was before Ecmascript 6 (so no class in JS), before bootstrap, before
11 | a lot of the current web technologies.
12 |
13 | Then it evolved randomly in a lot of directions. A module system was added in
14 | 2014 (maybe some of you will remember `odoo.define(...)`), then the code had to
15 | be improved for the new views, for studio. The complexity of the application
16 | increased a lot, code was getting more structured also.
17 |
18 | Then came the need to move to a more modern/productive framework. The Widget system
19 | (based on imperative principles) was not a good bet for the future. Odoo invested
20 | in its own framework (Owl, released in 2019), which is now the basis for the odoo
21 | web client.
22 |
23 | 2019-2022 has seen a huge amount of work in Odoo JS: the assets system was
24 | modernized (ES6 modules), the codebase was refactored, based on modern architecture
25 | and principles. It involved basically a complete rewrite using owl, services,
26 | registries, components, hooks.
27 |
28 | ## Odoo 16: A new Era
29 |
30 | The v16 is the beginning of a (mostly) completely new codebase. Here is a short
31 | list of most significant changes, in no particular order:
32 |
33 | - most of the UI is composed of `Owl` components
34 | - the new code does not use `jquery` anymore (we plan to remove jquery from our assets in the future)
35 | - the `moment` library has been replaced by `luxon` (to manipulate date/datetime)
36 | - we now use `bootstrap` 5, but only the layout and css (not using the js code if possible)
37 | - with Owl, we don't need to add much css classes (it was necessary before to target event handlers, but can
38 | now be done with `t-on-click` in templates)
39 | - assets (js/css/xml) are now declared in the manifest, can be easily split in
40 | bundles, and js files can use ES6 syntax
41 | - code is now organized by feature, not by type. So, we no longer have folders like
42 | `css`, `js`, `xml`. Instead, we organize files and folders according to their
43 | function (`reporting`, `notifications`, ...)
44 |
45 | ## Backend or frontend?
46 |
47 | Roughly speaking, Odoo has 3 main javascript codebases:
48 |
49 | ```mermaid
50 | graph TD
51 | A[Website]
52 | B[Web client]
53 | C[Point of Sale]
54 | ```
55 |
56 | - the website (public, also known in odoo as the `frontend`)
57 | - the webclient (private, also known as the `backend` (to be avoided, since it is confusing))
58 | - the point of sale
59 |
60 | The website is a classical MPA (Multi Page Application). Each page is rendered
61 | by the server. It will load some javascript to add a touch of life to the UI.
62 |
63 | The webclient and the point of sale are SPA (Single Page Application). The (only)
64 | page is rendered by the browser. It will then loads data from the server, as
65 | needed, and update the page without reloading the page.
66 |
67 | Since they are based on very different principles, the code of website is very
68 | different from the code of the web client/point of sale (even though they share
69 | some code, mostly in `addons/web/static/src/core`). This training will be
70 | more focused on the SPA aspect of Odoo.
71 |
72 | ## The different layers of Odoo Javascript in Odoo
73 |
74 | One can think of the Odoo web client as being built with four layers:
75 |
76 | ```mermaid
77 | graph TD
78 | A[Web Client]
79 | B[Views/Fields]
80 | C[Core]
81 | D[Owl]
82 |
83 | A --> B
84 | A --> C
85 | B --> C
86 | C --> D
87 |
88 | ```
89 |
90 | - `web client`: it is the presentation layer that describes the
91 | user interface (navbar, action system, ...)
92 | - `views and fields`: all the code that describes how to visualize and interact with data
93 | from the database, such as the form view, the list view or the kanban view.
94 | - `core`: it is the layer that defines the basic building blocks
95 | for an odoo application. Things such as `registries`, `services`, helpers,
96 | python engine, generic components.
97 | - `owl`: the low level component system. It defines the basic
98 | primitives for writing UI code, such as the Component or the `reactive` function.
99 |
100 | ## Folder structure
101 |
102 | Most of the time, javascript (and other assets) code should be structured like
103 | this:
104 |
105 | ```
106 | /static/
107 | src/
108 | notifications/
109 | notification_service.js
110 | notification.js
111 | notification.xml
112 | notification.scss
113 | some_component.js
114 | some_component.xml
115 | ...
116 | tests/
117 | helpers.js
118 | notification_tests.js
119 | ...
120 | ```
121 |
122 | Note that we don't have the `js/`, `scss`, or `xml` folder anymore. Code is
123 | grouped by concern. Tests should be located in a `static/tests` folder.
124 |
--------------------------------------------------------------------------------
/notes_testing.md:
--------------------------------------------------------------------------------
1 | # Notes: Testing Odoo
2 |
3 | Testing UI code is an important, but often overlooked, part of a solid development
4 | process. Odoo javascript test suite is quite large, but at the same time, not
5 | particularly intuitive. So, let us take some time to discuss the main ideas.
6 |
7 | Roughly speaking, there are two different kind of tests: integration tests (testing
8 | a feature/business flow, by running all the relevant code) and unit tests (testing
9 | some behaviour, usually by running only a component or a small unit of code).
10 |
11 | Both of these kind of tests are different:
12 |
13 | - integration tests are useful to make sure something works as expected. However,
14 | usually, they take more time to run, take more CPU/memory, and are harder to
15 | debug when they fail. On the flip side, they are necessary to prove that a system
16 | work and they are easier to write.
17 |
18 | - unit tests are useful to ensure that a specific piece of code works. They are
19 | quick to run, are focused on a specific feature. When they fail, they identify
20 | a problem in a smaller scope, so it is easier to find the issue. However, they
21 | are usually harder to write, since one needs to find a way to _isolate_ as much
22 | as possible something.
23 |
24 | Odoo (javascript) test suite contains both kind of tests: integration tests are
25 | made with _tours_ and unit tests with _QUnit_
26 |
27 | ## Tours
28 |
29 | A tour is basically a sequence of steps, with some selectors and parameters to
30 | describe what the step should do (click on an element, fill an input, ...). Then
31 | the code in the addon `web_tour` will execute each step sequentially, waiting
32 | between each step if necessary.
33 |
34 | ## QUnit tests
35 |
36 | A QUnit test is basically a short piece of code that exercise a feature, and
37 | make some assertions. The main test suite can be run by simply going to the
38 | `/web/tests` route.
39 |
--------------------------------------------------------------------------------
/notes_views.md:
--------------------------------------------------------------------------------
1 | # Notes: Views
2 |
3 | Views are among the most important components in Odoo: they allow users to interact
4 | with their data. Let us discuss how Odoo views are designed.
5 |
6 | The power of Odoo views is that they declare how a particular screen should work,
7 | with a xml document (usually named `arch`, short for `architecture`). This description
8 | can be extended/modified by xpaths serverside. Then the browser will load that
9 | document, parse it (fancy word to say that it will extract the useful information),
10 | then represent the data accordingly.
11 |
12 | The `arch` document is view specific. For example, here is how a `graph` view
13 | or a `calendar` view could be defined:
14 |
15 | ```xml
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ```
27 |
28 | ## The generic `View` component
29 |
30 | Most of the time, views are created with the help of a generic `View` component,
31 | located in `@web/views/view`. For example, here is what it look like for a kanban view:
32 |
33 | ```mermaid
34 | graph TD
35 | A[View]
36 | B[KanbanController]
37 |
38 | A ---|props| B
39 | ```
40 |
41 | The `View` component is responsible for many tasks:
42 |
43 | - loading the view arch description from the server
44 | - loading the search view description, if necessary
45 | - loading the active filters
46 | - if there is a `js_class` attribute on the root node of the arch, get the
47 | correct view from the view registry
48 | - creating a searchmodel (that manipulates the current domain/context/groupby/facets)
49 |
50 | ## Defining a javascript view
51 |
52 | A view is defined in the view registry by an object with a few specific keys.
53 |
54 | - `type`: the (base) type of a view (so, for example, `form`, `list`, ...)
55 | - `display_name`: what shoul be displayed in tooltip in the view switcher
56 | - `icon`: what icon to use in the view switcher
57 | - `multiRecord`: if the view is supposed to manage 1 or a set of records
58 | - `Controller`: the most important information: the component that will be used
59 | to render the view.
60 |
61 | Here is a minimal `Hello` view, which does not display anything:
62 |
63 | ```js
64 | /** @odoo-module */
65 |
66 | import { registry } from "@web/core/registry";
67 |
68 | export const helloView = {
69 | type: "hello",
70 | display_name: "Hello",
71 | icon: "fa fa-picture-o",
72 | multiRecord: true,
73 | Controller: Component,
74 | };
75 |
76 | registry.category("views").add("hello", helloView);
77 | ```
78 |
79 | ## The Standard View Architecture
80 |
81 | Most (or all?) odoo views share a common architecture:
82 |
83 | ```mermaid
84 | graph TD
85 | subgraph View description
86 | V(props function)
87 | G(generic props)
88 | X(arch parser)
89 | S(others ...)
90 | V --> X
91 | V --> S
92 | V --> G
93 | end
94 | A[Controller]
95 | L[Layout]
96 | B[Renderer]
97 | C[Model]
98 |
99 | V == compute props ==> A
100 | A --- L
101 | L --- B
102 | A --- C
103 |
104 | ```
105 |
106 | The view description can define a `props` function, which receive the standard
107 | props, and compute the base props of the concrete view. The `props` function is
108 | executed only once, and can be thought of as being some kind of factory. It is
109 | useful to parse the `arch` xml document, and to allow the view to be parameterized
110 | (for example, it can return a Renderer component that will be used as Renderer),
111 | but then it makes it easy to customize the specific renderer used by a sub view.
112 |
113 | Note that these props will be extended before being given to the Controller. In
114 | particular, the search props (domain/context/groupby) will be added.
115 |
116 | Then the root component, commonly called the `Controller`, coordinates everything.
117 | Basically, it uses the generic `Layout` component (to add a control panel),
118 | instantiates a `Model`, and uses a `Renderer` component in the `Layout` default
119 | slot. The `Model` is tasked with loading and updating data, and the `Renderer`
120 | is supposed to handle all rendering work, along with all user interactions.
121 |
122 | ### Parsing an arch
123 |
124 | The process of parsing an arch (xml document) is usually done with a `ArchParser`,
125 | specific to each view. It inherits from a generic `XMLParser` class. For example,
126 | it could look like this:
127 |
128 | ```js
129 | import { XMLParser } from "@web/core/utils/xml";
130 |
131 | export class GraphArchParser extends XMLParser {
132 | parse(arch, fields) {
133 | const result = {};
134 | this.visitXML(arch, (node) => {
135 | ...
136 | });
137 | return result;
138 | }
139 | }
140 | ```
141 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Introduction to JS framework
4 |
5 | ## Introduction
6 |
7 | For this training, we will put ourselves in the shoes of the IT staff for the fictional Awesome T-Shirt company, which is in the business of printing customised tshirts for online customers.
8 | The Awesome T-Shirt company uses Odoo for managing its orders, and built a dedicated odoo module to manage their workflow. The project is currently a simple kanban view, with a few columns.
9 |
10 | The usual process is the following: a customer looking for a nice t-shirt can simply order it on the Awesome T-Shirt website, and give the url for any image that he wants. He also has to fill some basic informations, such as the desired size, and amount of t-shirts. Once he confirms his order, and once the payment is validated, the system will create a task in our project application.
11 |
12 | The Awesome T-shirt big boss, Bafien Ckinpaers, is not happy with our implementation. He believe that by micromanaging more, he will be able to extract more revenue from his employees.
13 | As the IT staff for Awesome T-shirt, we are tasked with improving the system. Various independant tasks need to be done.
14 |
15 | Let us now practice our odoo skills!
16 |
17 | ## Setup
18 |
19 | Clone this repository, add it to your addons path, make sure you have
20 | a recent version of odoo (master), prepare a new database, install the `awesome_tshirt`
21 | addon, and ... let's get started!
22 |
23 | ## Notes
24 |
25 | Here are some short notes on various topics, in no particular order:
26 |
27 | - [The Odoo Javascript Ecosystem](notes_odoo_js_ecosystem.md)
28 | - [Architecture](notes_architecture.md)
29 | - [Views](notes_views.md)
30 | - [Fields](notes_fields.md)
31 | - [Concurrency](notes_concurrency.md)
32 | - [Network requests](notes_network_requests.md)
33 | - [Testing Odoo Code](notes_testing.md)
34 |
35 | ## Exercises
36 |
37 | - Part 1: [🦉 Owl framework 🦉](exercises_1_owl.md)
38 | - Part 2: [Odoo web framework](exercises_2_web_framework.md)
39 | - Part 3: [Fields and Views](exercises_3_fields_views.md)
40 | - Part 4: [Miscellaneous](exercises_4_misc.md)
41 | - Part 5: [Custom kanban view](exercises_5_custom_kanban_view.md)
42 | - Part 6: [Creating a view from scratch](exercises_6_creating_views.md)
43 | - Part 7: [Testing](exercises_7_testing.md)
44 |
--------------------------------------------------------------------------------