30 |
31 |
32 |
--------------------------------------------------------------------------------
/awesome_gallery/static/src/gallery_controller.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module */
2 |
3 | import { Layout } from "@web/search/layout";
4 | import { useService } from "@web/core/utils/hooks";
5 | import { usePager } from "@web/search/pager_hook";
6 |
7 | const { Component, onWillStart, onWillUpdateProps, useState } = owl;
8 |
9 | export class GalleryController extends Component {
10 | setup() {
11 | this.orm = useService("orm");
12 |
13 | this.model = useState(
14 | new this.props.Model(
15 | this.orm,
16 | this.props.resModel,
17 | this.props.fields,
18 | this.props.archInfo,
19 | this.props.domain
20 | )
21 | );
22 |
23 | usePager(() => {
24 | return {
25 | offset: this.model.pager.offset,
26 | limit: this.model.pager.limit,
27 | total: this.model.recordsLength,
28 | onUpdate: async ({ offset, limit }) => {
29 | this.model.pager.offset = offset;
30 | this.model.pager.limit = limit;
31 | await this.model.load();
32 | },
33 | };
34 | });
35 |
36 | onWillStart(async () => {
37 | await this.model.load();
38 | });
39 |
40 | onWillUpdateProps(async (nextProps) => {
41 | if (JSON.stringify(nextProps.domain) !== JSON.stringify(this.props.domain)) {
42 | this.model.domain = nextProps.domain;
43 | await this.model.load();
44 | }
45 | });
46 | }
47 | }
48 |
49 | GalleryController.template = "awesome_gallery.View";
50 | GalleryController.components = { Layout };
51 |
--------------------------------------------------------------------------------
/awesome_tshirt/static/src/order_form_view/order_form_view.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module */
2 |
3 | import { registry } from "@web/core/registry";
4 | import { FormController } from "@web/views/form/form_controller";
5 | import { formView } from "@web/views/form/form_view";
6 | import { useService } from "@web/core/utils/hooks";
7 | import { useDebounced } from "@web/core/utils/timing";
8 |
9 | class OrderFormController extends FormController {
10 | setup() {
11 | super.setup();
12 | this.orm = useService("orm");
13 | this.notificationService = useService("notification");
14 | this.debouncedPrintLabel = useDebounced(this.printLabel, 200);
15 | }
16 |
17 | async printLabel() {
18 | const serverResult = await this.orm.call(this.model.root.resModel, "print_label", [
19 | this.model.root.resId,
20 | ]);
21 |
22 | if (serverResult) {
23 | this.notificationService.add(this.env._t("Label successfully printed"), {
24 | type: "success",
25 | });
26 | } else {
27 | this.notificationService.add(this.env._t("Could not print the label"), {
28 | type: "danger",
29 | });
30 | }
31 |
32 | return serverResult;
33 | }
34 |
35 | get isPrintBtnPrimary() {
36 | return (
37 | this.model.root.data &&
38 | this.model.root.data.customer_id &&
39 | this.model.root.data.state === "printed"
40 | );
41 | }
42 | }
43 |
44 | OrderFormController.template = "awesome_tshirt.OrderFormView";
45 |
46 | export const orderFormView = {
47 | ...formView,
48 | Controller: OrderFormController,
49 | };
50 |
51 | registry.category("views").add("order_form_view", orderFormView);
52 |
--------------------------------------------------------------------------------
/awesome_tshirt/static/tests/tours/order_flow.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module **/
2 |
3 | import tour from "web_tour.tour";
4 |
5 | tour.register(
6 | "order_tour",
7 | {
8 | url: '/awesome_tshirt/order',
9 | test: true,
10 | },
11 | [
12 | {
13 | content: 'Put an image url',
14 | trigger: '#url',
15 | run: 'text test_url'
16 | },
17 | {
18 | content: 'Put a name',
19 | trigger: '#name',
20 | run: 'text test_name'
21 | },
22 | {
23 | content: "Add quantity",
24 | trigger: "#quantity",
25 | run: "text 123456",
26 | },
27 | {
28 | content: 'Put an address',
29 | trigger: '#address',
30 | run: 'text test_address'
31 | },
32 | {
33 | content: 'Put an email',
34 | trigger: '#email',
35 | run: 'text test_email'
36 | },
37 | {
38 | content: "Order",
39 | trigger: "button",
40 | run: () => {
41 | window.location.href = '/web';
42 | }
43 | },
44 | tour.stepUtils.showAppsMenuItem(),
45 | {
46 | content: "Go to awesome_tshirt app",
47 | trigger: '.o_app[data-menu-xmlid="awesome_tshirt.menu_root"]',
48 | },
49 | {
50 | content: "Go to order",
51 | trigger: 'a[data-menu-xmlid="awesome_tshirt.order"]',
52 | },
53 | {
54 | content: "Switch to list view",
55 | trigger: ".o_list",
56 | run: "click",
57 | },
58 | {
59 | content: "Is the new line there",
60 | trigger: "o_list_number:contains('123,456')",
61 | },
62 | ]
63 | );
64 |
--------------------------------------------------------------------------------
/awesome_tshirt/__manifest__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | {
3 | 'name': "Awesome Shirt",
4 |
5 | 'summary': """
6 | Short (1 phrase/line) summary of the module's purpose, used as
7 | subtitle on modules listing or apps.openerp.com""",
8 |
9 | 'description': """
10 | This app helps you to manage a business of printing customized t-shirts
11 | for online customers. It offers a public page allowing customers to make
12 | t-shirt orders.
13 |
14 | Note that this is just a toy app intended to learn the javascript
15 | framework.
16 | """,
17 |
18 | 'author': "Odoo",
19 | 'website': "https://www.odoo.com/",
20 |
21 | 'category': 'Productivity',
22 | 'version': '0.1',
23 | 'application': True,
24 | 'installable': True,
25 |
26 |
27 | # any module necessary for this one to work correctly
28 | 'depends': ['base', 'web', 'mail', 'awesome_gallery'],
29 |
30 | # always loaded
31 | 'data': [
32 | 'security/ir.model.access.csv',
33 | 'views/views.xml',
34 | 'views/templates.xml',
35 | ],
36 | # only loaded in demonstration mode
37 | 'demo': [
38 | 'demo/demo.xml',
39 | ],
40 | 'assets': {
41 | 'web.assets_backend': [
42 | 'awesome_tshirt/static/src/**/*',
43 | 'awesome_tshirt/static/tests/**/*',
44 | ('remove', 'awesome_tshirt/static/src/dashboard/**/*'),
45 | ],
46 | 'awesome_tshirt.dashboard': [
47 | # To include bootstrap scss variables
48 | ("include", 'web._assets_helpers'),
49 | ('include', 'web._assets_backend_helpers'),
50 | 'awesome_tshirt/static/src/dashboard/**/*',
51 | ],
52 | 'web.order_tests': [
53 | ("include", 'web.assets_frontend'),
54 | 'awesome_tshirt/static/tests/**/*',
55 | ],
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/awesome_tshirt/static/src/pie_chart/pie_chart.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module */
2 |
3 | import { loadJS } from "@web/core/assets";
4 | import { getColor } from "@web/views/graph/colors";
5 |
6 | const { Component, onWillStart, useRef, onMounted, onWillUnmount } = owl;
7 |
8 | export class PieChart extends Component {
9 | setup() {
10 | this.canvasRef = useRef("canvas");
11 |
12 | this.labels = Object.keys(this.props.data);
13 | this.data = Object.values(this.props.data);
14 | this.color = this.labels.map((_, index) => {
15 | return getColor(index);
16 | });
17 |
18 | onWillStart(() => {
19 | return loadJS(["/web/static/lib/Chart/Chart.js"]);
20 | });
21 |
22 | onMounted(() => {
23 | this.renderChart();
24 | });
25 |
26 | onWillUnmount(() => {
27 | if (this.chart) {
28 | this.chart.destroy();
29 | }
30 | });
31 | }
32 |
33 | onPieClick(ev, chartElem) {
34 | const clickedIndex = chartElem[0]._index;
35 | this.props.onPieClick(this.labels[clickedIndex]);
36 | }
37 |
38 | renderChart() {
39 | if (this.chart) {
40 | this.chart.destroy();
41 | }
42 | this.chart = new Chart(this.canvasRef.el, {
43 | type: "pie",
44 | data: {
45 | labels: this.labels,
46 | datasets: [
47 | {
48 | label: this.env._t(this.props.label),
49 | data: this.data,
50 | backgroundColor: this.color,
51 | },
52 | ],
53 | },
54 | options: {
55 | onClick: this.onPieClick.bind(this),
56 | },
57 | });
58 | }
59 | }
60 |
61 | PieChart.template = "awesome_tshirt.PieChart";
62 | PieChart.props = {
63 | data: { type: Object },
64 | label: { type: String },
65 | onPieClick: { type: Function },
66 | };
67 |
--------------------------------------------------------------------------------
/awesome_gallery/static/src/gallery_model.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module */
2 |
3 | import { KeepLast } from "@web/core/utils/concurrency";
4 |
5 | export class GalleryModel {
6 | constructor(orm, resModel, fields, archInfo, domain) {
7 | this.orm = orm;
8 | this.resModel = resModel;
9 | const { imageField, limit, tooltipField } = archInfo;
10 | this.imageField = imageField;
11 | this.fields = fields;
12 | this.limit = limit;
13 | this.domain = domain;
14 | this.tooltipField = tooltipField;
15 | this.keepLast = new KeepLast();
16 | this.pager = { offset: 0, limit: limit };
17 | if (!(imageField in this.fields)) {
18 | throw `image_field error: ${imageField} is not a field of ${resModel}`;
19 | }
20 | if (!(tooltipField in this.fields)) {
21 | throw `image_field error: ${tooltipField} is not a field of ${resModel}`;
22 | }
23 | }
24 |
25 | async load() {
26 | const { length, records } = await this.keepLast.add(
27 | this.orm.webSearchRead(
28 | this.resModel,
29 | this.domain,
30 | [this.imageField, this.tooltipField],
31 | {
32 | limit: this.pager.limit,
33 | offset: this.pager.offset,
34 | }
35 | )
36 | );
37 | this.recordsLength = length;
38 |
39 | switch (this.fields[this.tooltipField].type) {
40 | case "many2one":
41 | this.images = records.map((record) => ({
42 | ...record,
43 | [this.tooltipField]: record[this.tooltipField][1],
44 | }));
45 | break;
46 | case "integer":
47 | this.images = records.map((record) => ({
48 | ...record,
49 | [this.tooltipField]: String(record[this.tooltipField]),
50 | }));
51 | break;
52 | default:
53 | this.images = records;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/awesome_tshirt/static/src/customer_autocomplete/customer_autocomplete.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module **/
2 |
3 | import { useService } from "@web/core/utils/hooks";
4 | import { Domain } from "@web/core/domain";
5 | import { AutoComplete } from "@web/core/autocomplete/autocomplete";
6 | import { fuzzyLookup } from "@web/core/utils/search";
7 |
8 | const { Component } = owl;
9 |
10 | export class CustomerAutocomplete extends Component {
11 | setup() {
12 | this.action = useService("action");
13 | this.orm = useService("orm");
14 | this.tshirtService = useService("tshirtService");
15 | }
16 |
17 | get sources() {
18 | return [
19 | {
20 | placeholder: this.env._t("Loading..."),
21 | options: this.loadOptionsSources.bind(this),
22 | },
23 | ];
24 | }
25 |
26 | async loadOptionsSources(request) {
27 | if (!this.partners) {
28 | const partners = await this.tshirtService.loadCustomers();
29 | this.partners = partners.map((record) => ({
30 | label: record.display_name,
31 | res_id: record.id,
32 | }));
33 | }
34 |
35 | if (!request) {
36 | return this.partners.slice(0, 8);
37 | }
38 | const fuzzySearch = fuzzyLookup(request, this.partners, (partner) => partner.label).slice(
39 | 0,
40 | 8
41 | );
42 | if (!fuzzySearch.length) {
43 | fuzzySearch.push({
44 | label: this.env._t("No records"),
45 | classList: "o_m2o_no_result",
46 | unselectable: true,
47 | });
48 | }
49 | return fuzzySearch;
50 | }
51 |
52 | openOrdersFromCustomer(customerId, customerName) {
53 | this.action.doAction({
54 | type: "ir.actions.act_window",
55 | name: `Orders from ${customerName}`,
56 | res_model: "awesome_tshirt.order",
57 | domain: new Domain(`[('customer_id','=', ${customerId})]`).toList(),
58 | views: [
59 | [false, "list"],
60 | [false, "form"],
61 | ],
62 | });
63 | }
64 |
65 | onSelect(option) {
66 | this.openOrdersFromCustomer(option.res_id, option.label);
67 | }
68 | }
69 | CustomerAutocomplete.template = "awesome_tshirt.CustomerAutocomplete";
70 | CustomerAutocomplete.components = { AutoComplete };
71 |
--------------------------------------------------------------------------------
/awesome_tshirt/static/src/customer_list/customer_list.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module */
2 |
3 | import { useService } from "@web/core/utils/hooks";
4 | import { KeepLast } from "@web/core/utils/concurrency";
5 | import { fuzzyLookup } from "@web/core/utils/search";
6 | import { Pager } from "@web/core/pager/pager";
7 |
8 | const { Component, onWillStart, useState } = owl;
9 |
10 | export class CustomerList extends Component {
11 | setup() {
12 | this.orm = useService("orm");
13 | this.partners = useState({ data: [] });
14 | this.pager = useState({ offset: 0, limit: 20 });
15 | this.keepLast = new KeepLast();
16 | this.state = useState({
17 | searchString: "",
18 | displayActiveCustomers: false,
19 | });
20 | onWillStart(async () => {
21 | const { length, records } = await this.loadCustomers();
22 | this.partners.data = records;
23 | this.pager.total = length;
24 | });
25 | }
26 |
27 | get displayedPartners() {
28 | return this.filterCustomers(this.state.searchString);
29 | }
30 |
31 | async onChangeActiveCustomers(ev) {
32 | this.state.displayActiveCustomers = ev.target.checked;
33 | this.pager.offset = 0;
34 | const { length, records } = await this.keepLast.add(this.loadCustomers());
35 | this.partners.data = records;
36 | this.pager.total = length;
37 | }
38 |
39 | loadCustomers() {
40 | const { limit, offset } = this.pager;
41 | const domain = this.state.displayActiveCustomers ? [["has_active_order", "=", true]] : [];
42 | return this.orm.webSearchRead("res.partner", domain, ["display_name"], {
43 | limit: limit,
44 | offset: offset,
45 | });
46 | }
47 |
48 | filterCustomers(name) {
49 | if (name) {
50 | return fuzzyLookup(name, this.partners.data, (partner) => partner.display_name);
51 | } else {
52 | return this.partners.data;
53 | }
54 | }
55 |
56 | async onUpdatePager(newState) {
57 | Object.assign(this.pager, newState);
58 | const { records } = await this.loadCustomers();
59 | this.partners.data = records;
60 | this.filterCustomers(this.filterName);
61 | }
62 | }
63 |
64 | CustomerList.components = { Pager };
65 | CustomerList.template = "awesome_tshirt.CustomerList";
66 |
67 | CustomerList.props = {
68 | selectCustomer: {
69 | type: Function,
70 | },
71 | };
72 |
--------------------------------------------------------------------------------
/awesome_gallery/static/tests/gallery_view_tests.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module **/
2 |
3 | import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
4 | import { registry } from "@web/core/registry";
5 | import { tooltipService } from "@web/core/tooltip/tooltip_service";
6 | import { uiService } from "@web/core/ui/ui_service";
7 | import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
8 | import { actionService } from "@web/webclient/actions/action_service";
9 |
10 | const serviceRegistry = registry.category("services");
11 |
12 | let serverData;
13 | let target;
14 |
15 | QUnit.module("Views", (hooks) => {
16 | hooks.beforeEach(() => {
17 | serverData = {
18 | models: {
19 | order: {
20 | fields: {
21 | image_url: { string: "Image", type: "char" },
22 | description: { string: "Description", type: "char" },
23 | },
24 | records: [
25 | {
26 | id: 1,
27 | image_url: "",
28 | description: "A nice description",
29 | },
30 | {
31 | id: 2,
32 | image_url: "",
33 | description: "A second nice description",
34 | },
35 | ],
36 | },
37 | },
38 | views: {},
39 | };
40 | setupViewRegistries();
41 | serviceRegistry.add("tooltip", tooltipService);
42 | target = getFixture();
43 | serviceRegistry.add("ui", uiService);
44 | });
45 |
46 | QUnit.module("GalleryView");
47 |
48 | QUnit.test("open record on image click", async function (assert) {
49 | assert.expect(3);
50 |
51 | patchWithCleanup(actionService, {
52 | start() {
53 | const result = this._super(...arguments);
54 | return {
55 | ...result,
56 | switchView(viewType, { resId }) {
57 | assert.step(JSON.stringify({ viewType, resId }));
58 | },
59 | };
60 | },
61 | });
62 |
63 | await makeView({
64 | type: "gallery",
65 | resModel: "order",
66 | serverData,
67 | arch: '',
68 | });
69 | assert.containsOnce(target, ".o_control_panel");
70 | await click(target, ".row > div:nth-child(1) > div");
71 | assert.verifySteps([`{"viewType":"form","resId":1}`]);
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/awesome_tshirt/demo/demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 2
5 |
6 | s
7 | new
8 |
9 | https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/T-shirt2.jpg/640px-T-shirt2.jpg
10 |
11 |
12 |
13 | 1
14 |
15 | s
16 | cancelled
17 |
18 | https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/T-shirt2.jpg/640px-T-shirt2.jpg
19 |
20 |
21 |
22 | 3
23 |
24 | xl
25 | sent
26 |
27 | https://upload.wikimedia.org/wikipedia/commons/thumb/c/ca/Salerno_Calcio_T-Shirt_5.png/640px-Salerno_Calcio_T-Shirt_5.png
28 |
29 |
30 |
31 | 10
32 |
33 | m
34 | printed
35 |
36 |
37 | https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/T-Shirt_Wikipedia_white.jpg/640px-T-Shirt_Wikipedia_white.jpg
38 |
39 |
40 |
41 | 2
42 |
43 | s
44 |
45 | https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/T-shirt-2.jpg/640px-T-shirt-2.jpg
46 |
47 |
48 |
49 | 2
50 |
51 | s
52 | cancelled
53 |
54 | https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/T-shirt2.jpg/640px-T-shirt2.jpg
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/awesome_tshirt/static/src/dashboard/dashboard.js:
--------------------------------------------------------------------------------
1 | /** @odoo-module **/
2 |
3 | import { registry } from "@web/core/registry";
4 | import { Layout } from "@web/search/layout";
5 | import { getDefaultConfig } from "@web/views/view";
6 | import { useService } from "@web/core/utils/hooks";
7 | import { Domain } from "@web/core/domain";
8 | import { Card } from "../card/card";
9 | import { PieChart } from "../pie_chart/pie_chart";
10 | import { CustomerAutocomplete } from "../customer_autocomplete/customer_autocomplete";
11 | import { sprintf } from "@web/core/utils/strings";
12 |
13 | const { Component, useSubEnv, useState } = owl;
14 |
15 | class AwesomeDashboard extends Component {
16 | setup() {
17 | useSubEnv({
18 | config: {
19 | ...getDefaultConfig(),
20 | ...this.env.config,
21 | },
22 | });
23 | this.display = {
24 | controlPanel: { "top-right": false, "bottom-right": false },
25 | };
26 |
27 | this.action = useService("action");
28 | const tshirtService = useService("tshirtService");
29 | this.statistics = useState(tshirtService.statistics);
30 |
31 | this.keyToString = {
32 | average_quantity: this.env._t("Average amount of t-shirt by order this month"),
33 | average_time: this.env._t(
34 | "Average time for an order to go from 'new' to 'sent' or 'cancelled'"
35 | ),
36 | nb_cancelled_orders: this.env._t("Number of cancelled orders this month"),
37 | nb_new_orders: this.env._t("Number of new orders this month"),
38 | total_amount: this.env._t("Total amount of new orders this month"),
39 | };
40 | }
41 |
42 | openCustomerView() {
43 | this.action.doAction("base.action_partner_form");
44 | }
45 |
46 | openOrders(title, domain) {
47 | this.action.doAction({
48 | type: "ir.actions.act_window",
49 | name: title,
50 | res_model: "awesome_tshirt.order",
51 | domain: new Domain(domain).toList(),
52 | views: [
53 | [false, "list"],
54 | [false, "form"],
55 | ],
56 | });
57 | }
58 | openLast7DaysOrders() {
59 | const domain =
60 | "[('create_date','>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]";
61 | this.openOrders(this.env._t("Last 7 days orders", domain));
62 | }
63 |
64 | openLast7DaysCancelledOrders() {
65 | const domain =
66 | "[('create_date','>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d')), ('state','=', 'cancelled')]";
67 | this.openOrders(this.env._t("Last 7 days cancelled orders"), domain);
68 | }
69 |
70 | openFilteredBySizeOrders(size) {
71 | const title = sprintf(this.env._t("Filtered orders by %s size"), size);
72 | const domain = `[('size','=', '${size}')]`;
73 | this.openOrders(title, domain);
74 | }
75 | }
76 |
77 | AwesomeDashboard.components = { Layout, Card, PieChart, CustomerAutocomplete };
78 | AwesomeDashboard.template = "awesome_tshirt.clientaction";
79 |
80 | registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
81 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/awesome_tshirt/models/order.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 | import random
4 | import time
5 | from odoo import models, fields, api
6 | from odoo.osv import expression
7 | from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
8 | from datetime import datetime, timedelta, date
9 |
10 | _logger = logging.getLogger(__name__)
11 |
12 |
13 | class TShirtOrder(models.Model):
14 | _name = 'awesome_tshirt.order'
15 | _description = 'Awesome T-shirt Orders'
16 | _rec_name = 'customer_id'
17 | _inherit = ['mail.thread']
18 |
19 | @api.model
20 | def _expand_states(self, states, domain, order):
21 | return [key for key, val in type(self).state.selection]
22 |
23 | amount = fields.Float('Amount', compute='_compute_amount', store=True)
24 | customer_id = fields.Many2one('res.partner', string="Customer")
25 | image_url = fields.Char('Image', help="encodes the url of the image")
26 | is_late = fields.Boolean('Is late', compute='_compute_is_late')
27 | quantity = fields.Integer('Quantity', default="1")
28 | size = fields.Selection([
29 | ('s', 'S'),
30 | ('m', 'M'),
31 | ('l', 'L'),
32 | ('xl', 'XL'),
33 | ('xxl', 'XXL')], default='m', required="True")
34 | state = fields.Selection([
35 | ('new', 'New'),
36 | ('printed', 'Printed'),
37 | ('sent', 'Sent'),
38 | ('cancelled', 'Cancelled')], default='new', required="True", group_expand='_expand_states')
39 |
40 | @api.depends('quantity')
41 | def _compute_amount(self):
42 | for record in self:
43 | unit_price = 15
44 | if record.size == 's':
45 | unit_price = 12
46 | elif record.size in ['xl', 'xxl']:
47 | unit_price = 18
48 | if record.quantity > 5:
49 | unit_price = unit_price * 0.9
50 | record.amount = record.quantity * unit_price
51 |
52 | @api.depends('create_date')
53 | def _compute_is_late(self):
54 | for record in self:
55 | record.is_late = record.create_date < datetime.today() - timedelta(days=7)
56 |
57 | def print_label(self):
58 | """
59 | This method simulate the printing of a label. It is slow (> 500ms), and
60 | if randomly fails. It returns True if the label has been printed, False
61 | otherwise
62 | """
63 | time.sleep(0.5)
64 | if random.random() < 0.1:
65 | _logger.info('Printer not connected')
66 | return False
67 | _logger.info('Label printed')
68 | return True
69 |
70 | @api.model
71 | def get_empty_list_help(self, help):
72 | title = 'There is no t-shirt order'
73 | base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
74 | url = '%s/awesome_tshirt/order' % base_url
75 | content = 'People can make orders through the public page.' % {'url': url}
76 | return """
77 |
%s
78 |
%s
79 | """ % (title, content)
80 |
81 | @api.model
82 | def get_statistics(self):
83 | """
84 | Returns a dict of statistics about the orders:
85 | 'average_quantity': the average number of t-shirts by order
86 | 'average_time': the average time (in hours) elapsed between the
87 | moment an order is created, and the moment is it sent
88 | 'nb_cancelled_orders': the number of cancelled orders, this month
89 | 'nb_new_orders': the number of new orders, this month
90 | 'total_amount': the total amount of orders, this month
91 | """
92 | first_day = date.today().replace(day=1).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
93 | last_day = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
94 | this_month_domain = [('create_date', '>=', first_day), ('create_date', '<=', last_day)]
95 | new_this_month_domain = expression.AND([this_month_domain, [('state', '=', 'new')]])
96 | nb_new_orders = self.search_count(new_this_month_domain)
97 | cancelled_this_month_domain = expression.AND([this_month_domain, [('state', '=', 'cancelled')]])
98 | nb_cancelled_orders = self.search_count(cancelled_this_month_domain)
99 | total_amount = self.read_group(new_this_month_domain, ['amount'], [])[0]['amount']
100 | total_quantity = self.read_group(this_month_domain, ['quantity'], [])[0]['quantity']
101 | nb_orders = self.search_count(this_month_domain)
102 | orders_by_size = self.read_group([['state', '!=', 'cancelled']], [], ['size'])
103 |
104 | return {
105 | 'average_quantity': 0 if not nb_orders else round(total_quantity / nb_orders, 2),
106 | 'average_time': (random.random() * 44) + 4, # simulate a delay between 4 and 48 hours
107 | 'nb_cancelled_orders': nb_cancelled_orders,
108 | 'nb_new_orders': nb_new_orders,
109 | 'orders_by_size': {g['size']: g['quantity'] for g in orders_by_size},
110 | 'total_amount': total_amount or 0,
111 | }
112 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/awesome_tshirt/views/views.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | awesome_tshirt.orders.form
7 | awesome_tshirt.order
8 |
9 |
35 |
36 |
37 |
38 |
39 | awesome_tshirt.orders.form.simplified
40 | awesome_tshirt.order
41 |
42 |
50 |
51 |
52 |
53 |
54 | awesome_tshirt.orders.kanban
55 | awesome_tshirt.order
56 |
57 |
58 |
59 |
60 |
61 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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_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_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 |
--------------------------------------------------------------------------------
/awesome_tshirt/i18n/awesome_tshirt.pot:
--------------------------------------------------------------------------------
1 | # Translation of Odoo Server.
2 | # This file contains the translation of the following modules:
3 | # * awesome_tshirt
4 | #
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: Odoo Server 15.5alpha1+e\n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2022-09-10 11:50+0000\n"
10 | "PO-Revision-Date: 2022-09-10 11:50+0000\n"
11 | "Last-Translator: \n"
12 | "Language-Team: \n"
13 | "MIME-Version: 1.0\n"
14 | "Content-Type: text/plain; charset=UTF-8\n"
15 | "Content-Transfer-Encoding: \n"
16 | "Plural-Forms: \n"
17 |
18 | #. module: awesome_tshirt
19 | #. openerp-web
20 | #: code:addons/awesome_tshirt/static/src/TodoList/TodoList.xml:0
21 | #, python-format
22 | msgid "Add a todo"
23 | msgstr ""
24 |
25 | #. module: awesome_tshirt
26 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
27 | msgid "Address"
28 | msgstr ""
29 |
30 | #. module: awesome_tshirt
31 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__amount
32 | msgid "Amount"
33 | msgstr ""
34 |
35 | #. module: awesome_tshirt
36 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
37 | msgid "Amount:"
38 | msgstr ""
39 |
40 | #. module: awesome_tshirt
41 | #: model:ir.ui.menu,name:awesome_tshirt.menu_root
42 | msgid "Awesome T-Shirts"
43 | msgstr ""
44 |
45 | #. module: awesome_tshirt
46 | #: model:ir.model,name:awesome_tshirt.model_awesome_tshirt_order
47 | msgid "Awesome T-shirt Orders"
48 | msgstr ""
49 |
50 | #. module: awesome_tshirt
51 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__cancelled
52 | msgid "Cancelled"
53 | msgstr ""
54 |
55 | #. module: awesome_tshirt
56 | #. openerp-web
57 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
58 | #, python-format
59 | msgid "Cancelled Orders"
60 | msgstr ""
61 |
62 | #. module: awesome_tshirt
63 | #. openerp-web
64 | #: code:addons/awesome_tshirt/static/src/Counter/Counter.xml:0
65 | #, python-format
66 | msgid "Counter:"
67 | msgstr ""
68 |
69 | #. module: awesome_tshirt
70 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__create_uid
71 | msgid "Created by"
72 | msgstr ""
73 |
74 | #. module: awesome_tshirt
75 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__create_date
76 | msgid "Created on"
77 | msgstr ""
78 |
79 | #. module: awesome_tshirt
80 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
81 | msgid "Created:"
82 | msgstr ""
83 |
84 | #. module: awesome_tshirt
85 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__customer_id
86 | msgid "Customer"
87 | msgstr ""
88 |
89 | #. module: awesome_tshirt
90 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
91 | msgid "Customer:"
92 | msgstr ""
93 |
94 | #. module: awesome_tshirt
95 | #. openerp-web
96 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
97 | #, python-format
98 | msgid "Customers"
99 | msgstr ""
100 |
101 | #. module: awesome_tshirt
102 | #: model:ir.actions.client,name:awesome_tshirt.dashboard
103 | #: model:ir.ui.menu,name:awesome_tshirt.dashboard_menu
104 | msgid "Dashboard"
105 | msgstr ""
106 |
107 | #. module: awesome_tshirt
108 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__display_name
109 | msgid "Display Name"
110 | msgstr ""
111 |
112 | #. module: awesome_tshirt
113 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
114 | msgid "Email"
115 | msgstr ""
116 |
117 | #. module: awesome_tshirt
118 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__id
119 | msgid "ID"
120 | msgstr ""
121 |
122 | #. module: awesome_tshirt
123 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__image_url
124 | msgid "Image"
125 | msgstr ""
126 |
127 | #. module: awesome_tshirt
128 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
129 | msgid "Image URL"
130 | msgstr ""
131 |
132 | #. module: awesome_tshirt
133 | #. openerp-web
134 | #: code:addons/awesome_tshirt/static/src/Counter/Counter.xml:0
135 | #, python-format
136 | msgid "Increment"
137 | msgstr ""
138 |
139 | #. module: awesome_tshirt
140 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__is_late
141 | msgid "Is late"
142 | msgstr ""
143 |
144 | #. module: awesome_tshirt
145 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order____last_update
146 | msgid "Last Modified on"
147 | msgstr ""
148 |
149 | #. module: awesome_tshirt
150 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__write_uid
151 | msgid "Last Updated by"
152 | msgstr ""
153 |
154 | #. module: awesome_tshirt
155 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__write_date
156 | msgid "Last Updated on"
157 | msgstr ""
158 |
159 | #. module: awesome_tshirt
160 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.thank_you
161 | msgid "Make another order"
162 | msgstr ""
163 |
164 | #. module: awesome_tshirt
165 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
166 | msgid "Name"
167 | msgstr ""
168 |
169 | #. module: awesome_tshirt
170 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__new
171 | msgid "New"
172 | msgstr ""
173 |
174 | #. module: awesome_tshirt
175 | #. openerp-web
176 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
177 | #, python-format
178 | msgid "New Orders"
179 | msgstr ""
180 |
181 | #. module: awesome_tshirt
182 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
183 | msgid "Order"
184 | msgstr ""
185 |
186 | #. module: awesome_tshirt
187 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
188 | msgid "Order awesome custom t-shirts"
189 | msgstr ""
190 |
191 | #. module: awesome_tshirt
192 | #: model:ir.actions.act_window,name:awesome_tshirt.orders
193 | #: model:ir.ui.menu,name:awesome_tshirt.order
194 | msgid "Orders"
195 | msgstr ""
196 |
197 | #. module: awesome_tshirt
198 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__printed
199 | msgid "Printed"
200 | msgstr ""
201 |
202 | #. module: awesome_tshirt
203 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__quantity
204 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
205 | msgid "Quantity"
206 | msgstr ""
207 |
208 | #. module: awesome_tshirt
209 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
210 | msgid "Quantity:"
211 | msgstr ""
212 |
213 | #. module: awesome_tshirt
214 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__sent
215 | msgid "Sent"
216 | msgstr ""
217 |
218 | #. module: awesome_tshirt
219 | #. openerp-web
220 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
221 | #, python-format
222 | msgid "Shirt orders by size"
223 | msgstr ""
224 |
225 | #. module: awesome_tshirt
226 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__size
227 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
228 | msgid "Size"
229 | msgstr ""
230 |
231 | #. module: awesome_tshirt
232 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
233 | msgid "Size:"
234 | msgstr ""
235 |
236 | #. module: awesome_tshirt
237 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__state
238 | msgid "State"
239 | msgstr ""
240 |
241 | #. module: awesome_tshirt
242 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.thank_you
243 | msgid "Thanks for your order!"
244 | msgstr ""
245 |
246 | #. module: awesome_tshirt
247 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__size__xl
248 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
249 | msgid "XL"
250 | msgstr ""
251 |
252 | #. module: awesome_tshirt
253 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__size__xxl
254 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
255 | msgid "XXL"
256 | msgstr ""
257 |
258 | #. module: awesome_tshirt
259 | #: model:ir.model.fields,help:awesome_tshirt.field_awesome_tshirt_order__image_url
260 | msgid "encodes the url of the image"
261 | msgstr ""
262 |
263 | #. module: awesome_tshirt
264 | #. openerp-web
265 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
266 | #, python-format
267 | msgid "Average amount of t-shirt by order this month"
268 | msgstr ""
269 |
270 | #. module: awesome_tshirt
271 | #. openerp-web
272 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
273 | #, python-format
274 | msgid "Average time for an order to go from 'new' to 'sent' or 'cancelled'"
275 | msgstr ""
276 |
277 | #. module: awesome_tshirt
278 | #. openerp-web
279 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
280 | #, python-format
281 | msgid "Last 7 days cancelled orders"
282 | msgstr ""
283 |
284 | #. module: awesome_tshirt
285 | #. openerp-web
286 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
287 | #, python-format
288 | msgid "Last 7 days orders"
289 | msgstr ""
290 |
291 | #. module: awesome_tshirt
292 | #. openerp-web
293 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
294 | #, python-format
295 | msgid "Number of cancelled orders this month"
296 | msgstr ""
297 |
298 | #. module: awesome_tshirt
299 | #. openerp-web
300 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
301 | #, python-format
302 | msgid "Number of new orders this month"
303 | msgstr ""
304 |
305 |
306 | #. module: awesome_tshirt
307 | #. openerp-web
308 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
309 | #, python-format
310 | msgid "Total amount of new orders this month"
311 | msgstr ""
312 |
313 | #. module: awesome_tshirt
314 | #. openerp-web
315 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
316 | #, python-format
317 | msgid "Filtered orders by %s size"
318 | msgstr ""
319 |
320 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/awesome_tshirt/i18n/fr.po:
--------------------------------------------------------------------------------
1 | # Translation of Odoo Server.
2 | # This file contains the translation of the following modules:
3 | # * awesome_tshirt
4 | #
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: Odoo Server 15.5alpha1+e\n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2022-09-10 11:51+0000\n"
10 | "PO-Revision-Date: 2022-09-10 11:51+0000\n"
11 | "Last-Translator: \n"
12 | "Language-Team: \n"
13 | "MIME-Version: 1.0\n"
14 | "Content-Type: text/plain; charset=UTF-8\n"
15 | "Content-Transfer-Encoding: \n"
16 | "Plural-Forms: \n"
17 |
18 | #. module: awesome_tshirt
19 | #. openerp-web
20 | #: code:addons/awesome_tshirt/static/src/TodoList/TodoList.xml:0
21 | #, python-format
22 | msgid "Add a todo"
23 | msgstr "Ajouter un todo"
24 |
25 | #. module: awesome_tshirt
26 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
27 | msgid "Address"
28 | msgstr "Addresse"
29 |
30 | #. module: awesome_tshirt
31 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__amount
32 | msgid "Amount"
33 | msgstr "Quantité"
34 |
35 | #. module: awesome_tshirt
36 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
37 | msgid "Amount:"
38 | msgstr "Quantité"
39 |
40 | #. module: awesome_tshirt
41 | #: model:ir.ui.menu,name:awesome_tshirt.menu_root
42 | msgid "Awesome T-Shirts"
43 | msgstr "Awesome T-Shirts"
44 |
45 | #. module: awesome_tshirt
46 | #: model:ir.model,name:awesome_tshirt.model_awesome_tshirt_order
47 | msgid "Awesome T-shirt Orders"
48 | msgstr "Commande d'Awesome T-shirt"
49 |
50 | #. module: awesome_tshirt
51 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__cancelled
52 | msgid "Cancelled"
53 | msgstr "Annulé"
54 |
55 | #. module: awesome_tshirt
56 | #. openerp-web
57 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
58 | #, python-format
59 | msgid "Cancelled Orders"
60 | msgstr "Commandes annulées"
61 |
62 | #. module: awesome_tshirt
63 | #. openerp-web
64 | #: code:addons/awesome_tshirt/static/src/Counter/Counter.xml:0
65 | #, python-format
66 | msgid "Counter:"
67 | msgstr "Compteur:"
68 |
69 | #. module: awesome_tshirt
70 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__create_uid
71 | msgid "Created by"
72 | msgstr "Créee par"
73 |
74 | #. module: awesome_tshirt
75 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__create_date
76 | msgid "Created on"
77 | msgstr "Créee le"
78 |
79 | #. module: awesome_tshirt
80 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
81 | msgid "Created:"
82 | msgstr "Créee:"
83 |
84 | #. module: awesome_tshirt
85 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__customer_id
86 | msgid "Customer"
87 | msgstr "Client"
88 |
89 | #. module: awesome_tshirt
90 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
91 | msgid "Customer:"
92 | msgstr "Client:"
93 |
94 | #. module: awesome_tshirt
95 | #. openerp-web
96 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
97 | #, python-format
98 | msgid "Customers"
99 | msgstr "Clients"
100 |
101 | #. module: awesome_tshirt
102 | #: model:ir.actions.client,name:awesome_tshirt.dashboard
103 | #: model:ir.ui.menu,name:awesome_tshirt.dashboard_menu
104 | msgid "Dashboard"
105 | msgstr "Tableau de bord"
106 |
107 | #. module: awesome_tshirt
108 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__display_name
109 | msgid "Display Name"
110 | msgstr "Nom d'affichage"
111 |
112 | #. module: awesome_tshirt
113 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
114 | msgid "Email"
115 | msgstr "Email"
116 |
117 | #. module: awesome_tshirt
118 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__id
119 | msgid "ID"
120 | msgstr "ID"
121 |
122 | #. module: awesome_tshirt
123 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__image_url
124 | msgid "Image"
125 | msgstr "Image"
126 |
127 | #. module: awesome_tshirt
128 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
129 | msgid "Image URL"
130 | msgstr "URL de l'image"
131 |
132 | #. module: awesome_tshirt
133 | #. openerp-web
134 | #: code:addons/awesome_tshirt/static/src/Counter/Counter.xml:0
135 | #, python-format
136 | msgid "Increment"
137 | msgstr "Incrémenté"
138 |
139 | #. module: awesome_tshirt
140 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__is_late
141 | msgid "Is late"
142 | msgstr "Est en retard"
143 |
144 | #. module: awesome_tshirt
145 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order____last_update
146 | msgid "Last Modified on"
147 | msgstr "Modifié la dernière fois le"
148 |
149 | #. module: awesome_tshirt
150 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__write_uid
151 | msgid "Last Updated by"
152 | msgstr "Mis à jour la dernière fois par"
153 |
154 | #. module: awesome_tshirt
155 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__write_date
156 | msgid "Last Updated on"
157 | msgstr "Mis à jour la dernière fois le"
158 |
159 | #. module: awesome_tshirt
160 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.thank_you
161 | msgid "Make another order"
162 | msgstr "Faire une autre commande"
163 |
164 | #. module: awesome_tshirt
165 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
166 | msgid "Name"
167 | msgstr "Nom"
168 |
169 | #. module: awesome_tshirt
170 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__new
171 | msgid "New"
172 | msgstr "Nouveau"
173 |
174 | #. module: awesome_tshirt
175 | #. openerp-web
176 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
177 | #, python-format
178 | msgid "New Orders"
179 | msgstr "Nouvelles commandes"
180 |
181 | #. module: awesome_tshirt
182 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
183 | msgid "Order"
184 | msgstr "Commande"
185 |
186 | #. module: awesome_tshirt
187 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
188 | msgid "Order awesome custom t-shirts"
189 | msgstr "Commandez des super t-shirts personnalisés"
190 |
191 | #. module: awesome_tshirt
192 | #: model:ir.actions.act_window,name:awesome_tshirt.orders
193 | #: model:ir.ui.menu,name:awesome_tshirt.order
194 | msgid "Orders"
195 | msgstr "Commandes"
196 |
197 | #. module: awesome_tshirt
198 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__printed
199 | msgid "Printed"
200 | msgstr "Imprimé"
201 |
202 | #. module: awesome_tshirt
203 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__quantity
204 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
205 | msgid "Quantity"
206 | msgstr "Quantité:"
207 |
208 | #. module: awesome_tshirt
209 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
210 | msgid "Quantity:"
211 | msgstr "Quantité:"
212 |
213 | #. module: awesome_tshirt
214 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__state__sent
215 | msgid "Sent"
216 | msgstr "Envoyé"
217 |
218 | #. module: awesome_tshirt
219 | #. openerp-web
220 | #: code:addons/awesome_tshirt/static/src/client_action.xml:0
221 | #, python-format
222 | msgid "Shirt orders by size"
223 | msgstr "Commande de t-shirt par taille"
224 |
225 | #. module: awesome_tshirt
226 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__size
227 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
228 | msgid "Size"
229 | msgstr "Taille"
230 |
231 | #. module: awesome_tshirt
232 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.orders_kanban_view
233 | msgid "Size:"
234 | msgstr "Taille:"
235 |
236 | #. module: awesome_tshirt
237 | #: model:ir.model.fields,field_description:awesome_tshirt.field_awesome_tshirt_order__state
238 | msgid "State"
239 | msgstr "État"
240 |
241 | #. module: awesome_tshirt
242 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.thank_you
243 | msgid "Thanks for your order!"
244 | msgstr ""
245 |
246 | #. module: awesome_tshirt
247 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__size__xl
248 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
249 | msgid "XL"
250 | msgstr ""
251 |
252 | #. module: awesome_tshirt
253 | #: model:ir.model.fields.selection,name:awesome_tshirt.selection__awesome_tshirt_order__size__xxl
254 | #: model_terms:ir.ui.view,arch_db:awesome_tshirt.order_public_page
255 | msgid "XXL"
256 | msgstr ""
257 |
258 | #. module: awesome_tshirt
259 | #: model:ir.model.fields,help:awesome_tshirt.field_awesome_tshirt_order__image_url
260 | msgid "encodes the url of the image"
261 | msgstr "Encodez l'url de l'image"
262 |
263 | #. module: awesome_tshirt
264 | #. openerp-web
265 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
266 | #, python-format
267 | msgid "Average amount of t-shirt by order this month"
268 | msgstr "Montant en moyenne de t-shirt par commande ce mois-ci"
269 |
270 | #. module: awesome_tshirt
271 | #. openerp-web
272 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
273 | #, python-format
274 | msgid "Average time for an order to go from 'new' to 'sent' or 'cancelled'"
275 | msgstr "Temps moyen pour qu'une commande passe de 'nouvelle' à 'envoyée' ou 'annulée'."
276 |
277 | #. module: awesome_tshirt
278 | #. openerp-web
279 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
280 | #, python-format
281 | msgid "Last 7 days cancelled orders"
282 | msgstr "Annulation des 7 derniers jours"
283 |
284 | #. module: awesome_tshirt
285 | #. openerp-web
286 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
287 | #, python-format
288 | msgid "Last 7 days orders"
289 | msgstr "Commandes des 7 derniers jours"
290 |
291 | #. module: awesome_tshirt
292 | #. openerp-web
293 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
294 | #, python-format
295 | msgid "Number of cancelled orders this month"
296 | msgstr "Nombre de commandes annulées ce mois"
297 |
298 | #. module: awesome_tshirt
299 | #. openerp-web
300 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
301 | #, python-format
302 | msgid "Number of new orders this month"
303 | msgstr "Nombre de nouvelles commande ce mois"
304 |
305 |
306 | #. module: awesome_tshirt
307 | #. openerp-web
308 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
309 | #, python-format
310 | msgid "Total amount of new orders this month"
311 | msgstr "Montant total des nouvelles commandes ce mois"
312 |
313 | #. module: awesome_tshirt
314 | #. openerp-web
315 | #: code:addons/awesome_tshirt/static/src/client_action.js:0
316 | #, python-format
317 | msgid "Filtered orders by %s size"
318 | msgstr "Commandes filtrées par la taille %s"
319 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------