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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..d55a8430e6c
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,34 @@
+
+
+
+ Estate Property Tag
+ estate.property.tag
+ list,form
+
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
+
+ estate.property.tag.form
+ estate.property.tag
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..3c9f7e92215
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,58 @@
+
+
+
+ Estate Property Type
+ estate.property.type
+ list,form
+
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
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 :
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+