Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
39c9e64
[ADD] Tutorial - Ch 1, 2, 3 - Add module, models, and basic fields
andha-odoo Mar 16, 2026
9448e18
[IMP] Tutorial - Chapter 4 - Security
andha-odoo Mar 16, 2026
c8a3e91
[IMP] Tutorial - Chapter 5 - Actions and Menus
andha-odoo Mar 16, 2026
077ffd7
[IMP] Tutorial - Chapter 5 - Fields, Attributes, View, Default Values
andha-odoo Mar 17, 2026
b266f26
[IMP] Tutorial - Chapter 6 - List, Form, and Search Views
andha-odoo Mar 17, 2026
1d80309
[FIX] Datetime calculation as lambda and renaming values
andha-odoo Mar 17, 2026
e9e2d4c
[IMP] Tutorial - Chapter 7 - Relations Between Models
andha-odoo Mar 17, 2026
cde4aaf
[FIX] styling fixes
andha-odoo Mar 17, 2026
5bc4790
[IMP] Tutorial - Chapter 8 - Computed Fields and Onchanges
andha-odoo Mar 17, 2026
f7a41c9
[IMP] Tutorial - Chapter 9 - Ready For Some Action?
andha-odoo Mar 18, 2026
a4e18a2
[IMP] Tutorial - Chapter 10 - Constraints
andha-odoo Mar 18, 2026
6d77cf4
[IMP] Tutorial - Chapter 11 - Sprinkles
andha-odoo Mar 18, 2026
0b90b2f
[FIX] Rename access record entries
andha-odoo Mar 18, 2026
f7402bc
[FIX] Rename access record entries (again)
andha-odoo Mar 18, 2026
d2011ff
[FIX] Rename access record entries (again x2)
andha-odoo Mar 19, 2026
286105a
[FIX] Rename access record entries (again x3)
andha-odoo Mar 19, 2026
80e0a16
[LINT] Cleaning up various .py and .xml files
andha-odoo Mar 19, 2026
c267b65
[IMP] Tutorial - Chapter 12 - Inheritance
andha-odoo Mar 19, 2026
aa77f29
[IMP] Tutorial - Chapter 13 - Invoicing
andha-odoo Mar 19, 2026
9ce82fb
[IMP] Tutorial - Chapter 14 - Kanban View
andha-odoo Mar 19, 2026
dcdfb4c
[LINT] Remove unused variable declaration
andha-odoo Mar 19, 2026
55b2bc5
[ADD] Web Framework Tutorial - Chapter 1
andha-odoo Mar 23, 2026
0edbed8
[ADD] Web Framework Tutorial - Chapter 2 - Ch 1...8
andha-odoo Mar 23, 2026
b1bc98e
[IMP] Web Framework Tutorial - Chapter 2 - Ch 9, 10, 11
andha-odoo Mar 25, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# Linting
pyproject.toml
Comment on lines 128 to +132
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not push this

8 changes: 0 additions & 8 deletions awesome_dashboard/static/src/dashboard.js

This file was deleted.

8 changes: 0 additions & 8 deletions awesome_dashboard/static/src/dashboard.xml

This file was deleted.

112 changes: 112 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Component, onWillStart, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { AwesomeDashboardItem } from "./dashboard_item";
import { rpc } from "@web/core/network/rpc";
import { PieChart } from "./pie_chart/pie_chart";
import { Dialog } from "@web/core/dialog/dialog";
import { CheckBox } from "@web/core/checkbox/checkbox";
import { browser } from "@web/core/browser/browser";

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

setup() {
this.action = useService("action");
this.stats = useState(useService("awesome_dashboard.statistics"));
this.items = registry.category("awesome_dashboard").getAll();
this.dialog = useService("dialog");

const hiddenItems = JSON.parse(
browser.localStorage
.getItem("disabled_dashboard_items")
?.split(",") || '[]',
);
this.state = useState({ disabledItems: hiddenItems });

onWillStart(async () => {
const res = await rpc("/awesome_dashboard/statistics");
console.log(res);
Object.assign(this.stats, res);
});
}

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

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

openConfig() {
this.dialog.add(ConfigDialog, {
items: this.items,
disabled: this.state.disabledItems,
onUpdate: (newDisabledItems) => {
this.state.disabledItems = newDisabledItems;
browser.localStorage.setItem(
"disabled_dashboard_items",
JSON.stringify(newDisabledItems),
);
},
});
}
}

class ConfigDialog extends Component {
static template = "awesome_dashboard.config";
static components = { CheckBox, Dialog };

static props = {
items: Array,
close: Function,
disabled: Array,
onUpdate: Function,
};

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

apply() {
const disabledIds = this.items
.filter((i) => !i.isEnabled)
.map((i) => i.id);

this.props.onUpdate(disabledIds);
this.props.close();
}

onCheck(item, value) {
console.log(item, value)
item.isEnabled = value;
const updatedDisabledItemsList = Object.values(this.items)
.filter((item) => !item.isEnabled)
.map((item) => item.id);

browser.localStorage.setItem(
"disabled_dashboard_items",
updatedDisabledItemsList,
);

this.props.onUpdate(updatedDisabledItemsList);
}
}

registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
3 changes: 3 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.o_dashboard {
background-color: gray;
}
43 changes: 43 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_dashboard.AwesomeDashboard">
<Layout display="{ controlPanel: {} }" className="'o_dashboard h-100'">
<t t-set-slot="layout-buttons">
<button class="btn btn-primary" t-on-click="openCustomers">
Customers
</button>
<button class="btn btn-primary" t-on-click="openLeads">
Leads
</button>
</t>
<t t-set-slot="control-panel-additional-actions">
<button class="btn btn-primary" t-on-click="openConfig">
Configuration <i class="fa fa-cog" />
</button>
</t>
<t t-foreach="items" t-as="item" t-key="item.id">
<AwesomeDashboardItem size="item.size || 1">
<t
t-set="itemProp"
t-value="item.props ? item.props(this.stats) : undefined"
/>
<t t-component="item.Component" t-props="itemProp" />
</AwesomeDashboardItem>
</t>
</Layout>
</t>

<t t-name="awesome_dashboard.config">
<Dialog title="'Dashboard items configuration'">
<p>Which cards do you want to see?</p>
<t t-foreach="this.items" t-as="item" t-key="item.id">
<CheckBox value="item.isEnabled" onChange="(val) => this.onCheck(item, val)">
<t t-esc="item.description" />
</CheckBox>
</t>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="apply">Apply</button>
</t>
</Dialog>
</t>
</templates>
21 changes: 21 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component } from "@odoo/owl";
import { Layout } from "@web/search/layout";

export class AwesomeDashboardItem extends Component {
static components = { Layout };
static template = "awesome_dashboard.AwesomeDashboardItem";

static defaultProps = {
size: 1,
};

static props = {
size: { type: Number, optional: true },
slots: {
type: Object,
shape: {
default: true,
},
},
};
}
13 changes: 13 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_dashboard.AwesomeDashboardItem">
<div
class="card border-dark"
t-attf-style="width: {{ 18 * props.size }}rem;"
>
<p class="card-text">
<t t-slot="default" />
</p>
</div>
</t>
</templates>
65 changes: 65 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_items.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NumberCard } from "./number_card/number_card";
import { PieChartCard } from "./pie_chart_card/pie_chart_card";
import {registry} from "@web/core/registry";

const items = [
{
id: "average_quantity",
description: "Average amount of t-shirt",
Component: NumberCard,
props: (data) => ({
title: "Average amount of t-shirt by order this month",
value: data.average_quantity,
}),
},
{
id: "average_time",
description: "Average order processing time",
Component: NumberCard,
size: 2,
props: (data) => ({
title: "Average time for an order to go from 'new' to 'sent' or 'cancelled' ",
value: data.average_time,
}),
},
{
id: "nb_new_orders",
description: "Number of new orders",
Component: NumberCard,
props: (data) => ({
title: "Number of new orders this month",
value: data.nb_new_orders,
}),
},
{
id: "nb_cancelled_orders",
description: "Number of cancelled orders",
Component: NumberCard,
props: (data) => ({
title: "Number of cancelled orders this month",
value: data.nb_cancelled_orders,
}),
},
{
id: "total_amount",
description: "Total new orders",
Component: NumberCard,
props: (data) => ({
title: "Total amount of new orders this month",
value: data.total_amount,
}),
},
{
id: "orders_by_size",
description: "Shirt orders by size",
Component: PieChartCard,
props: (data) => ({
title: "Shirt orders by size",
data: data.orders_by_size,
}),
},
];

items.forEach((item) => {
registry.category("awesome_dashboard").add(item.id, item);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Component } from "@odoo/owl";

export class NumberCard extends Component {
static template = "awesome_dashboard.NumberCard";
static props = {
title: String,
value: Number,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_dashboard.NumberCard">
<div class="o_number-card">
<p class="text-primary" t-esc="props.title"></p>
<p class="text-center text-success" t-esc="props.value"></p>
</div>
</t>
</templates>
86 changes: 86 additions & 0 deletions awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Component, useEffect, useRef, onWillStart } from "@odoo/owl";
import { Layout } from "@web/search/layout";
import { loadJS } from "@web/core/assets";

const D3_COLORS = [
"#1f77b4",
"#ff7f0e",
"#aec7e8",
"#ffbb78",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5",
"#8c564b",
"#c49c94",
"#e377c2",
"#f7b6d2",
"#7f7f7f",
"#c7c7c7",
"#bcbd22",
"#dbdb8d",
"#17becf",
"#9edae5",
];

export class PieChart extends Component {
static components = { Layout };
static template = "awesome_dashboard.PieChart";

static defaultProps = {
s: 0,
m: 0,
l: 0,
xl: 0,
xxl: 0,
};

setup() {
onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
this.canvasRef = useRef("canvas");
useEffect(() => this.renderChart());
}

destroyChart() {
if (this.chart) {
this.chart.destroy();
}
}

renderChart() {
this.destroyChart();
const ctx = this.canvasRef.el.getContext("2d");
this.chart = new Chart(ctx, this.getChartConfig());
}

getChartConfig() {
const data = this.props.data || {};
const labels = Object.keys(data);
const counts = Object.values(data);

return {
type: "pie",
data: {
labels: labels,
datasets: [
{
data: counts,
backgroundColor: labels.map((_, index) => D3_COLORS[index % 20]),
hoverOffset: 4
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
}
}
},
};
}
}
Loading