├── .gitignore ├── awesome_gallery ├── __init__.py ├── __manifest__.py ├── models │ ├── __init__.py │ ├── ir_action.py │ └── ir_ui_view.py ├── rng │ └── gallery.rng ├── static │ ├── src │ │ ├── gallery_arch_parser.js │ │ ├── gallery_controller.js │ │ ├── gallery_controller.xml │ │ ├── gallery_image │ │ │ ├── gallery_image.js │ │ │ └── gallery_image.xml │ │ ├── gallery_model.js │ │ ├── gallery_renderer.js │ │ ├── gallery_renderer.xml │ │ └── gallery_view.js │ └── tests │ │ └── gallery_view_tests.js └── validation.py ├── awesome_tshirt ├── __init__.py ├── __manifest__.py ├── controllers │ ├── __init__.py │ └── controllers.py ├── demo │ └── demo.xml ├── i18n │ ├── awesome_tshirt.pot │ └── fr.po ├── models │ ├── __init__.py │ ├── ir_http.py │ ├── order.py │ └── res_partner.py ├── security │ └── ir.model.access.csv ├── static │ ├── src │ │ ├── autoreload_kanban_view │ │ │ └── autoreload_kanban_view.js │ │ ├── card │ │ │ ├── card.js │ │ │ └── card.xml │ │ ├── control_panel_patch │ │ │ ├── control_panel_patch.js │ │ │ ├── control_panel_patch.scss │ │ │ └── control_panel_patch.xml │ │ ├── counter │ │ │ ├── counter.js │ │ │ └── counter.xml │ │ ├── customer_autocomplete │ │ │ ├── customer_autocomplete.js │ │ │ └── customer_autocomplete.xml │ │ ├── customer_kanban_view │ │ │ ├── customer_kanban_view.js │ │ │ ├── customer_kanban_view.scss │ │ │ └── customer_kanban_view.xml │ │ ├── customer_list │ │ │ ├── customer_list.js │ │ │ ├── customer_list.scss │ │ │ └── customer_list.xml │ │ ├── dashboard │ │ │ ├── dashboard.js │ │ │ ├── dashboard.scss │ │ │ └── dashboard.xml │ │ ├── dashboard_loader.js │ │ ├── image_preview_field │ │ │ ├── image_preview_field.js │ │ │ └── image_preview_field.xml │ │ ├── kitten_mode │ │ │ ├── kitten_command.js │ │ │ ├── kitten_service.js │ │ │ └── kitten_service.scss │ │ ├── late_order_boolean_field │ │ │ ├── late_order_boolean_field.js │ │ │ └── late_order_boolean_field.xml │ │ ├── order_form_view │ │ │ ├── order_form_view.js │ │ │ └── order_form_view.xml │ │ ├── order_warning_widget │ │ │ ├── order_warning_widget.js │ │ │ └── order_warning_widget.xml │ │ ├── pie_chart │ │ │ ├── pie_chart.js │ │ │ └── pie_chart.xml │ │ ├── stat_systray │ │ │ ├── stat_systray.js │ │ │ └── stat_systray.xml │ │ ├── todo │ │ │ ├── todo.js │ │ │ └── todo.xml │ │ ├── todo_list │ │ │ ├── todo_list.js │ │ │ └── todo_list.xml │ │ ├── tshirt_service.js │ │ └── utils.js │ └── tests │ │ ├── counter_tests.js │ │ └── tours │ │ └── order_flow.js ├── tests │ └── test_order_tour.py └── views │ ├── templates.xml │ └── views.xml ├── exercises_1_owl.md ├── exercises_2_web_framework.md ├── exercises_3_fields_views.md ├── exercises_4_misc.md ├── exercises_5_custom_kanban_view.md ├── exercises_6_creating_views.md ├── exercises_7_testing.md ├── images ├── 1.0.png ├── 1.1.png ├── 1.2.png ├── 1.4.png ├── 1.5.png ├── 1.7.png ├── 1.8.png ├── 1.9.png ├── 2.0.png ├── 2.1.png ├── 2.2.png ├── 2.3.png ├── 2.5.png ├── 2.6.png ├── 3.3.png ├── 3.5.png ├── 3.6.png ├── 4.1.png ├── 4.2.png ├── 4.4.png ├── 4.5.1.png ├── 4.5.2.png ├── 4.6.png ├── 4.7.png ├── 5.0.png ├── 5.2.png ├── 5.3.png ├── 5.4.png ├── 5.6.png ├── 5.7.png ├── 6.0.png ├── 6.1.1.png ├── 6.1.2.png ├── 6.2.png ├── 6.4.png ├── 6.6.png ├── 6.8.png ├── 6.9.png ├── 7.2.png └── 7.3.png ├── notes_architecture.md ├── notes_concurrency.md ├── notes_fields.md ├── notes_network_requests.md ├── notes_odoo_js_ecosystem.md ├── notes_testing.md ├── notes_views.md └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | -------------------------------------------------------------------------------- /awesome_gallery/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import models 3 | from . import validation 4 | -------------------------------------------------------------------------------- /awesome_gallery/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | 'name': "Gallery View", 4 | 'summary': "Defines the 'gallery' view", 5 | 'description': """ 6 | Defines a new type of view ('awesome_gallery') which allows to visualize images. 7 | """, 8 | 9 | 'version': '0.1', 10 | 'depends': ['web'], 11 | 'data': [], 12 | 'assets': { 13 | 'web.assets_backend': [ 14 | 'awesome_gallery/static/tests/**/*', 15 | 'awesome_gallery/static/src/**/*', 16 | ], 17 | }, 18 | 'license': 'AGPL-3' 19 | } 20 | -------------------------------------------------------------------------------- /awesome_gallery/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # import filename_python_file_within_folder_or_subfolder 3 | from . import ir_action 4 | from . import ir_ui_view 5 | -------------------------------------------------------------------------------- /awesome_gallery/models/ir_action.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import fields, models 3 | 4 | 5 | class ActWindowView(models.Model): 6 | _inherit = 'ir.actions.act_window.view' 7 | 8 | view_mode = fields.Selection(selection_add=[ 9 | ('gallery', "Awesome Gallery") 10 | ], ondelete={'gallery': 'cascade'}) 11 | -------------------------------------------------------------------------------- /awesome_gallery/models/ir_ui_view.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import fields, models 3 | 4 | 5 | class View(models.Model): 6 | _inherit = 'ir.ui.view' 7 | 8 | type = fields.Selection(selection_add=[('gallery', "Awesome Gallery")]) 9 | -------------------------------------------------------------------------------- /awesome_gallery/rng/gallery.rng: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /awesome_gallery/static/src/gallery_arch_parser.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { XMLParser } from "@web/core/utils/xml"; 4 | 5 | export class GalleryArchParser extends XMLParser { 6 | parse(arch) { 7 | const xmlDoc = this.parseXML(arch); 8 | const imageField = xmlDoc.getAttribute("image_field"); 9 | const limit = xmlDoc.getAttribute("limit") || 80; 10 | const tooltipField = xmlDoc.getAttribute("tooltip_field"); 11 | return { 12 | imageField, 13 | limit, 14 | tooltipField, 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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_gallery/static/src/gallery_controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /awesome_gallery/static/src/gallery_image/gallery_image.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | const { Component } = owl; 4 | import { useTooltip } from "@web/core/tooltip/tooltip_hook"; 5 | 6 | export class GalleryImage extends Component { 7 | setup() { 8 | useTooltip("tooltip", { 9 | tooltip: this.props.image[this.props.tooltipField], 10 | }); 11 | } 12 | onClick() { 13 | this.props.onClick(this.props.image.id); 14 | } 15 | } 16 | 17 | GalleryImage.template = "awesome_gallery.GalleryImage"; 18 | GalleryImage.props = { 19 | image: { type: Object }, 20 | className: { type: String }, 21 | imageField: { type: String }, 22 | tooltipField: { type: String }, 23 | onClick: { type: Function }, 24 | }; 25 | -------------------------------------------------------------------------------- /awesome_gallery/static/src/gallery_image/gallery_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /awesome_gallery/static/src/gallery_renderer.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | import { useService } from "@web/core/utils/hooks"; 3 | const { Component } = owl; 4 | import { GalleryImage } from "./gallery_image/gallery_image"; 5 | 6 | export class GalleryRenderer extends Component { 7 | setup() { 8 | this.action = useService("action"); 9 | } 10 | 11 | onImageClick(resId) { 12 | this.action.switchView("form", { resId }); 13 | } 14 | } 15 | 16 | GalleryRenderer.components = { GalleryImage }; 17 | GalleryRenderer.template = "awesome_gallery.Renderer"; 18 | -------------------------------------------------------------------------------- /awesome_gallery/static/src/gallery_renderer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /awesome_gallery/static/src/gallery_view.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | import { GalleryController } from "./gallery_controller"; 5 | import { GalleryArchParser } from "./gallery_arch_parser"; 6 | import { GalleryModel } from "./gallery_model"; 7 | import { GalleryRenderer } from "./gallery_renderer"; 8 | 9 | export const galleryView = { 10 | type: "gallery", 11 | display_name: "Gallery", 12 | icon: "fa fa-picture-o", 13 | multiRecord: true, 14 | Controller: GalleryController, 15 | ArchParser: GalleryArchParser, 16 | Model: GalleryModel, 17 | Renderer: GalleryRenderer, 18 | 19 | props(genericProps, view) { 20 | const { ArchParser } = view; 21 | const { arch } = genericProps; 22 | const archInfo = new ArchParser().parse(arch); 23 | 24 | return { 25 | ...genericProps, 26 | Model: view.Model, 27 | Renderer: view.Renderer, 28 | archInfo, 29 | }; 30 | }, 31 | }; 32 | 33 | registry.category("views").add("gallery", galleryView); 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /awesome_gallery/validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | 5 | from lxml import etree 6 | 7 | from odoo.loglevels import ustr 8 | from odoo.tools import misc, view_validation 9 | 10 | _logger = logging.getLogger(__name__) 11 | 12 | _gallery_validator = None 13 | 14 | 15 | @view_validation.validate('gallery') 16 | def schema_gallery(arch, **kwargs): 17 | """ Check the gallery view against its schema 18 | 19 | :type arch: etree._Element 20 | """ 21 | global _gallery_validator 22 | 23 | if _gallery_validator is None: 24 | with misc.file_open(os.path.join('awesome_gallery', 'rng', 'gallery.rng')) as f: 25 | _gallery_validator = etree.RelaxNG(etree.parse(f)) 26 | 27 | if _gallery_validator.validate(arch): 28 | return True 29 | 30 | for error in _gallery_validator.error_log: 31 | _logger.error(ustr(error)) 32 | return False 33 | -------------------------------------------------------------------------------- /awesome_tshirt/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import controllers 4 | from . import models 5 | from . import tests 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /awesome_tshirt/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import controllers -------------------------------------------------------------------------------- /awesome_tshirt/controllers/controllers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import random 5 | 6 | from odoo import http 7 | from odoo.http import request 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class AwesomeTshirt(http.Controller): 13 | @http.route(['/awesome_tshirt/order'], type='http', auth='public') 14 | def make_order(self): 15 | """ 16 | Renders the public page to make orders 17 | """ 18 | return request.render('awesome_tshirt.order_public_page') 19 | 20 | @http.route(['/awesome_tshirt/validate_order'], type='http', auth="public", methods=['POST'], website=True) 21 | def validate_order(self, name, email, address, quantity, size, url): 22 | """ 23 | Creates an order (and optionnaly a partner) with the given data 24 | """ 25 | Partner = request.env['res.partner'].sudo() 26 | customer = Partner.search([('email', '=', email)], limit=1) 27 | if not customer: 28 | customer = Partner.create({ 29 | 'street': address, 30 | 'email': email, 31 | 'name': name, 32 | }) 33 | request.env['awesome_tshirt.order'].create({ 34 | 'customer_id': customer.id, 35 | 'quantity': quantity, 36 | 'size': size, 37 | 'image_url': url, 38 | }) 39 | return request.render('awesome_tshirt.thank_you') 40 | 41 | @http.route('/awesome_tshirt/statistics', type='json', auth='user') 42 | def get_statistics(self): 43 | return http.request.env['awesome_tshirt.order'].get_statistics() 44 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /awesome_tshirt/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import order 4 | from . import res_partner 5 | from . import ir_http 6 | -------------------------------------------------------------------------------- /awesome_tshirt/models/ir_http.py: -------------------------------------------------------------------------------- 1 | from odoo import models 2 | 3 | class IrHttp(models.AbstractModel): 4 | _inherit = 'ir.http' 5 | 6 | def session_info(self): 7 | res = super(IrHttp, self).session_info() 8 | res['tshirt_statistics'] = self.env["awesome_tshirt.order"].get_statistics() 9 | return res 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /awesome_tshirt/models/res_partner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import api, fields, models 4 | 5 | 6 | class ResPartner(models.Model): 7 | _name = 'res.partner' 8 | _inherit = 'res.partner' 9 | 10 | order_ids = fields.One2many('awesome_tshirt.order', 'customer_id', string="Orders") 11 | has_active_order = fields.Boolean(compute='_compute_has_active_order', store=True) 12 | 13 | @api.depends('order_ids', 'order_ids.state') 14 | def _compute_has_active_order(self): 15 | for record in self: 16 | record.has_active_order = record.order_ids.filtered(lambda r: r.state not in ['sent', 'cancelled']) 17 | -------------------------------------------------------------------------------- /awesome_tshirt/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | awesome_tshirt.access_awesome_tshirt_order,access_awesome_tshirt_order,awesome_tshirt.model_awesome_tshirt_order,base.group_user,1,1,1,1 -------------------------------------------------------------------------------- /awesome_tshirt/static/src/autoreload_kanban_view/autoreload_kanban_view.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { KanbanController } from "@web/views/kanban/kanban_controller"; 4 | import { registry } from "@web/core/registry"; 5 | import { kanbanView } from "@web/views/kanban/kanban_view"; 6 | import { useInterval } from "../utils"; 7 | 8 | class AutoreloadKanbanController extends KanbanController { 9 | setup() { 10 | super.setup(); 11 | useInterval(() => { 12 | this.model.load(); 13 | }, 30_000); 14 | } 15 | } 16 | 17 | const AutoreloadKanbanView = { 18 | ...kanbanView, 19 | Controller: AutoreloadKanbanController, 20 | }; 21 | 22 | registry.category("views").add("autoreloadkanban", AutoreloadKanbanView); 23 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/card/card.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | const { Component } = owl; 4 | 5 | export class Card extends Component {} 6 | 7 | Card.template = "awesome_tshirt.Card"; 8 | Card.props = { 9 | slots: { 10 | type: Object, 11 | shape: { 12 | default: Object, 13 | title: { type: Object, optional: true }, 14 | }, 15 | }, 16 | className: { 17 | type: String, 18 | optional: true, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/card/card.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 |
8 |
9 |

