diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..d61078eff20 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -1,30 +1,29 @@ # -*- coding: utf-8 -*- { - 'name': "Awesome Dashboard", - - 'summary': """ + "name": "Awesome Dashboard", + "summary": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'description': """ + "description": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'author': "Odoo", - 'website': "https://www.odoo.com/", - 'category': 'Tutorials', - 'version': '0.1', - 'application': True, - 'installable': True, - 'depends': ['base', 'web', 'mail', 'crm'], - - 'data': [ - 'views/views.xml', + "author": "Odoo", + "website": "https://www.odoo.com/", + "category": "Tutorials", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["base", "web", "mail", "crm"], + "data": [ + "views/views.xml", ], - 'assets': { - 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + "assets": { + "web.assets_backend": [ + "awesome_dashboard/static/src/dashboard_loader.js", + ], + "awesome_dashboard.dashboard": [ + "awesome_dashboard/static/src/dashboard/**/*", ], }, - 'license': 'AGPL-3' + "license": "AGPL-3", } diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 05977d3bd7f..61d533e2d55 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -4,12 +4,12 @@ import random from odoo import http -from odoo.http import request logger = logging.getLogger(__name__) + class AwesomeDashboard(http.Controller): - @http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user') + @http.route("/awesome_dashboard/statistics", type="jsonrpc", auth="user") def get_statistics(self): """ Returns a dict of statistics about the orders: @@ -22,15 +22,14 @@ def get_statistics(self): """ return { - 'average_quantity': random.randint(4, 12), - 'average_time': random.randint(4, 123), - 'nb_cancelled_orders': random.randint(0, 50), - 'nb_new_orders': random.randint(10, 200), - 'orders_by_size': { - 'm': random.randint(0, 150), - 's': random.randint(0, 150), - 'xl': random.randint(0, 150), + "average_quantity": random.randint(4, 12), + "average_time": random.randint(4, 123), + "nb_cancelled_orders": random.randint(0, 50), + "nb_new_orders": random.randint(10, 200), + "orders_by_size": { + "m": random.randint(0, 150), + "s": random.randint(0, 150), + "xl": random.randint(0, 150), }, - 'total_amount': random.randint(100, 1000) + "total_amount": random.randint(100, 1000), } - 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/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..66986a51e89 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,97 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { Component, useState } from "@odoo/owl"; +import { DashboardItem } from "./document_item/document_item"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { DashboardSettings } from "./dashboard_settings/dashboard_settings"; +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"; +export class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, DashboardSettings}; + + setup() { + this.action = useService("action"); + this.statistics = useService("awesome_dashboard.statistics"); + this.display = { + controlPanel: {}, + }; + this.dialog = useService("dialog"); + this.allItems = registry.category("awesome_dashboard").getAll(); + + this.hiddenItems = JSON.parse(localStorage.getItem("dashboard_hidden") || "[]"); + + this.items = this.allItems.filter( + (item) => !this.hiddenItems.includes(item.id) + ); + this.state = useState({ + disabledItems: + browser.localStorage.getItem("disabledDashboardItems")?.split(",") || + [], + }); + } + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }); + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + } + + + openCustomerView() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: _t("All leads"), + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + }); + } + } + 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("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..3c21a3315cc --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,17 @@ +.o_dashboard { + background-color : gray; + height : 100%; +} +.o_card { + background: white; + padding: 20px; + margin: 10px; + border: 1px solid #ccc; + text-align: center; +} + +.pie-container { + width: 220px; + height: 220px; + margin: auto; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..0efd1df6d34 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+
+
+ +
+
+ + + Which cards do you whish to see ? + + + + + + + + + + + +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 00000000000..8324a9224ce --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,75 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./piechart_card/piechart_card"; + +const dashboardItems = [ + { + id: "average_quantity", + description: "Average T-Shirts", + Component: NumberCard, + size: 3, + props: (data) => ({ + title: "Average t-shirt per order", + value: data.average_quantity, + }), + }, + { + id: "average_time", + description: "Average processing time", + Component: NumberCard, + size: 3, + props: (data) => ({ + title: "Average processing time", + value: data.average_time, + }), + }, + { + id: "nb_new_orders", + description: "New Orders", + Component: NumberCard, + size: 3, + props: (data) => ({ + title: "New Orders", + value: data.nb_new_orders, + }), + }, + { + id: "nb_cancelled_orders", + description: "Cancelled Orders", + Component: NumberCard, + size: 3, + props: (data) => ({ + title: "Cancelled Orders", + value: data.nb_cancelled_orders, + }), + }, + { + id: "total_amount", + description: "Total Amount", + Component: NumberCard, + size: 3, + props: (data) => ({ + title: "Total Amount", + value: data.total_amount, + }), + }, + { + id: "orders_by_size", + description: "Orders by Size", + Component: PieChartCard, + size: 3, + props: (data) => ({ + title: "Orders by Size", + data: data.orders_by_size || {}, + }), + }, +]; + + +const dashboardItemRegistry = registry.category("awesome_dashboard"); + +for (const item of dashboardItems) { + dashboardItemRegistry.add(item.id, item); +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js new file mode 100644 index 00000000000..18434b0c72d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js @@ -0,0 +1,31 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class DashboardSettings extends Component { + static template = "awesome_dashboard.DashboardSettings"; + + setup() { + this.state = useState({ + hidden: new Set(this.props.hiddenItems), + }); + } + + toggle(id) { + if (this.state.hidden.has(id)) { + this.state.hidden.delete(id); + } else { + this.state.hidden.add(id); + } + } + + apply() { + const hiddenItems = Array.from(this.state.hidden); + + localStorage.setItem("dashboard_hidden", JSON.stringify(hiddenItems)); + + this.props.close(); + + location.reload(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml new file mode 100644 index 00000000000..c48d9774f9b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml @@ -0,0 +1,30 @@ + + + + +
+

