This commit is contained in:
2025-08-19 15:05:55 +07:00
parent 70ae66ee4b
commit f78dc34aee
161 changed files with 13703 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Cheat Module for Odoo Development
> [!WARNING]
> This module is purely experimental and for educational purpose use only.
>
> Do not use it in any environment but in an experimental one, definitely not in a production environment.
>
> I'm not responsible for any damage or harm by the use of anything from this repo.
>
> Use it at your own risk.
> [!CAUTION]
> Do not use this module unless you have reviewed the source codes thoroughly, understand what it does and in an experimental environment.
This module contains codes that are commonly used in Odoo development when using Odoo Web Framework.
This module is not meant to be used in production but to be copied and pasted to the real module.
Please watch this videos for more details:
[![EXPLORING_ODOO](https://img.youtube.com/vi/l4zH2c7b34g/0.jpg)](https://youtu.be/l4zH2c7b34g)
[![EXPLORING_ODOO](https://img.youtube.com/vi/3veS34mM70c/0.jpg)](https://youtu.be/3veS34mM70c)
[![EXPLORING_ODOO](https://img.youtube.com/vi/mM04bjAImOU/0.jpg)](https://youtu.be/mM04bjAImOU)
[![EXPLORING_ODOO](https://img.youtube.com/vi/Lv4UP-Is4Dg/0.jpg)](https://youtu.be/Lv4UP-Is4Dg)
+3
View File
@@ -0,0 +1,3 @@
from . import controllers
from . import models
from . import tools
+28
View File
@@ -0,0 +1,28 @@
# For more details see https://www.odoo.com/documentation/17.0/developer/reference/backend/module.html
{
"name": "Cheat Module for Odoo Web Framework",
# The first 2 numbers are Odoo major version, the last 3 are x.y.z version of the module.
"version": "18.0.1.0.0",
"depends": ["web", "cheat_module", "contacts"],
"author": "Yoni Tjio",
# Categories are freeform, for existing categories visit https://github.com/odoo/odoo/blob/17.0/odoo/addons/base/data/ir_module_category_data.xml
"category": "Customizations",
"description": """
Cheat Module for Odoo Web Framework
""",
# data files always loaded at installation
"data": [
'security/ir.model.access.csv',
'views/cheat_web_views.xml',
'views/contacts_views.xml'
],
"assets": {
"web.assets_backend": [
"cheat_web/static/src/**/*",
],
},
"application": False,
"installable": True,
"auto_install": False,
"license": "Other proprietary",
}
+1
View File
@@ -0,0 +1 @@
from . import main
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from odoo import http
class CheatWebController(http.Controller):
@http.route('/cheat/webrpc', type='json', auth='user', website=True)
def do_something(self):
return { "result": "Ok" }
@http.route('/cheat/webrpcwithparam', type='json', auth='user', website=True)
def do_something_else(self, param1, param2):
return {
"result": "Ok",
"param1": param1,
"param2": param2
}
@http.route('/cheat/webrpc/<int:param1>/<string:param2>', type='json', auth='user', website=True)
def do_something_with_route_param(self, param1, param2):
return {
"result": "Ok",
"param1": param1,
"param2": param2
}
+4
View File
@@ -0,0 +1,4 @@
from . import ir_action
from . import ir_ui_view
from . import cheat_web
from . import res_users_settings
+29
View File
@@ -0,0 +1,29 @@
# Do not forget to add this file to __init__.py
# Refer to https://www.odoo.com/documentation/17.0/contributing/development/coding_guidelines.html for coding guidelines
from odoo import _, fields, models, api
class CheatWeb(models.Model):
_name = "cheat.web"
char_field = fields.Char(string="Char Field", required=True)
int_field = fields.Integer(string="Integer Field")
def do_something(self):
return { "result": "Ok" }
def do_something_else(self, param1 = 0, param2 = 1):
return {
"result": "Ok",
"param1": param1,
"param2": param2
}
@api.model
def do_model_method(self, param1 = 0, param2 = 1):
return {
"result": "Ok",
"param1": param1,
"param2": param2
}
+11
View File
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ActWindowView(models.Model):
_inherit = 'ir.actions.act_window.view'
view_mode = fields.Selection(selection_add=[
('hello', "Hello"),
('statistic', "Statistic"),
('cheat', "Cheat")
], ondelete={'hello': 'cascade', 'statistic': 'cascade', 'cheat': 'cascade'})
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
import logging
from odoo import fields, models
_logger = logging.getLogger(__name__)
class View(models.Model):
_inherit = 'ir.ui.view'
type = fields.Selection(selection_add=[
('hello', "Hello"),
('statistic', 'Statistic'),
('cheat', "Cheat")
]
)
def _validate_tag_cheat(self, node, name_manager, node_info):
_logger.info("----------Cheat view validation")
def _get_view_info(self):
return {
'hello': {'icon': 'fa fa-smile-o'},
'statistic': {'icon': 'fa fa-info'},
'cheat': {'icon': 'fa fa-bookmark-o'}
} | super()._get_view_info()
+8
View File
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class Users(models.Model):
_inherit = 'res.users.settings'
cheat_web_user_setting_char_field = fields.Char("User Char Field", default="default")
cheat_web_user_setting_integer_field = fields.Integer("User Integer Field", default=0)
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<rng:define name="cheat">
<rng:element name="cheat">
<rng:optional><rng:attribute name="limit"/></rng:optional>
<rng:zeroOrMore>
<rng:element name="field">
<rng:attribute name="name"/>
<rng:optional><rng:attribute name="conserve-line-breaks"/></rng:optional>
</rng:element>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:start>
<rng:choice>
<rng:ref name="cheat" />
</rng:choice>
</rng:start>
</rng:grammar>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<rng:define name="hello">
<rng:element name="hello">
<rng:empty/>
</rng:element>
</rng:define>
<rng:start>
<rng:choice>
<rng:ref name="hello" />
</rng:choice>
</rng:start>
</rng:grammar>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<rng:define name="statistic">
<rng:element name="statistic">
<rng:empty/>
</rng:element>
</rng:define>
<rng:start>
<rng:choice>
<rng:ref name="statistic" />
</rng:choice>
</rng:start>
</rng:grammar>
+2
View File
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cheat_web_model,access_cheat_web_model,model_cheat_web,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_cheat_web_model access_cheat_web_model model_cheat_web base.group_user 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+136
View File
@@ -0,0 +1,136 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import {
Component,
onWillStart,
onWillRender,
onRendered,
onMounted,
onWillUpdateProps,
onWillPatch,
onPatched,
onWillUnmount,
onWillDestroy,
onError,
useState,
useRef
} from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { Layout } from "@web/search/layout";
class CheatOwl extends Component {
static template = "cheat_owl";
static components = { Layout };
static props = {
...standardActionServiceProps,
};
getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
setup() {
this.useRefCardTextRef = useRef("useRefCardText");
//#region non reactive
this.nonReactiveRandomValue = this.getRandomInteger(0, 100);
this.nonReactiveRandomValue1Ref = useRef("nonReactiveRandomValue1");
this.nonReactiveRandomValue2Ref = useRef("nonReactiveRandomValue2");
this.nonReactiveRandomValue3Ref = useRef("nonReactiveRandomValue3");
this.nonReactiveRandomValue4Ref = useRef("nonReactiveRandomValue4");
//#endregion
//#region reactive
this.state = useState({
randomValue: this.getRandomInteger(0, 100),
});
//#endregion
//#region input binding
this.bindingValue = "";
this.inputBindingRef = useRef("inputBinding");
//#endregion
//#region two way input binding
this.bindingState = useState({
valueStandardInputBinding: "",
valueTextAreaInputBinding: "",
valueCheckBoxInputBinding: false,
valueRadioButtonInputBinding: "",
valueSelectionInputBinding: "",
valueRangeInputBinding: 0,
});
//#endregion
//#region owl lifecycle
onWillStart(() => {
console.log("onWIllStart.");
});
onWillRender(() => {
console.log("onWillRender.");
});
onRendered(() => {
console.log("onRendered.");
});
onMounted(() => {
console.log("onMounted.");
});
onWillUpdateProps((nextProps) => {
console.log("onWillUpdateProps:", nextProps);
});
onWillPatch(() => {
console.log("onWillPatch.");
});
onPatched(() => {
console.log("onPatched.");
});
onWillUnmount(() => {
console.log("onWillUnmount.");
});
onWillDestroy(() => {
console.log("onWillDestroy.");
});
onError(() => {
console.log("onError.");
});
//#endregion
}
//#region functions
generateNonReactiveRandomValue() {
let randomVal = this.getRandomInteger(0, 100);
this.nonReactiveRandomValue1Ref.el.innerText = randomVal;
this.nonReactiveRandomValue2Ref.el.innerText = randomVal;
this.nonReactiveRandomValue3Ref.el.innerText = randomVal;
this.nonReactiveRandomValue4Ref.el.innerText = randomVal;
}
generateRandomValue() {
this.state.randomValue = this.getRandomInteger(0, 100);
}
changeUseRefCardTextColor() {
this.useRefCardTextRef.el.classList.toggle("text-danger");
}
getInputBindingValue(){
this.inputBindingRef.el.innerText = this.bindingValue;
}
changeValueStandardInputBinding(){
this.bindingState.valueStandardInputBinding = "Random value: " + this.getRandomInteger(0, 100);
}
//#endregion
}
registry.category("actions").add("cheat_owl", CheatOwl);
+199
View File
@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cheat_owl" owl="1">
<Layout display="{ controlPanel: {} }" className="'overflow-auto h-100'">
<div class="cheat-web-container">
<div class="row g-2">
<div class="col-xl-2 col-sm-6">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-use-ref" aria-expanded="true" aria-controls="collapse-collapsed">
<em>useRef</em> Example
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-use-ref" class="collapse">
<div class="card-body">
<p class="card-text" t-ref="useRefCardText">
This text color can be changed by clicking the button below.
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end">
<button t-on-click="changeUseRefCardTextColor" class="btn btn-sm btn-outline-primary w-50">Change Color</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-use-state" aria-expanded="true" aria-controls="collapse-collapsed">
<em>useState</em> Example
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-use-state" class="collapse">
<div class="card-body">
<p class="card-text">
This is a random value: <br/>
<span class="text-primary"><t t-out="this.state.randomValue" /></span>
<span class="text-danger ms-1"><t t-out="this.state.randomValue" /></span>
<span class="text-warning ms-1"><t t-out="this.state.randomValue" /></span>
<span class="text-info ms-1"><t t-out="this.state.randomValue" /></span>
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end">
<button t-on-click="generateRandomValue" class="btn btn-sm btn-outline-primary w-50">Randomize</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-non-reactive" aria-expanded="true" aria-controls="collapse-collapsed">
Non Reactive Example
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-non-reactive" class="collapse">
<div class="card-body">
<p class="card-text">
This is a random value: <br/>
<span class="text-primary" t-ref="nonReactiveRandomValue1"><t t-out="this.nonReactiveRandomValue" /></span>
<span class="text-danger ms-1" t-ref="nonReactiveRandomValue2"><t t-out="this.nonReactiveRandomValue" /></span>
<span class="text-warning ms-1" t-ref="nonReactiveRandomValue3"><t t-out="this.nonReactiveRandomValue" /></span>
<span class="text-info ms-1" t-ref="nonReactiveRandomValue4"><t t-out="this.nonReactiveRandomValue" /></span>
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end">
<button t-on-click="generateNonReactiveRandomValue" class="btn btn-sm btn-outline-primary w-50">Randomize</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-1 g-2">
<div class="col-12">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-input-binding" aria-expanded="true" aria-controls="collapse-collapsed">
Input Binding
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-input-binding" class="collapse">
<div class="row m-2 mt-0 g-2">
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-keyboard-o" /> Input</h5>
<div class="card-body">
<p class="card-text">
<input t-model="this.bindingValue"/>
<hr/>
<div>Input value: <span t-ref="inputBinding"> <t t-out="this.bindingValue" /></span></div>
<button class="btn btn-sm btn-outline-primary" t-on-click="getInputBindingValue">Get Input Value</button>
</p>
</div>
</div>
</div>
</div>
<div class="row m-2 mt-0 g-2">
<div class="hr-sect">Two-way Binding</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-keyboard-o" /> Input</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<input t-model="this.bindingState.valueStandardInputBinding"/>
<div><hr/>Input value: <t t-out="this.bindingState.valueStandardInputBinding" /></div>
<button class="btn btn-sm btn-outline-primary" t-on-click="changeValueStandardInputBinding">Randomize</button>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-font" /> Textarea</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<textarea t-model="this.bindingState.valueTextAreaInputBinding"/>
<div><hr/>Input value: <t t-out="this.bindingState.valueTextAreaInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-check-square" /> Checkbox</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="cbInputBindingCheck" t-model="this.bindingState.valueCheckBoxInputBinding"/>
<label class="form-check-label" for="cbInputBindingCheck">Checkbox</label>
</div>
<div><hr/>Input value: <t t-out="this.bindingState.valueCheckBoxInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-stop-circle-o" /> Radio Buttons</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<div>
<div class="form-check">
<input class="form-check-input" type="radio" id="cbInputBindingOne" value="one" t-model="this.bindingState.valueRadioButtonInputBinding"/>
<label class="form-check-label" for="cbInputBindingOne">One</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="cbInputBindingTwo" value="two" t-model="this.bindingState.valueRadioButtonInputBinding" />
<label class="form-check-label" for="cbInputBindingTwo">Two</label>
</div>
</div>
<div><hr/>Input value: <t t-out="this.bindingState.valueRadioButtonInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-caret-square-o-down" /> Select</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<select class="form-select" t-model="this.bindingState.valueSelectionInputBinding">
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<div><hr/>Input value: <t t-out="this.bindingState.valueSelectionInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-arrows-h" /> Range</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<input class="form-range" min="0" max="5" step="0.5" type="range" t-model="this.bindingState.valueRangeInputBinding"/>
<div><hr/>Input value: <t t-out="this.bindingState.valueRangeInputBinding" /></div>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
</t>
</templates>
+79
View File
@@ -0,0 +1,79 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { Component, useState, markup } from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { Layout } from "@web/search/layout";
import { HAccordion } from "./core/haccordion/haccordion";
import { Collapsible } from "./core/collapsible/collapsible";
import { CheatOwlQwebInlineTemplate } from "./cheat_owl_qweb_inline_template"
class CheatOwlQweb extends Component {
static template = "cheat_owl_qweb";
static components = { Layout, CheatOwlQwebInlineTemplate, HAccordion, Collapsible };
static props = {
...standardActionServiceProps,
};
//#region Misc Functions
getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
get randomString() {
var letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var llen = letters.length;
var res = "";
for (var i = 0; i < 6; i++) {
res += letters[Math.floor(Math.random() * llen)];
}
return res;
}
get randomBoolean() {
return Math.round(Math.random()) == 1;
}
get dynamicTemplate() {
if (this.state.dynamicTemplate === "one") {
return "cheat_owl_qweb_sub_template_one"
} else if (this.state.dynamicTemplate === "two"){
return "cheat_owl_qweb_sub_template_two"
} else if (this.state.dynamicTemplate === "three"){
return "cheat_owl_qweb_sub_template_three"
} else {
return "cheat_owl_qweb_sub_template_four"
}
}
//#endregion
setup() {
this.state = useState({
randomValue: this.getRandomInteger(0, 100),
checkBoxValue: "one",
dynamicTag: "div",
dynamicTemplate: "one"
});
this.markup = markup;
this.divText = "<div>some text 1</div>";
this.markupDivText = markup("<div>some text 2</div>");
this.arrayOfObjects = [
{
id: 1,
name: "Item No. 1",
},
{
id: 2,
name: "Item No. 2",
},
{
id: 3,
name: "Item No. 3",
},
];
this.aSetOfObjects = new Set(this.arrayOfObjects);
}
}
registry.category("actions").add("cheat_owl_qweb", CheatOwlQweb);
+404
View File
@@ -0,0 +1,404 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cheat_owl_qweb">
<Layout display="{ controlPanel: {} }" className="'overflow-auto h-100'">
<HAccordion name="'hacc'" startItem="0">
<t t-set-slot="basics-1" title.translate="The Basics - I">
<div class="d-flex w-100 flex-column gap-2 pt-2">
<div class="row g-2 w-100">
<div class="col-xl-4 col-sm-12">
<Collapsible name="'inline-template'" title.translate="Inline Template">
<CheatOwlQwebInlineTemplate />
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'white-spaces'" title.translate="White Spaces">
<p>
<span>This is white spaces in a <span class="badge">&lt;span&gt;</span> : ' '
</span>
</p>
<p>
<span>This is a <span class="badge">&lt;span&gt;</span> with a line break:
'
'
</span>
</p>
<p>
<pre>This is white spaces in a &lt;pre&gt;: ' '</pre>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'outputs'" title.translate="Displaying Data">
<p>
<span>This is an html fragment <span class="badge">t-out</span> :
<pre><t t-out="divText" /></pre>
</span>
<span>This is an html fragment <span class="badge">t-out</span> with markup:
<pre><t t-out="markupDivText" /></pre>
</span>
</p>
<p>
<span>This is an html fragment <span class="badge">t-esc</span> :
<pre><t t-esc="divText" /></pre>
</span>
<span>This is an html fragment <span class="badge">t-esc</span> with markup:
<pre><t t-esc="markupDivText" /></pre>
</span>
</p>
</Collapsible>
</div>
</div>
<div class="row g-2 w-100">
<div class="col-xl-4 col-sm-12">
<Collapsible name="'variables'" title.translate="Variables">
<p>
<t t-set="foo" t-value="'bar'" />
<span>The variable <span class="badge">foo</span> is set to <span class="badge"><t t-out="foo"/></span>.</span>
</p>
<p>
<t t-set="wal">
<strong>do</strong>
</t>
<span>The variable <span class="badge">wal</span> is set to <span class="badge"><t t-out="wal"/></span> but inside tags.</span>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'expressions'" title.translate="Basic Expressions">
<p>
<span>This is evaluation result of <span class="badge">1 + 3</span> :
<pre><t t-out="1 + 3" /></pre>
</span>
<span>With <span class="badge">state.randomValue = <t t-out="state.randomValue"/></span>, this expression <span class="badge">1 + state.randomValue</span> will result to:
<pre><t t-out="1 + state.randomValue" /></pre>
</span>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'logical-expressions'" title.translate="Logical Expressions">
<p>
<table class="table table-striped table-hover">
<thead>
<tr class="table-secondary">
<th>Operation</th>
<th>Operator</th>
<th>Example</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Equality</td>
<td>==</td>
<td>1 == 3</td>
<td><t t-out="1 == 3" /></td>
</tr>
<tr>
<td>And</td>
<td>and</td>
<td>true and false</td>
<td><t t-out="true and false" /></td>
</tr>
<tr>
<td>Or</td>
<td>or</td>
<td>true or false</td>
<td><t t-out="true or false" /></td>
</tr>
<tr>
<td>Greater Than</td>
<td>gt</td>
<td>1 gt 1</td>
<td><t t-out="1 gt 1" /></td>
</tr>
<tr>
<td>Greater Than or Equal</td>
<td>gte</td>
<td>1 gte 1</td>
<td><t t-out="1 gte 1" /></td>
</tr>
<tr>
<td>Less Than</td>
<td>lt</td>
<td>1 lt 1</td>
<td><t t-out="1 lt 1" /></td>
</tr>
<tr>
<td>Less Than or Equal</td>
<td>lte</td>
<td>1 lte 1</td>
<td><t t-out="1 lte 1" /></td>
</tr>
</tbody>
</table>
</p>
</Collapsible>
</div>
</div>
</div>
</t>
<t t-set-slot="basics-2" title.translate="The Basics - II">
<div class="d-flex w-100 flex-column gap-2 pt-2">
<div class="row g-2 w-100">
<div class="col-xl-4 col-sm-12">
<Collapsible name="'conditionals'" title.translate="Conditionals">
<p>
<div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbConditionalOne"
name="cbConditionals"
value="one"
t-model="state.checkBoxValue" />
<label class="form-check-label" for="cbConditionalOne">One</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbConditionalTwo"
name="cbConditionals"
value="two"
t-model="state.checkBoxValue" />
<label class="form-check-label" for="cbConditionalTwo">Two</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbConditionalThree"
name="cbConditionals"
value="three"
t-model="state.checkBoxValue" />
<label class="form-check-label" for="cbConditionalThree">Three</label>
</div>
</div>
<span t-if="state.checkBoxValue === 'one'">This will be displayed if the first radio button above is selected.</span>
<span t-elif="state.checkBoxValue === 'two'">This will be displayed if the second radio button is checked.</span>
<span t-else="">This will be displayed if the third radio button is checked.</span>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'loops'" title.translate="Loops">
<p>
<span>This list is made using <span class="badge">t-foreach</span> loop:</span>
<ul>
<t t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<li><t t-out="'Item: ' + i.toString()"/></li>
</t>
</ul>
</p>
<p>
<span>This list is made using <span class="badge">t-foreach</span> loop too but applied to the element instead of the <span class="badge">t</span> element:</span>
<ul>
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i"><t t-out="'Item: ' + i.toString()"/></li>
</ul>
</p>
<p>
<span>These demonstrate the <span class="badge">t-foreach</span>'s special variables:</span>
<table class="table table-striped table-hover mt-1">
<thead>
<tr class="table-secondary">
<td>x_index</td>
<td>x_value</td>
<td>x_first</td>
<td>x_last</td>
</tr>
</thead>
<tbody>
<tr t-foreach="arrayOfObjects" t-as="item" t-key="item.id">
<td><t t-out="item_index"/></td>
<td><t t-out="item_value.toString()"/></td>
<td><t t-out="item_first"/></td>
<td><t t-out="item_last"/></td>
</tr>
</tbody>
</table>
</p>
<p>
<span>This list is made using <span class="badge">Set</span> and spread operator <span class="badge">...</span> :</span>
<ul>
<t t-foreach="[...aSetOfObjects]" t-as="i" t-key="i.id">
<li><t t-out="i.name"/></li>
</t>
</ul>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'dynamics'" title.translate="Dynamics">
<p>
<span>The placeholder attribute is set using random value:</span>
<input type="text" class="form-control mt-1" t-att-placeholder="randomString" />
</p>
<p>
<span>The placeholder attribute is set using string interpolation with random value:</span>
<input type="text" class="form-control mt-1" t-attf-placeholder="This is a random string: {{randomString}}" />
</p>
<p>
<span>The CSS class is set using object with CSS class as key and boolean value to indicate the inclusion
<span class="badge">{'text-info': true, 'fw-bold': true}</span>:</span>
<p t-att-class="{'text-info': true, 'fw-bold': true}">This is an example text.</p>
</p>
<p>
<span>This is an exmple of dynamic tag:</span>
<div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTagDiv"
name="cbDynamicTag"
value="div"
t-model="state.dynamicTag" />
<label class="form-check-label" for="cbDynamicTagDiv">Div</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTagSpan"
name="cbDynamicTag"
value="pre"
t-model="state.dynamicTag" />
<label class="form-check-label" for="cbDynamicTagSpan">Pre</label>
</div>
</div>
<t t-tag="state.dynamicTag">
<span>This text is inside <span class="badge"><t t-out="state.dynamicTag"/> element</span></span>
</t>
</p>
</Collapsible>
</div>
</div>
</div>
</t>
<t t-set-slot="more-on-templates" title.translate="More On Templates">
<div class="d-flex w-100 flex-column gap-2 pt-2">
<div class="row g-2 w-100">
<div class="col-xl-6 col-sm-12">
<Collapsible name="'sub-templates'" title.translate="Sub Templates">
<p>
<div>Sub templates are called with <span class="badge">t-call</span>:</div>
<t t-call="cheat_owl_qweb_sub_template_two"/>
</p>
<p>
<div>This templates is using special placeholder <span class="badge">t-out="0"</span>:</div>
<t t-call="cheat_owl_qweb_sub_template_three">
<span>This text is rendered inside the sub template.</span>
</t>
</p>
<p>
<div>Variables are passed down to the sub template:</div>
<t t-set="parentTemplateVariable" t-value="'This is a string declared on parent template.'"/>
<t t-call="cheat_owl_qweb_sub_template_four">
<t t-set="scopedTemplateVariable" t-value="'This is declared inside the t element.'"/>
</t>
<!-- scopedTemplateVariable does not exist here -->
</p>
<p>
<span>This is an example of dynamic sub templates:</span>
<div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateOne"
name="cbDynamicTemplate"
value="one"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateOne">One</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateTwo"
name="cbDynamicTemplate"
value="two"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateTwo">Two</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateThree"
name="cbDynamicTemplate"
value="three"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateThree">Three</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateFour"
name="cbDynamicTemplate"
value="four"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateFour">Four</label>
</div>
</div>
<t t-call="{{ dynamicTemplate }}"/>
</p>
</Collapsible>
</div>
<div class="col-xl-6 col-sm-12">
<Collapsible name="'template-inheritance'" title.translate="Template Inheritance">
<p>
<div>We call the parent template, but since there's an extension child which modified the content we get the modified version:</div>
<t t-call="cheat_owl.qweb_parent_template"/>
</p>
<p>
<div>This is the primary child template, which we can call directly by it's name:</div>
<t t-call="cheat_owl.qweb_primary_child_template"/>
</p>
</Collapsible>
</div>
</div>
</div>
</t>
<t t-set-slot="subscribe" title.translate="Subscribe for more tutorials">
<div class="d-flex d-flex h-100 justify-content-center align-items-start">
<div class="card" style="width: 150px;">
<img src="/cheat_web/static/img/exploring-odoo.png" class="card-img-top" />
<div class="card-body text-center">
<p class="card-text">
<a href="https://youtube.com/@exploring-odoo">Exploring Odoo</a>
</p>
</div>
</div>
</div>
</t>
</HAccordion>
</Layout>
</t>
<t t-name="cheat_owl_qweb_sub_template_one">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_one</span>.</div>
</t>
<t t-name="cheat_owl_qweb_sub_template_two">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_two</span>.</div>
<t t-call="cheat_owl_qweb_sub_template_one"/>
</t>
<t t-name="cheat_owl_qweb_sub_template_three">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_three</span>.</div>
<t t-out="0" />
</t>
<t t-name="cheat_owl_qweb_sub_template_four">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_four</span>.</div>
<div><t t-out="parentTemplateVariable" /></div>
<div><t t-out="scopedTemplateVariable" /></div>
</t>
<t t-name="cheat_owl.qweb_parent_template">
<p>This is parent template.</p>
</t>
<t t-name="cheat_owl.qweb_primary_child_template" t-inherit="cheat_owl.qweb_parent_template" t-inherit-mode="primary">
<xpath expr="//p[1]" position="after">
<p>This is primary child template.</p>
</xpath>
</t>
<t t-inherit="cheat_owl.qweb_parent_template" t-inherit-mode="extension">
<xpath expr="//p[1]" position="after">
<p>This is extension child template.</p>
</xpath>
</t>
</templates>
@@ -0,0 +1,11 @@
/** @odoo-module */
import { Component, xml } from "@odoo/owl";
export class CheatOwlQwebInlineTemplate extends Component {
static template = xml`
<p>
This text is in an inline template
</p>`;
static props = { };
}
+63
View File
@@ -0,0 +1,63 @@
.cheat-web-container {
margin: 2rem !important;
}
.cheat-web-user-profile {
margin: 1rem;
}
.cheat-web-user-profile .card {
width: 400px;
border: none;
border-radius: 10px;
background-color: #fff;
}
.cheat-web-user-profile .stats {
background: #f2f5f8 !important;
color: #000 !important;
}
.cheat-web-user-profile .articles {
font-size: 10px;
color: #a1aab9;
}
.cheat-web-user-profile .followers {
font-size: 10px;
color: #a1aab9;
}
.cheat-web-user-profile .rating {
font-size: 10px;
color: #a1aab9;
}
.cheat-web-user-profile .stat-number,
.cheat-web-user-profile .stat-string {
font-weight: 500;
}
.cheat-web-container .card-header .fa {
transition: 0.3s transform ease-in-out;
}
.cheat-web-container .card-header .collapsed .fa {
transform: rotate(90deg);
}
.hr-sect {
display: flex;
flex-basis: 100%;
align-items: center;
color: rgba(0, 0, 0, 0.35);
margin: 8px 0px;
}
.hr-sect::before,
.hr-sect::after {
content: "";
flex-grow: 1;
background: rgba(0, 0, 0, 0.35);
height: 1px;
font-size: 0px;
line-height: 0px;
margin: 0px 8px;
}
+298
View File
@@ -0,0 +1,298 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Component, useState, xml } from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { Layout } from "@web/search/layout";
import { AlertDialog, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { Dialog } from "@web/core/dialog/dialog";
import { rpc } from "@web/core/network/rpc";
import { user } from "@web/core/user";
import { url } from "@web/core/utils/urls";
const { DateTime } = luxon;
//#region custom dialog
class MyDialog extends Component {
static components = { Dialog };
static template = xml`
<Dialog size="'md'" title="'This is my dialog title'">
<p>
This is my dialog content.
</p>
</Dialog>
`;
}
class MyDialogWithButtons extends Component {
static components = { Dialog };
static template = xml`
<Dialog size="'md'" title="'This is my dialog with buttons'">
<p>
This is my dialog with buttons content.
</p>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="onConfirm">Ok</button>
<button class="btn btn-secondary" t-on-click="onCancel">Cancel</button>
</t>
</Dialog>
`;
setup() {
this.notification = useService("notification");
}
onConfirm() {
this.notification.add('You confirmed the dialog.');
this.props.close();
}
onCancel() {
this.notification.add('You canceled the dialog.');
this.props.close();
}
}
//#endregion
class CheatWeb extends Component {
//#region statics
static template = "cheat_web";
static components = { Layout };
static props = {
...standardActionServiceProps,
};
//#endregion
setup() {
//#region services
this.orm = useService("orm");
this.notification = useService("notification");
this.dialog = useService("dialog");
//#endregion
this.user = user;
this.state = useState({
userCharValue: user.settings.cheat_web_user_setting_char_field,
userIntegerValue: user.settings.cheat_web_user_setting_integer_field
});
}
//#region user
get userImage(){
return url("/web/image", {
model: 'res.users',
id: user.userId,
field: "avatar_128",
});
}
async dumpUserSettings(){
console.log("User:", user);
console.log("User settings:", user.settings);
}
async dumpState(){
console.log("state:", this.state);
}
async saveUserSettings(){
await user.setUserSettings(
"cheat_web_user_setting_char_field",
this.state.userCharValue
);
await user.setUserSettings(
"cheat_web_user_setting_integer_field",
this.state.userIntegerValue
);
}
//#endregion
//#region misc function
getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min) ) + min;
}
//#endregion
//#region rpc
async rpcDoSomething() {
const res = await rpc("/cheat/webrpc");
console.log(res);
}
async rpcDoSomethingElse() {
const res = await rpc("/cheat/webrpcwithparam", { param1: 1, param2: "2"});
console.log(res);
}
async rpcDoSomethingWithRouteParam() {
const param1 = 3;
const param2 = 4;
const res = await rpc(`/cheat/webrpc/${param1}/${param2}`);
console.log(res);
}
//#endregion
//#region orm
async ormCreate() {
const res = await this.orm.create("cheat.web", [
{
char_field: "Char Field #" + DateTime.now().toUnixInteger(),
int_field: this.getRandomInteger(1, 1000)
},
{
char_field: "Char Field #" + DateTime.now().toUnixInteger(),
int_field: this.getRandomInteger(1, 1000)
},
]);
console.log(res);
}
async ormSearch() {
const res = await this.orm.search("cheat.web", [['id', '>=', 0]], { order: "id desc", limit: 100, offset: 0 });
console.log(res);
}
async ormSearchCount() {
const res = await this.orm.searchCount("cheat.web", [['id', '>=', 0]]);
console.log(res);
}
async ormSearchRead() {
const res = await this.orm.searchRead("cheat.web", [['id', '>=', 0]], ["char_field", "int_field"]);
console.log(res);
}
async ormRead() {
const ids = await this.orm.search("cheat.web", [['id', '>=', 0]]);
if (ids.length > 0){
const res = await this.orm.read("cheat.web", [ids[0]], ["char_field"]);
console.log(res);
}
}
async ormWrite() {
const ids = await this.orm.search("cheat.web", [['id', '>=', 0]]);
if (ids.length > 0) {
let res = await this.orm.read("cheat.web", [ids[0]], ["char_field"]);
console.log('Old Values:', res);
await this.orm.write('cheat.web', [ids[0]],
{
'char_field': `Updated #${DateTime.now().toUnixInteger()}`
}
);
res = await this.orm.read("cheat.web", [ids[0]], ["char_field"]);
console.log('New Values:', res);
}
}
async ormUnlink() {
let ids = await this.orm.search("cheat.web", [['id', '>=', 0]], { order: "id desc" });
if (ids.length > 0) {
console.log('Search Result:', ids);
await this.orm.unlink('cheat.web', [ids[0]]);
ids = await this.orm.search("cheat.web", [['id', '>=', 0]], { order: "id desc" });
console.log('Search Result:', ids);
}
}
async ormDoSomething() {
const res = await this.orm.call("cheat.web", "do_something", [1]);
console.log(res);
}
async ormDoSomethingElse() {
const res = await this.orm.call("cheat.web", "do_something_else", [1], { param1: '1', param2: "2"});
console.log(res);
}
async ormDoModelMethod() {
const res = await this.orm.call("cheat.web", "do_model_method", [], { param1: "3", param2: "4" });
console.log(res);
}
//#endregion
//#region notification
async notifSimple(){
this.notification.add('This is a simple Notification');
}
async notifSticky(){
this.notification.add('This is a sticky Notification', {
title: 'Sticky Notification',
type: 'success',
sticky: true,
});
}
async notifCallback(){
this.notification.add('This is a Notification with onClose callback', {
title: 'Sticky Notification',
type: 'info',
sticky: true,
onClose: () => {
this.notification.add('This is from onclose callback.');
}
});
}
async notifWithButtons(){
this.notification.add('This is a Notification with Buttons', {
title: 'Sticky Notification',
type: 'warning',
sticky: true,
buttons: [
{
name: 'Button 1',
onClick: () => {
this.notification.add('This is from button 1 onclick.');
}
},
{
name: 'Button 2',
onClick: () => {
this.notification.add('This is from button 2 onclick.');
}
}
]
});
}
//#endregion
//#region dialog
async dialogAlert(){
this.dialog.add(AlertDialog, {
title: 'This is an Alert Dialog',
body: 'This is the alert message.',
contentClass: 'text-danger'
});
}
async dialogConfirm(){
this.dialog.add(ConfirmationDialog, {
title: 'This is an Confirmation Dialog',
body: 'This is the dialog message.',
confirmLabel: 'Click here to confirm.',
cancelLabel: "Click here to cancel.",
cancel: async () => {
this.notification.add('You canceled the dialog.');
},
confirm: async () => {
this.notification.add('You confirmed the dialog.');
},
});
}
async dialogCustom(){
this.dialog.add(MyDialog);
}
async dialogCustomWithButtons(){
this.dialog.add(MyDialogWithButtons);
}
//#endregion
}
registry.category("actions").add("cheat_web", CheatWeb);
+152
View File
@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cheat_web" owl="1">
<Layout display="{ controlPanel: {} }" className="'overflow-auto h-100'">
<div class="cheat-web-container">
<div class="accordion shadow-sm" id="mainAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseRpc" aria-expanded="true" aria-controls="collapseRpc">
RPC
</button>
</h2>
<div id="collapseRpc" class="accordion-collapse collapse show" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="rpcDoSomething">RPC Call</button>
<button class="btn btn-primary" t-on-click="rpcDoSomethingElse">RPC Call With Param</button>
<button class="btn btn-primary" t-on-click="rpcDoSomethingWithRouteParam">RPC Call With Router Param</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOrm" aria-expanded="true" aria-controls="collapseOrm">
ORM
</button>
</h2>
<div id="collapseOrm" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="ormCreate">ORM Create</button>
<button class="btn btn-primary" t-on-click="ormSearch">ORM Search</button>
<button class="btn btn-primary" t-on-click="ormSearchCount">ORM Search Count</button>
<button class="btn btn-primary" t-on-click="ormSearchRead">ORM Search Read</button>
<button class="btn btn-primary" t-on-click="ormRead">ORM Read</button>
<button class="btn btn-primary" t-on-click="ormWrite">ORM Write</button>
<button class="btn btn-primary" t-on-click="ormUnlink">ORM Unlink</button>
</div>
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="ormDoSomething">ORM Call</button>
<button class="btn btn-primary" t-on-click="ormDoSomethingElse">ORM Call With Param</button>
<button class="btn btn-primary" t-on-click="ormDoModelMethod">ORM Call Model Method</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseNotification" aria-expanded="true" aria-controls="collapseNotification">
Notification
</button>
</h2>
<div id="collapseNotification" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="notifSimple">Simple Notification</button>
<button class="btn btn-primary" t-on-click="notifSticky">Sticky Notification</button>
<button class="btn btn-primary" t-on-click="notifCallback">Notification with On Close Callback</button>
<button class="btn btn-primary" t-on-click="notifWithButtons">Notification with Buttons</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDialogBox" aria-expanded="true" aria-controls="collapseDialogBox">
Dialog Box
</button>
</h2>
<div id="collapseDialogBox" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="dialogAlert">Alert Dialog</button>
<button class="btn btn-primary" t-on-click="dialogConfirm">Confirmation Dialog</button>
<button class="btn btn-primary" t-on-click="dialogCustom">Custom Dialog</button>
<button class="btn btn-primary" t-on-click="dialogCustomWithButtons">Custom Dialog with Buttons</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseUserService" aria-expanded="true" aria-controls="collapseUserService">
User Information
</button>
</h2>
<div id="collapseUserService" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container d-flex justify-content-center cheat-web-user-profile">
<div class="card p-3">
<div class="d-flex align-items-center">
<div class="image">
<img t-att-src="userImage" class="rounded" width="155" />
</div>
<div class="ms-3 w-100">
<h4 class="mb-0 mt-0"><t t-out="this.user.name" /></h4>
<span><t t-out="this.user.login" /></span>
<div class="p-2 mt-2 bg-primary d-flex justify-content-between rounded text-white stats">
<div class="d-flex flex-column">
<span class="articles">Language</span>
<span class="stat-string"><t t-out="this.user.lang" /></span>
</div>
<div class="d-flex flex-column">
<span class="followers">Timezone</span>
<span class="stat-string"><t t-out="this.user.tz" /></span>
</div>
</div>
<div class="button mt-2 d-flex flex-row align-items-center">
<button t-on-click="dumpUserSettings" class="btn btn-sm btn-outline-primary w-100">Dump User Data</button>
<button data-bs-toggle="collapse" data-bs-target="#collapseUserSettings" class="btn btn-sm btn-outline-primary w-100 ms-3">Show Settings</button>
</div>
</div>
</div>
<div class="collapse" id="collapseUserSettings">
<div class="card-body">
<div class="row mb-3">
<label for="userSettingCharField" class="col-sm-4 col-form-label">Char Field</label>
<div class="col-sm-8">
<input class="form-control" id="userSettingCharField" t-model.lazy="this.state.userCharValue" />
</div>
</div>
<div class="row mb-3">
<label for="userSettingIntegerField" class="col-sm-4 col-form-label">Integer Field</label>
<div class="col-sm-8">
<input class="form-control" id="userSettingIntegerField" t-model.lazy.number="this.state.userIntegerValue" />
</div>
</div>
<div class="button mt-2 d-flex flex-row align-items-center">
<button t-on-click="dumpState" class="btn btn-sm btn-outline-primary w-100">Dump State</button>
<button t-on-click="saveUserSettings" class="btn btn-sm btn-outline-primary w-100 ms-3">Save</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
</t>
</templates>
@@ -0,0 +1,17 @@
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
export class Collapsible extends Component {
static template = "web.collapsible";
static components = { };
static props = {
name: { type: String },
title: { type: String },
slots: { type: Object, optional: true },
collapsed: { type: Boolean, optional: true}
};
static defaultProps = {
collapsed: true
}
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web.collapsible" owl="1">
<div class="card">
<h5 class="card-header">
<a class="d-block" data-bs-toggle="collapse" t-attf-href="{{ '#' + props.name }}" aria-expanded="true" aria-controls="collapse-collapsed">
<t t-out="props.title" />
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div t-att-id="props.name" t-attf-class="{{ props.collapsed ? 'collapse' : 'collapse show' }}">
<div class="card-body">
<p class="card-text text-wrap overflow-auto" t-ref="useRefCardText">
<t t-slot="default"/>
</p>
</div>
<div class="card-footer" t-if="props.slots['footer']">
<t t-slot="footer"/>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,25 @@
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
export class HAccordion extends Component {
static template = "web.haccordion";
static components = { };
static props = {
name: { type: String },
startItem: { type: Number, optional: true },
slots: { type: Object, optional: true },
};
static defaultProps = {
startItem: 0
}
setup() {
this.state = useState({ activeItem: this.props.startItem });
this.itemNames = Object.keys(this.props.slots);
}
onClick(item) {
this.state.activeItem = item;
}
}
@@ -0,0 +1,71 @@
// Variables
$tab-width: 60px;
$screen-width: 100vw;
.haccordion-root {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
align-content: center;
position: relative;
min-height: 100%;
height: 100%;
width: 100%;
.haccordion-item {
flex-grow: 1;
height: 100%;
overflow: auto;
transition: width 500ms ease;
}
.haccordion-fold {
width: $tab-width !important;
display: flex;
cursor: pointer;
& .haccordion-title-container {
width: 100%;
height: 100%;
display: flex;
background-color: $o-gray-800 !important;
& .haccordion-title {
white-space: nowrap;
transform: rotate(-90deg);
align-self: end;
position: relative;
bottom: 75px;
left: 5px;
width: 100%;
& * {
color: $o-gray-600 !important;
&:hover {
color: $o-gray-300 !important;
}
}
}
}
& .haccordion-content {
display: none;
}
}
.haccordion-expand {
width: 100%;
& .haccordion-title {
display: none;
}
& .haccordion-content {
padding-left: 10px;
display: revert;
height: 100%;
}
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web.haccordion" owl="1">
<div class="haccordion-root">
<div t-attf-class="{{ 'haccordion-item ' + (this.state.activeItem === item_index ? 'haccordion-expand' : 'haccordion-fold') }}"
t-foreach="itemNames"
t-as="item"
t-key="item_index"
t-on-click="() => this.state.activeItem = item_index" >
<div class="haccordion-title-container">
<div class="haccordion-title" t-attf-id="{{ props.name + '-' + item + '-' + item_index.toString()}}">
<h1><t t-out="props.slots[item].title" /></h1>
</div>
</div>
<div class="haccordion-content p-4">
<h1>
<t t-out="props.slots[item].title" />
</h1>
<t t-slot="{{ item }}"/>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,41 @@
import { visitXML } from "@web/core/utils/xml";
import { Field } from "@web/views/fields/field";
export class CheatArchParser {
parseFieldNode(node, models, modelName) {
return Field.parseFieldNode(node, models, modelName, "cheat");
}
parse(xmlDoc, models, modelName) {
console.log("Cheat Arch Parser - xmlDoc: ", xmlDoc);
console.log("Cheat Arch Parser - models: ", models);
console.log("Cheat Arch Parser - modelName: ", modelName);
const fieldNodes = {};
const limit = xmlDoc.getAttribute("limit") || 80;
let display_name_field = [...xmlDoc.children].find(o => o.attributes['name'].value === "display_name");
if (!display_name_field){
display_name_field = document.createElement("field");
display_name_field.setAttribute("name", "display_name");
xmlDoc.appendChild(display_name_field);
}
visitXML(xmlDoc, (node) => {
console.log("Cheat Arch Parser - node: ", node);
if (node.tagName === "field") {
const fieldNode = this.parseFieldNode(node, models, modelName);
const conserveLineBreaks = node.getAttribute("conserve-line-breaks");
fieldNode.conserveLineBreaks = conserveLineBreaks === 'true';
fieldNodes[fieldNode.name] = fieldNode;
}
});
return {
fieldNodes,
limit
};
}
}
@@ -0,0 +1,98 @@
import { _t } from "@web/core/l10n/translation";
import { Component, useState, useRef, onWillPatch } from "@odoo/owl";
import { Layout } from "@web/search/layout";
import { usePager } from "@web/search/pager_hook";
import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { useModel } from "@web/model/model";
import { extractFieldsFromArchInfo } from "@web/model/relational_model/utils";
import { standardViewProps } from "@web/views/standard_view_props";
import { executeButtonCallback } from "@web/views/view_button/view_button_hook";
export class CheatController extends Component {
static template = `cheat_web.CheatView`;
static props = {
...standardViewProps,
offset: { type: Number, optional: true },
Model: Function,
Renderer: Function,
archInfo: Object,
}
static components = {
Layout,
SearchBar
};
setup() {
console.log("Statistic Controller - this: ", this);
console.log("Statistic Controller - props: ", this.props);
this.rootRef = useRef("root");
this.props.offset = 0;
this.model = useState(useModel(this.props.Model, this.modelParams));
usePager(() => {
return {
offset: this.model.offset,
limit: this.model.limit,
total: this.model.recordsLength,
onUpdate: async ({ offset, limit }) => {
this.props.offset = offset;
this.props.limit = limit;
await this.model.load(this.props);
},
};
});
this.searchBarToggler = useSearchBarToggler();
this.firstLoad = true;
onWillPatch(() => {
this.firstLoad = false;
});
}
get modelParams() {
const { activeFields, fields } = extractFieldsFromArchInfo(
this.props.archInfo,
this.props.fields
);
for (let [key, value] of Object.entries(activeFields)) {
let fieldNode = this.props.archInfo.fieldNodes[key];
value.conserveLineBreaks = fieldNode.conserveLineBreaks;
}
return {
resModel: this.props.resModel,
fields,
activeFields,
};
}
get rendererProps() {
return {
model: this.model,
records: this.model.records,
activeFields: this.model.config.activeFields,
editRecord: this.onClickEdit.bind(this),
};
}
get className() {
return this.props.className;
}
async onClickCreate() {
return executeButtonCallback(this.rootRef.el, () => this.props.createRecord());
}
async onClickEdit(ev, record) {
return executeButtonCallback(this.rootRef.el, () => this.props.selectRecord(record.id, true));
}
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.CheatView">
<div t-att-class="className" t-ref="root">
<Layout display="props.display" className="'h-100 overflow-auto'">
<t t-set-slot="control-panel-create-button">
<button type="button" class="btn btn-primary o_list_button_add" t-on-click="onClickCreate">
New
</button>
</t>
<t t-set-slot="layout-actions">
<SearchBar t-if="searchBarToggler.state.showSearchBar" autofocus="firstLoad"/>
</t>
<t t-component="props.Renderer" t-props="rendererProps"/>
</Layout>
</div>
</t>
</templates>
@@ -0,0 +1,46 @@
/** @odoo-module */
import { markRaw } from "@odoo/owl";
import { KeepLast } from "@web/core/utils/concurrency";
import { Model } from "@web/model/model";
import { getFieldsSpec } from "@web/model/relational_model/utils";
import { orderByToString } from "@web/search/utils/order_by";
export class CheatModel extends Model {
setup(params) {
console.log("Cheat Model - this: ", this);
console.log("Cheat Model - setup params: ", params);
this.keepLast = markRaw(new KeepLast());
this.config = params;
}
_getNextConfig(currentConfig, params) {
const config = Object.assign({}, currentConfig, params);
return config;
}
async load(params = {}) {
console.log("Cheat Model - load params: ", params);
const config = this._getNextConfig(this.config, params);
const kwargs = {
specification: getFieldsSpec(config.activeFields, config.fields, config.context),
offset: config.offset,
order: orderByToString(config.orderBy),
limit: config.limit,
context: { ...config.context }
};
const { length, records } = await this.keepLast.add(
this.orm.webSearchRead(config.resModel, config.domain, kwargs)
);
this.offset = config.offset;
this.limit = config.limit;
this.records = records;
this.recordsLength = length;
this.config = config;
}
}
@@ -0,0 +1,43 @@
import { Component, useRef } from "@odoo/owl";
import { executeButtonCallback } from "@web/views/view_button/view_button_hook";
export class CheatRenderer extends Component {
static template = `cheat_web.CheatRenderer`;
static props = {
model: Object,
records: Object,
activeFields: Object,
editRecord: Function
}
setup() {
console.log("Cheat Renderer - this: ", this);
this.rootRef = useRef("renderer_root");
}
getInputId(record, field){
return record.id + '_' + field
}
getFirstFiveFields(){
const fields = Object.keys(this.props.activeFields).filter(o => o !== "display_name" & o !== "id") .sort().slice(0, 4);
return fields;
}
getLabel(field){
const config = this.props.model.config;
return config.fields[field].string;
}
getFieldValue(record, field){
return record[field];
}
getConserveLineBreakSetting(field){
return this.props.activeFields[field].conserveLineBreaks;
}
onEditButtonClick(ev, record) {
this.props.editRecord(ev, record);
}
}
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.CheatRenderer">
<div class="row m-2" t-ref="renderer_root">
<t t-foreach="props.records" t-as="record" t-key="record.id">
<div class="col-lg-6 col-sm-12 p-2">
<div class="card">
<div class="row g-0">
<div class="col-md-3 d-flex justify-content-center align-items-center bg-secondary rounded-start">
<h1><t t-out="record.id"/></h1>
</div>
<div class="col-md-9">
<div class="card-body">
<h2 class="card-title"><t t-out="record.display_name"/></h2>
<div class="card-text card-text border rounded">
<t t-foreach="getFirstFiveFields()" t-as="field" t-key="field_index">
<div t-attf-class="form-group d-flex {{ field_last ? '' : 'border-bottom' }}">
<t t-set="fieldId" t-value="getInputId(record, field)"/>
<label t-att-for="fieldId" class="col-sm-4 col-form-label me-1 px-1 bg-secondary">
<t t-out="getLabel(field)"/>
</label>
<div class="col-sm-8 px-1">
<t t-if="getConserveLineBreakSetting(field)" >
<textarea readonly="1" rows="5" class="form-control-plaintext" t-att-id="fieldId">
<t t-out="getFieldValue(record, field)" />
</textarea>
</t>
<t t-else="" >
<input type="text" readonly="1" class="form-control-plaintext text-truncate"
t-att-id="fieldId" t-att-value="getFieldValue(record, field)"/>
</t>
</div>
</div>
</t>
</div>
<div class="d-flex justify-content-end">
<button t-on-click="(ev) => this.onEditButtonClick(ev, record)" class="btn btn-primary mt-2">Edit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
</templates>
@@ -0,0 +1,33 @@
import { registry } from "@web/core/registry";
import { CheatModel } from "./cheat_model";
import { CheatController } from "./cheat_controller";
import { CheatArchParser } from "./cheat_arch_parser";
import { CheatRenderer } from "./cheat_renderer";
export const cheatView = {
type: "cheat",
searchMenuTypes: ["filter", "favorite"],
Controller: CheatController,
Renderer: CheatRenderer,
Model: CheatModel,
ArchParser: CheatArchParser,
props(genericProps, view) {
console.log("Cheat View - this: ", this);
console.log("Cheat View - genericProps: ", genericProps);
console.log("Cheat View - view: ", view);
const { ArchParser } = view;
const { arch, relatedModels, resModel } = genericProps;
const archInfo = new ArchParser().parse(arch, relatedModels, resModel);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer,
archInfo,
};
},
};
registry.category("views").add("cheat", cheatView);
@@ -0,0 +1,18 @@
import { Component } from "@odoo/owl";
import { standardViewProps } from "@web/views/standard_view_props";
import { Layout } from "@web/search/layout";
export class HelloController extends Component {
static template = `cheat_web.HelloView`;
static props = {
...standardViewProps,
aValue: { type: String }
};
static components = { Layout };
setup() {
console.log("Hello Controller - this: ", this);
console.log("Hello Controller - props: ", this.props);
}
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.HelloView">
<div class="h-100" t-ref="root">
<Layout display="props.display" className="'h-100'">
<div class="d-flex flex-column h-75 justify-content-center align-items-center">
<h1><span><i class="fa fa-smile-o" style="font-size: xxx-large !important;"></i></span></h1>
<h1>Hello there!</h1>
</div>
</Layout>
</div>
</t>
</templates>
@@ -0,0 +1,20 @@
import { registry } from "@web/core/registry";
import { HelloController } from "./hello_controller";
export const helloView = {
type: "hello",
Controller: HelloController,
props(genericProps, view) {
console.log("Hello View - this: ", this);
console.log("Hello View - genericProps: ", genericProps);
console.log("Hello View - view: ", view);
return {
...genericProps,
aValue: 'A value from view type.'
};
},
};
registry.category("views").add("hello", helloView);
@@ -0,0 +1,32 @@
import { Component, useState, useRef, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { standardViewProps } from "@web/views/standard_view_props";
import { Layout } from "@web/search/layout";
export class StatisticController extends Component {
static template = `cheat_web.StatisticView`;
static props = {
...standardViewProps,
Model: Function,
Renderer: Function,
};
static components = { Layout };
setup() {
console.log("Statistic Controller - this: ", this);
console.log("Statistic Controller - props: ", this.props);
this.orm = useService("orm");
this.model = new this.props.Model(
this.orm,
this.props.resModel,
this.props.fields
);
onWillStart(async () => {
await this.model.load(this.props);
});
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.StatisticView">
<div class="h-100">
<Layout display="props.display" className="'h-100'">
<t t-component="props.Renderer" model="model"/>
</Layout>
</div>
</t>
</templates>
@@ -0,0 +1,21 @@
import { KeepLast } from "@web/core/utils/concurrency";
export class StatisticModel {
constructor(orm, resModel, fields) {
this.orm = orm;
this.resModel = resModel;
this.fields = fields;
this.keepLast = new KeepLast();
console.log("Statistic Model - this: ", this);
}
async load(params) {
console.log("Statistic Model - load params: ", params);
const recordCount = await this.keepLast.add(
this.orm.searchCount(this.resModel, [])
);
this.recordCount = recordCount;
}
}
@@ -0,0 +1,26 @@
import {
Component
} from "@odoo/owl";
export class StatisticRenderer extends Component {
static template = `cheat_web.StatisticRenderer`;
static props = {
model: Object
}
fieldInfo(name){
return this.props.model.fields[name];
}
fieldCount() {
return Object.keys(this.props.model.fields).length;
}
fieldNames() {
return Object.keys(this.props.model.fields).sort();
}
setup() {
console.log("Statistic Renderer - this: ", this);
}
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.StatisticRenderer">
<div class="row m-3 d-flex justify-content-center align-items-center">
<div class="card p-0">
<div class="row g-0 ">
<div class="col-md-1 col-2 d-flex flex-column pt-4 justify-content-start align-items-center bg-secondary rounded-start">
<h1><t t-out="props.model.recordCount"/></h1>
<div>records</div>
</div>
<div class="col-md-11 col-10">
<div class="card-body overflow-y-auto">
<h2 class="card-title">Model: <t t-out="props.model.resModel"/></h2>
<div class="card-text">
<div>Fields count: <t t-out="fieldCount()"/></div>
<div style="max-height: 75dvh;">
<table class="table table-striped table-hover mt-3 no-cellpadding">
<thead>
<tr class="table-secondary">
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr t-foreach="fieldNames()" t-as="field" t-key="field_index">
<t t-set="fInfo" t-value="fieldInfo(field)" />
<td><t t-out="fInfo['name']"/></td>
<td><t t-out="fInfo['type']"/></td>
<td><t t-out="fInfo['help']"/></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,25 @@
import { registry } from "@web/core/registry";
import { StatisticModel } from "./statistic_model";
import { StatisticController } from "./statistic_controller";
import { StatisticRenderer } from "./statistic_renderer";
export const statisticView = {
type: "statistic",
Controller: StatisticController,
Renderer: StatisticRenderer,
Model: StatisticModel,
props(genericProps, view) {
console.log("Statistic View - this: ", this);
console.log("Statistic View - genericProps: ", genericProps);
console.log("Statistic View - view: ", view);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer
};
},
};
registry.category("views").add("statistic", statisticView);
+1
View File
@@ -0,0 +1 @@
from . import view_validation
+34
View File
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
import os
import logging
from lxml import etree
from odoo import tools
from odoo.tools.view_validation import validate
_logger = logging.getLogger(__name__)
_schema_validator = {}
def _get_validator(view_type):
""" Return a validator for the given view type, or None. """
if view_type not in _schema_validator:
with tools.file_open(os.path.join('cheat_web', 'rng', '%s_view.rng' % view_type)) as frng:
try:
relaxng_doc = etree.parse(frng)
_schema_validator[view_type] = etree.RelaxNG(relaxng_doc)
except Exception:
_schema_validator[view_type] = None
return _schema_validator[view_type]
@validate('hello', 'statistic', 'cheat')
def view_schema_validation(arch, **kwargs):
""" Get RNG validator and validate RNG file."""
validator = _get_validator(arch.tag)
if validator and not validator.validate(arch):
for error in validator.error_log:
_logger.error("%s", error)
return False
return True
+107
View File
@@ -0,0 +1,107 @@
<odoo>
<data>
<!-- List view, also called list view on models -->
<record id="cheat_web_view_list" model="ir.ui.view">
<field name="name">cheat.web.view.list</field>
<field name="model">cheat.web</field>
<field name="arch" type="xml">
<list string="Cheat Web">
<field name="char_field" />
<field name="int_field" />
</list>
</field>
</record>
<!-- Form view on models -->
<record id="cheat_web_view_form" model="ir.ui.view">
<field name="name">cheat.web.view.form</field>
<field name="model">cheat.web</field>
<field name="arch" type="xml">
<form string="Cheat Web">
<sheet>
<group>
<field name="char_field" />
<field name="int_field" />
</group>
</sheet>
</form>
</field>
</record>
<!-- Search view on models -->
<record id="cheat_web_view_search" model="ir.ui.view">
<field name="name">cheat.web.view.search</field>
<field name="model">cheat.web</field>
<field name="arch" type="xml">
<search>
<field name="char_field" />
<field name="int_field" />
</search>
</field>
</record>
<!-- Hello view on models -->
<record id="cheat_web_view_hello" model="ir.ui.view">
<field name="name">cheat.web.view.hello</field>
<field name="model">cheat.web</field>
<field name="arch" type="xml">
<hello/>
</field>
</record>
<!-- Statistic view on models -->
<record id="cheat_web_view_statistic" model="ir.ui.view">
<field name="name">cheat.web.view.statistic</field>
<field name="model">cheat.web</field>
<field name="arch" type="xml">
<statistic/>
</field>
</record>
<!-- Cheat view on models -->
<record id="cheat_web_view_cheat" model="ir.ui.view">
<field name="name">cheat.web.view.cheat</field>
<field name="model">cheat.web</field>
<field name="arch" type="xml">
<cheat limit="20">
<field name="char_field" />
<field name="int_field" />
</cheat>
</field>
</record>
<!-- Action opening view on model -->
<record model="ir.actions.act_window" id="action_cheat_web_custom_view_type">
<field name="name">Custom View Type</field>
<field name="res_model">cheat.web</field>
<field name="view_mode">list,form,hello,statistic,cheat</field>
</record>
<record model="ir.actions.client" id="action_cheat_web">
<field name="name">Web Framework</field>
<field name="tag">cheat_web</field>
</record>
<record model="ir.actions.client" id="action_cheat_owl">
<field name="name">Web Library</field>
<field name="tag">cheat_owl</field>
</record>
<record model="ir.actions.client" id="action_cheat_owl_qweb">
<field name="name">QWeb</field>
<field name="tag">cheat_owl_qweb</field>
</record>
<menuitem name="Web" id="cheat_web_menu_top" parent="cheat_module.cheat_menu_root"
sequence="100" />
<menuitem name="Web Framework" id="cheat_web_menu" parent="cheat_web_menu_top"
action="action_cheat_web" sequence="10" />
<menuitem name="Web Library" id="cheat_owl_menu" parent="cheat_web_menu_top"
action="action_cheat_owl" sequence="20" />
<menuitem name="QWeb" id="cheat_owl_qweb_menu" parent="cheat_web_menu_top"
action="action_cheat_owl_qweb" sequence="30" />
<menuitem name="Custom View Type" id="cheat_web_custom_view_type"
parent="cheat_web_menu_top"
action="action_cheat_web_custom_view_type" sequence="40" />
</data>
</odoo>
+36
View File
@@ -0,0 +1,36 @@
<odoo>
<data>
<record id="view_partner_hello" model="ir.ui.view">
<field name="name">res.partner.hello</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<hello />
</field>
</record>
<record id="view_partner_statistic" model="ir.ui.view">
<field name="name">res.partner.statistic</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<statistic />
</field>
</record>
<record id="view_partner_cheat" model="ir.ui.view">
<field name="name">res.partner.cheat</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<cheat>
<field name="complete_name"/>
<field name="contact_address" conserve-line-breaks="true"/>
<field name="email"/>
<field name="type"/>
</cheat>
</field>
</record>
<record id="contacts.action_contacts" model="ir.actions.act_window">
<field name="view_mode">kanban,list,form,activity,hello,statistic,cheat</field>
</record>
</data>
</odoo>