Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
017bfa2
tuto first commit
renol-odoo Feb 16, 2026
f0d1b51
chapter3
renol-odoo Feb 16, 2026
22b1a88
chapter4
renol-odoo Feb 16, 2026
2317b4d
chapter5
renol-odoo Feb 16, 2026
069aec8
[MOV] estate: ´chapter2_estate´ renamed into ´estate´
renol-odoo Feb 17, 2026
52e29ea
[FIX] estate: `__manifest__.py` data order fix
renol-odoo Feb 17, 2026
5c1a596
[IMP] estate: chapter6 - basic views
renol-odoo Feb 17, 2026
449981f
[FIX] estate: model attributes fix
renol-odoo Feb 17, 2026
9c3cca8
[IMP] estate: chapter7 - model relations
renol-odoo Feb 17, 2026
bc56d9a
[IMP] estate: chapter8 - computed & onchange
renol-odoo Feb 17, 2026
def1c15
[FIX] estate: correct date_availability default
renol-odoo Feb 18, 2026
4be4b21
[IMP] estate: chapter9 - buttons
renol-odoo Feb 18, 2026
49d8f41
[IMP] estate: chapter10 - constraints
renol-odoo Feb 18, 2026
0647fef
[IMP] estate: chapter11 - sprinkles
renol-odoo Feb 19, 2026
52efca0
[IMP] estate: chapter12 - inheritance
renol-odoo Feb 19, 2026
ff7d4a9
[FIX] estate: cancel properties delete fix
renol-odoo Feb 19, 2026
787585f
[IMP] estate_account: chapter13 - other modules interaction
renol-odoo Feb 19, 2026
e3aa5b0
[IMP] estate: chapter14 - QWeb kanban
renol-odoo Feb 20, 2026
e3f3feb
[LINT] estate: chapter15 - code polishing
renol-odoo Feb 20, 2026
1261527
[IMP] estate_account: extra - smart btn Property <-> Invoice(s)
renol-odoo Feb 22, 2026
31b1cf4
[LINT] estate: code polishing 2nd pass
renol-odoo Feb 22, 2026
25ac295
[IMP] awesome_owl: chapter1 - components
renol-odoo Feb 23, 2026
b081dc7
[IMP] awesome_dashboard: chapter2 - dashboard
renol-odoo Feb 26, 2026
3b03525
[IMP] estate: module data
renol-odoo Feb 26, 2026
e971a81
[IMP] estate: unit tests
renol-odoo Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Odoo tutorials
# Odoo tutorials - renol technical training