Dashboard Settings

+ + +
+ + + + + +
+
+ +
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/document_item/document_item.js b/awesome_dashboard/static/src/dashboard/document_item/document_item.js new file mode 100644 index 00000000000..32c602f2d38 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/document_item/document_item.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +import { Component} from "@odoo/owl"; +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem" + + static props = { + value:Number, + slots: { + type: Object, + shape: { + default: Object + }, + }, + size: { + type: Number, + default: 1, + optional: true, + }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/document_item/document_item.xml b/awesome_dashboard/static/src/dashboard/document_item/document_item.xml new file mode 100644 index 00000000000..9230aa367a4 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/document_item/document_item.xml @@ -0,0 +1,13 @@ + + + +
+
+

+ + + +

+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..16fccaa748b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { type: String }, + value: { type: Number }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..c9bea358755 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,10 @@ + + + + +
+
+

+

+ + 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..a1a3b555a21 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js @@ -0,0 +1,48 @@ +/** @odoo-module **/ + +import { Component, onWillStart, onMounted, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + + setup() { + this.canvasRef = useRef("canvas"); + + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + onMounted(() => { + this.renderChart(); + }); + } + + renderChart() { + const canvas = this.canvasRef.el; + + if (!canvas) { + console.error("Canvas not found"); + return; + } + + const ctx = canvas.getContext("2d"); + + const data = this.props.data || {}; + const labels = Object.keys(data); + const values = Object.values(data); + + new Chart(ctx, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + data: values, + }, + ], + }, + }); +} +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml similarity index 55% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/piechart/piechart.xml index 1a2ac9a2fed..04b1c63ec09 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml @@ -1,8 +1,8 @@ - - hello dashboard + + diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js new file mode 100644 index 00000000000..b9b53961086 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +import { PieChart } from "../piechart/piechart"; +export class PieChartCard extends Component { + static components = { PieChart }; + static template = "awesome_dashboard.PieChartCard"; + + static components = { PieChart }; + static props = { + title: { type: String }, + values: { type: Object }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml new file mode 100644 index 00000000000..8f57c90f160 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml @@ -0,0 +1,14 @@ + + + + +
+
+ +
+ +
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..50dca99db84 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + +const statisticsService = { + start() { + const statistics = reactive({ isReady: false }); + + async function loadData() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updates, { isReady: true }); + } + + setInterval(loadData, 10 * 1000); + loadData(); + + return statistics; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..c4685cf5e6a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,16 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; + +class DashboardAction extends Component { + static components = { LazyComponent }; + + static template = xml` + + `; + +} + +registry.category("actions").add("awesome_dashboard.dashboard", DashboardAction); diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 55002ab81de..d6f9b54dba2 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -1,43 +1,39 @@ # -*- coding: utf-8 -*- { - 'name': "Awesome Owl", - - 'summary': """ + "name": "Awesome Owl", + "summary": """ Starting module for "Discover the JS framework, chapter 1: Owl components" """, - - 'description': """ + "description": """ Starting module for "Discover the JS framework, chapter 1: Owl components" """, - - 'author': "Odoo", - 'website': "https://www.odoo.com", - + "author": "Odoo", + "website": "https://www.odoo.com", # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list - 'category': 'Tutorials', - 'version': '0.1', - + "category": "Tutorials", + "version": "0.1", # any module necessary for this one to work correctly - 'depends': ['base', 'web'], - 'application': True, - 'installable': True, - 'data': [ - 'views/templates.xml', + "depends": ["base", "web"], + "application": True, + "installable": True, + "data": [ + "views/templates.xml", + ], - 'assets': { - 'awesome_owl.assets_playground': [ - ('include', 'web._assets_helpers'), - ('include', 'web._assets_backend_helpers'), - 'web/static/src/scss/pre_variables.scss', - 'web/static/lib/bootstrap/scss/_variables.scss', - 'web/static/lib/bootstrap/scss/_maps.scss', - ('include', 'web._assets_bootstrap'), - ('include', 'web._assets_core'), - 'web/static/src/libs/fontawesome/css/font-awesome.css', - 'awesome_owl/static/src/**/*', + "assets": { + "awesome_owl.assets_playground": [ + ("include", "web._assets_helpers"), + ("include", "web._assets_backend_helpers"), + "web/static/src/scss/pre_variables.scss", + "web/static/lib/bootstrap/scss/_variables.scss", + "web/static/lib/bootstrap/scss/_maps.scss", + ("include", "web._assets_bootstrap"), + ("include", "web._assets_core"), + "web/static/src/libs/fontawesome/css/font-awesome.css", + "awesome_owl/static/src/**/*", ], }, - 'license': 'AGPL-3' + "license": "AGPL-3", } diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..aecf201cb8f --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,22 @@ +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({isOpen: true }); + } + + toggleContent(){ + this.state.isOpen = !this.state.isOpen + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..388c854888a --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,17 @@ + + + + +
+
+
+ +

+ + + +

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..7e1fb58d793 --- /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: 1}); + } + + increment(){ + this.state.value++; + if(this.props.onchange) + this.props.onchange(); + } + +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..d9871a82587 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,16 @@ + + + + + + + Counter: + + + + + + + + + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..5fb2275ee62 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,19 @@ -import { Component } from "@odoo/owl"; - +import {Component,useState,markup} from "@odoo/owl"; +import {Counter} from "./counter/counter"; +import {Card} from "./card/card"; +import {TodoList} from "./todo/todo_list" export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card , TodoList}; + // value1=markup('
Hello
'); + + setup(){ + this.str1 = markup("
some content
"); + this.str2 = "
some content
"; + this.state = useState({ value: 2}); + } + + incrementSum(){ + this.state.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..45b95460ffc 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,15 @@ - -
- hello world -
-
- + + +