10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/control_panel_patch/control_panel_patch.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { ControlPanel } from "@web/search/control_panel/control_panel"; 4 | import { patch } from "@web/core/utils/patch"; 5 | import { useService } from "@web/core/utils/hooks"; 6 | import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; 7 | 8 | patch(ControlPanel.prototype, "awesome_tshirt.ControlPanelBafienEyes.onClick", { 9 | setup() { 10 | this._super(...arguments); 11 | this.dialog = useService("dialog"); 12 | }, 13 | 14 | openDialog() { 15 | this.dialog.add(ConfirmationDialog, { 16 | body: this.env._t( 17 | "Bafien is watching you. This interaction is recorded and may be used in legal proceedings if necessary. Do you agree to these terms?" 18 | ), 19 | }); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/control_panel_patch/control_panel_patch.scss: -------------------------------------------------------------------------------- 1 | .blink { 2 | animation: blink-animation 1s steps(5, start) infinite; 3 | -webkit-animation: blink-animation 1s steps(5, start) infinite; 4 | } 5 | 6 | @keyframes blink-animation { 7 | to { 8 | visibility: hidden; 9 | } 10 | } 11 | 12 | @-webkit-keyframes blink-animation { 13 | to { 14 | visibility: hidden; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/control_panel_patch/control_panel_patch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
  • 7 |
    8 |
    9 | 10 |
    11 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/counter/counter.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | const { Component, useState } = owl; 4 | 5 | export class Counter extends Component { 6 | setup() { 7 | this.state = useState({ value: 1 }); 8 | } 9 | 10 | increment() { 11 | this.state.value = this.state.value + 1; 12 | } 13 | } 14 | 15 | Counter.template = "awesome_tshirt.Counter"; 16 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/counter/counter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

    Counter:

    5 | 6 |
    7 |
    8 | -------------------------------------------------------------------------------- /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_autocomplete/customer_autocomplete.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 12 |
    13 |
    14 |
    15 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/customer_kanban_view/customer_kanban_view.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | import { KanbanController } from "@web/views/kanban/kanban_controller"; 3 | import { kanbanView } from "@web/views/kanban/kanban_view"; 4 | import { registry } from "@web/core/registry"; 5 | import { CustomerList } from "../customer_list/customer_list"; 6 | 7 | class CustomerKanbanController extends KanbanController { 8 | setup() { 9 | super.setup(); 10 | this.archInfo = { ...this.props.archInfo }; 11 | this.archInfo.className += " flex-grow-1"; 12 | } 13 | 14 | selectCustomer(customer_id, customer_name) { 15 | this.env.searchModel.setDomainParts({ 16 | customer: { 17 | domain: [["customer_id", "=", customer_id]], 18 | facetLabel: customer_name, 19 | }, 20 | }); 21 | } 22 | } 23 | 24 | CustomerKanbanController.components = { ...KanbanController.components, CustomerList }; 25 | CustomerKanbanController.template = "awesome_tshirt.CustomerKanbanView"; 26 | export const customerKanbanView = { 27 | ...kanbanView, 28 | Controller: CustomerKanbanController, 29 | }; 30 | 31 | registry.category("views").add("customer_kanban", customerKanbanView); 32 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/customer_kanban_view/customer_kanban_view.scss: -------------------------------------------------------------------------------- 1 | .o_awesome_tshirt_sidebar { 2 | width: 300px; 3 | } 4 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/customer_kanban_view/customer_kanban_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 | 7 |
    8 |
    9 | 10 | 11 | archInfo 12 | 13 | 14 | 15 | model.useSampleModel ? 'o_view_sample_data' : '' + "d-flex" 16 | 17 |
    18 |
    19 | -------------------------------------------------------------------------------- /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_tshirt/static/src/customer_list/customer_list.scss: -------------------------------------------------------------------------------- 1 | .o_awesome_tshirt_customer_hover { 2 | &:hover { 3 | background-color: $gray-200; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/customer_list/customer_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 28 | 29 |
    8 | Customers with active orders 9 |
    10 | 11 | 14 |
    15 | 16 |
    22 | 23 |
    27 |
    30 |
    31 |
    32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/dashboard/dashboard.scss: -------------------------------------------------------------------------------- 1 | $o-awesome_tshirt-bg-color: $gray-700; 2 | 3 | .o_awesome_tshirt_dashboard { 4 | background-color: $o-awesome_tshirt-bg-color; 5 | } 6 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/dashboard/dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 | 15 | 16 |
    17 | 18 | Shirt orders by size 19 | 20 | 21 |
    22 |
    23 | 24 |
    25 | 26 | 27 |
    28 |
    29 | 30 | 31 |

    32 |
    33 |
    34 |
    35 |
    36 | 37 |
    38 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/dashboard_loader.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | import { LazyComponent } from "@web/core/assets"; 5 | 6 | const { Component, xml } = owl; 7 | 8 | class AwesomeDashboardLoader extends Component {} 9 | 10 | AwesomeDashboardLoader.components = { LazyComponent }; 11 | AwesomeDashboardLoader.template = xml` 12 | 13 | `; 14 | 15 | registry.category("actions").add("awesome_tshirt.dashboard", AwesomeDashboardLoader); 16 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/image_preview_field/image_preview_field.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | import { CharField } from "@web/views/fields/char/char_field"; 5 | import { useCommand } from "@web/core/commands/command_hook"; 6 | const { Component } = owl; 7 | 8 | class ImagePreviewField extends Component { 9 | setup() { 10 | useCommand(this.env._t("Open image in a new tab"), () => { 11 | window.open(this.props.value, "_blank"); 12 | }); 13 | } 14 | } 15 | 16 | ImagePreviewField.template = "awesome_tshirt.ImagePreviewField"; 17 | ImagePreviewField.components = { CharField }; 18 | ImagePreviewField.supportedTypes = ["char"]; 19 | 20 | registry.category("fields").add("image_preview", ImagePreviewField); 21 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/image_preview_field/image_preview_field.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

    MISSING TSHIRT DESIGN

    7 |
    8 | 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/kitten_mode/kitten_command.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | 5 | const commandProviderRegistry = registry.category("command_provider"); 6 | 7 | commandProviderRegistry.add("Kitten", { 8 | provide: (env, options) => { 9 | const result = []; 10 | const { active, enable, disable } = env.services.kitten; 11 | if (active) { 12 | result.push({ 13 | action: disable, 14 | category: "kitten", 15 | name: env._t("Disable kitten mode"), 16 | }); 17 | } else { 18 | result.push({ 19 | action: enable, 20 | category: "kitten", 21 | name: env._t("Actvate kitten mode"), 22 | }); 23 | } 24 | return result; 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/kitten_mode/kitten_service.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | import { browser } from "@web/core/browser/browser"; 5 | import { routeToUrl } from "@web/core/browser/router_service"; 6 | 7 | export const kitten_service = { 8 | dependencies: ["router"], 9 | async start(env, { router }) { 10 | let { search, route } = router.current; 11 | let active = search.kitten === 1; 12 | if (active) { 13 | document.documentElement.classList.add("o-kitten-mode"); 14 | } 15 | 16 | return { 17 | active, 18 | enable() { 19 | active = true; 20 | route = router.current; 21 | search.kitten = "1"; 22 | browser.location.href = browser.location.origin + routeToUrl(route); 23 | }, 24 | disable() { 25 | active = false; 26 | route = router.current; 27 | search.kitten = ""; 28 | browser.location.href = browser.location.origin + routeToUrl(route); 29 | }, 30 | }; 31 | }, 32 | }; 33 | 34 | registry.category("services").add("kitten", kitten_service); 35 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/kitten_mode/kitten_service.scss: -------------------------------------------------------------------------------- 1 | .o-kitten-mode { 2 | background-image: url(https://upload.wikimedia.org/wikipedia/commons/5/58/Mellow_kitten_%28Unsplash%29.jpg); 3 | background-size: cover; 4 | background-attachment: fixed; 5 | 6 | & > * { 7 | opacity: 0.9; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/late_order_boolean_field/late_order_boolean_field.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | import { BooleanField } from "@web/views/fields/boolean/boolean_field"; 5 | 6 | class LateOrderBooleanField extends BooleanField {} 7 | 8 | LateOrderBooleanField.template = "awesome_tshirt.LateOrderBooleanField"; 9 | 10 | registry.category("fields").add("late_boolean", LateOrderBooleanField); 11 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/late_order_boolean_field/late_order_boolean_field.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Late! 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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/src/order_form_view/order_form_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/order_warning_widget/order_warning_widget.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | 5 | const { Component, markup } = owl; 6 | 7 | class OrderWarningWidget extends Component { 8 | get warnings() { 9 | const warningsList = []; 10 | if (this.props.record.data.image_url.length === 0) { 11 | warningsList.push(markup("There is no image ")); 12 | } 13 | if (this.props.record.data.amount > 100) { 14 | warningsList.push(markup("Add promotional material ")); 15 | } 16 | return warningsList; 17 | } 18 | } 19 | 20 | OrderWarningWidget.template = "awesome_tshirt.OrderWarningWidget"; 21 | OrderWarningWidget.fieldDependencies = { 22 | image_url: { type: "char" }, 23 | amount: { type: "float" }, 24 | }; 25 | registry.category("view_widgets").add("order_warning", OrderWarningWidget); 26 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/order_warning_widget/order_warning_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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_tshirt/static/src/pie_chart/pie_chart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 |
    6 | 7 |
    8 |
    9 |
    10 |
    11 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/stat_systray/stat_systray.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module **/ 2 | 3 | import { registry } from "@web/core/registry"; 4 | import { useService } from "@web/core/utils/hooks"; 5 | import { Domain } from "@web/core/domain"; 6 | const { Component, useState } = owl; 7 | 8 | export class StatSystray extends Component { 9 | setup() { 10 | const tshirtService = useService("tshirtService"); 11 | this.statistics = useState(tshirtService.statistics); 12 | this.action = useService("action"); 13 | } 14 | 15 | openNewOrders() { 16 | this.action.doAction({ 17 | type: "ir.actions.act_window", 18 | name: "New orders", 19 | res_model: "awesome_tshirt.order", 20 | domain: new Domain("[('state','=', 'new')]").toList(), 21 | views: [ 22 | [false, "list"], 23 | [false, "form"], 24 | ], 25 | }); 26 | } 27 | } 28 | StatSystray.template = "awesome_tshirt.StatSystray"; 29 | 30 | export const systrayItem = { 31 | Component: StatSystray, 32 | }; 33 | registry.category("systray").add("awesome_tshirt.Statistics", systrayItem, { sequence: 1000 }); 34 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/stat_systray/stat_systray.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 8 |
    9 |
    10 |
    11 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/todo/todo.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | const { Component } = owl; 4 | 5 | export class Todo extends Component { 6 | onClick(ev) { 7 | this.props.toggleState(this.props.id); 8 | } 9 | 10 | onRemove() { 11 | this.props.removeTodo(this.props.id); 12 | } 13 | } 14 | 15 | Todo.template = "awesome_tshirt.Todo"; 16 | Todo.props = { 17 | id: { type: Number }, 18 | description: { type: String }, 19 | done: { type: Boolean }, 20 | toggleState: { type: Function }, 21 | removeTodo: { type: Function}, 22 | }; 23 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/todo/todo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | 10 | 11 | 12 |
    13 |
    14 |
    15 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/todo_list/todo_list.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { Todo } from "../todo/todo"; 4 | import { useAutofocus } from "../utils"; 5 | 6 | const { Component, useState } = owl; 7 | 8 | export class TodoList extends Component { 9 | setup() { 10 | this.nextId = 0; 11 | this.todoList = useState([]); 12 | useAutofocus("todoListInput"); 13 | } 14 | 15 | addTodo(ev) { 16 | if (ev.keyCode === 13 && ev.target.value != "") { 17 | this.todoList.push({ id: this.nextId++, description: ev.target.value, done: false }); 18 | ev.target.value = ""; 19 | } 20 | } 21 | 22 | toggleTodo(todoId) { 23 | const todo = this.todoList.find((todo) => todo.id === todoId); 24 | if (todo) { 25 | todo.done = !todo.done; 26 | } 27 | } 28 | 29 | removeTodo(todoId) { 30 | const todoIndex = this.todoList.findIndex((todo) => todo.id === todoId); 31 | if (todoIndex >= 0) { 32 | this.todoList.splice(todoIndex, 1); 33 | } 34 | } 35 | } 36 | 37 | TodoList.components = { Todo }; 38 | TodoList.template = "awesome_tshirt.TodoList"; 39 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/todo_list/todo_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | 7 | 8 | 9 |
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/tshirt_service.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { registry } from "@web/core/registry"; 4 | import { session } from "@web/session"; 5 | import { memoize } from "@web/core/utils/functions"; 6 | 7 | const { reactive } = owl; 8 | 9 | export const tshirtService = { 10 | dependencies: ["rpc", "orm"], 11 | async start(env, { rpc, orm }) { 12 | const statistics = reactive({}); 13 | 14 | if (session.tshirt_statistics) { 15 | Object.assign(statistics, session.tshirt_statistics); 16 | } else { 17 | Object.assign(statistics, await rpc("/awesome_tshirt/statistics")); 18 | } 19 | 20 | setInterval(async () => { 21 | Object.assign(statistics, await rpc("/awesome_tshirt/statistics")); 22 | }, 60000); 23 | 24 | async function loadCustomers() { 25 | return await orm.searchRead("res.partner", [], ["display_name"]); 26 | } 27 | 28 | return { 29 | statistics, 30 | loadCustomers: memoize(loadCustomers), 31 | }; 32 | }, 33 | }; 34 | 35 | registry.category("services").add("tshirtService", tshirtService); 36 | -------------------------------------------------------------------------------- /awesome_tshirt/static/src/utils.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module **/ 2 | 3 | const { onMounted, onWillUnmount, useRef } = owl; 4 | 5 | export function useInterval(func, ms) { 6 | let intervalId; 7 | onMounted(() => { 8 | intervalId = setInterval(func, ms); 9 | }); 10 | 11 | onWillUnmount(() => { 12 | clearInterval(intervalId); 13 | }); 14 | } 15 | 16 | export function useAutofocus(name) { 17 | const ref = useRef(name); 18 | onMounted(() => ref.el && ref.el.focus()); 19 | } 20 | -------------------------------------------------------------------------------- /awesome_tshirt/static/tests/counter_tests.js: -------------------------------------------------------------------------------- 1 | /** @odoo-module **/ 2 | 3 | import { Counter } from "../src/counter/counter"; 4 | import { click, getFixture, mount } from "@web/../tests/helpers/utils"; 5 | 6 | let target; 7 | 8 | QUnit.module("Components", (hooks) => { 9 | hooks.beforeEach(async () => { 10 | target = getFixture(); 11 | }); 12 | 13 | QUnit.module("Counter"); 14 | 15 | QUnit.test("Counter is correctly incremented", async (assert) => { 16 | await mount(Counter, target); 17 | assert.strictEqual(target.querySelector("body > p").innerHTML, "Counter: 1"); 18 | await click(target, ".btn-primary"); 19 | assert.strictEqual(target.querySelector("body > p").innerHTML, "Counter: 2"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /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/tests/test_order_tour.py: -------------------------------------------------------------------------------- 1 | from odoo.tests import HttpCase 2 | from odoo.tests.common import tagged 3 | 4 | @tagged('post_install', '-at_install') 5 | class TestOrderTour(HttpCase): 6 | def test_order_tour(self): 7 | # You can also use watch=True to see the browser in action 8 | self.start_tour('/web', 'order_tour', login="admin") 9 | -------------------------------------------------------------------------------- /awesome_tshirt/views/templates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 83 | 84 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /awesome_tshirt/views/views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | awesome_tshirt.orders.form 7 | awesome_tshirt.order 8 | 9 |
    10 |
    11 | 12 |
    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
    31 | 32 | 33 |
    34 |
    35 |
    36 |
    37 | 38 | 39 | awesome_tshirt.orders.form.simplified 40 | awesome_tshirt.order 41 | 42 |
    43 | 44 | 45 | 46 | 47 | 48 | 49 |
    50 |
    51 |
    52 | 53 | 54 | awesome_tshirt.orders.kanban 55 | awesome_tshirt.order 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    Customer:
    63 |
    Size:
    64 |
    Quantity:
    65 |
    Amount:
    66 |
    Created:
    67 |
    68 |
    69 |
    70 |
    71 |
    72 |
    73 |
    74 | 75 | 76 | awesome_tshirt.orders.list 77 | awesome_tshirt.order 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | awesome_tshirt.orders.gallery 92 | awesome_tshirt.order 93 | 94 | 95 | 96 | 97 | 98 | 99 | awesome_tshirt.orders.search 100 | awesome_tshirt.order 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | awesome_tshirt.res.partner.form 110 | res.partner 111 | 112 | 113 | 114 | customer_form_view 115 | 116 | 117 | 118 | 119 | 120 | 121 | T-shirt Orders 122 | awesome_tshirt.order 123 | kanban,tree,form,gallery 124 | 125 | 126 | 127 | Dashboard 128 | awesome_tshirt.dashboard 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 |
    137 |
    138 | -------------------------------------------------------------------------------- /exercises_1_owl.md: -------------------------------------------------------------------------------- 1 | # Part 1: Owl Framework 🦉 2 | 3 | Components are the basic UI building blocks in Odoo. Odoo components are made 4 | with the Owl framework, which is a component system custom made for Odoo. 5 | 6 | Let us take some time to get used to Owl itself. The exercises in this section 7 | may be artificial, but their purpose is to understand and practice the basic 8 | notions of Owl quickly 9 | 10 | ![1.0](images/1.0.png) 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 | ![1.1](images/1.1.png) 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 | ![1.2](images/1.2.png) 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 | ![1.4](images/1.4.png) 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 | ![1.5](images/1.5.png) 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 | ![1.7](images/1.7.png) 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 | ![1.8](images/1.8.png) 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 |

    234 | Go somewhere 235 |
    236 |
    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 | ![1.9](images/1.9.png) 258 | 259 |
    260 | 261 |
    262 | Resources 263 | 264 | - [owl: slots](https://github.com/odoo/owl/blob/master/doc/reference/slots.md) 265 | - [owl: slot props](https://github.com/odoo/owl/blob/master/doc/reference/slots.md#slots-and-props) 266 | - [bootstrap: documentation on cards](https://getbootstrap.com/docs/5.2/components/card/) 267 | 268 |
    269 | 270 | ## 1.10 Miscellaneous small tasks 271 | 272 | - add prop validation on the Card component 273 | - try to express in the prop validation system that it requires a `default` 274 | slot, and an optional `title` slot 275 | 276 |
    277 | Resources 278 | 279 | - [owl: props validation](https://github.com/odoo/owl/blob/master/doc/reference/props.md#props-validation) 280 | 281 |
    282 | -------------------------------------------------------------------------------- /exercises_2_web_framework.md: -------------------------------------------------------------------------------- 1 | # Part 2: Odoo web Framework 2 | 3 | We will now learn to use the Odoo javascript framework. In this module, we will 4 | improve our Awesome dashboard. This will be a good opportunity to discover many useful features. 5 | 6 | ![2.0](images/2.0.png) 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 | ![2.1](images/2.1.png) 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 | ![2.2](images/2.2.png) 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 | ![2.3](images/2.3.png) 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 | ![2.5](images/2.5.png) 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 | ![2.6](images/2.6.png) 165 | 166 |
    167 | 168 |
    169 | Resources 170 | 171 | - [odoo: translating modules (slightly outdated)](https://www.odoo.com/documentation/master/developer/howtos/translations.html) 172 | - [example: use of env.\_t function](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/account/static/src/components/bills_upload/bills_upload.js#L64) 173 | - [code: translation code in web/](https://github.com/odoo/odoo/blob/16d55910c151daafa00338c26298d28463254a55/addons/web/static/src/core/l10n/translation.js#L16) 174 | 175 |
    176 | -------------------------------------------------------------------------------- /exercises_3_fields_views.md: -------------------------------------------------------------------------------- 1 | # Part 3: Fields and Views 2 | 3 | Fields and views are among the most important concepts in the Odoo user interface. 4 | They are key to many important user interactions, and should therefore work 5 | perfectly. 6 | 7 | ## 3.1 An `image_preview` field 8 | 9 | Each new order on the website will be created as an `awesome_tshirt.order`. This 10 | model has a `image_url` field (of type char), which is currently only visible as 11 | a string. We want to be able to see it in the form view. 12 | 13 | For this task, we need to create a new field component `image_preview`. This 14 | component is specified as follows: 15 | 16 | in readonly mode, it is only an image tag with the correct src if field is set. 17 | In edit mode, it also behaves like classical char fields (you can use the `CharField` 18 | in your template by passing it props): an `input` should be displayed with the 19 | text value of the field, so it can be edited 20 | 21 | - register your field in the proper registry 22 | - update the arch of the form view to use your new field. 23 | 24 | Note: it is possible to solve this exercise by inheriting `CharField`, but the 25 | goal of this exercise is to create a field from scratch. 26 | 27 |
    28 | Resources 29 | 30 | - [code: CharField](https://github.com/odoo/odoo/blob/baecd946a09b5744f9cb60318563a9720c5475f9/addons/web/static/src/views/fields/char/char_field.js) 31 | - [owl: `t-props` directive](https://github.com/odoo/owl/blob/master/doc/reference/props.md#dynamic-props) 32 | 33 |
    34 | 35 | ## 3.2 Improving the image_preview field 36 | 37 | We want to improve the widget of the previous task to help the staff recognize 38 | orders for which some action should be done. In particular, we want to display a warning 'MISSING TSHIRT DESIGN' in red, if there is no image url 39 | specified on the order. 40 | 41 | ## 3.3 Customizing a field component 42 | 43 | Let's see how to use inheritance to extend an existing component. 44 | 45 | There is a `is_late`, readonly, boolean field on the task model. That would be 46 | a useful information to see on the list/kanban/view. Then, let us say that 47 | we want to add a red word `Late!` next to it whenever it is set to true. 48 | 49 | - create a new field `late_order_boolean` inheriting from `BooleanField` 50 | - use it in the list/kanban/form view 51 | - modify it to add a red `Late` next to it, as requested 52 | 53 |
    54 | Preview 55 | 56 | ![3.3](images/3.3.png) 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 | ![3.5](images/3.5.png) 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 | ![3.6](images/3.6.png) 143 | 144 |
    145 | 146 |
    147 | Resources 148 | 149 | - [example: extending a view (js)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/mass_mailing/static/src/views/mailing_contact_view_list.js) 150 | - [example: extending a view (xml)](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/mass_mailing/static/src/views/mass_mailing_views.xml) 151 | - [example: using a `js_class` attribute](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/mass_mailing/views/mailing_contact_views.xml#L44) 152 | - [code: orm service](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/web/static/src/core/orm_service.js) 153 | - [example: using the orm service](https://github.com/odoo/odoo/blob/f7b8f07501315233c8208e99b311935815039a3a/addons/account/static/src/components/open_move_widget/open_move_widget.js) 154 | 155 |
    156 | 157 | ## 3.7 Auto reload the kanban view 158 | 159 | Bafien is upset: he wants to see the kanban view of the tshirt orders on his 160 | external monitor, but it needs to be up to date. He is tired of clicking on 161 | the `refresh` icon every 30s, so he tasked you to find a way to do it automatically. 162 | 163 | Just like the previous exercise, that kind of customization requires creating a 164 | new javascript view. 165 | 166 | - extend the kanban view/controller to reload its data every minute 167 | - register it in the view registry, under the `awesome_tshirt.autoreloadkanban` 168 | - use it in the arch of the kanban view (with the `js_class` attribute) 169 | 170 | Note: make sure that if you use a `setInterval`, or something similar, that it is 171 | properly cancelled when your component is unmounted. Otherwise, you would introduce 172 | a memory leak. 173 | -------------------------------------------------------------------------------- /exercises_4_misc.md: -------------------------------------------------------------------------------- 1 | # Part 4: Miscenalleous tasks 2 | 3 | ![4.7](images/4.7.png) 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 | ![4.1](images/4.1.png) 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 | ![4.2](images/4.2.png) 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 | ![4.4](images/4.4.png) 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 | ![4.5.1](images/4.5.1.png) 165 | ![4.5.2](images/4.5.2.png) 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 | ![4.6](images/4.6.png) 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 | ![4.7](images/4.7.png) 237 | 238 |
    239 | 240 |
    241 | Resources 242 | 243 | - [odoo: router service](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html#router-service) 244 | 245 |
    246 | 247 | ## 4.8 Lazy loading our dashboard 248 | 249 | This is not really necessary, but the exercise is interesting. Imagine that 250 | our awesome dashboard is a large application, with potentially multiple external 251 | libraries, lots of code/styles/templates. Also, suppose that the dashboard is 252 | only used by some users in some business flows, so we want to lazy load it, in 253 | order to speed up the loading of the web client in most cases. 254 | 255 | So, let us do that! 256 | 257 | - modify the manifest to create a new bundle `awesome_tshirt.dashboard` 258 | - add the `AwesomeDashboard` code to this bundle 259 | - remove it from the `web.assets_backend` bundle (so it is not loaded twice!) 260 | 261 | So far, we removed the dashboard from the main bundle, but it should now be 262 | lazily loaded. Right now, there is not client action registered in the action 263 | registry. 264 | 265 | - create a new file `dashboard_loader.js` 266 | - copy the code registering the awesomedashboard to the dashboard loader 267 | - register the awesomedashboard as a lazy_component 268 | - modify the code in dashboard_loader to use the LazyComponent 269 | 270 |
    271 | Resources 272 | 273 | - [odoo: documentation on assets](https://www.odoo.com/documentation/master/developer/reference/frontend/assets.html) 274 | - [code: LazyComponent](https://github.com/odoo/odoo/blob/2971dc0a98bd263f06f79702700d32e5c1b87a17/addons/web/static/src/core/assets.js#L255) 275 | 276 |
    277 | -------------------------------------------------------------------------------- /exercises_5_custom_kanban_view.md: -------------------------------------------------------------------------------- 1 | # Part 5: Custom Kanban View (hard) 2 | 3 | This is a more complicated project that will showcase some non trivial aspects 4 | of the framework. The goal is to practice composing views, coordinating various 5 | aspects of the UI and doing it in a maintainable way. 6 | 7 | Bafien had the greatest idea ever (after the freeze!): a mix of a Kanban View 8 | and a list view would be perfect for your needs! In a nutshell, he wants a list 9 | of customers on the left of the task kanban view. When you click on a customer 10 | on the left sidebar, the kanban view on the right is filtered to only display orders 11 | linked to that customer. 12 | 13 | ![5.0](images/5.0.png) 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 | ![5.2](images/5.2.png) 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 | ![5.3](images/5.3.png) 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 | ![5.4](images/5.4.png) 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 | ![5.6](images/5.6.png) 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 | ![5.7](images/5.7.png) 143 | 144 |
    145 | 146 |
    147 | Resources 148 | 149 | - [odoo: Pager](https://www.odoo.com/documentation/master/developer/reference/frontend/owl_components.html#pager) 150 | 151 |
    152 | -------------------------------------------------------------------------------- /exercises_6_creating_views.md: -------------------------------------------------------------------------------- 1 | # Part 6: Making a view from scratch 2 | 3 | Let us see how one can create a new view, completely from scratch. In a way, it 4 | is not very difficult to do so, but there are no really good resources on how to 5 | do it. Note that most situations should be solved by either a customized 6 | existing view, or a client action. 7 | 8 | For this exercise, let's assume that we want to create a `gallery` view, which is 9 | a view that let us represent a set of records with a image field. In our 10 | Awesome Tshirt scenario, we would like to be able to see a set of t-shirts images. 11 | 12 | The problem could certainly be solved with a kanban view, but this means that it 13 | is not possible to have in the same action our normal kanban view and the gallery 14 | view. 15 | 16 | Let us make a gallery view. Each gallery view will be defined by a `image_field` 17 | attribute in its arch: 18 | 19 | ```xml 20 | 21 | ``` 22 | 23 | ![6.0](images/6.0.png) 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 | ![6.1.1](images/6.1.1.png) 46 | ![6.1.1](images/6.1.2.png) 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 | ![6.2](images/6.2.png) 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 | ![6.4](images/6.4.png) 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 | ![6.6](images/6.6.png) 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 | ![6.8](images/6.8.png) 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 | ![6.9](images/6.9.png) 182 | 183 |
    184 | 185 |
    186 | Resources 187 | 188 | - [code: usePager hook](https://github.com/odoo/odoo/blob/master/addons/web/static/src/search/pager_hook.js) 189 | 190 |
    191 | 192 | ## 6.10 Validating views 193 | 194 | We have a nice and useful view so far. But in real life, we may have issue with 195 | users incorrectly encoding the `arch` of their Gallery view: it is currently 196 | only an unstructured piece of xml. 197 | 198 | Let us add some validation! XML document in Odoo can be described with a rng 199 | file (relax ng), and then validated. 200 | 201 | - add a rng file that describes the current grammar: 202 | - a mandatory attribute `image_field` 203 | - an optional attribute: `tooltip_field` 204 | - add some code to make sure all views are validated against this rng file 205 | - while we are at it, let us make sure that `image_field` and `tooltip_field` are 206 | fields from the current model. 207 | 208 | Since validating rng file is not trivial, here is a snippet to help: 209 | 210 | ```python 211 | # -*- coding: utf-8 -*- 212 | import logging 213 | import os 214 | 215 | from lxml import etree 216 | 217 | from odoo.loglevels import ustr 218 | from odoo.tools import misc, view_validation 219 | 220 | _logger = logging.getLogger(__name__) 221 | 222 | _viewname_validator = None 223 | 224 | @view_validation.validate('viewname') 225 | def schema_viewname(arch, **kwargs): 226 | """ Check the gallery view against its schema 227 | 228 | :type arch: etree._Element 229 | """ 230 | global _viewname_validator 231 | 232 | if _viewname_validator is None: 233 | with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f: 234 | _viewname_validator = etree.RelaxNG(etree.parse(f)) 235 | 236 | if _viewname_validator.validate(arch): 237 | return True 238 | 239 | for error in _viewname_validator.error_log: 240 | _logger.error(ustr(error)) 241 | return False 242 | 243 | ``` 244 | 245 |
    246 | Resources 247 | 248 | - [example: graph view rng file](https://github.com/odoo/odoo/blob/master/odoo/addons/base/rng/graph_view.rng) 249 | 250 |
    251 | -------------------------------------------------------------------------------- /exercises_7_testing.md: -------------------------------------------------------------------------------- 1 | # Part 7: Testing 2 | 3 | Automatically testing code is important when working on a codebase, to ensure that 4 | we don't introduce (too many) bugs or regressions. Let us see how to test our 5 | code. 6 | 7 | ## 7.1 Integration testing 8 | 9 | To make sure our application works as expected, we can test a business flow with 10 | a tour: this is a sequence of steps that we can execute. Each step wait until 11 | some desired DOM state is reached, then performs an action. If at some point, it 12 | is unable to go to the next step for a long time, the tour fails. 13 | 14 | Let's write a tour to ensure that it is possible to perform an tshirt order, 15 | from our public route 16 | 17 | - add a `/static/tests/tours` folder 18 | - add a `/static/tests/tours/order_flow.js` file 19 | - add a tour that performs the following steps: 20 | - open the `/awesome_tshirt/order` route 21 | - fill the order form 22 | - validate it 23 | - navigate to our webclient 24 | - open the list view for the the t-shirt order 25 | - check that our order can be found in the list 26 | - run the tour manually 27 | - add a python test to run it also 28 | - run the tour from the terminal 29 | 30 |
    31 | Resources 32 | 33 | - [odoo: integration testing](https://www.odoo.com/documentation/15.0/developer/reference/backend/testing.html#integration-testing) 34 | 35 |
    36 | 37 | ## 7.2 Unit testing a Component 38 | 39 | It is also useful to test independantly a component or a piece of code. Unit 40 | tests are useful to quickly locate an issue. 41 | 42 | - add a `static/tests/counter_tests.js` file 43 | - add a QUnit test that instantiate a counter, clicks on it, and make sure it is 44 | incremented 45 | 46 |
    47 | Preview 48 | 49 | ![7.2](images/7.2.png) 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 | ![7.3](images/7.3.png) 77 | 78 |
    79 | 80 |
    81 | Resources 82 | 83 | - [example of testing a list view](https://github.com/odoo/odoo/blob/master/addons/web/static/tests/views/list_view_tests.js) 84 | 85 |
    86 | -------------------------------------------------------------------------------- /images/1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.0.png -------------------------------------------------------------------------------- /images/1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.1.png -------------------------------------------------------------------------------- /images/1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.2.png -------------------------------------------------------------------------------- /images/1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.4.png -------------------------------------------------------------------------------- /images/1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.5.png -------------------------------------------------------------------------------- /images/1.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.7.png -------------------------------------------------------------------------------- /images/1.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.8.png -------------------------------------------------------------------------------- /images/1.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/1.9.png -------------------------------------------------------------------------------- /images/2.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.0.png -------------------------------------------------------------------------------- /images/2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.1.png -------------------------------------------------------------------------------- /images/2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.2.png -------------------------------------------------------------------------------- /images/2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.3.png -------------------------------------------------------------------------------- /images/2.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.5.png -------------------------------------------------------------------------------- /images/2.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/2.6.png -------------------------------------------------------------------------------- /images/3.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/3.3.png -------------------------------------------------------------------------------- /images/3.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/3.5.png -------------------------------------------------------------------------------- /images/3.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/3.6.png -------------------------------------------------------------------------------- /images/4.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.1.png -------------------------------------------------------------------------------- /images/4.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.2.png -------------------------------------------------------------------------------- /images/4.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.4.png -------------------------------------------------------------------------------- /images/4.5.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.5.1.png -------------------------------------------------------------------------------- /images/4.5.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.5.2.png -------------------------------------------------------------------------------- /images/4.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.6.png -------------------------------------------------------------------------------- /images/4.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/4.7.png -------------------------------------------------------------------------------- /images/5.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.0.png -------------------------------------------------------------------------------- /images/5.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.2.png -------------------------------------------------------------------------------- /images/5.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.3.png -------------------------------------------------------------------------------- /images/5.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.4.png -------------------------------------------------------------------------------- /images/5.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.6.png -------------------------------------------------------------------------------- /images/5.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/5.7.png -------------------------------------------------------------------------------- /images/6.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.0.png -------------------------------------------------------------------------------- /images/6.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.1.1.png -------------------------------------------------------------------------------- /images/6.1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.1.2.png -------------------------------------------------------------------------------- /images/6.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.2.png -------------------------------------------------------------------------------- /images/6.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.4.png -------------------------------------------------------------------------------- /images/6.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.6.png -------------------------------------------------------------------------------- /images/6.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.8.png -------------------------------------------------------------------------------- /images/6.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/6.9.png -------------------------------------------------------------------------------- /images/7.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/7.2.png -------------------------------------------------------------------------------- /images/7.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ged-odoo/odoo-js-training-public/9383c2a4cc363a96870bf2da4ed84c00588561cd/images/7.3.png -------------------------------------------------------------------------------- /notes_architecture.md: -------------------------------------------------------------------------------- 1 | # Notes: Architecture 2 | 3 | Let us discuss here how Odoo javascript code is designed. Roughly speaking, 4 | all features are made with a combination of components, services, registries, 5 | hooks or helper/utility functions. 6 | 7 | ```mermaid 8 | graph TD 9 | A[Components] 10 | B[Services] 11 | C[Registries] 12 | D[Hooks] 13 | ``` 14 | 15 | ## Component Tree 16 | 17 | From a very high level stand point, the javascript code defines a (dynamic) tree 18 | of components. For example, with an active list view, it might look like this: 19 | 20 | ```mermaid 21 | graph TD 22 | 23 | A[WebClient] 24 | B[ActionController] 25 | C[NavBar] 26 | 27 | subgraph "Current action (list view)" 28 | E[ListController] 29 | F[Field] 30 | G[Field] 31 | H[...] 32 | end 33 | S[Systray] 34 | T[Systray Item] 35 | V[...] 36 | U[UserMenu] 37 | 38 | A --- B 39 | A --- C 40 | B --- E 41 | E --- F 42 | E --- G 43 | E --- H 44 | C --- S 45 | S --- T 46 | S --- V 47 | S --- U 48 | ``` 49 | 50 | In this case, if the user clicks on a record, it may open a form view, and the 51 | content of the current action would be replaced with a form view. The, switching 52 | a notebook tab would destroy the content of the previous tab, and render the 53 | new content. 54 | 55 | This is how Owl applications work: the visible components are displayed, updated, 56 | and replaced with other components, depending on the user actions. 57 | 58 | ## Services 59 | 60 | Documentation: [services](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html) 61 | 62 | In practice, every component (except the root component) may be destroyed at 63 | any time and replaced (or not) with another component. This means that each 64 | component internal state is not persistent. This is fine in many cases, but there 65 | certainly are situations where we want to keep some data around. For example, 66 | all discuss messages, or the current menu. 67 | 68 | Also, it may happen that we need to write some code that is not a component. 69 | Maybe something that process all barcodes, or that manages the user configuration 70 | (context, ...). 71 | 72 | The Odoo framework defines the notion of `service`, which is a persistent piece 73 | of code that exports state and/or functions. Each service can depend on other 74 | services, and components can import a service. 75 | 76 | The following example registers a simple service that displays a notification every 5 seconds: 77 | 78 | ```js 79 | import { registry } from "@web/core/registry"; 80 | 81 | const myService = { 82 | dependencies: ["notification"], 83 | start(env, { notification }) { 84 | let counter = 1; 85 | setInterval(() => { 86 | notification.add(`Tick Tock ${counter++}`); 87 | }, 5000); 88 | }, 89 | }; 90 | 91 | registry.category("services").add("myService", myService); 92 | ``` 93 | 94 | Note that services are registered in a `registry`. See below for more on that. 95 | 96 | Services can be accessed by any component. Imagine that we have a service to 97 | maintain some shared state: 98 | 99 | ```js 100 | import { registry } from "@web/core/registry"; 101 | 102 | const sharedStateService = { 103 | start(env) { 104 | let state = {}; 105 | 106 | return { 107 | getValue(key) { 108 | return state[key]; 109 | }, 110 | setValue(key, value) { 111 | state[key] = value; 112 | }, 113 | }; 114 | }, 115 | }; 116 | 117 | registry.category("services").add("shared_state", sharedStateService); 118 | ``` 119 | 120 | Then, any component can do this: 121 | 122 | ```js 123 | import { useService } from "@web/core/utils/hooks"; 124 | 125 | setup() { 126 | this.sharedState = useService("shared_state"); 127 | const value = this.sharedState.getValue("somekey"); 128 | // do something with value 129 | } 130 | ``` 131 | 132 | ## Registries 133 | 134 | Documentation: [registries](https://www.odoo.com/documentation/master/developer/reference/frontend/registries.html) 135 | 136 | Registries are central to the code architecture: they maintain a collection of 137 | key/value pairs, that are used in many places to read some information. This is 138 | the main way to extend or customize the web client. 139 | 140 | For example, a common usecase is to register a field or a view in a registry, 141 | then add the information in a view arch xml, so the web client will know what 142 | it should use. 143 | 144 | But fields and views are only two usecases. There are many situations where we 145 | decides to go with a registry, because it makes it easy to extend. For example, 146 | 147 | - service registry 148 | - field registry 149 | - user menu registry 150 | - effect registry 151 | - systray registry 152 | - ... 153 | 154 | ## Extending/Customizing Odoo JS Code 155 | 156 | As seen above, registries are really the most robust extension point of Odoo JS 157 | code. They provide an official API, and are designed to be used. So, one can 158 | do a lot of things with just registries, by adding and/or replacing values. 159 | 160 | Another less robust way of customizing the web client is by monkey patching a 161 | component and/or class. 162 | 163 | Documentation: [patching code](https://www.odoo.com/documentation/master/developer/reference/frontend/patching_code.html) 164 | 165 | This is totally okay if there are no other way to do it, but you should be aware 166 | that this is less robust: any change in the patched code may break the customizing, 167 | even if it is just a function rename! 168 | 169 | ## Example: the main component registry 170 | 171 | A very common need is to add some components as a direct child of the root component. 172 | This is how some features are done: 173 | 174 | - notifications: we need a container component to render all active notifications 175 | - discuss: need a container component to add all discuss chat window 176 | - voip: need the possibility to open/close a dialing panel on top of the UI 177 | 178 | To make it easy, the web client is actually looking into a special registry, `main_components`, 179 | to determine which component should be rendered. This is done by the `MainComponentsContainer` 180 | component, which basically performs a `t-foreach` on each key in that registry. 181 | 182 | ```mermaid 183 | graph TD 184 | A[WebClient] 185 | B[Navbar] 186 | C[ActionContainer] 187 | A --- B 188 | A --- C 189 | A --- D 190 | subgraph from main_components registry 191 | D[MainComponentsContainer] 192 | D --- E[NotificationContainer] 193 | D --- F[DialogContainer] 194 | D --- G[VOIPPanel] 195 | D --- H[...] 196 | end 197 | ``` 198 | 199 | Adding a component to that list is as simple as subscribing to the `main_components` 200 | registry. Also, remember that the template of a component can look like this: 201 | 202 | ```xml 203 | 204 | 205 | some content here 206 | 207 | 208 | ``` 209 | 210 | So, your component may be empty until some condition happens. 211 | 212 | ## Example: the notification system 213 | 214 | Often, we can think of a feature as a combination of the blocks above. Let us 215 | see for example how the notification system can be designed. We have: 216 | 217 | - a `Notification` component, which receive some props and renders a notification 218 | - a `notification` service, which exports a reactive list of active notifications, and 219 | a few methods to manipulate them (add/close) 220 | - a `NotificationContainer`, which subscribe to the `notification` service, and 221 | render them with a `t-foreach` loop. 222 | 223 | With that system, code anywhere in Odoo can uses the `notification` service to 224 | add/close a notification. This will cause an update to the internal list of 225 | notification, which will in turn trigger a render by the `NotificationContainer`. 226 | -------------------------------------------------------------------------------- /notes_concurrency.md: -------------------------------------------------------------------------------- 1 | # Notes: Concurrency 2 | 3 | Concurrency is a huge topic in web application: a network request is asynchronous, 4 | so there are a lot of issues/situations that can arises. One need to be careful 5 | when writing asynchronous code. 6 | 7 | Roughly speaking, there are two things that we should consider: 8 | 9 | - writing efficient code, meaning that we want to parallelize as much as possible. 10 | Doing 3 requests sequentially takes longer than doing them in parallel 11 | - writing robust code: at each moment, the internal state of the application 12 | should be consistent, and the resulting UI should match the user expectation, 13 | regardless of the order in which requests returned (remember that a request 14 | can take an arbitrary amount of time to return) 15 | 16 | ## Parallel versus sequential 17 | 18 | Let's talk about efficiency. Assume that we need to load from the server two 19 | (independant) pieces of information. We can do it in two different ways: 20 | 21 | ```js 22 | async sequentialLoadData() { 23 | const data1 = await loadData1(); 24 | const data2 = await loadData2(); 25 | // ... 26 | } 27 | 28 | async parallelLoadData() { 29 | const [data1, data2] = await Promise.all([loadData1(), loadData2()]); 30 | // ... 31 | } 32 | 33 | ``` 34 | 35 | The difference will be visible in the network tab: either the requests are fired 36 | sequentially, or in parallel. Obviously, if the two requests are independant, 37 | it is better to make them in parallel. If they are dependant, then they have to 38 | be done sequentially: 39 | 40 | ```js 41 | async sequentialDependantLoadData() { 42 | const data1 = await loadData1(); 43 | const data2 = await loadData2(data1); 44 | // ... 45 | } 46 | ``` 47 | 48 | Note that this has implications for the way we design (asynchronous) components: 49 | each component can load its data with an asynchronous `onWillStart` method. But 50 | since a child component is only rendered once its parent component is ready, this 51 | means that all `onWillStart` will run sequentially. As such, there should ideally 52 | only ever be one or two levels of components that load data in such a way. Otherwise, 53 | you end up with a loading cascade, which can be slow. 54 | 55 | A way to solve these issues may be to write a controller or a python model method to 56 | gather all the data directly, so it can be loaded in a single round-trip to the 57 | server. 58 | 59 | ## Avoiding corrupted state 60 | 61 | A common concurrency issue is to update the internal state in a non atomic way. 62 | This results in a period of time during which the component is inconsistent, and 63 | may misbehave or crash if rendered. For example: 64 | 65 | ```js 66 | async incorrectUpdate(id) { 67 | this.id = id; 68 | this.data = await loadRecord(id); 69 | } 70 | ``` 71 | 72 | In the above example, the internal state of the component is inconsistent while 73 | the load request is ongoing: it has the new `id`, but the `data` is from the 74 | previous record. It should be fixed by updating the state atomically: 75 | 76 | ```js 77 | async correctUpdate(id) { 78 | this.data = await loadRecord(id); 79 | this.id = id; 80 | } 81 | ``` 82 | 83 | ## Mutex 84 | 85 | As we have seen, some operations have to be sequential. But in practice, actual 86 | code is often hard to coordinate properly. An UI is active all the time, and various 87 | updates can be done (almost) simultaneously, or at any time. In that case, it can become difficult to maintain integrity. 88 | 89 | Let us discuss a simple example: imagine a `Model` that maintains the state of 90 | a record. The user can perform various actions: 91 | 92 | - update a field, which triggers a call to the server to apply computed fields (`onchange`), 93 | - save the record, which is a call to the server `save` method, 94 | - go to the next record 95 | 96 | So, what happens if the user update a field, then clicks on save while the onchange is ongoing? 97 | We obviously want to save the record with the updated value, so the code that 98 | perform the save operation need to wait for the return of the onchange. 99 | 100 | Another similar scenario: the user save the record, then go to the next record. 101 | In that case, we also need to wait for the save operation to be completed before 102 | loading the next record. 103 | 104 | If you think about it, it becomes quickly difficult to coordinate all these 105 | operations. Even more so when you add additional transformations (such as updating 106 | relational fields, loading other data, grouping data, drilling down in some groups, 107 | folding columns in kanban view, ...) 108 | 109 | Many of these interactions can be coordinated with the help of a `Mutex`: it is 110 | basically a queue which wait for the previous _job_ to be complete before executing 111 | the new one. So, here is how the above example could be modelled (in pseudo-code): 112 | 113 | ```js 114 | import { Mutex } from "@web/core/utils/concurrency"; 115 | 116 | class Model { 117 | constructor() { 118 | this.mutex = new Mutex(); 119 | } 120 | update(newValue) { 121 | this.mutex.exec(async () => { 122 | this.state = await this.applyOnchange(newValue); 123 | }); 124 | } 125 | save() { 126 | this.mutex.exec(() => { 127 | this._save(); 128 | }); 129 | } 130 | _save() { 131 | // actual save code 132 | } 133 | openRecord(id) { 134 | this.mutex.exec(() => { 135 | this.state = await this.loadRecord(id) 136 | }); 137 | } 138 | } 139 | ``` 140 | 141 | ## KeepLast 142 | 143 | As seen above, many user interactions need to be properly coordinated. Let us 144 | imagine the following scenario: the user selects a menu in the Odoo web client. 145 | Just after, the user changes her/his mind and select another menu. What should 146 | happen? 147 | 148 | If we don't do anything, there is a risk that the web client displays either of 149 | the action, then switch immediately to the other, depending on the order in which 150 | the requests ends. 151 | 152 | We can solve this by using a mutex: 153 | 154 | ```js 155 | // in web client 156 | 157 | selectMenu(id) { 158 | this.mutex.exec(() => this.loadMenu(id)); 159 | } 160 | ``` 161 | 162 | This will make it determinist: each action from the user will be executed, then 163 | the next action will take place. However, this is not optimal: we 164 | probably want to stop (as much as possible) the first action, and start immediately 165 | the new action, so the web client will only display the second action, and will 166 | do it as fast as possible. 167 | 168 | This can be done by using the `KeepLast` primitive from Odoo: it is basically 169 | like a Mutex, except that it cancels the current action, if any (not really 170 | cancelling, but keeping the promise pending, without resolving it). So, the 171 | code above could be written like this: 172 | 173 | ```js 174 | import { KeepLast } from "@web/core/utils/concurrency"; 175 | // in web client 176 | 177 | class WebClient { 178 | constructor() { 179 | this.keepLast = new KeepLast(); 180 | } 181 | 182 | selectMenu(id) { 183 | this.keepLast.add(() => this.loadMenu(id)); 184 | } 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /notes_fields.md: -------------------------------------------------------------------------------- 1 | # Notes: Fields 2 | 3 | In the context of the javascript framework, fields are components specialized for 4 | visualizing/editing a specific field for a given record. 5 | 6 | For example, a (python) model may define a boolean field, which will be represented 7 | by a field component `BooleanField`. 8 | 9 | Usually, fields can display data in `readonly` or in `edit` mode. Also, they are 10 | often specific to a field type: `boolean`, `float`, `many2one`, ... 11 | 12 | Fields have to be registered in the `fields` registry. Once it's done, they can 13 | be used in some views (namely: `form`, `list`, `kanban`) by using the `widget` 14 | attribute: 15 | 16 | ```xml 17 | 18 | ``` 19 | 20 | Note that fields may in some case be used outside the context of a view. 21 | 22 | ## Generic Field Component 23 | 24 | Just like concrete views are designed to be created by a generic `View` component, 25 | concrete fields are also designed to be created by a generic component, `Field`. 26 | 27 | For example: 28 | 29 | ```xml 30 | 36 | ``` 37 | 38 | This example show some of the props accepted by the `Field` component. Then, it 39 | will make sure it loads the correct component from the `fields` registry, prepare 40 | the base props, and create its child. Note that the `Field` component is _dom less_: 41 | it only exists as a wrapper for the concrete field instance. 42 | 43 | Here is what it look like for the form view: 44 | 45 | ```mermaid 46 | graph TD 47 | A[FormRenderer] 48 | B[Field] --- C[BooleanField] 49 | D[Field] --- E[Many2OneField] 50 | 51 | A --- B 52 | A --- D 53 | A --- F[...] 54 | 55 | ``` 56 | 57 | ## Defining a field component 58 | 59 | A field component is basically just a component registered in the `fields` registry. 60 | It may define some additional static keys (metadata), such as `displayName` or `supportedTypes`, 61 | and the most important one: `extractProps`, which prepare the base props received 62 | by the `CharField`. 63 | 64 | Let us discuss a (simplified) implementation of a `CharField`: 65 | 66 | First, here is the template: 67 | 68 | ```xml 69 | 70 | 71 | 72 | 73 | 74 | 80 | 81 | 82 | ``` 83 | 84 | It features a readonly mode, an edit mode, which is an input with a few attributes. 85 | Now, here is the code: 86 | 87 | ```js 88 | export class CharField extends Component { 89 | get formattedValue() { 90 | return formatChar(this.props.value, { isPassword: this.props.isPassword }); 91 | } 92 | 93 | updateValue(ev) { 94 | let value = ev.target.value; 95 | if (this.props.shouldTrim) { 96 | value = value.trim(); 97 | } 98 | this.props.update(value); 99 | } 100 | } 101 | 102 | CharField.template = "web.CharField"; 103 | CharField.displayName = _lt("Text"); 104 | CharField.supportedTypes = ["char"]; 105 | 106 | CharField.extractProps = ({ attrs, field }) => { 107 | return { 108 | shouldTrim: field.trim && !archParseBoolean(attrs.password), 109 | maxLength: field.size, 110 | isPassword: archParseBoolean(attrs.password), 111 | placeholder: attrs.placeholder, 112 | }; 113 | }; 114 | 115 | registry.category("fields").add("char", CharField); 116 | ``` 117 | 118 | There are a few important things to notice: 119 | 120 | - the `CharField` receives its (raw) value in props. It needs to format it before displaying it 121 | - it receives a `update` function in its props, which is used by the field to notify 122 | the owner of the state that the value of this field has been changed. Note that 123 | the field does not (and should not) maintain a local state with its value. Whenever 124 | the change has been applied, it will come back (possibly after an onchange) by the 125 | way of the props. 126 | - it defines an `extractProps` function. This is a step that translates generic 127 | standard props, specific to a view, to specialized props, useful to the component. 128 | This allows the component to have a better API, and may make it so that it is 129 | reusable. 130 | 131 | Note that the exact API for fields is not really documented anywhere. 132 | -------------------------------------------------------------------------------- /notes_network_requests.md: -------------------------------------------------------------------------------- 1 | # Notes: Network Requests 2 | 3 | A web app such as the Odoo web client would not be very useful if it was unable 4 | to talk to the server. Loading data and calling model methods from the browser 5 | is a very common need. 6 | 7 | Roughly speaking, there are two different kind of requests: 8 | 9 | - calling a controller (an arbitrary route) 10 | - calling a method on a model (`/web/dataset/call_kw/some_model/some_method`). This 11 | will call the python code from the corresponding method, and return the result. 12 | 13 | In odoo these two kind of requests are done with `XmlHTTPRequest`s, in `jsonrpc`. 14 | 15 | ## Calling a method on a model (orm service) 16 | 17 | Let us first see the most common request: calling a method on a model. This is 18 | usually what we need to do. 19 | 20 | There is a service dedicated to do just that: `orm_service`, located in `core/orm_service.js` 21 | It provides a way to call common model methods, as well as a generic `call` method: 22 | 23 | ```js 24 | setup() { 25 | this.orm = useService("orm"); 26 | onWillStart(async () => { 27 | // will read the fields 'id' and 'descr' from the record with id=3 of my.model 28 | const data = await this.orm.read("my.model", [3], ["id", "descr"]); 29 | // ... 30 | }); 31 | } 32 | ``` 33 | 34 | Here is a list of its various methods: 35 | 36 | - `create(model, records, kwargs)` 37 | - `nameGet(model, ids, kwargs)` 38 | - `read(model, ids, fields, kwargs)` 39 | - `readGroup(model, domain, fields, groupby, kwargs)` 40 | - `search(model, domain, kwargs)` 41 | - `searchRead(model, domain, fields, kwargs)` 42 | - `searchCount(model, domain, kwargs)` 43 | - `unlink(model, ids, kwargs)` 44 | - `webReadGroup(model, domain, fields, groupby, kwargs)` 45 | - `webSearchRead(model, domain, fields, kwargs)` 46 | - `write(model, ids, data, kwargs)` 47 | 48 | Also, in case one needs to call an arbitrary method on a model, there is: 49 | 50 | - `call(model, method, args, kwargs)` 51 | 52 | Note that the specific methods should be preferred, since they can perform some 53 | light validation on the shape of their arguments. 54 | 55 | ## Calling a controller (rpc service) 56 | 57 | Whenever we need to call a specific controller, we need to use the (low level) 58 | `rpc` service. It only exports a single function that perform the request: 59 | 60 | ``` 61 | rpc(route, params, settings) 62 | ``` 63 | 64 | Here is a short explanation on the various arguments: 65 | 66 | - `route` is the target route, as a string. For example `/myroute/` 67 | - `params`, optional, is an object that contains all data that will be given to the controller 68 | - `settings`, optional, for some advance control on the request (make it silent, or 69 | using a specific xhr instance) 70 | 71 | For example, a basic request could look like this: 72 | 73 | ```js 74 | setup() { 75 | this.rpc = useService("rpc"); 76 | onWillStart(async () => { 77 | const result = await this.rpc("/my/controller", {a: 1, b: 2}); 78 | // ... 79 | }); 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /notes_odoo_js_ecosystem.md: -------------------------------------------------------------------------------- 1 | # Notes: The Odoo Javascript Ecosystem 2 | 3 | A quick overview 4 | 5 | ## Historical context 6 | 7 | First web client was in Odoo v6.1 (port of a gtk application). Back then, 8 | not many large web applications, so Odoo (then openERP) was built with jquery 9 | and a custom framework (mostly a Class and a Widget implementation). Remember 10 | that it was before Ecmascript 6 (so no class in JS), before bootstrap, before 11 | a lot of the current web technologies. 12 | 13 | Then it evolved randomly in a lot of directions. A module system was added in 14 | 2014 (maybe some of you will remember `odoo.define(...)`), then the code had to 15 | be improved for the new views, for studio. The complexity of the application 16 | increased a lot, code was getting more structured also. 17 | 18 | Then came the need to move to a more modern/productive framework. The Widget system 19 | (based on imperative principles) was not a good bet for the future. Odoo invested 20 | in its own framework (Owl, released in 2019), which is now the basis for the odoo 21 | web client. 22 | 23 | 2019-2022 has seen a huge amount of work in Odoo JS: the assets system was 24 | modernized (ES6 modules), the codebase was refactored, based on modern architecture 25 | and principles. It involved basically a complete rewrite using owl, services, 26 | registries, components, hooks. 27 | 28 | ## Odoo 16: A new Era 29 | 30 | The v16 is the beginning of a (mostly) completely new codebase. Here is a short 31 | list of most significant changes, in no particular order: 32 | 33 | - most of the UI is composed of `Owl` components 34 | - the new code does not use `jquery` anymore (we plan to remove jquery from our assets in the future) 35 | - the `moment` library has been replaced by `luxon` (to manipulate date/datetime) 36 | - we now use `bootstrap` 5, but only the layout and css (not using the js code if possible) 37 | - with Owl, we don't need to add much css classes (it was necessary before to target event handlers, but can 38 | now be done with `t-on-click` in templates) 39 | - assets (js/css/xml) are now declared in the manifest, can be easily split in 40 | bundles, and js files can use ES6 syntax 41 | - code is now organized by feature, not by type. So, we no longer have folders like 42 | `css`, `js`, `xml`. Instead, we organize files and folders according to their 43 | function (`reporting`, `notifications`, ...) 44 | 45 | ## Backend or frontend? 46 | 47 | Roughly speaking, Odoo has 3 main javascript codebases: 48 | 49 | ```mermaid 50 | graph TD 51 | A[Website] 52 | B[Web client] 53 | C[Point of Sale] 54 | ``` 55 | 56 | - the website (public, also known in odoo as the `frontend`) 57 | - the webclient (private, also known as the `backend` (to be avoided, since it is confusing)) 58 | - the point of sale 59 | 60 | The website is a classical MPA (Multi Page Application). Each page is rendered 61 | by the server. It will load some javascript to add a touch of life to the UI. 62 | 63 | The webclient and the point of sale are SPA (Single Page Application). The (only) 64 | page is rendered by the browser. It will then loads data from the server, as 65 | needed, and update the page without reloading the page. 66 | 67 | Since they are based on very different principles, the code of website is very 68 | different from the code of the web client/point of sale (even though they share 69 | some code, mostly in `addons/web/static/src/core`). This training will be 70 | more focused on the SPA aspect of Odoo. 71 | 72 | ## The different layers of Odoo Javascript in Odoo 73 | 74 | One can think of the Odoo web client as being built with four layers: 75 | 76 | ```mermaid 77 | graph TD 78 | A[Web Client] 79 | B[Views/Fields] 80 | C[Core] 81 | D[Owl] 82 | 83 | A --> B 84 | A --> C 85 | B --> C 86 | C --> D 87 | 88 | ``` 89 | 90 | - `web client`: it is the presentation layer that describes the 91 | user interface (navbar, action system, ...) 92 | - `views and fields`: all the code that describes how to visualize and interact with data 93 | from the database, such as the form view, the list view or the kanban view. 94 | - `core`: it is the layer that defines the basic building blocks 95 | for an odoo application. Things such as `registries`, `services`, helpers, 96 | python engine, generic components. 97 | - `owl`: the low level component system. It defines the basic 98 | primitives for writing UI code, such as the Component or the `reactive` function. 99 | 100 | ## Folder structure 101 | 102 | Most of the time, javascript (and other assets) code should be structured like 103 | this: 104 | 105 | ``` 106 | /static/ 107 | src/ 108 | notifications/ 109 | notification_service.js 110 | notification.js 111 | notification.xml 112 | notification.scss 113 | some_component.js 114 | some_component.xml 115 | ... 116 | tests/ 117 | helpers.js 118 | notification_tests.js 119 | ... 120 | ``` 121 | 122 | Note that we don't have the `js/`, `scss`, or `xml` folder anymore. Code is 123 | grouped by concern. Tests should be located in a `static/tests` folder. 124 | -------------------------------------------------------------------------------- /notes_testing.md: -------------------------------------------------------------------------------- 1 | # Notes: Testing Odoo 2 | 3 | Testing UI code is an important, but often overlooked, part of a solid development 4 | process. Odoo javascript test suite is quite large, but at the same time, not 5 | particularly intuitive. So, let us take some time to discuss the main ideas. 6 | 7 | Roughly speaking, there are two different kind of tests: integration tests (testing 8 | a feature/business flow, by running all the relevant code) and unit tests (testing 9 | some behaviour, usually by running only a component or a small unit of code). 10 | 11 | Both of these kind of tests are different: 12 | 13 | - integration tests are useful to make sure something works as expected. However, 14 | usually, they take more time to run, take more CPU/memory, and are harder to 15 | debug when they fail. On the flip side, they are necessary to prove that a system 16 | work and they are easier to write. 17 | 18 | - unit tests are useful to ensure that a specific piece of code works. They are 19 | quick to run, are focused on a specific feature. When they fail, they identify 20 | a problem in a smaller scope, so it is easier to find the issue. However, they 21 | are usually harder to write, since one needs to find a way to _isolate_ as much 22 | as possible something. 23 | 24 | Odoo (javascript) test suite contains both kind of tests: integration tests are 25 | made with _tours_ and unit tests with _QUnit_ 26 | 27 | ## Tours 28 | 29 | A tour is basically a sequence of steps, with some selectors and parameters to 30 | describe what the step should do (click on an element, fill an input, ...). Then 31 | the code in the addon `web_tour` will execute each step sequentially, waiting 32 | between each step if necessary. 33 | 34 | ## QUnit tests 35 | 36 | A QUnit test is basically a short piece of code that exercise a feature, and 37 | make some assertions. The main test suite can be run by simply going to the 38 | `/web/tests` route. 39 | -------------------------------------------------------------------------------- /notes_views.md: -------------------------------------------------------------------------------- 1 | # Notes: Views 2 | 3 | Views are among the most important components in Odoo: they allow users to interact 4 | with their data. Let us discuss how Odoo views are designed. 5 | 6 | The power of Odoo views is that they declare how a particular screen should work, 7 | with a xml document (usually named `arch`, short for `architecture`). This description 8 | can be extended/modified by xpaths serverside. Then the browser will load that 9 | document, parse it (fancy word to say that it will extract the useful information), 10 | then represent the data accordingly. 11 | 12 | The `arch` document is view specific. For example, here is how a `graph` view 13 | or a `calendar` view could be defined: 14 | 15 | ```xml 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ``` 27 | 28 | ## The generic `View` component 29 | 30 | Most of the time, views are created with the help of a generic `View` component, 31 | located in `@web/views/view`. For example, here is what it look like for a kanban view: 32 | 33 | ```mermaid 34 | graph TD 35 | A[View] 36 | B[KanbanController] 37 | 38 | A ---|props| B 39 | ``` 40 | 41 | The `View` component is responsible for many tasks: 42 | 43 | - loading the view arch description from the server 44 | - loading the search view description, if necessary 45 | - loading the active filters 46 | - if there is a `js_class` attribute on the root node of the arch, get the 47 | correct view from the view registry 48 | - creating a searchmodel (that manipulates the current domain/context/groupby/facets) 49 | 50 | ## Defining a javascript view 51 | 52 | A view is defined in the view registry by an object with a few specific keys. 53 | 54 | - `type`: the (base) type of a view (so, for example, `form`, `list`, ...) 55 | - `display_name`: what shoul be displayed in tooltip in the view switcher 56 | - `icon`: what icon to use in the view switcher 57 | - `multiRecord`: if the view is supposed to manage 1 or a set of records 58 | - `Controller`: the most important information: the component that will be used 59 | to render the view. 60 | 61 | Here is a minimal `Hello` view, which does not display anything: 62 | 63 | ```js 64 | /** @odoo-module */ 65 | 66 | import { registry } from "@web/core/registry"; 67 | 68 | export const helloView = { 69 | type: "hello", 70 | display_name: "Hello", 71 | icon: "fa fa-picture-o", 72 | multiRecord: true, 73 | Controller: Component, 74 | }; 75 | 76 | registry.category("views").add("hello", helloView); 77 | ``` 78 | 79 | ## The Standard View Architecture 80 | 81 | Most (or all?) odoo views share a common architecture: 82 | 83 | ```mermaid 84 | graph TD 85 | subgraph View description 86 | V(props function) 87 | G(generic props) 88 | X(arch parser) 89 | S(others ...) 90 | V --> X 91 | V --> S 92 | V --> G 93 | end 94 | A[Controller] 95 | L[Layout] 96 | B[Renderer] 97 | C[Model] 98 | 99 | V == compute props ==> A 100 | A --- L 101 | L --- B 102 | A --- C 103 | 104 | ``` 105 | 106 | The view description can define a `props` function, which receive the standard 107 | props, and compute the base props of the concrete view. The `props` function is 108 | executed only once, and can be thought of as being some kind of factory. It is 109 | useful to parse the `arch` xml document, and to allow the view to be parameterized 110 | (for example, it can return a Renderer component that will be used as Renderer), 111 | but then it makes it easy to customize the specific renderer used by a sub view. 112 | 113 | Note that these props will be extended before being given to the Controller. In 114 | particular, the search props (domain/context/groupby) will be added. 115 | 116 | Then the root component, commonly called the `Controller`, coordinates everything. 117 | Basically, it uses the generic `Layout` component (to add a control panel), 118 | instantiates a `Model`, and uses a `Renderer` component in the `Layout` default 119 | slot. The `Model` is tasked with loading and updating data, and the `Renderer` 120 | is supposed to handle all rendering work, along with all user interactions. 121 | 122 | ### Parsing an arch 123 | 124 | The process of parsing an arch (xml document) is usually done with a `ArchParser`, 125 | specific to each view. It inherits from a generic `XMLParser` class. For example, 126 | it could look like this: 127 | 128 | ```js 129 | import { XMLParser } from "@web/core/utils/xml"; 130 | 131 | export class GraphArchParser extends XMLParser { 132 | parse(arch, fields) { 133 | const result = {}; 134 | this.visitXML(arch, (node) => { 135 | ... 136 | }); 137 | return result; 138 | } 139 | } 140 | ``` 141 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Odoo Logo](https://odoocdn.com/openerp_website/static/src/img/assets/png/odoo_logo_small.png) 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 | --------------------------------------------------------------------------------