diff --git a/.gitignore b/.gitignore
index b6e47617de1..eb45fe797be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,5 @@ dmypy.json
# Pyre type checker
.pyre/
+
+.vscode
diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index a1cd72893d7..89481aa5f4a 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -24,7 +24,11 @@
'assets': {
'web.assets_backend': [
'awesome_dashboard/static/src/**/*',
+ ('remove', 'awesome_dashboard/static/src/dashboard/**/*'),
+ ],
+ 'awesome_dashboard.dashboard': [
+ 'awesome_dashboard/static/src/dashboard/**/*',
],
},
- 'license': 'AGPL-3'
+ 'license': 'AGPL-3',
}
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js
deleted file mode 100644
index c4fb245621b..00000000000
--- a/awesome_dashboard/static/src/dashboard.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Component } from "@odoo/owl";
-import { registry } from "@web/core/registry";
-
-class AwesomeDashboard extends Component {
- static template = "awesome_dashboard.AwesomeDashboard";
-}
-
-registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml
deleted file mode 100644
index 1a2ac9a2fed..00000000000
--- a/awesome_dashboard/static/src/dashboard.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- hello dashboard
-
-
-
diff --git a/awesome_dashboard/static/src/dashboard/cachingService/caching_service.js b/awesome_dashboard/static/src/dashboard/cachingService/caching_service.js
new file mode 100644
index 00000000000..d36ab2138ef
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/cachingService/caching_service.js
@@ -0,0 +1,24 @@
+import { registry, reactive } from "@web/core/registry";
+import { rpc } from "@web/core/network/rpc";
+import { memoize } from "@web/core/utils/functions"
+
+
+export async function _loadStatistics() {
+ // console.log("loading statistics")
+ const result = await rpc("/awesome_dashboard/statistics")
+ // console.log(result)
+ return result
+}
+
+export const myCaching = {
+ start(env) {
+ return {
+ loadStatistics() {
+ let memoLoadStatistics = memoize(_loadStatistics)
+ return memoLoadStatistics
+ }
+ }
+ },
+ }
+
+registry.category("services").add("myCaching", myCaching);
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..12a7b388882
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,72 @@
+import { Component, onWillStart, useState, onMounted } from "@odoo/owl";
+import { Layout } from "@web/search/layout"
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { DashboardItem } from "./dashboardItem/dashboard_item";
+import { rpc } from "@web/core/network/rpc";
+import { Piechart } from "./piechart/piechart";
+import { MyDialog } from "./mydialog/mydialog";
+import { browser } from "@web/core/browser/browser";
+
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+
+ static components = {
+ DashboardItem,
+ Layout,
+ Piechart,
+ MyDialog
+ }
+ setup() {
+ this.action = useService("action")
+ this.caching = useService("myCaching")
+ this.statistics = useState({})
+ this.items = registry.category("awesome_dashboard").get("items");
+ this.dialog = useService("dialog")
+ this.state = useState({
+ disabledItems: JSON.parse(browser.localStorage.getItem("disabledDashboardItems")?.split(",") || "{}")
+ })
+
+ onWillStart(async () => {
+ const stats = await this.caching.loadStatistics()
+ Object.assign(this.statistics, await stats());
+ })
+ onMounted(() => {
+ setInterval(async () => {
+ const stats = await this.caching.loadStatistics()
+ Object.assign(this.statistics, await stats());
+ }, 5000)
+ })
+ }
+
+ _updateConfiguration(newDisabledItems) {
+ this.state.disabledItems = newDisabledItems
+ console.log(this.state.disabledItems.average_quantity)
+ console.log(this.items[0])
+ }
+
+ openConfiguration() {
+ this.dialog.add(MyDialog, {
+ items: this.items,
+ disabledItems: this.state.disabledItems,
+ updateConfiguration: this._updateConfiguration.bind(this),
+ })
+ }
+
+ kanban_action() {
+ this.action.doAction("base.action_partner_form")
+ }
+
+ leads_action() {
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ name: 'crm action',
+ target: 'current',
+ res_model: 'crm.lead',
+ views: [[false, 'list'], [false, 'form']],
+ });
+ }
+}
+
+registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss
new file mode 100644
index 00000000000..64881f8dcb7
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.scss
@@ -0,0 +1,4 @@
+.o_dashboard {
+ background-color: #7095bb;
+ height: 100%;
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..f82bfd76952
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.js
new file mode 100644
index 00000000000..2aee1cc02e0
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.js
@@ -0,0 +1,10 @@
+import { Component, useState } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.dashboarditem";
+
+ static props = {
+ size: { type: Number, optional: true, default: 1 },
+ slots: { type: Object, optional: true}
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.xml
new file mode 100644
index 00000000000..55073fa8335
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js
new file mode 100644
index 00000000000..715e67199ae
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,69 @@
+import { NumberCard } from "./numberCard/number_card";
+import { PieChartCard } from "./pieChartCard/piechart_card";
+import { registry } from "@web/core/registry";
+
+
+const items = [
+ {
+ id: "average_quantity",
+ description: "Average amount of t-shirt",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Average amount of t-shirt by order this month",
+ value: data.average_quantity
+ })
+ },
+ {
+ id: "average_time",
+ description: "Average delivery time",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Average time it takes to deliver a t-shirt",
+ value: data.average_time
+ })
+ },
+ {
+ id: "nb_cancelled_orders",
+ description: "Number of cancelled orders",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Number of orders that were cancelled.",
+ value: data.nb_cancelled_orders
+ })
+ },
+ {
+ id: "nb_new_orders",
+ description: "Number of new orders",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Number of orders that were new.",
+ value: data.nb_new_orders
+ })
+ },
+ {
+ id: "total_amount",
+ description: "Total amount of orders",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Total number of orders",
+ value: data.total_amount
+ })
+ },
+ {
+ id: "pie_chart",
+ description: "Orders by size",
+ Component: PieChartCard,
+ size: 2,
+ props: (data) => ({
+ title: "Display the orders in a pie chart by size",
+ value: data.orders_by_size
+ })
+ },
+]
+
+registry.category("awesome_dashboard").add("items", items);
diff --git a/awesome_dashboard/static/src/dashboard/mydialog/mydialog.js b/awesome_dashboard/static/src/dashboard/mydialog/mydialog.js
new file mode 100644
index 00000000000..958cfae6799
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/mydialog/mydialog.js
@@ -0,0 +1,37 @@
+import { Component, useState } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { Dialog } from "@web/core/dialog/dialog";
+import { CheckBox } from "@web/core/checkbox/checkbox";
+import { browser } from "@web/core/browser/browser";
+
+
+export class MyDialog extends Component {
+ static template = "awesome_dashboard.mydialog";
+
+ static components = {
+ Dialog,
+ CheckBox
+ }
+
+ static props = {
+ disabledItems: Object,
+ updateConfiguration: Function,
+ items: Array,
+ close: Function
+ }
+
+ setup() {
+ console.log(this.props)
+ }
+
+ onChange(ev, item) {
+ // this.disabledItems[item.id] = ev
+ this.props.disabledItems[item.id] = ev
+ }
+
+ done() {
+ browser.localStorage.setItem("disabledDashboardItems", JSON.stringify(this.props.disabledItems))
+ this.props.updateConfiguration(this.props.disabledItems)
+ this.props.close()
+ }
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/mydialog/mydialog.xml b/awesome_dashboard/static/src/dashboard/mydialog/mydialog.xml
new file mode 100644
index 00000000000..a5de80547ca
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/mydialog/mydialog.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/numberCard/number_card.js b/awesome_dashboard/static/src/dashboard/numberCard/number_card.js
new file mode 100644
index 00000000000..280d9b894e2
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/numberCard/number_card.js
@@ -0,0 +1,11 @@
+import { loadJS } from "@web/core/assets"
+import { Component, useState, onWillStart, onMounted, useRef, reactive, onWillUnmount, onWillPatch, onWillUpdateProps } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.numberCard";
+
+ static props = {
+ title: String,
+ value: Number
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/numberCard/number_card.xml b/awesome_dashboard/static/src/dashboard/numberCard/number_card.xml
new file mode 100644
index 00000000000..6e753dba1e4
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/numberCard/number_card.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/pieChartCard/piechart_card.js b/awesome_dashboard/static/src/dashboard/pieChartCard/piechart_card.js
new file mode 100644
index 00000000000..c66ce310f65
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pieChartCard/piechart_card.js
@@ -0,0 +1,13 @@
+import { loadJS } from "@web/core/assets"
+import { Component, useState, onWillStart, onMounted, useRef, reactive, onWillUnmount, onWillPatch, onWillUpdateProps } from "@odoo/owl";
+import { Piechart } from "../piechart/piechart";
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.pieChartCard";
+ static components = { Piechart }
+
+ static props = {
+ title: String,
+ value: Object
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/pieChartCard/piechart_card.xml b/awesome_dashboard/static/src/dashboard/pieChartCard/piechart_card.xml
new file mode 100644
index 00000000000..df5a4168b3e
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pieChartCard/piechart_card.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.js b/awesome_dashboard/static/src/dashboard/piechart/piechart.js
new file mode 100644
index 00000000000..861b1f615dc
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js
@@ -0,0 +1,55 @@
+import { loadJS } from "@web/core/assets"
+import { Component, useState, onWillStart, onMounted, useRef, reactive, onWillUnmount, onWillPatch, onWillUpdateProps } from "@odoo/owl";
+
+export class Piechart extends Component {
+ static template = "awesome_dashboard.piechart";
+
+ static props = {
+ pieData: Object
+ }
+
+ setup() {
+ this.chartRef = useRef("chart")
+
+ this.state = useState({})
+
+ onWillStart(async () => {
+ await loadJS("/web/static/lib/Chart/Chart.js")
+ })
+ onMounted(async () => {
+ // chart stuff
+ const ordersBySize = this.props.pieData
+ const data = {
+ labels: ["m", "s", "xl"],
+ datasets: [{
+ label: "tshirt orders by size",
+ data: [ordersBySize.m, ordersBySize.s, ordersBySize.xl],
+ backgroundColor: [
+ 'rgb(255, 99, 132)',
+ 'rgb(54, 162, 235)',
+ 'rgb(255, 205, 86)'
+ ],
+ }],
+ }
+ const config = {
+ type: 'pie',
+ data: data,
+ };
+
+ this.chart = new Chart(this.chartRef.el, config)
+ })
+ onWillUpdateProps((nextProps) => {
+ const ordersBySize = nextProps.pieData
+
+ if (this.chart && ordersBySize) {
+ this.chart.data.datasets[0].data = [
+ ordersBySize.m || 0,
+ ordersBySize.s || 0,
+ ordersBySize.xl || 0
+ ];
+
+ this.chart.update();
+ }
+ });
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.xml b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml
new file mode 100644
index 00000000000..e84fd8f4fad
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js
new file mode 100644
index 00000000000..5b57fd27a1d
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,10 @@
+import { registry } from "@web/core/registry";
+import { LazyComponent } from "@web/core/assets";
+import { Component, xml } from "@odoo/owl";
+
+class AwesomeDashboardLoader extends Component {
+ static components = { LazyComponent };
+ static template = "awesome_dashboard.awesomedashboardloader"
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader);
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard_action.xml b/awesome_dashboard/static/src/dashboard_action.xml
new file mode 100644
index 00000000000..11fee2a0f0f
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..92d207206ab
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,19 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.card";
+
+ static props = {
+ title : String,
+ slots : { type: Object, optional: true}
+ }
+
+ setup() {
+ this.state = useState({ open: false });
+ this.changeState = this.changeState.bind(this)
+ }
+
+ changeState() {
+ this.state.open = !this.state.open
+ }
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..95d3f84033a
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..5591d195618
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,20 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "awesome_owl.counter";
+
+ static props = {
+ onChange: { type: Function, optional: true }
+ }
+
+ setup() {
+ this.state = useState({ value: 0 });
+ }
+
+ increment() {
+ if (this.props.onChange){
+ this.props.onChange();
+ }
+ this.state.value++;
+ }
+}
diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml
new file mode 100644
index 00000000000..56e10866159
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/global_counter/global_counter.js b/awesome_owl/static/src/global_counter/global_counter.js
new file mode 100644
index 00000000000..d7f5d8acfd7
--- /dev/null
+++ b/awesome_owl/static/src/global_counter/global_counter.js
@@ -0,0 +1,28 @@
+import { Component, useState } from "@odoo/owl";
+import { Counter } from "../counter/counter";
+
+export class GlobalCounter extends Component {
+ static template = "awesome_owl.global_counter";
+
+ static components = {
+ Counter
+ }
+
+ static props = {
+ buttons: Number
+ }
+
+ setup() {
+ this.buttons = []
+ for (let i = 0; i < this.props.buttons; i++) {
+ this.buttons.push(i)
+ }
+
+ this.state = useState({ value: 2 });
+ this.incrementSum = this.incrementSum.bind(this)
+ }
+
+ incrementSum() {
+ this.state.value++;
+ }
+}
diff --git a/awesome_owl/static/src/global_counter/global_counter.xml b/awesome_owl/static/src/global_counter/global_counter.xml
new file mode 100644
index 00000000000..5e3df1ebcef
--- /dev/null
+++ b/awesome_owl/static/src/global_counter/global_counter.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 4ac769b0aa5..ba912ce86bd 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,5 +1,22 @@
-import { Component } from "@odoo/owl";
+import { Component, useState, markup } from "@odoo/owl";
+import { Counter } from "./counter/counter";
+import { Card } from "./card/card";
+import { GlobalCounter } from "./global_counter/global_counter";
+import { TodoItem } from "./todolist/todo_item";
+import { TodoList } from "./todolist/todo_list";
export class Playground extends Component {
+ setup(){
+ this.normal_string = "normal string"
+ this.html_string = markup("Visit W3Schools.com!")
+ }
+
static template = "awesome_owl.playground";
+ static components = {
+ Counter,
+ Card,
+ TodoList,
+ GlobalCounter,
+
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..58abf4ff77e 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -1,10 +1,16 @@
-
hello world
+
+
+
+
+
-
diff --git a/awesome_owl/static/src/todolist/todo_item.js b/awesome_owl/static/src/todolist/todo_item.js
new file mode 100644
index 00000000000..9e0cd1e4f75
--- /dev/null
+++ b/awesome_owl/static/src/todolist/todo_item.js
@@ -0,0 +1,24 @@
+import { Component, useState } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.todoitem";
+
+ static props = {
+ todo: Object,
+ toggleState: Function,
+ removeTodo: Function
+ }
+
+ setup() {
+ this.toggleStateItem = this.toggleStateItem.bind(this)
+ this.removeTodoItem = this.removeTodoItem.bind(this)
+ }
+
+ toggleStateItem() {
+ this.props.toggleState(this.props.todo.id)
+ }
+
+ removeTodoItem() {
+ this.props.removeTodo(this.props.todo.id)
+ }
+}
diff --git a/awesome_owl/static/src/todolist/todo_item.xml b/awesome_owl/static/src/todolist/todo_item.xml
new file mode 100644
index 00000000000..e9c2668e3d9
--- /dev/null
+++ b/awesome_owl/static/src/todolist/todo_item.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js
new file mode 100644
index 00000000000..25b7d36ccdd
--- /dev/null
+++ b/awesome_owl/static/src/todolist/todo_list.js
@@ -0,0 +1,56 @@
+import { Component, useState, useRef, onMounted } from "@odoo/owl";
+import { TodoItem } from "./todo_item";
+
+export class TodoList extends Component {
+ static template = "awesome_owl.todolist";
+
+ static components = {
+ TodoItem,
+ }
+
+ static props = {
+ }
+
+ setup() {
+ this.state = useState({
+ text: "",
+ todos: [],
+
+ })
+ this.last_id = 0
+ this.inputRef = useRef('input')
+
+ onMounted(() => {
+ this.inputRef.el.focus()
+ });
+
+ this.toggleState = this.toggleState.bind(this)
+ this.removeTodo = this.removeTodo.bind(this)
+ }
+
+ toggleState(id) {
+ const index = this.state.todos.findIndex((elem) => elem.id === id);
+ if (index >= 0) {
+ // remove the element at index from list
+ this.state.todos[index].isCompleted = !this.state.todos[index].isCompleted
+ }
+ }
+
+ removeTodo(id) {
+ const index = this.state.todos.findIndex((elem) => elem.id === id);
+ if (index >= 0) {
+ // remove the element at index from list
+ this.state.todos.splice(index, 1)
+ }
+
+ }
+
+ addTodo(event) {
+ if (this.state.text == "") return
+ if (event.keyCode == 13) {
+ const newTodo = { id: this.last_id, description: this.state.text, isCompleted: false }
+ this.state.todos.push(newTodo)
+ this.last_id = this.last_id + 1
+ }
+ }
+}
diff --git a/awesome_owl/static/src/todolist/todo_list.xml b/awesome_owl/static/src/todolist/todo_list.xml
new file mode 100644
index 00000000000..6666f2f0e22
--- /dev/null
+++ b/awesome_owl/static/src/todolist/todo_list.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/estate/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
new file mode 100644
index 00000000000..b3cc5efd590
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,22 @@
+{
+ 'name': 'estate-kehey',
+ 'depends': [
+ 'base',
+ ],
+ 'data': [
+ 'data/security.xml',
+ 'data/ir.model.access.csv',
+ 'views/estate_property_actions.xml',
+ 'views/estate_property_tag_view_list.xml',
+ 'views/estate_property_type_view_form.xml',
+ 'views/estate_property_type_view_list.xml',
+ 'views/estate_property_view_form.xml',
+ 'views/estate_property_view_kanban.xml',
+ 'views/estate_property_view_list.xml',
+ 'views/estate_property_view_search.xml',
+ 'views/users_extra_views.xml',
+ 'data/estate_menus.xml',
+ ],
+ 'application': True,
+ 'category': 'Real Estate/Brokerage',
+}
diff --git a/estate/data/estate_menus.xml b/estate/data/estate_menus.xml
new file mode 100644
index 00000000000..112da7f4918
--- /dev/null
+++ b/estate/data/estate_menus.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv
new file mode 100644
index 00000000000..bc273d0702a
--- /dev/null
+++ b/estate/data/ir.model.access.csv
@@ -0,0 +1,14 @@
+id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
+access_estate_property_manager,estate.property.manager,model_estate_property,estate_group_manager,1,1,1,0
+access_estate_property_type_manager,estate.property.type.manager,model_estate_property_type,estate_group_manager,1,1,1,1
+access_estate_property_tag_manager,estate.property.tag.manager,model_estate_property_tag,estate_group_manager,1,1,1,1
+access_estate_property_offer_manager,estate.property.offer.manager,model_estate_property_offer,estate_group_manager,1,1,1,1
+access_estate_property_agent,estate.property.agent,model_estate_property,estate_group_user,1,1,1,0
+access_estate_property_type_agent,estate.property.type.agent,model_estate_property_type,estate_group_user,1,0,0,0
+access_estate_property_tag_agent,estate.property.tag.agent,model_estate_property_tag,estate_group_user,1,0,0,0
+access_estate_property_offer_agent,estate.property.offer.agent,model_estate_property_offer,estate_group_user,1,1,1,0
+
+access_estate_property_admin,estate.property.admin,model_estate_property,base.group_system,1,1,1,1
+access_estate_property_type_admin,estate.property.type.admin,model_estate_property_type,base.group_system,1,1,1,1
+access_estate_property_tag_admin,estate.property.tag.admin,model_estate_property_tag,base.group_system,1,1,1,1
+access_estate_property_offer_admin,estate.property.offer.adin,model_estate_property_offer,base.group_system,1,1,1,1
\ No newline at end of file
diff --git a/estate/data/security.xml b/estate/data/security.xml
new file mode 100644
index 00000000000..286d5e61048
--- /dev/null
+++ b/estate/data/security.xml
@@ -0,0 +1,38 @@
+
+
+
+
+ Real Estate
+
+
+
+ Agent
+
+
+
+
+ Manager
+
+
+
+
+ Agents can only see unassigned or own properties
+
+ ['|', ('salesman_id', '=', user.partner_id.id), ('salesman_id', '=', False)]
+
+
+
+
+
+
+
+ Managers can see all properties
+
+ [(1, '=', 1)]
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/git-challenge.txt b/estate/git-challenge.txt
new file mode 100644
index 00000000000..c44ca2ecb56
--- /dev/null
+++ b/estate/git-challenge.txt
@@ -0,0 +1,2 @@
+my best ascii drawing small fix
+adding the final touches to my ascii
\ No newline at end of file
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..f8ae845d7ac
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,8 @@
+from . import (
+ estate_property,
+ estate_property_offer,
+ estate_property_tag,
+ estate_property_type,
+ inhereted_users,
+)
+
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..08549809ba1
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,108 @@
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, exceptions, fields, models
+
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "estate model"
+ _order = "id desc"
+
+ name = fields.Char(required=True)
+ salesman_id = fields.Many2one("res.partner", string="Salesman")
+ buyer_id = fields.Many2one("res.users", default=lambda self: self.env.user, string="Buyer")
+ type_id = fields.Many2one("estate.property.type")
+ tags_id = fields.Many2many("estate.property.tag")
+ offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
+ active = fields.Boolean(default=True)
+ state = fields.Selection(
+ required=True,
+ copy=False,
+ default="new",
+ selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")],
+ )
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Datetime(copy=False, default=fields.Datetime.today() + (relativedelta(months=3)))
+ expected_price = fields.Float(required=True)
+ selling_price = fields.Float(readonly=True, copy=False)
+ bedrooms = fields.Integer(default=2)
+ living_area = fields.Integer()
+ facades = fields.Integer()
+ garage = fields.Boolean()
+ garden = fields.Boolean()
+ garden_area = fields.Integer()
+ garden_orientation = fields.Selection(
+ string='type',
+ selection=[('north', 'North'), ('south', 'South'), ('East', 'east'), ('West', 'west')],
+ )
+ total_area = fields.Integer(string="Total Area", compute="_compute_total_surface")
+ best_offer = fields.Float(string="Best Offer", compute="_compute_best_offer")
+
+ # ==========constraints===================
+ _check_positive_expected_price = models.Constraint("CHECK (expected_price > 0)", "expected price should be bigger than 0")
+ _check_positive_selling_price = models.Constraint("CHECK (selling_price > 0)", "expected price should be bigger than 0")
+
+ @api.constrains("selling_price", "expected_price")
+ def _check_enough_selling_price(self):
+ for record in self:
+ offer_made = "accepted" in record.offer_ids.mapped("status")
+ price_good_enough = record.selling_price > 0.9 * record.expected_price
+ if not price_good_enough and offer_made:
+ to_low_user_error = "selling price is too low for the expected price"
+ raise exceptions.ValidationError(to_low_user_error)
+
+ @api.constrains("state")
+ def _no_sell_without_offer(self):
+ for record in self:
+ if record.state == "sold" and "accepted" not in record.offer_ids.mapped("status"):
+ only_sold_if_accepted = "can only sell a property with an accepted offer"
+ raise exceptions.UserError(only_sold_if_accepted)
+
+ # ==========computed fields===============
+ @api.depends('garden_area', 'living_area')
+ def _compute_total_surface(self):
+ for record in self:
+ record.total_area = record.garden_area + record.living_area
+
+ @api.depends('offer_ids')
+ def _compute_best_offer(self):
+ for record in self:
+ if not record.offer_ids:
+ record.best_offer = 0
+ else:
+ record.best_offer = max(record.offer_ids.mapped("price"))
+
+ # ============onchage fields==============
+ @api.onchange("garden")
+ def _onchange_garden(self):
+ if self.garden:
+ self.garden_area = 10
+ self.garden_orientation = "north"
+ else:
+ self.garden_area = 0
+ self.garden_orientation = False
+
+ # ==========button functions==============
+ def action_property_sold(self):
+ for record in self:
+ if record.state == "cancelled":
+ no_sell_cancelled_error = "Can't sell a cancelled property"
+ raise exceptions.UserError(no_sell_cancelled_error)
+ record.state = "sold"
+ return True
+
+ def action_property_cancelled(self):
+ for record in self:
+ if record.state == "sold":
+ no_sell_a_sold_property = "Can't cancel a sold property"
+ raise exceptions.UserError(no_sell_a_sold_property)
+ record.state = "cancelled"
+ return True
+
+ @api.ondelete(at_uninstall=False)
+ def ondelete(self):
+ for property in self:
+ if property.state in ("new", "cancelled"):
+ no_delete_new_or_cancelled_record = "cannot delete new or cancelled record"
+ raise exceptions.UserError(no_delete_new_or_cancelled_record)
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..328bd4be46b
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,72 @@
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, exceptions, fields, models
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "estate offer model"
+ _order = "price desc"
+
+ name = fields.Char(required=True)
+ price = fields.Float()
+ status = fields.Selection(
+ string='status',
+ copy=False,
+ selection=[('accepted', 'Accepted'), ('refused', 'Refused')],
+ )
+ partner_id = fields.Many2one("res.partner", required=True)
+ property_id = fields.Many2one("estate.property", required=True, ondelete="cascade")
+ date_deadline = fields.Datetime(string="Deadline", compute="compute_deadline", inverse="_inverse_deadline")
+ validity = fields.Integer(string="validity", default=7)
+ property_type_id = fields.Many2one("estate.property.type", related="property_id.type_id", string="Property Type", store=True)
+
+ # =========contraints============
+ _check_positive_offer_price = models.Constraint("CHECK (price > 0)", "expected price should be bigger than 0")
+
+ @api.depends('validity', "create_date")
+ def compute_deadline(self):
+ for record in self:
+ if record.create_date:
+ record.date_deadline = record.create_date + relativedelta(days=record.validity)
+ else:
+ record.date_deadline = fields.Datetime.today() + relativedelta(days=record.validity)
+
+ def _inverse_deadline(self):
+ for record in self:
+ record.validity = (record.date_deadline - record.create_date).days
+
+ # ===========button actions===========
+ def action_accept(self):
+
+ for record in self:
+ if "accepted" in record.property_id.offer_ids.mapped("status"):
+ already_accepted_exception = "already accepted an offer!"
+ raise exceptions.UserError(already_accepted_exception)
+ record.property_id.buyer_id = record.partner_id
+ record.property_id.state = "offer_accepted"
+ record.status = "accepted"
+ record.property_id.selling_price = record.price
+
+ def action_refuse(self):
+ for record in self:
+ for property in record.property_id:
+ record.status = "refused"
+
+ @api.model
+ def create(self, vals):
+ for to_create in vals:
+ property = self.env["estate.property"].browse(to_create["property_id"])
+ # If the property is already sold we can't add it
+ if property.state == "sold":
+ no_create_on_sold = "no create on already sold property"
+ raise exceptions.UserError(no_create_on_sold)
+
+ # Set a minimum bidding limit
+ new_bid = to_create["price"]
+ for offer in property.offer_ids:
+ if offer.price > float(new_bid):
+ cant_bit_lower_exception = "can't bid lower than the highest bid"
+ raise exceptions.UserError(cant_bit_lower_exception)
+ property.state = "offer_received"
+ return super().create(vals)
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..de7a90154b1
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,11 @@
+from odoo import fields, models
+
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _description = "estate tag model"
+ _order = "name"
+
+ name = fields.Char(required=True)
+
+ _check_unique_tag = models.Constraint("UNIQUE(name)", "tags should be unique")
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..c44ccf2cdfc
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,20 @@
+from odoo import api, fields, models
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "estate type model"
+ _order = "sequence, name"
+
+ name = fields.Char()
+ property_ids = fields.One2many("estate.property", "type_id")
+ sequence = fields.Integer(default=0)
+ offer_ids = fields.One2many("estate.property.offer", "property_type_id")
+ offer_count = fields.Integer(compute="_compute_offer_count")
+
+ _check_unique_type = models.Constraint("UNIQUE(name)", "types should be unique")
+
+ @api.depends("offer_ids")
+ def _compute_offer_count(self):
+ for property_type_record in self:
+ property_type_record.offer_count = len(property_type_record.offer_ids)
diff --git a/estate/models/inhereted_users.py b/estate/models/inhereted_users.py
new file mode 100644
index 00000000000..738f23ef0ea
--- /dev/null
+++ b/estate/models/inhereted_users.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+
+
+class InheritedModel(models.Model):
+ _inherit = "res.users"
+
+ property_ids = fields.One2many(
+ "estate.property",
+ "salesman_id",
+ domain=[("state", "in", ["new", "offer_received"])])
diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py
new file mode 100644
index 00000000000..dfd37f0be11
--- /dev/null
+++ b/estate/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_estate
diff --git a/estate/tests/test_estate.py b/estate/tests/test_estate.py
new file mode 100644
index 00000000000..14b82ca5f82
--- /dev/null
+++ b/estate/tests/test_estate.py
@@ -0,0 +1,69 @@
+from odoo import Command
+from odoo.exceptions import UserError
+from odoo.tests import tagged
+from odoo.tests.common import TransactionCase
+from odoo.tests.form import Form
+
+
+# The CI will run these tests after all the modules are installed,
+# not right after installing the one defining it.
+@tagged('post_install', '-at_install')
+class EstateTestCase(TransactionCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.buyer_partner = cls.env['res.users'].create({
+ 'name': 'some guy',
+ 'login': 'some guy login',
+ })
+
+ sold_property_with_accepted_offer = {
+ "name": "property1",
+ "expected_price": "110.0",
+ "selling_price": "100.0",
+ "offer_ids": [Command.create({"name": "first offer", "price": "1100.0", "status": "accepted", "partner_id": cls.buyer_partner.partner_id.id})],
+ }
+ cls.properties = cls.env['estate.property'].create([sold_property_with_accepted_offer])
+ cls.properties.write({"state": "sold"})
+
+ property_with_no_offer = {
+ "name": "property2",
+ "expected_price": "110.0",
+ "selling_price": "100.0",
+ "offer_ids": [],
+ }
+ cls.env['estate.property'].create([property_with_no_offer])
+
+ def test_create_offer_on_sold(self):
+ sold_property_with_accepted_offer = self.properties.search([("name", "=", "property1")])
+
+ with self.assertRaises(UserError):
+ self.env["estate.property.offer"].create({
+ "name": "invalid offer",
+ "price": "1200.0",
+ "property_id": sold_property_with_accepted_offer.id,
+ "partner_id": self.buyer_partner.partner_id.id,
+ })
+
+ def test_no_sell_with_no_offer(self):
+ property_with_no_offer = self.properties.search([("name", "=", "property2")])
+ with self.assertRaises(UserError):
+ property_with_no_offer.write({
+ "state": "sold",
+ })
+
+ def test_is_sold_property_marked(self):
+ sold_property_with_accepted_offer = self.properties.search([("name", "=", "property1")])
+ self.assertRecordValues(sold_property_with_accepted_offer, [{"state": "sold"}])
+
+ property_with_no_offer = self.properties.search([("name", "=", "property2")])
+ self.assertRecordValues(property_with_no_offer, [{"state": "new"}])
+
+ def test_garden_reset(self):
+ property_with_no_offer = self.properties.search([("name", "=", "property2")])
+ self.assertRecordValues(property_with_no_offer, [{"garden_area": 0, "garden_orientation": False, "garden": False}])
+ with Form(property_with_no_offer) as property_form:
+ property_form.garden = True
+ property = property_form.save()
+ self.assertRecordValues(property, [{"garden_area": 10, "garden_orientation": "north", "garden": True}])
diff --git a/estate/views/estate_property_actions.xml b/estate/views/estate_property_actions.xml
new file mode 100644
index 00000000000..a31f2bc8571
--- /dev/null
+++ b/estate/views/estate_property_actions.xml
@@ -0,0 +1,28 @@
+
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
+
+ Offers
+ estate.property.offer
+ list,form
+ [("property_type_id", "==", active_id)]
+
+
+
+ Properties
+ estate.property
+ list,form,kanban
+ {'search_default_state': True}
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_tag_view_list.xml b/estate/views/estate_property_tag_view_list.xml
new file mode 100644
index 00000000000..3f4b563fcbf
--- /dev/null
+++ b/estate/views/estate_property_tag_view_list.xml
@@ -0,0 +1,13 @@
+
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_type_view_form.xml b/estate/views/estate_property_type_view_form.xml
new file mode 100644
index 00000000000..08bf6eef7d6
--- /dev/null
+++ b/estate/views/estate_property_type_view_form.xml
@@ -0,0 +1,29 @@
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_type_view_list.xml b/estate/views/estate_property_type_view_list.xml
new file mode 100644
index 00000000000..65d009a53a7
--- /dev/null
+++ b/estate/views/estate_property_type_view_list.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_view_form.xml b/estate/views/estate_property_view_form.xml
new file mode 100644
index 00000000000..3808445486f
--- /dev/null
+++ b/estate/views/estate_property_view_form.xml
@@ -0,0 +1,87 @@
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_view_kanban.xml b/estate/views/estate_property_view_kanban.xml
new file mode 100644
index 00000000000..2707891db08
--- /dev/null
+++ b/estate/views/estate_property_view_kanban.xml
@@ -0,0 +1,39 @@
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+ Expected price:
+
+
+
+
+ Best Offer:
+
+
+
+
+
+ Selling price:
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_view_list.xml b/estate/views/estate_property_view_list.xml
new file mode 100644
index 00000000000..f72fc523a99
--- /dev/null
+++ b/estate/views/estate_property_view_list.xml
@@ -0,0 +1,20 @@
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_view_search.xml b/estate/views/estate_property_view_search.xml
new file mode 100644
index 00000000000..af84c967570
--- /dev/null
+++ b/estate/views/estate_property_view_search.xml
@@ -0,0 +1,22 @@
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/users_extra_views.xml b/estate/views/users_extra_views.xml
new file mode 100644
index 00000000000..a984be48ae4
--- /dev/null
+++ b/estate/views/users_extra_views.xml
@@ -0,0 +1,16 @@
+
+
+
+ res.users.inheret
+ res.users
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/estate_account/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py
new file mode 100644
index 00000000000..ff5ddec2581
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,15 @@
+{
+ 'name': 'estate_account-kehey',
+ 'depends': [
+ 'base',
+ 'estate',
+ 'account',
+ ],
+ 'data': [
+
+ ],
+ 'views': [
+
+ ],
+ 'application': False,
+}
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..5e1963c9d2f
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_property
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..6ab5d8ad6bd
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,28 @@
+from odoo import Command, models
+
+
+class InheritedModel(models.Model):
+ _inherit = "estate.property"
+
+ def action_property_sold(self):
+ for record in self:
+ res = super().action_property_sold()
+ self.env["account.move"].sudo().create(
+ {
+ "partner_id": record.buyer_id.partner_id.id,
+ "move_type": "out_invoice",
+ "invoice_line_ids": [
+ Command.create({
+ "name": "six percent charge",
+ "quantity": "1",
+ "price_unit": record.selling_price * 0.06,
+ }),
+ Command.create({
+ "name": "administration fee",
+ "quantity": "1",
+ "price_unit": 100.0,
+ }),
+ ],
+ },
+ )
+ return res