Hello World + + +

+
The Sum is:
+ + + +
diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js new file mode 100644 index 00000000000..4b0f57a065f --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,27 @@ +import {Component} from "@odoo/owl"; + + +export class TodoItem extends Component{ + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { + id: Number, + description: String, + isCompleted: Boolean, + }, + }, + toggleState: Function, + removeTodo: Function, + +}; + onChange(todoId){ + this.props.toggleState(this.props.todo.id); + } + + onRemove(todoId){ + this.props.removeTodo(this.props.todo.id); + } + + } diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml new file mode 100644 index 00000000000..ee2886f8ce5 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,14 @@ + + + + + +
+ +
+
+
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js new file mode 100644 index 00000000000..5a35a9fa762 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,47 @@ +import { Component,useState} from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; +export class TodoList extends Component { + + static template = "awesome_owl.TodoList"; + static components = { TodoItem}; + + setup() { + this.todos = useState([]); + this.nextId = 1; + useAutofocus("input"); +} + + addTodo(ev) { + if (ev.keyCode === 13) { + const description = ev.target.value.trim(); + + if (!description) { + return; + } + + this.todos.push({ + id: this.nextId++, + description: ev.target.value, + isCompleted: false, + }); + + ev.target.value = ""; + } + } + + toggleTodo(todoId) { + const todo = this.todos.find((todo) => todo.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + + const index = this.todos.findIndex((todo) => todo.id === todoId); + if (index >= 0) { + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml new file mode 100644 index 00000000000..c990153850e --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,28 @@ + + + + +
+ +

Todo List

+ + + +
+ + + + + +
+ +
+ +
+
+ diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..4c33c6677ed --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import {useRef,onMounted} from "@odoo/owl"; + +export function useAutofocus(input) { + const ref = useRef(input); + onMounted(() => { + ref.el.focus(); + }); +} diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..8f5ced4b094 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,2 @@ +# import the model directory +from . import models # noqa: F401 diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..f1581e314e1 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,22 @@ +{ + "name": "Real Estate Advertisement ", + "version": "1.0", + "depends": ["base"], + "website": "https://www.odoo.com/app/estate", + "summary": "This module is for Real estate advertisement.", + "category": "estate", + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offers_views.xml", + "views/estate_menus.xml", + "views/res_users_view.xml", + "data/estate_demo_data.xml", + ], + "installable": True, + "application": True, + "author": "odoo-pupat", + "license": "LGPL-3", +} diff --git a/estate/data/estate_demo_data.xml b/estate/data/estate_demo_data.xml new file mode 100644 index 00000000000..5102d4bbe82 --- /dev/null +++ b/estate/data/estate_demo_data.xml @@ -0,0 +1,70 @@ + + + + Home + + + + Luxurious + + + + Chitrakut Residency + 900000 + 191980 + Best residency in this city + + + + + + Villa + + + + Cozy + + + + Rooftop House + 950000 + 191989 + It feels like heaven + + + + + + Penthouse + + + + Furnished + + + + Swadesh PG + 70000 + 191980 + Best PG in this city + + + + + + Palace + + + + Precious + + + + De glance Palace + 1000000 + 191970 + Great Indian Palace + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..8733276da2f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,7 @@ +from . import ( + estate_property, # noqa: F401 + estate_property_offer, # noqa: F401 + estate_property_tag, # noqa: F401 + estate_property_type, # noqa: F401 + res_users, # noqa: F401 +) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..2723099b206 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,137 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare, float_is_zero + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "estate property definition" + _order = "id desc" + + name = fields.Char(string="Property Name", required=True) + description = fields.Text(string="Description", required=True) + postcode = fields.Char(string="Postcode") + validity = fields.Integer(default=7) + date_deadline = fields.Date() + date_availability = fields.Date( + string="Available From", + default=lambda self: fields.Date.add(fields.Date.today(), months=3), + copy=False, + ) + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + available = fields.Char() + bedrooms = fields.Integer(string="Bedrooms", default=2) + living_area = fields.Integer(string="Living Area (sqm)") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area (sqm)") + garden_orientation = fields.Selection( + string="Garden Orientation", + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + ) + + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + required=True, + default="new", + copy=False, + ) + + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + + salesman_id = fields.Many2one( + "res.users", + string="Salesman", + default=lambda self: self.env.user, + ) + buyer_id = fields.Many2one("res.partner", string=" Buyer") + + property_offer_ids = fields.One2many("estate.property.offer", "property_id") + property_tag_ids = fields.Many2many("estate.property.tag", string="tag") + total_area = fields.Float(compute="_compute_total_area", string="Total Area") + best_price = fields.Float( + string="Best Offer", + compute="_compute_best_price", + store=True, + ) + + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", + "Expected price must be positive", + ) + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for estate in self: + estate.total_area = estate.garden_area + estate.living_area + + @api.depends("property_offer_ids.price") + def _compute_best_price(self): + for record in self: + if record.property_offer_ids: + record.best_price = max(record.property_offer_ids.mapped("price")) + else: + record.best_price = 0.0 + + @api.constrains("selling_price", "expected_price") + def _check_selling_price_validation(self): + for record in self: + if float_is_zero(record.selling_price, precision_digits=2): + continue + + if float_is_zero(record.expected_price, precision_digits=2): + continue + + price_limit = record.expected_price * 0.9 + if ( + float_compare(record.selling_price, price_limit, precision_digits=2) + == -1 + ): + raise ValidationError( + _( + "Selling price must not be less than 90%% of the expected price.", + ), + ) + + @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 = "" + + @api.ondelete(at_uninstall=False) + def _unlink_if_not_allowed(self): + for record in self: + if record.state not in ("new", "cancelled"): + raise UserError(_("User can delete only new or cancelled property")) + + def action_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError(_("property can't be cancelled")) + record.state = "sold" + return True + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError(_("Cancelled property can't be sold")) + record.state = "cancelled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..afe92c86d7d --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,103 @@ +from datetime import timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer description" + _order = "price desc" + + price = fields.Float(string="Price") + property_offer_ids = fields.Integer(string="Offer") + status = fields.Selection( + string="Status", + copy=False, + selection=[("accepted", "Accepted"), ("refused", "Refused")], + ) + validity = fields.Integer(string="Validity(days)", default=7) + date_deadline = fields.Date( + compute="_compute_sum_date", + inverse="_compute_validity", + string="Deadline", + ) + + partner_id = fields.Many2one("res.partner", required=True, string="Partner") + property_id = fields.Many2one("estate.property", required=True) + property_type_id = fields.Many2one( + "estate.property.type", + related="property_id.property_type_id", + store=True, + readonly=True, + ) + + @api.depends("validity") + def _compute_sum_date(self): + for record in self: + record.date_deadline = fields.Date.today() + timedelta(days=record.validity) + + _check_price = models.Constraint( + "CHECK(price > 0)", + "Offer Price field should always be positive", + ) + + def _compute_validity(self): + for record in self: + fields.Date.today() == record.date_deadline - timedelta( + days=record.validity, + ) + + @api.onchange("date_deadline") + def _onchange_validity(self): + if self.date_deadline: + create_date = fields.Date.to_date(self.create_date) or fields.Date.today() + self.validity = (self.date_deadline - create_date).days + + @api.model + def create(self, vals_list): + + for vals in vals_list: + property_id = vals.get("property_id") + price = vals.get("price") + + if property_id and price: + property_rec = self.env["estate.property"].browse(property_id) + + if property_rec.best_price and price <= property_rec.best_price: + raise UserError( + _("Offer price must be higher than the current best price."), + ) + if property_rec.state == "new": + property_rec.state = "offer_received" + + return super().create(vals_list) + + def action_accepted(self): + accepted_records = self.search_count( + [ + ("property_id", "=", self.property_id), + ("status", "=", "accepted"), + ], + limit=1, + ) + if accepted_records: + raise UserError(_(" multiple offer can't be accepted")) + + self.status = "accepted" + self.property_id.selling_price = self.price + self.property_id.buyer_id = self.partner_id + self.property_id.state = "offer_accepted" + other_offers = self.search( + [ + ("property_id", "=", self.property_id), + ("status", "!=", "accepted"), + ], + ) + other_offers.write({"status": "refused"}) + return True + + def action_refused(self): + for record in self: + record.status = "refused" + return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..6b09bf79d5b --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "estate property tag" + _order = "name" + + name = fields.Char(string="property tag", required=True) + color = fields.Integer() + + _check_name_unique = models.Constraint( + "unique(name)", + "The Property Tag must be unique.", + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..71872bf072c --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,37 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "estate property type" + _order = "sequence,name" + + name = fields.Char(string="Property Category", required=True) + offer_count = fields.Integer(compute="_compute_offer_count", string="Offers") + property_type_id = fields.Integer() + sequence = fields.Integer( + "sequence", + default=1, + help="Used in ordering property,often sold property types are displayed", + ) + property_ids = fields.One2many("estate.property", "property_type_id") + property_offer_ids = fields.One2many("estate.property.offer", "property_type_id") + + _check_name_unique = models.Constraint( + "unique(name)", + "The Property type must be unique.", + ) + + @api.depends("property_offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.property_offer_ids) + + def action_view_offers(self): + return { + "type": "ir.actions.act_window", + "name": "Offers", + "res_model": "estate.property.offer", + "view_mode": "list,form", + "domain": [("property_type_id", "=", self.id)], + } diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..7a1e696367b --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesman_id", + string="Available Properties", + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0c0b62b7fee --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..1c32d774ffe --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,35 @@ + + + + + + + Property Tags + estate.property.tag + list,form + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..16c4ce521c0 --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,46 @@ + + + + Estate Property offer + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + + estate.property.offer.list + estate.property.offer + + + + + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..7b25da3827f --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,153 @@ + + + + Estate Property + estate.property + list,form,kanban + {'search_default_availability' : True} + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+ +
+ Expected Price : +
+
+
+ Best Price : +
+
+
+
+ Selling Price : +
+
+ +
+
+
+
+
+
+ + + estate_property_search + estate.property + + + + + + + + + + + + + + + + + + + + + + estate.property.view.form + estate.property + +
+
+
+ +
+

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/estate/views/res_users_view.xml b/estate/views/res_users_view.xml new file mode 100644 index 00000000000..074c21c0306 --- /dev/null +++ b/estate/views/res_users_view.xml @@ -0,0 +1,23 @@ + + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..ce9807d14fd --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models # noqa: F401 diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..e1e0a0e7a24 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "Real estate Account", + "version": "1.0", + "depends": ["estate", "account"], + "website": "https://www.odoo.com/app/estate_account", + "summary": "This module is for Real estate invoice creation.", + "category": "estate", + "data": [], + "installable": True, + "author": "odoo-pupat", + "license": "LGPL-3", +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..9d5e62fe812 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property # noqa: F401 diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..1d637f4e8f9 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,32 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + + selling_price = self.selling_price * 0.06 + self.env["account.move"].create( + { + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create( + { + "name": "6 % profit", + "quantity": 1, + "price_unit": selling_price, + } + ), + Command.create( + { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": 100.00, + } + ), + ], + } + ) + return super().action_sold() diff --git a/purchase_global_discount/__init__.py b/purchase_global_discount/__init__.py new file mode 100644 index 00000000000..3f1aab2085f --- /dev/null +++ b/purchase_global_discount/__init__.py @@ -0,0 +1,4 @@ +from . import ( + models, # noqa : F401 + wizard, # noqa : F401 +) diff --git a/purchase_global_discount/__manifest__.py b/purchase_global_discount/__manifest__.py new file mode 100644 index 00000000000..1762d608944 --- /dev/null +++ b/purchase_global_discount/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "purchase_dicount", + "description": "Add discount button with its wizard", + "author": "odoo-pupat", + "website": "https://www.odoo.com/", + "category": "Purchase-custom", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["purchase"], + "data": [ + "security/ir.model.access.csv", + "views/purchase_order_views.xml", + "wizard/purchase_order_discount_views.xml", + ], + "assets": {}, + "license": "LGPL-3", +} diff --git a/purchase_global_discount/models/__init__.py b/purchase_global_discount/models/__init__.py new file mode 100644 index 00000000000..307a154a11f --- /dev/null +++ b/purchase_global_discount/models/__init__.py @@ -0,0 +1,3 @@ +from . import ( + purchase_order, # noqa: F401 +) diff --git a/purchase_global_discount/models/purchase_order.py b/purchase_global_discount/models/purchase_order.py new file mode 100644 index 00000000000..c7013a035e8 --- /dev/null +++ b/purchase_global_discount/models/purchase_order.py @@ -0,0 +1,15 @@ +from odoo import models + + +class InheritedPurchaseOrder(models.Model): + _inherit = "purchase.order" + + def action_open_discount_wizard(self): + self.ensure_one() + return { + "name": "Discount", + "type": "ir.actions.act_window", + "res_model": "purchase.order.discount", + "view_mode": "form", + "target": "new", + } diff --git a/purchase_global_discount/security/ir.model.access.csv b/purchase_global_discount/security/ir.model.access.csv new file mode 100644 index 00000000000..f68ace605d0 --- /dev/null +++ b/purchase_global_discount/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_purchase_discount_wizard,purchase.discount.wizard,model_purchase_order_discount,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/purchase_global_discount/views/purchase_order_views.xml b/purchase_global_discount/views/purchase_order_views.xml new file mode 100644 index 00000000000..5352365e4d8 --- /dev/null +++ b/purchase_global_discount/views/purchase_order_views.xml @@ -0,0 +1,15 @@ + + + + purchase.view.form.purchase.discount + purchase.order + + + +
+
+
+
+
+
diff --git a/purchase_global_discount/wizard/__init__.py b/purchase_global_discount/wizard/__init__.py new file mode 100644 index 00000000000..07afb7cc183 --- /dev/null +++ b/purchase_global_discount/wizard/__init__.py @@ -0,0 +1 @@ +from . import purchase_order_discount # noqa: F401 diff --git a/purchase_global_discount/wizard/purchase_order_discount.py b/purchase_global_discount/wizard/purchase_order_discount.py new file mode 100644 index 00000000000..a258d8c8083 --- /dev/null +++ b/purchase_global_discount/wizard/purchase_order_discount.py @@ -0,0 +1,50 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class PurchaseOrderDiscount(models.TransientModel): + _name = "purchase.order.discount" + _description = "Discount Wizard" + + purchase_order_id = fields.Many2one( + "purchase.order", + default=lambda self: self.env.context.get("active_id"), + required=True, + ) + discount_percentage = fields.Float(string="Percentage") + discount_type = fields.Selection( + selection=[ + ("percentage", "%"), + ("amount", "$"), + ], + default="percentage", + ) + percentage = fields.Float(compute="_compute_initial_discount") + + @api.depends("discount_type", "discount_percentage") + def _compute_initial_discount(self): + if self.discount_type == "amount": + if self.purchase_order_id.amount_untaxed == 0: + raise ValidationError("No more discount possible") + self.percentage = ( + self.discount_percentage * 100 / self.purchase_order_id.amount_untaxed + ) + else: + self.percentage = self.discount_percentage + + def action_apply_discount(self): + self.ensure_one() + if self.discount_type == "amount": + self.purchase_order_id.order_line.write( + { + "discount": ( + self.discount_percentage + * 100 + / self.purchase_order_id.amount_untaxed + ) + } + ) + else: + self.purchase_order_id.order_line.write( + {"discount": self.discount_percentage} + ) diff --git a/purchase_global_discount/wizard/purchase_order_discount_views.xml b/purchase_global_discount/wizard/purchase_order_discount_views.xml new file mode 100644 index 00000000000..f0acb906e5c --- /dev/null +++ b/purchase_global_discount/wizard/purchase_order_discount_views.xml @@ -0,0 +1,22 @@ + + + + + purchase.order.discount.form + purchase.order.discount + +
+ + + + + + + +
+
+
+