diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py index b0f26a9a602..c3d410ea160 100644 --- a/awesome_dashboard/__init__.py +++ b/awesome_dashboard/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- +from . import models from . import controllers diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..35cb019c772 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/AwesomeDashboard', - '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/AwesomeDashboard", + "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/**/*", + ], + "awesome_dashboard.dashboard": [ + "awesome_dashboard/static/src/dashboard/**/*", ], }, - 'license': 'AGPL-3' + "license": "AGPL-3", } diff --git a/awesome_dashboard/i18n/hi_IN.po b/awesome_dashboard/i18n/hi_IN.po new file mode 100644 index 00000000000..5fd2048af8b --- /dev/null +++ b/awesome_dashboard/i18n/hi_IN.po @@ -0,0 +1,70 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * awesome_dashboard +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-06-24 12:17+0000\n" +"PO-Revision-Date: 2025-06-24 12:17+0000\n" +"Last-Translator: \n" +"Language-Team: Hindi\n" +"Language: hi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.xml:0 +msgid "Apply" +msgstr "लागू करें" + +#. module: awesome_dashboard +#: model:ir.ui.menu,name:awesome_dashboard.menu_root +msgid "Awesome Dashboard" +msgstr "ऑसम डैशबोर्ड" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.xml:0 +msgid "Cancel" +msgstr "रद्द करें" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0 +msgid "Customers" +msgstr "ग्राहक" + +#. module: awesome_dashboard +#: model:ir.actions.client,name:awesome_dashboard.dashboard +#: model:ir.ui.menu,name:awesome_dashboard.dashboard_menu +msgid "Dashboard" +msgstr "डैशबोर्ड" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0 +msgid "Dashboard Settings" +msgstr "डैशबोर्ड सेटिंग्स" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0 +msgid "Leads" +msgstr "लीड्स" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml:0 +msgid "Loading chart..." +msgstr "चार्ट लोड हो रहा है..." + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.xml:0 +msgid "Select items to display on your dashboard:" +msgstr "अपने डैशबोर्ड पर प्रदर्शित करने के लिए आइटम चुनें:" \ No newline at end of file diff --git a/awesome_dashboard/models/__init__.py b/awesome_dashboard/models/__init__.py new file mode 100644 index 00000000000..8835165330f --- /dev/null +++ b/awesome_dashboard/models/__init__.py @@ -0,0 +1 @@ +from . import res_users diff --git a/awesome_dashboard/models/res_users.py b/awesome_dashboard/models/res_users.py new file mode 100644 index 00000000000..f45a90576db --- /dev/null +++ b/awesome_dashboard/models/res_users.py @@ -0,0 +1,21 @@ +from odoo import fields, models, api + + +class ResUsers(models.Model): + _inherit = "res.users" + + dashboard_disabled_items = fields.Char(default="") + + @api.model + def set_dashboard_settings(self, disable_item_ids): + if self.env.user: + items = ",".join(map(str, disable_item_ids)) + self.env.user.sudo().write({"dashboard_disabled_items": items}) + return True + return False + + @api.model + def get_dashboard_settings(self): + if self.env.user and self.env.user.dashboard_disabled_items: + return self.env.user.dashboard_disabled_items.split(",") + return [] diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 637fa4bb972..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @odoo-module **/ - -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/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..21ba27fdfeb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,81 @@ +import { Component, useState, onWillStart } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboardItem/dashboard_item"; +import { Piechart } from "./pieChart/pieChart"; +import { DashboardSettings } from "./dashboardSetting/dashboard_setting"; +import { rpc } from "@web/core/network/rpc"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, Piechart }; + + setup() { + const dashboardItemsRegistryData = registry.category("awesome_dashboard"); + this.action = useService("action"); + this.items = dashboardItemsRegistryData.getAll(); + this.statisticServices = useService("awesome_dashboard.statistics"); + this.state = useState({ statistic: this.statisticServices.statistic }); + this.dialogService = useService("dialog"); + this.displayData = useState({ + disabledItems: [], + isLoading: true, + }); + onWillStart(async () => { + try { + // this.displayData.isLoading = true; + const fetchedDisabledItems = await rpc( + "/web/dataset/call_kw/res.users/get_dashboard_settings", + { + model: "res.users", + method: "get_dashboard_settings", + args: [], + kwargs: {}, + } + ); + this.displayData.disabledItems = fetchedDisabledItems; + } catch (error) { + console.error( + "Error loading initial dashboard settings from server:", + error + ); + this.displayData.disabledItems = []; + } finally { + this.displayData.isLoading = false; + } + }); + } + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + async openLeads(activity) { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Journal Entry", + target: "current", + res_id: activity.res_id, + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + }); + } + + updateConfiguration(newUncheckedItems) { + this.displayData.disabledItems.length = 0; + this.displayData.disabledItems.push(...newUncheckedItems); + } + + openSetting() { + this.dialogService.add(DashboardSettings, { + items: this.items, + initialUncheckedItems: this.state.uncheckedItems, + updateConfiguration: this.updateConfiguration.bind(this), + }); + } +} + +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..ce3f5a1db7d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,42 @@ + .o_dashboard{ + background-color: burlywood; + } + + .o_dashboard_stat_block { + text-align: center; + margin-bottom: 24px; +} + +.o_dashboard_stat_label { + font-weight: normal; + margin-bottom: 10px; + display: block; +} + +.o_dashboard_stat_value { + font-size: 48px; + color: #228B22; + font-weight: bold; +} + +.o_dashboard_item { + background: #fff; + border-radius: 0.75rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.07); + padding: 1rem; + margin: 1rem; + display: inline-flex; + justify-content: center; + vertical-align: top; + min-height: 3rem; +} + +@media (max-width: 426px) { + .o_dashboard_item { + width: 100% !important; + display: flex; + margin-left: 0.5rem; + margin-right: 0.5rem; + box-sizing: border-box; + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..a2f20e7b399 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,25 @@ + + + + + + + Customers + Leads + + + + + + + + + + + + + + + + + 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..69786baa6a2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + slots: { + type: Object, + shape: { default: true }, + }, + size: { + type: Number, + default: 1, + 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..ed25f824fcb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboard_item.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.js b/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.js new file mode 100644 index 00000000000..c0170405e27 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.js @@ -0,0 +1,51 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { rpc } from "@web/core/network/rpc"; + +export class DashboardSettings extends Component { + static template = "awesome_dashboard.DashboardSettings"; + static components = { Dialog }; + + static props = { + close: { type: Function }, + }; + + setup() { + const items = this.props.items || {}; + const initialUncheckedItems = this.props.initialUncheckedItems || []; + + this.dialogDisplayItems = useState( + Object.values(items).map((item) => ({ + ...item, + checked: !initialUncheckedItems.includes(item.id), + })) + ); + } + + onChange(checked, itemInDialog) { + const targetItem = this.dialogDisplayItems.find( + (i) => i.id === itemInDialog.id + ); + if (targetItem) { + targetItem.checked = checked; + } + } + + async confirmChanges() { + const newUncheckedItems = this.dialogDisplayItems + .filter((item) => !item.checked) + .map((item) => item.id); + + await rpc("/web/dataset/call_kw/res.users/set_dashboard_settings", { + model: "res.users", + method: "set_dashboard_settings", + args: [newUncheckedItems], + kwargs: {}, + }); + + if (this.props.updateConfiguration) { + this.props.updateConfiguration(newUncheckedItems); + } + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.xml b/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.xml new file mode 100644 index 00000000000..ff76f84df48 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboardSetting/dashboard_setting.xml @@ -0,0 +1,26 @@ + + + + + + Select items to display on your dashboard: + + + + + + + + + Apply + Cancel + + + + 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..c669074d80b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,70 @@ +import { registry } from "@web/core/registry"; +import { Numbercard } from "./numbercard/numbercard"; +import { Piechartcard } from "./piechartcard/piechartcard"; + +const items = [ + { + id: "nb_new_orders", + description: "The number of new orders, this month", + Component: Numbercard, + size: 1, + props: (data) => ({ + title: "New Orders This Month:", + value: data.data.nb_new_orders, + }), + }, + { + id: "total_amount", + description: "The total amount of orders, this month", + Component: Numbercard, + size: 2, + props: (data) => ({ + title: "Total Amount This Month:", + value: data.data.total_amount, + }), + }, + { + id: "average_quantity", + description: "The average number of t-shirts by order", + Component: Numbercard, + size: 1, + props: (data) => ({ + title: "Avg. T-Shirts per Order:", + value: data.data.average_quantity, + }), + }, + { + id: "nb_cancelled_orders", + description: "The number of cancelled orders, this month", + Component: Numbercard, + size: 1, + props: (data) => ({ + title: "Cancelled Orders:", + value: data.data.nb_cancelled_orders, + }), + }, + { + id: "average_time", + description: + "The average time (in hours) elapsed between the moment an order is created, and the moment is it sent", + Component: Numbercard, + size: 1, + props: (data) => ({ + title: "Avg. Time New → Sent/Cancelled:", + value: data.data.average_time, + }), + }, + { + id: "orders_by_size", + description: "Number of shirts ordered based on size", + Component: Piechartcard, + size: 3, + props: (data) => ({ + title: "Shirt orders by size:", + value: data.data.orders_by_size, + }), + }, +]; +items.forEach((item) => { + registry.category("awesome_dashboard").add(item.id, item); +}); diff --git a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js new file mode 100644 index 00000000000..b4bfc2f54c9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js @@ -0,0 +1,8 @@ +import { Component } from "@odoo/owl"; +export class Numbercard extends Component { + static template = "awesome_dashboard.Numbercard"; + static props = { + title: { type: String }, + value: { type: [String,Number] }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml new file mode 100644 index 00000000000..cf51eaab6aa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + 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..465be4451b1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pieChart/pieChart.js @@ -0,0 +1,75 @@ +import { + Component, + onWillStart, + useRef, + onMounted, + useEffect, +} from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; +import { useService } from "@web/core/utils/hooks"; + +export class Piechart extends Component { + static template = "awesome_dashboard.Piechart"; + static props = { + data: { type: Object }, + }; + setup() { + this.chart = null; + this.canvasRef = useRef("canvas"); + this.action = useService("action"); + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + const chartData = { + labels: Object.keys(this.props.data), + datasets: [ + { + data: Object.values(this.props.data), + }, + ], + }; + + onMounted(() => { + this.chart = new Chart(this.canvasRef.el, { + type: "pie", + data: chartData, + options: { + onClick: (event, elements) => { + if (elements.length > 0) { + const idx = elements[0].index; + const size = chartData.labels[idx]; + this.getOrdersBySize(size); + } + }, + }, + }); + }); + useEffect( + () => { + if (this.chart && this.props.data) { + this.chart.data.labels = Object.keys(this.props.data); + this.chart.data.datasets[0].data = Object.values(this.props.data); + this.chart.update(); + } + }, + () => [this.props.data] + ); + } + getOrdersBySize(size) { + this.action.doAction({ + type: "ir.actions.act_window", + name: `Orders with Size ${size.toUpperCase()}`, + res_model: "sale.order", + views: [[false, "list"]], + domain: [ + [ + "order_line.product_template_attribute_value_ids.display_name", + "ilike", + size, + ], + ], + }); + } +} 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..6b79e8f4832 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pieChart/pieChart.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.js b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.js new file mode 100644 index 00000000000..42d0899cde3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.js @@ -0,0 +1,11 @@ +import { Component } from "@odoo/owl"; +import { Piechart } from "../pieChart/pieChart"; + +export class Piechartcard extends Component { + static template = "awesome_dashboard.Piechartcard"; + static components = { Piechart }; + static props = { + title: { type: String }, + value: { type: Object }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml new file mode 100644 index 00000000000..8100ca003c8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + Loading chart... + + + + + diff --git a/awesome_dashboard/static/src/dashboard/statistics.js b/awesome_dashboard/static/src/dashboard/statistics.js new file mode 100644 index 00000000000..e78e3ff276e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics.js @@ -0,0 +1,30 @@ +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc" +const statisticsService = { + dependencies: [], + start() { + const statistic = reactive({ data: null, loading: true, error: null }); + + async function fetchData() { + statistic.loading = true; + statistic.error = null; + try { + statistic.data = await rpc("/awesome_dashboard/statistics"); + } catch (error) { + statistic.error; + } finally { + statistic.loading = false; + } + } + + fetchData(); + + setInterval(fetchData, 5000); + return { statistic }; + }, +}; + +registry + .category("services") + .add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..4b03d25d652 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class DashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry + .category("actions") + .add("awesome_dashboard.dashboard", DashboardLoader); diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..ea558342369 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,17 @@ +import { Component, useState } from "@odoo/owl"; +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: String, + slots: { + type: Object, + shape: { default: true }, + }, + }; + setup() { + this.state = useState({ isToggled: true }); + } + toggle() { + this.state.isToggled = !this.state.isToggled; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..aebf5e14b0f --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,16 @@ + + + + + + + + Toggle Button + + + + + + + + diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..17f693d139e --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,18 @@ +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() { + this.state.value++; + if (this.props.onChange) { + this.props.onChange(this.state.value); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.scss b/awesome_owl/static/src/counter/counter.scss new file mode 100644 index 00000000000..954fd0b5bbf --- /dev/null +++ b/awesome_owl/static/src/counter/counter.scss @@ -0,0 +1,21 @@ +.counter-container { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin: 1rem 1rem; +} + +.counter-btn { + background-color: #0d6efd; + color: #fff; + border: none; + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 1rem; + cursor: pointer; + + &:hover { + background-color: #0b5ed7; + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..5816629a650 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,10 @@ + + + + + + Counter: + Increment + + + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..03dcf3fe074 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,19 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todoList/todoList"; export class Playground extends Component { - static template = "awesome_owl.playground"; + static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + + setup() { + this.sum = useState({ value: 0 }); + } + + sumofCounter() { + this.sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..85d62633652 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,22 @@ - + - hello world + + + This is Card 1 + + + + + + + The sum of two counter : + + + - + diff --git a/awesome_owl/static/src/todoList/todoItem.js b/awesome_owl/static/src/todoList/todoItem.js new file mode 100644 index 00000000000..92c8e48ce4d --- /dev/null +++ b/awesome_owl/static/src/todoList/todoItem.js @@ -0,0 +1,28 @@ +import { Component } from "@odoo/owl"; +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { + id: { type: Number }, + description: { type: String }, + isCompleted: { type: Boolean }, + }, + }, + toggleState: { type: Function }, + removeTodo: { type: Function }, + }; + + onChange() { + if (this.props.toggleState) { + this.props.toggleState(this.props.todo.id); + } + } + + onremoveTodo() { + if (this.props.removeTodo) { + this.props.removeTodo(this.props.todo.id); + } + } +} diff --git a/awesome_owl/static/src/todoList/todoItem.xml b/awesome_owl/static/src/todoList/todoItem.xml new file mode 100644 index 00000000000..942ef6f4b3e --- /dev/null +++ b/awesome_owl/static/src/todoList/todoItem.xml @@ -0,0 +1,11 @@ + + + + + + : + + + + + diff --git a/awesome_owl/static/src/todoList/todoList.js b/awesome_owl/static/src/todoList/todoList.js new file mode 100644 index 00000000000..14a18418e64 --- /dev/null +++ b/awesome_owl/static/src/todoList/todoList.js @@ -0,0 +1,42 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todoItem"; +import { useAutofocus } from "../utils/utils"; +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.todos = useState([ + { id: 1, description: "buy milk", isCompleted: false }, + { id: 2, description: "write tutorial", isCompleted: true }, + ]); + this.todoCounter = useState({ value: 3 }); + this.inputRef = useAutofocus("input"); + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value != "") { + this.todos.push({ + id: this.todoCounter.value, + description: ev.target.value, + isCompleted: false, + }); + this.todoCounter.value++; + ev.target.value = ""; + } + } + + toggleItem(id) { + const todoTask = this.todos.find((todo) => todo.id === id); + if (todoTask) { + todoTask.isCompleted = !todoTask.isCompleted; + } + } + + deleteTodo(id) { + const index = this.todos.findIndex((elem) => elem.id === id); + if (index >= 0) { + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todoList/todoList.xml b/awesome_owl/static/src/todoList/todoList.xml new file mode 100644 index 00000000000..d41eb656ad3 --- /dev/null +++ b/awesome_owl/static/src/todoList/todoList.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/awesome_owl/static/src/utils/utils.js b/awesome_owl/static/src/utils/utils.js new file mode 100644 index 00000000000..b54fe04517d --- /dev/null +++ b/awesome_owl/static/src/utils/utils.js @@ -0,0 +1,10 @@ +import { onMounted, useRef } from "@odoo/owl"; +export function useAutofocus(input) { + const inputRef = useRef(input); + onMounted(() => { + if (inputRef.el) { + inputRef.el.focus(); + } + }); + return inputRef; +} 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..8fe5f9fb54c --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,30 @@ +{ + "name": "Estate", + "version": "1.2", + "depends": ["base", "mail"], + "data": [ + "security/estate_security.xml", + "security/ir.model.access.csv", + "views/estate_business_trip_views.xml", + "views/res_users_views.xml", + "views/estate_property_offer_count_views.xml", + "report/estate_property_reports.xml", + "report/estate_property_templates.xml", + "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_menus.xml", + ], + "demo": [ + "demo/estate.property.type.csv", + "demo/estate_property.xml", + "demo/estate_property_offer.xml", + "demo/estate_property_type.xml", + "demo/estate_mail_message_subtype.xml", + ], + "test": ["tests/test_estate_property.py"], + "category": "Real Estate/Brokerage", + "appication": True, + "sequence": 1, + "license": "LGPL-3", +} diff --git a/estate/demo/estate.property.type.csv b/estate/demo/estate.property.type.csv new file mode 100644 index 00000000000..23b45d983a3 --- /dev/null +++ b/estate/demo/estate.property.type.csv @@ -0,0 +1,5 @@ +id,name +estate_property_type_residential,Residential +estate_property_type_commercial,Commercial +estate_property_type_industrial,Industrial +estate_property_type_land,Land diff --git a/estate/demo/estate_mail_message_subtype.xml b/estate/demo/estate_mail_message_subtype.xml new file mode 100644 index 00000000000..d0bc2976eb5 --- /dev/null +++ b/estate/demo/estate_mail_message_subtype.xml @@ -0,0 +1,9 @@ + + + + Trip confirmed + business.trip + + Hello, User + + diff --git a/estate/demo/estate_property.xml b/estate/demo/estate_property.xml new file mode 100644 index 00000000000..486d3d87e9a --- /dev/null +++ b/estate/demo/estate_property.xml @@ -0,0 +1,56 @@ + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 6 + 100 + 4 + True + True + 100000 + south + + + Trailer home + cancelled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 1 + 10 + 4 + False + + + + Lake House + new + Beautiful house by the lake + 67890 + 1980-05-15 + 250000 + 3 + 80 + 2 + True + + + diff --git a/estate/demo/estate_property_offer.xml b/estate/demo/estate_property_offer.xml new file mode 100644 index 00000000000..dc2439f24e6 --- /dev/null +++ b/estate/demo/estate_property_offer.xml @@ -0,0 +1,34 @@ + + + + + + 100000000 + 14 + + + + + 1500000000 + 14 + + + + + 1500000001 + 14 + + + + + + + + + + + + + + + diff --git a/estate/demo/estate_property_type.xml b/estate/demo/estate_property_type.xml new file mode 100644 index 00000000000..b75600bb38d --- /dev/null +++ b/estate/demo/estate_property_type.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f268541e8c6 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,6 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import res_users +from . import estate_business_trip diff --git a/estate/models/estate_business_trip.py b/estate/models/estate_business_trip.py new file mode 100644 index 00000000000..fa2a2bd22fc --- /dev/null +++ b/estate/models/estate_business_trip.py @@ -0,0 +1,23 @@ +from odoo import models, fields + + +class EstateBusinessTrip(models.Model): + _name = "estate.business.trip" + _inherit = ["mail.thread"] + _description = "Estate Business Trip" + + name = fields.Char(tracking=True) + partner_id = fields.Many2one("res.partner", "Responsible", tracking=True) + guest_ids = fields.Many2many( + "res.partner", + "Participants", + ) + state = fields.Selection( + [("draft", "New"), ("confirmed", "Confirmed")], tracking=True + ) + + def _track_subtype(self, init_values): + self.ensure_one() + if "state" in init_values and self.state == "confirmed": + return self.env.ref("estate.mt_state_change") + return super()._track_subtype(init_values) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..e83c4b3199a --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,134 @@ +from odoo import fields, models, api +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Propety" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + string="Available Date", + default=lambda self: (fields.Date.today() + relativedelta(months=3)), + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True) + 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="Garden Orientation", + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + default="north", + ) + active = fields.Boolean() + 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") + buyer_id = fields.Many2one("res.partner", string="Buyer") + salesperson_id = fields.Many2one( + "res.users", string="Salesperson", default=lambda self: self.env.user + ) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many( + comodel_name="estate.property.offer", + inverse_name="property_id", + string="Offers", + ) + + total_area = fields.Float(string="Total Area", compute="_compute_total_area") + + best_offer = fields.Float( + string="Best Offer", + compute="_compute_best_offer", + store=True, + ) + + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.company, + ) + + _sql_constraints = [ + ( + "check_expected_price_field", + "CHECK(expected_price > 0)", + "Expected price must be positive!", + ), + ( + "check_selling_price", + "CHECK(selling_price >= 0)", + "Selling price must be positive!", + ), + ] + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for property in self: + property.total_area = property.living_area + (property.garden_area or 0) + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for property in self: + best_offer = max(property.offer_ids.mapped("price"), default=0) + property.best_offer = best_offer + + @api.onchange("garden") + def _onchange_garden(self): + if not self.garden: + self.garden_area = 0 + self.garden_orientation = False + else: + self.garden_area = 10 + self.garden_orientation = "north" + + def action_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError("A cancelled property cannot be set as sold") + accepted_offers = record.offer_ids.filtered( + lambda o: o.status == "accepted" + ) + if not accepted_offers: + raise UserError( + "Property must be in 'Accepted' state before it can be sold." + ) + record.state = "sold" + return True + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("A a sold property cannot be cancelled.") + record.state = "cancelled" + return True + + @api.ondelete(at_uninstall=False) + def _unlink_check(self): + for record in self: + if record.state not in ["new", "cancelled"]: + raise UserError("You can delete only a new or cancelled property.") + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..95d8958798e --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,149 @@ +from odoo import fields, models, api +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _order = "price desc" + + price = fields.Float(required=True) + status = fields.Selection( + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused"), + ("cancelled", "Cancelled"), + ], + string="Status", + ) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + required=True, + ) + validity = fields.Integer( + string="Validity (days)", + default=7, + ) + + # Stat Button + property_type_id = fields.Many2one( + related="property_id.property_type_id", + string="Property Type", + store=True, + ) + + # SQL Constraints + sql_constraints = [ + ("check_offer_price", "CHECK(price>=0)", "The offer price must be positive.") + ] + + @api.depends("validity") + def _compute_date_deadline(self): + for date in self: + create_date = date.create_date or fields.Datetime.now() + date.deadline = ( + fields.Datetime.from_string(create_date) + + relativedelta(days=date.validity) + ).date() + + def _inverse_date_deadline(self): + for date in self: + create_date = date.create_date or fields.Datetime.now() + if date.deadline: + delta = ( + fields.Date.from_string(date.deadline) + - fields.Datetime.from_string(create_date).date() + ) + date.validity = delta.days + + def action_accept(self): + for record in self: + if ( + float_compare( + record.property_id.expected_price * 0.9, + record.price, + precision_digits=2, + ) + > 0 + ): + raise ValidationError( + "The offer price must be at least 90% of the expected price." + ) + accepted_offer = self.search( + [ + ("property_id", "=", record.property_id.id), + ("status", "=", "accepted"), + ], + limit=1, + ) + if accepted_offer: + raise UserError("The offer for this property is already accepted.") + other_offers = self.search( + [ + ("property_id", "=", record.property_id.id), + ("id", "!=", record.id), + ("status", "!=", "refused"), + ] + ) + other_offers.write({"status": "refused"}) + record.property_id.write( + { + "selling_price": record.price, + "buyer_id": record.partner_id, + "state": "offer_accepted", + } + ) + record.write({"status": "accepted"}) + return True + + def action_refused(self): + for record in self: + if record.status == "accepted": + record.property_id.write( + {"state": "offer_received", "buyer_id": False, "selling_price": 0.0} + ) + record.write({"status": "refused"}) + else: + record.status = "refused" + return True + + @api.constrains("price") + def _check_selling_price(self): + for record in self: + if ( + float_compare( + record.price, + 0.9 * record.property_id.expected_price, + precision_digits=2, + ) + < 0 + ): + raise ValidationError( + "The selling price must be at least 90% of the expected price." + ) + return True + + # CRUD Methods + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + property_id = vals.get("property_id") + price = vals.get("price", 0.0) + if property_id: + property_obj = self.env["estate.property"].browse(property_id) + best_price = property_obj.best_offer or 0.0 + if price < best_price: + raise UserError( + "Offer price must be greater than or equal to the best offer price." + ) + records = super().create(vals_list) + for record in records: + if record.partner_id: + record.property_id.state = "offer_received" + return records diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..586f14d0aa0 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer(string="Color") + + _sql_constraints = [ + ("unique_name_field", "UNIQUE(name)", "The name 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..c5e9479d554 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,32 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _order = "sequence,name" + + name = fields.Char(required=True) + sequence = fields.Integer() + property_ids = fields.One2many( + "estate.property", "property_type_id", string="Properties" + ) + offer_ids = fields.One2many( + comodel_name="estate.property.offer", + inverse_name="property_type_id", + string="Offers", + ) + + offer_count = fields.Integer( + string="Offer Count", + compute="_compute_offer_count", + ) + + _sql_constraints = [ + ("unique_name_field", "UNIQUE(name)", "The name must be unique."), + ] + + # compute method : offer count + def _compute_offer_count(self): + for property_type in self: + property_type.offer_count = len(property_type.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..14d8f3039b4 --- /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( + comodel_name="estate.property", + inverse_name="salesperson_id", + string="Properties", + ) diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..2b866841c8d --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,46 @@ + + + + + European A4 low margin + + A4 + 0 + 0 + Portrait + 5 + 5 + 5 + 5 + + 0 + 80 + + + + + Property Offers + estate.property + qweb-pdf + estate.report_property_template_offers + estate.report_property_template_offers + 'Property Offers - %s' % (object.name) + + report + + + + + + Salesman Properties + res.users + qweb-pdf + estate.report_salesman_template_properties + estate.report_salesman_template_properties + 'Salesman Properties - %s' % (object.name) + + report + + + + diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml new file mode 100644 index 00000000000..6215f20a665 --- /dev/null +++ b/estate/report/estate_property_templates.xml @@ -0,0 +1,120 @@ + + + + + + + + Price + Partner + Validity(days) + Deadline + State + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Salesman: + + + + Expected Price: + + + + Status: + + + + + + No Offer have been made yet!! + + + + + + + + + + + + + + + + Properties: + + + + + Property Name: + + + + Expected Price: + + + + Status: + + + + + + + + No Properties have been assigned + yet!! + + + + + + diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..977400c921f --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,66 @@ + + + + + Real Estate Brokerage related permissions + 1 + + + + Agent + + + + + Manager + + + + + + Agents: see or modify properties with no salesperson or themselves as + salesperson + + + + + + + [ + '|', + '|', ('salesperson_id', '=', user.id), + ('salesperson_id', '=', False), + ('write_uid', '=', user.id) + ] + + + + A description of the rule's on offer + + + + + + + [ + '|', + '|', ('property_id.salesperson_id', '=', user.id), + ('property_id.salesperson_id', '=', False), + ('write_uid', '=', user.id) + ] + + + + + Estate: Agents see properties of their company + + + + + + + [('company_id', '=', user.company_id.id)] + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..cf2f6d363de --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,16 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 +access_res_users,access_res_users,model_res_users,base.group_user,1,1,1,1 +access_property_manager,access_estate_property_manager,model_estate_property,estate_group_manager,1,1,1,0 +access_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate_group_manager,1,1,1,1 +access_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate_group_manager,1,1,1,1 +access_property_offer_manager,access_estate_property_offer_manager,model_estate_property_offer,estate_group_manager,1,1,1,1 +access_res_users_manager,access_res_users_manager,model_res_users,estate_group_manager,1,1,1,1 +access_property_agent,access_estate_property_agent,model_estate_property,estate_group_user,1,1,1,0 +access_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0 +access_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate_group_user,1,1,0,0 +access_property_tag_agent,access_estate_property_tag_agent,model_estate_property_tag,estate_group_user,1,1,0,0 +access_business_trip_user,access_estate_business_trip_user,model_estate_business_trip,base.group_user,1,1,1,1 diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..576617cccff --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..b15a8546476 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,88 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import Form + + +class EstateTestCase(TransactionCase): + @classmethod + def setUpClass(cls): + return super().setUpClass() + + def test_forbidden_action_on_sold_property(self): + property = self.env["estate.property"].create( + { + "name": "City Apartment", + "expected_price": 17500, + } + ) + + self.env["estate.property.offer"].create( + { + "price": 150000.00, + "partner_id": self.env.ref("base.res_partner_1").id, + "deadline": "2025-09-14", + "property_id": property.id, + "status": "accepted", + } + ) + + property.action_sold() + + with self.assertRaises( + UserError, msg="Cannot create an offer for a sold property" + ): + self.env["estate.property.offer"].create( + { + "price": 150000000.00, + "partner_id": self.env.ref("base.res_partner_1").id, + "deadline": "2025-09-14", + "property_id": property.id, + } + ) + + +def test_no_accept_offer(self): + property = self.env["estate.property"].create( + { + "name": "City Apartment2", + "expected_price": 16500, + } + ) + + self.env["estate.property.offer"].create( + { + "price": 150000.00, + "partner_id": self.env.ref("base.res_partner_1").id, + "deadline": "2025-09-14", + "property_id": property.id, + } + ) + + with self.assertRaises(UserError, msg="Can not sold without accept offer"): + property.action_sold() + + +def test_reset_garden_area_and_orientation(self): + property = self.env["estate.property"].create( + { + "name": "TEST PROPERTY", + "expected_price": "1334", + "garden": True, + "garden_area": 50, + "garden_orientation": "north", + } + ) + + with Form(property) as form: + form.garden = False + form.save() + + self.assertFalse(property.garden, "The garden checkbox is not selected..") + self.assertFalse( + property.garden_area, + "Garden area should be reset when garden checkbox is not selected.", + ) + self.assertFalse( + property.garden_orientation, + "Orientation should be reset when garden checkbox is not selected.", + ) diff --git a/estate/views/estate_business_trip_views.xml b/estate/views/estate_business_trip_views.xml new file mode 100644 index 00000000000..0a46fd25cf6 --- /dev/null +++ b/estate/views/estate_business_trip_views.xml @@ -0,0 +1,28 @@ + + + + + Communications + estate.business.trip + form + + + + business.trip.form + estate.business.trip + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..8713083bfa3 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_count_views.xml b/estate/views/estate_property_offer_count_views.xml new file mode 100644 index 00000000000..59b6d3fc939 --- /dev/null +++ b/estate/views/estate_property_offer_count_views.xml @@ -0,0 +1,22 @@ + + + + Property Offers + estate.property.offer + list + [('property_id.property_type_id', '=', active_id)] + + + + estate.property.offer.list + 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..3292938d0a4 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,20 @@ + + + + Property Tag + estate.property.tag + list,form + + + + + estate.property.tag.list + 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..16bee696c9a --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,59 @@ + + + + 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..1c402c03bcd --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,185 @@ + + + + Manage Property + estate.property + list,form,kanban + {'search_default_available_properties':1} + + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + estate.property.kanban.view + estate.property + + + + + + + + + + + + + + Expected Price: + + + Best Price: + + + Selling Price: + + + + + + + + + + + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..619ea2df62c --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,17 @@ + + + + + res.users.view.form + 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..10e3c695ced --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "Estate Account", + "version": "1.2", + "depends": ["base", "estate", "account"], + "data": [ + "security/ir.model.access.csv", + "report/estate_account_property_templates.xml", + ], + "appication": True, + "sequence": 1, + "license": "LGPL-3", +} 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..f24e919d80a --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,34 @@ +from odoo import models +from odoo import Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + for property in self: + property.check_access_rights("write") + property.check_access_rule("write") + self.env["account.move"].sudo().create( + { + "partner_id": property.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create( + { + "name": property.name, + "quantity": 1, + "price_unit": property.selling_price * 0.06, + }, + ), + Command.create( + { + "name": "Administrative fees", + "quantity": 1.0, + "price_unit": 100.0, + } + ), + ], + } + ) + return super().action_sold() diff --git a/estate_account/report/estate_account_property_templates.xml b/estate_account/report/estate_account_property_templates.xml new file mode 100644 index 00000000000..127e909eaa5 --- /dev/null +++ b/estate_account/report/estate_account_property_templates.xml @@ -0,0 +1,12 @@ + + + + + + + !!!Invoiced has already been created!!! + + + + diff --git a/estate_account/security/ir.model.access.csv b/estate_account/security/ir.model.access.csv new file mode 100644 index 00000000000..6531f331708 --- /dev/null +++ b/estate_account/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_estate_property,estate.property,model_estate_property,base.group_user,1,1,1,1
Select items to display on your dashboard:
+ +
Counter:
This is Card 1