This repository hosts the code for the bases of the modules used in the
[official Odoo tutorials](https://www.odoo.com/documentation/latest/developer/tutorials.html).
Expand Down
77 changes: 76 additions & 1 deletion awesome_dashboard/static/src/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,83 @@
import { Component } from "@odoo/owl";
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { Dialog } from "@web/core/dialog/dialog";
import { CheckBox } from "@web/core/checkbox/checkbox";
import { browser } from "@web/core/browser/browser";

import { DashboardItem } from "./dashboard_item";
import { PieChart } from "./pie_chart";

class AwesomeDashboard extends Component {
static template = "awesome_dashboard.AwesomeDashboard";
static components = { Layout, DashboardItem, PieChart };

setup() {
this.action = useService("action")
this.stat = useState(useService("awesome_dashboard.statistics"));
this.item_list = registry.category("awesome_dashboard").getAll();

this.dialog = useService("dialog");
this.state = useState({
disabledItems: browser.localStorage.getItem("disabledDashboardItems")?.split(",") || []
});
}

openCustomers() {
this.action.doAction("base.action_partner_form");
}

openLeads() {
this.action.doAction({
type: 'ir.actions.act_window',
name: "Get Leads",
res_model: 'crm.lead',
views: [[false, "list"], [false, "form"]],
});
}

updateConfiguration(newDisabledItems) {
this.state.disabledItems = newDisabledItems;
}

openConfiguration() {
this.dialog.add(ConfigurationDialog, {
items: this.item_list,
disabledItems: this.state.disabledItems,
onUpdateConfiguration: this.updateConfiguration.bind(this),
})
}
}


class ConfigurationDialog extends Component {
static template = "awesome_dashboard.ConfigurationDialog";
static components = { Dialog, CheckBox };
static props = ["close", "items", "disabledItems", "onUpdateConfiguration"];

setup() {
this.items = useState(this.props.items.map(item => {
return {
...item,
enabled: !this.props.disabledItems.includes(item.id),
}
}));
}

done() {
this.props.close();
}

onChange(checked, changedItem) {
changedItem.enabled = checked;
const newDisabledItems = Object.values(this.items).filter(
(item) => !item.enabled
).map((item) => item.id)

browser.localStorage.setItem("disabledDashboardItems", newDisabledItems);
this.props.onUpdateConfiguration(newDisabledItems);
}
}

registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
5 changes: 5 additions & 0 deletions awesome_dashboard/static/src/dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$bgcolor: gray;

.o_dashboard {
background-color: $bgcolor;
}
34 changes: 33 additions & 1 deletion awesome_dashboard/static/src/dashboard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,39 @@
<templates xml:space="preserve">

<t t-name="awesome_dashboard.AwesomeDashboard">
hello dashboard
<Layout display="{controlPanel: {} }" className="'o_dashboard h-100'">
<t t-set-slot="control-panel-create-button">
<button class="btn btn-primary" type="action" t-on-click="openCustomers">Customers</button>
<button class="btn btn-primary" type="action" t-on-click="openLeads">Leads</button>
</t>
<t t-set-slot="control-panel-additional-actions">
<button t-on-click="openConfiguration" class="btn">
<i class="fa fa-cog"></i>
</button>
</t>
<t t-foreach="item_list" t-as="item" t-key="item.id">
<DashboardItem t-if="!state.disabledItems.includes(item.id)" size="item.size || 1">
<t t-set="itemProp" t-value="item.props ? item.props(stat) : {'data': stat}"/>
<t t-component="item.Component" t-props="itemProp"/>
</DashboardItem>
</t>
</Layout>
</t>

<t t-name="awesome_dashboard.ConfigurationDialog">
<Dialog title="'Dashboard items configuration'">
Which cards do you whish to see ?
<t t-foreach="items" t-as="item" t-key="item.id">
<CheckBox value="item.enabled" onChange="(ev) => this.onChange(ev, item)">
<t t-esc="item.description"/>
</CheckBox>
</t>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="done">
Done
</button>
</t>
</Dialog>
</t>

</templates>
15 changes: 15 additions & 0 deletions awesome_dashboard/static/src/dashboard_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component } from "@odoo/owl";


export class DashboardItem extends Component {
static template = "awesome_dashboard.DashboardItem"
static props = {
size: { type: Number, optional: true, default: 1},
}

get widthStyle() {
return `width: ${18 * this.props.size}rem;`;
}


}
10 changes: 10 additions & 0 deletions awesome_dashboard/static/src/dashboard_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.DashboardItem">
<div class="card d-inline-block align-top" t-att-style="widthStyle">
<t t-slot="default"/>
</div>
</t>

</templates>
66 changes: 66 additions & 0 deletions awesome_dashboard/static/src/items.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { registry } from "@web/core/registry";
import { NumberCard } from "./number_card";
import { PieCard } from "./pie_card";

const item_list = [
{
id: "average_quantity",
description: "Average amount of t-shirt",
Component: NumberCard,
props: (data) => ({
title: "Average amount of t-shirt by order",
value: data.average_quantity
}),
},
{
id: "average_time",
description: "Average process time",
Component: NumberCard,
size: 2,
props: (data) => ({
title: "Average time (in hours) processing elapsed",
value: data.average_time
}),
},
{
id: "nb_cancelled_orders",
description: "Number of cancelled orders, this month",
Component: NumberCard,
props: (data) => ({
title: "Number of cancelled orders",
value: data.nb_cancelled_orders
}),
},
{
id: "nb_new_orders",
description: "Number of new orders this month",
Component: NumberCard,
props: (data) => ({
title: "Number of new orders",
value: data.nb_new_orders
}),
},
{
id: "total_amount",
description: "Total amount of new orders this month",
Component: NumberCard,
props: (data) => ({
title: "Total amount of new orders",
value: data.total_amount
}),
},
{
id: "orders_by_size",
description: "Number of orders by size",
Component: PieCard,
size: 2,
props: (data) => ({
title: "Number of orders by size",
value: data.orders_by_size
}),
}
]

item_list.forEach(item => {
registry.category("awesome_dashboard").add(item.id, item);
})
17 changes: 17 additions & 0 deletions awesome_dashboard/static/src/number_card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Component, xml } from "@odoo/owl";


export class NumberCard extends Component {
static template = xml`
<div class="card-body text-center">
<t t-esc="props.title"/>
<div class="text-success h2 mt-3">
<t t-esc="props.value"/>
</div>
</div>
`
static props = {
title: { type: String, required: true },
value: { type: Number, required: true },
}
}
17 changes: 17 additions & 0 deletions awesome_dashboard/static/src/pie_card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Component, xml } from "@odoo/owl";
import { PieChart } from "./pie_chart";


export class PieCard extends Component {
static components = { PieChart };
static template = xml`
<div class="card-body text-center">
<t t-esc="props.title"/>
<PieChart data="props.value"/>
</div>
`
static props = {
title: { type: String, required: true },
value: { type: Number, required: true },
}
}
43 changes: 43 additions & 0 deletions awesome_dashboard/static/src/pie_chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Component, onWillStart, useEffect, useRef } from "@odoo/owl";
import { loadJS } from "@web/core/assets";


export class PieChart extends Component {
static template = "awesome_dashboard.PieChart"
static props = {
data: { type: Object, required: true },
}

setup() {
this.canvasRef = useRef("canvas");
onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
useEffect(() => {
if (!this.chart) {
this.renderChart();
} else {
this.updateChart();
}
},
() => [this.props.data]
);
}

renderChart() {
this.chart = new Chart(this.canvasRef.el, {
type: 'pie',
data: {
datasets: [{data: Object.values(this.props.data)}],
labels: Object.keys(this.props.data)
}
});
}

updateChart() {
if (this.chart) {
this.chart.data.labels = Object.keys(this.props.data);
this.chart.data.datasets[0].data = Object.values(this.props.data);
this.chart.update();
}
}

}
8 changes: 8 additions & 0 deletions awesome_dashboard/static/src/pie_chart.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.PieChart">
<canvas id="canvas" t-ref="canvas"></canvas>
</t>

</templates>
30 changes: 30 additions & 0 deletions awesome_dashboard/static/src/statistics_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { browser } from "@web/core/browser/browser";
import { reactive } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";

export const statisticsService = {
start(env) {
let stats = reactive({});

async function _loadStats() {
try {
const data = await rpc("/awesome_dashboard/statistics");
Object.assign(stats, data);
} catch (e) {
console.error("Failed to load stats", e);
}
}

_loadStats();

browser.setInterval(() => {
_loadStats();
console.log("Stats updated: ", stats);
}, 100000);

return stats;
},
};

registry.category("services").add("awesome_dashboard.statistics", statisticsService);
15 changes: 15 additions & 0 deletions awesome_owl/static/src/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component, useState } from "@odoo/owl";


export class Card extends Component {
static props = ['title', 'slots'];
static template = "awesome_owl.card"

setup(){
this.state = useState({isOpen: true});
}

toggleContent() {
this.state.isOpen = !this.state.isOpen;
}
}
18 changes: 18 additions & 0 deletions awesome_owl/static/src/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.card">
<div class="card d-inline-block m-2" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">
<t t-esc="props.title"/>
<button class="btn" t-on-click="toggleContent">Toggle</button>
</h5>
<p class="card-text" t-if="state.isOpen">
<t t-slot="default"/>
</p>
</div>
</div>
</t>

</templates>
Loading