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..c5b63b98530 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,9 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', + ], }, '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..768b0969ec9 --- /dev/null +++ b/awesome_dashboard/i18n/hi_IN.po @@ -0,0 +1,80 @@ +# 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:25+0000\n" +"PO-Revision-Date: 2025-06-24 12:25+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. 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/dashboard_setting/dashboard_setting.xml:0 +msgid "Cancel" +msgstr "रद्द करना" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0 +msgid "Customer" +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 +#: model:ir.model.fields,field_description:awesome_dashboard.field_res_users__dashboard_disabled_items +msgid "Dashboard Disabled Items" +msgstr "डैशबोर्ड अक्षम आइटम" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0 +#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_setting/dashboard_setting.xml:0 +msgid "Done" +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/dashboard_setting/dashboard_setting.xml:0 +msgid "Select items to display on your dashboard:" +msgstr "अपने डैशबोर्ड पर प्रदर्शित करने के लिए आइटम चुनें:" + +#. module: awesome_dashboard +#: model:ir.model,name:awesome_dashboard.model_res_users +msgid "User" +msgstr "उपयोगकर्ता" + +#. module: awesome_dashboard +#. odoo-javascript +#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0 +msgid "Which cards do you wish to see?" +msgstr "आप कौन से कार्ड देखना चाहते हैं?" 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..3d33ced63d3 --- /dev/null +++ b/awesome_dashboard/models/res_users.py @@ -0,0 +1,21 @@ +from odoo import models, fields, api + + +class ResUsers(models.Model): + _inherit = 'res.users' + + dashboard_disabled_items = fields.Char(string='Dashboard Disabled Items') + + @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..e0135dbdea5 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,108 @@ +/** @odoo-module **/ + +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 { PieChart } from "./piechart/piechart"; +import { DashboardItem } from "./dashboarditem/dashboarditem"; +import { PieChartCard } from "./piechartcard/piechartcard"; +import { NumberCard } from "./numbercard/numbercard"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; +import { rpc } from "@web/core/network/rpc"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem ,PieChart,PieChartCard,NumberCard}; + + setup() { + const dashboardItemsRegistry = registry.category("awesome_dashboard"); + this.items = dashboardItemsRegistry.getAll(); + this.action = useService("action"); + this.statistics = useService("awesome_dashboard.statistics"); + this.statistics = useState(this.statistics.stats); + this.dialog = useService("dialog"); + this.dialog_state = useState({ + disabledItems: [], + isLoading: true, + }); + onWillStart(async () => { + try { + const fetchedDisabledItems = await rpc("/web/dataset/call_kw/res.users/get_dashboard_settings", { + model: 'res.users', + method: 'get_dashboard_settings', + args: [], + kwargs: {}, + }); + this.dialog_state.disabledItems = fetchedDisabledItems; + } catch (error) { + console.error("Error loading initial dashboard settings from server:", error); + this.dialog_state.disabledItems = []; + } finally { + this.dialog_state.isLoading = false; + } + }); + } + + openCustomers(){ + this.action.doAction("base.action_partner_form"); + } + async openLeads(){ + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + target: "current", + }); + } + updateConfiguration(newDisabledItems) { + this.dialog_state.disabledItems = newDisabledItems; + } + + openSetting() { + this.action.doAction("base_setup.action_general_configuration"); + } + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.dialog_state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }) + } +} + +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), + } + })) + } + + onDone() { + 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.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..57af8b92cf9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,48 @@ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+
+
+ + + Which cards do you wish to see? + + + + + + + + + + +
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..0fae5b95d56 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,72 @@ +/** @odoo-module **/ + +import { PieChartCard } from "./piechartcard/piechartcard"; +import { NumberCard } from "./numbercard/numbercard"; +import { registry } from "@web/core/registry"; + +export const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt by order this month", + Component: NumberCard, + size: 1.5, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity, + }), + }, + { + id: "average_time", + description: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + Component: NumberCard, + size: 1.2, + props: (data) => ({ + title: "Avg. time: 'sent'/'cancelled'", + value: data.average_time, + }), + }, + { + id: "nb_new_orders", + description: "Number of new orders this month", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "New Orders This Month", + value: data.nb_new_orders, + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders this month", + Component: NumberCard, + size: 1.1, + props: (data) => ({ + title: "Cancelled Orders This Month", + value: data.nb_cancelled_orders, + }), + }, + { + id: "total_amount", + description: "Total amount of new orders this month", + Component: NumberCard, + size: 1.1, + props: (data) => ({ + title: "Total New Order Amount ", + value: data.total_amount, + }), + }, + { + id: "sales_distribution", + description: "Sales Distribution by T-Shirt Size", + Component: PieChartCard, + size: 1.5, + props: (data) => ({ + title: "Sales Distribution by Size", + value: data.orders_by_size, + }), + }, +]; + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_setting/dashboard_setting.js b/awesome_dashboard/static/src/dashboard/dashboard_setting/dashboard_setting.js new file mode 100644 index 00000000000..0f2a2fd9abd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_setting/dashboard_setting.js @@ -0,0 +1,42 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { rpc } from "@web/core/network/rpc"; + +export class DashboardSetting extends Component { + static template = "awesome_dashboard.setting"; + static components = { Dialog }; + static props = { + close: { type: Function }, + }; + setup() { + const items = this.props.items || {}; + const initialDisabledItems = this.props.initialDisabledItems || []; + this.settingDisplayItems = Object.values(items).map((item) => ({ + ...item, + checked: !initialDisabledItems.includes(item.id), + })); + } + + onChange(checked, itemInDialog) { + const targetItem = this.settingDisplayItems.find(i => i.id === itemInDialog.id); + if (targetItem) { + targetItem.checked = checked; + } + } + + async confirmDone() { + const newDisableItems = this.settingDisplayItems.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: [newDisableItems], + kwargs: {}, + }); + if (this.props.updateSettings) { + this.props.updateSettings(newDisableItems); + } + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_setting/dashboard_setting.xml b/awesome_dashboard/static/src/dashboard/dashboard_setting/dashboard_setting.xml new file mode 100644 index 00000000000..d9f5d9da26c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_setting/dashboard_setting.xml @@ -0,0 +1,26 @@ + + + + +
+

Select items to display on your dashboard:

+
+ + +
+
+ + + + +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.js b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.js new file mode 100644 index 00000000000..54dc03a4aff --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + size: { type: Number, optional: true, default: 1 }, + slots: { type: Object, default: 1 }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.xml b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.xml new file mode 100644 index 00000000000..1b8eb092108 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.xml @@ -0,0 +1,11 @@ + + + + +
+ +
+
+
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..5e598ba9b18 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.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: String, + value: 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..ba454191ba3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml @@ -0,0 +1,9 @@ + + + +

+

+ +

+
+
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..c77c707afca --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js @@ -0,0 +1,77 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useRef, onMounted, onWillUnmount,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("pie_chart_canvas"); + this.action = useService("action"); + + onWillStart(async () => await loadJS("/web/static/lib/Chart/Chart.js")) + + useEffect(() => { + if (this.canvasRef.el) { + this.drawChart(); + } + }, () => [this.props.data]); + + onMounted(() => { + if (this.props.data) { + this.drawChart(); + } + }); + + onWillUnmount(() => { + if (this.chart) { + this.chart.destroy(); + } + }); + } + + drawChart() { + debugger + if (this.chart) { + this.chart.destroy(); + this.chart = null; + } + + const chartData = { + labels: Object.keys(this.props.data), + datasets: [{ + data: Object.values(this.props.data), + }] + }; + + 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.openOrdersBySize(size); + } + } + } + }); + } + + openOrdersBySize(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]], + context: {create: false} + }); + } +} 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..9df7143e204 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml @@ -0,0 +1,6 @@ + + + + + + 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..9c3a2337657 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +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: String, + value: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..fc9b65d6a3c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml @@ -0,0 +1,14 @@ + + + + +
+

+ +
+
+ +
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..9aabe7426fe --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics.js @@ -0,0 +1,28 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const statistics = { + start() { + const stats = reactive({}); + const loadStatistics = async () => { + try { + const data = await rpc("/awesome_dashboard/statistics"); + console.log('Statistics loaded:', data); + Object.assign(stats, data); + } catch (error) { + console.error('Error loading statistics:', error); + } + }; + loadStatistics(); + setInterval(loadStatistics, 60000); + + return { + stats, + loadStatistics}; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statistics); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..4832cbcd00f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component,xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class DashboardAction extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", DashboardAction); diff --git a/awesome_dashboard/static/src/scss/dashboard.scss b/awesome_dashboard/static/src/scss/dashboard.scss new file mode 100644 index 00000000000..01624bac362 --- /dev/null +++ b/awesome_dashboard/static/src/scss/dashboard.scss @@ -0,0 +1,28 @@ +.o_dashboard{ + background-color: gray; +} +.o_dashboard_stat_block { + text-align: center; + margin-bottom: 24px; +} + +.o_dashboard_title_block { + font-weight: normal; + margin-bottom: 10px; + color: #446ae7; + display: block; +} + +.o_dashboard_value_block { + font-size: 48px; + color: #c74747; + font-weight: bold; +} + +@media (max-width: 425px) { + .dashboard-items .d-flex > * { + flex: 0 0 100%; + max-width: 100%; + width: 100%; + } +} diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 77abad510ef..f2ca8c123f6 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -23,6 +23,7 @@ 'depends': ['base', 'web'], 'application': True, 'installable': True, + 'sequence': 1, 'data': [ 'views/templates.xml', ], diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..c766b301312 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +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; + if (this.props.onChange) { + this.props.onChange(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..780d04428d3 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,16 @@ + + + +
+
+
+ + +
+

+ +

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..920abc9ef38 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +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.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..1e2551034b6 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + +
+ Counter: + +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..832cb7e2a76 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,17 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component,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 components = { Counter,Card,TodoList }; + setup() { + this.state = useState({ sum: 2 }); + } + incrementSum(value) { + this.state.sum += value; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..7301047d940 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,24 @@ -
- hello world + hello world + +
+
+
+

The Sum is:

+
+
+
+ +

This is the content of Card 1.

+
+ + + +
+
-
diff --git a/awesome_owl/static/src/todolist/todoitem.js b/awesome_owl/static/src/todolist/todoitem.js new file mode 100644 index 00000000000..d8e53bc425a --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.js @@ -0,0 +1,29 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todoTask: { + type: Object, + shape: { + id: { type: Number, optional: false }, + description: { type: String, optional: false }, + isCompleted: { type: Boolean, optional: false }, + } + }, + toggleState: { type: Function, optional: true }, + deleteTodo: { type: Function, optional: true } + } + onChange() { + if (this.props.toggleState) { + this.props.toggleState(this.props.todoTask.id) + } + } + onClick(){ + if(this.props.deleteTodo){ + this.props.deleteTodo(this.props.todoTask.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..128cdf4cdc3 --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.xml @@ -0,0 +1,19 @@ + + + +
+ + + + + + + + + + +
+
+
diff --git a/awesome_owl/static/src/todolist/todolist.js b/awesome_owl/static/src/todolist/todolist.js new file mode 100644 index 00000000000..126e4413fe0 --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +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.inputRef = useAutofocus("input") + this.todoTasks = useState([ + { id: 1, description: "watering plants", isCompleted: true }, + { id: 2, description: "write tutorial", isCompleted: true }, + { id: 3, description: "buy milk", isCompleted: false } + ]); + this.todoCounter = useState({ value: 4 }); + } + addTodo = (ev) => { + if (ev.keyCode === 13 && ev.target.value != '') { + this.todoTasks.push({ id: this.todoCounter.value, description: ev.target.value, isCompleted: false }); + this.todoCounter.value++; + ev.target.value = ""; + } + } + toggleTaskState(id) { + const toggleTask = this.todoTasks.find(task => task.id === id); + if (toggleTask) { + toggleTask.isCompleted = !toggleTask.isCompleted + } + } + removeTodoTask(id) { + const index = this.todoTasks.findIndex((task) => task.id === id); + if (index >= 0) { + this.todoTasks.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..b5f20999d3c --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.xml @@ -0,0 +1,11 @@ + + + +
+ + + + +
+
+
diff --git a/awesome_owl/static/src/utils/utils.js b/awesome_owl/static/src/utils/utils.js new file mode 100644 index 00000000000..9d3fc4152c9 --- /dev/null +++ b/awesome_owl/static/src/utils/utils.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +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..7a55038c8ac --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,29 @@ +{ + 'name': 'Real Estate', + 'category': 'Real Estate/Brokerage', + 'description': """This module is sale estate module""", + 'depends': ['base', 'mail'], + 'data': [ + "security/estate_security.xml", + "security/ir.model.access.csv", + "views/business_trip_views.xml", + "views/res_users_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_type_views.xml", + "report/estate_property_templates.xml", + "report/estate_property_reports.xml", + "views/estate_property_views.xml", + "views/estate_menus.xml", + ], + 'demo': [ + "demo/mail_message_subtype_data.xml", + "demo/estate.property.type.csv", + "demo/estate_property_demo.xml", + "demo/estate_offer_demo.xml", + "demo/estate_type_demo.xml", + ], + 'application': True, + 'license': 'OEEL-1', + "sequence": 1, +} 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_offer_demo.xml b/estate/demo/estate_offer_demo.xml new file mode 100644 index 00000000000..e7baf21d529 --- /dev/null +++ b/estate/demo/estate_offer_demo.xml @@ -0,0 +1,18 @@ + + + + + + 1200000 + 14 + + + + + 1300000000 + 14 + + + + + diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml new file mode 100644 index 00000000000..2b9daac7400 --- /dev/null +++ b/estate/demo/estate_property_demo.xml @@ -0,0 +1,43 @@ + + + + Big Villa + new + A nice and big villa + 12345 + 1600000 + 6 + 100 + 4 + 1 + 1 + 100000 + south + + + Property with Inline Offers + new + A nice and big 123 + 12323345 + 1 + 2 + 10 + 3 + 1 + 1 + 1000 + south + + + diff --git a/estate/demo/estate_type_demo.xml b/estate/demo/estate_type_demo.xml new file mode 100644 index 00000000000..df4dac7be32 --- /dev/null +++ b/estate/demo/estate_type_demo.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/estate/demo/mail_message_subtype_data.xml b/estate/demo/mail_message_subtype_data.xml new file mode 100644 index 00000000000..ebc28d8c784 --- /dev/null +++ b/estate/demo/mail_message_subtype_data.xml @@ -0,0 +1,9 @@ + + + + Trip confirmed + business.trip + + Business Trip confirmed! + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..ec4c91ef224 --- /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 business_trip diff --git a/estate/models/business_trip.py b/estate/models/business_trip.py new file mode 100644 index 00000000000..bf9bac0c72a --- /dev/null +++ b/estate/models/business_trip.py @@ -0,0 +1,19 @@ +from odoo import fields, models + + +class BusinessTrip(models.Model): + _name = 'business.trip' + _inherit = ['mail.thread'] + _description = '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..4db41206577 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,96 @@ +from odoo import api, fields, models +from dateutil.relativedelta import relativedelta +from datetime import date +from odoo.exceptions import UserError +from odoo.exceptions import ValidationError + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property Model" + _order = 'id desc' + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + default=lambda self: (date.today() + relativedelta(months=3)).strftime('%Y-%m-%d'), + copy=False + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean(default=True) + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection([('north', 'North'), ('east', 'East'), ('west', 'West'), ('south', 'South')], + required=True) + active = fields.Boolean(default=True) + state = fields.Selection( + [('new', 'New'), ('received', 'Offer Received'), ('accepted', 'Offer Accepted'), ('sold', 'Sold'), + ('cancelled', 'Cancelled')], default='new', string='Status') + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + user_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) + partner_id = fields.Many2one('res.partner', string='Buyer', copy=False) + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + total_area = fields.Integer(compute='_compute_total_area', store=True) + best_price = fields.Float( + string='Best Offer', + compute='_compute_best_price', + store=True, + readonly=True + ) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + + _sql_constraints = [ + ('check_expected_price_positive', 'CHECK(expected_price > 0)', 'Expected price must be positive!'), + ('check_selling_price_positive', 'CHECK(selling_price > 0)', 'Selling price must be positive!'), + ] + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + best_offer = max(record.offer_ids.mapped('price'), default=0) + record.best_price = best_offer + + @api.onchange('garden') + def _onchange_garden(self): + for record in self: + if record.garden: + record.garden_area = 10 + record.garden_orientation = 'north' + + def action_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError("Property is cancelled. No further actions can be taken on this property.") + 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' + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError("Property is already sold. No further actions can be taken on this property.") + record.state = 'cancelled' + + @api.constrains('selling_price') + def _check_selling_price(self): + for record in self: + if record.selling_price and record.selling_price < record.expected_price * 0.9: + raise ValidationError("The selling price cannot be lower than 90% of the expected price.") + + @api.ondelete(at_uninstall=False) + def _unlink_property(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise UserError("You cannot delete a property that is not in 'New' or 'Cancelled' state.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..8ed4ce65170 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,95 @@ +from odoo import api, fields, models +from datetime import date, timedelta +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property offer Model" + _order = 'price desc' + + price = fields.Float() + status = fields.Selection( + [('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False + ) + partner_id = fields.Many2one('res.partner', string='Partner', required=True) + property_id = fields.Many2one('estate.property', string='Property', required=True) + validity = fields.Integer(string='Validity(days)', default=7) + date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_deadline', string='Deadline') + property_type_id = fields.Many2one('estate.property.type', related='property_id.property_type_id', + string='Property Type', store=True) + + _sql_constraints = [ + ('check_offer_price_positive', 'CHECK(price > 0)', 'Offer price must be positive!'), + ] + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + if record.validity: + record.date_deadline = date.today() + timedelta(days=record.validity) + else: + record.date_deadline = False + + def _inverse_deadline(self): + for record in self: + if record.date_deadline: + record.validity = (record.date_deadline - date.today()).days + else: + record.validity = 7 + + def action_accept_offer(self): + for record in self: + if record.status == 'accepted': + raise UserError("This offer has already been accepted.") + check_other_accepted = self.search([ + ('property_id', '=', record.property_id.id), + ('status', '=', 'accepted') + ]) + if check_other_accepted: + raise UserError("Another offer has already been accepted for this property.") + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.partner_id = record.partner_id + record.property_id.state = 'accepted' + other_offers = self.search([ + ('property_id', '=', record.property_id.id), + ('id', '!=', record.id), + ('status', '!=', 'refused') + ]) + other_offers.write({'status': 'refused'}) + return True + + def action_refuse_offer(self): + for record in self: + record.status = 'refused' + check_other_accepted = self.search([ + ('status', '=', 'accepted') + ]) + if not check_other_accepted: + record.property_id.selling_price = 0 + record.property_id.partner_id = False + return True + + @api.ondelete(at_uninstall=False) + def _reset_selling_price(self): + for offer in self: + if offer.status == 'accepted': + offer.property_id.selling_price = 0.0 + + @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_price 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 = '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..061b2bc1f0d --- /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 Model" + _order = 'name' + + name = fields.Char(required=True) + color = fields.Integer(string='Color Index') + + _sql_constraints = [ + ('check_property_tag', 'UNIQUE(name)', '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..1fad846b00f --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,26 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type Model" + _order = 'name' + + name = fields.Char(required=True) + property_ids = fields.One2many( + 'estate.property', 'property_type_id', string='Properties' + ) + sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.") + offer_ids = fields.One2many( + 'estate.property.offer', 'property_type_id', string='Offers' + ) + offer_count = fields.Integer(compute='_compute_offer_count', string='Offer Count') + + _sql_constraints = [ + ('check_property_type', 'UNIQUE(name)', 'Property Type must be unique'), + ] + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..33cca251040 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many('estate.property', 'user_id', string='Properties', + domain=[('state', 'not in', ['sold', 'cancelled'])]) diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..b01ca962216 --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,40 @@ + + + + A4 low margin + + A4 + 0 + 0 + Portrait + 5 + 5 + 5 + 5 + + 0 + 80 + + + Estate Property Badge + estate.property + qweb-pdf + estate.estate_property_badge_report_template + estate.estate_property_badge_report_template + 'Property Name- %s' % (object.name).replace('/','') + + + report + + + Salesman Property + res.users + qweb-pdf + estate.res_users_badge_report_template + estate.res_users_badge_report_template + 'Salesman- %s' % (object.name).replace('/','') + + + report + + diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml new file mode 100644 index 00000000000..537b6693d29 --- /dev/null +++ b/estate/report/estate_property_templates.xml @@ -0,0 +1,137 @@ + + + + + + diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..406155c65d8 --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,58 @@ + + + + Manage Real Estate + 1 + + + Agent + + + + Manager + + + + + A description of the rule's role + + + + + + + [ + '|', + '|', ('user_id', '=', user.id), + ('user_id', '=', False), + ('write_uid', '=', user.id) + ] + + + + A description of the rule's on offer + + + + + + + [ + '|', + '|', ('property_id.user_id', '=', user.id), + ('property_id.user_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..1ff8a040653 --- /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,1 +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_type_agent,access_estate_property_type_agent,model_estate_property_type,estate_group_user,1,0,0,0 +access_property_tag_agent,access_estate_property_tag_agent,model_estate_property_tag,estate_group_user,1,0,0,0 +access_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0 +access_business_trip,access_business_trip,model_business_trip,base.group_user,1,1,1,0 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..3ce9c645552 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,78 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import Form + + +class TestEstateProperty(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.properties = cls.env['estate.property'] + + def test_creation_offer(self): + property = self.properties.create({ + 'name': 'Test Property', + 'expected_price': 1000, + 'garden': True, + 'garden_area': 20, + 'garden_orientation': 'south', + 'state': 'sold', + }) + + with self.assertRaises( + UserError, msg="Cannot create an offer for a sold property" + ): + self.env["estate.property.offer"].create( + { + "price": 1500.00, + "partner_id": self.env.ref("base.res_partner_1").id, + "date_deadline": "2025-09-14", + "property_id": property.id, + } + ) + + def test_sold_property(self): + property = self.properties.create({ + 'name': 'Test Property Demo', + 'expected_price': 100, + 'garden': True, + 'garden_area': 20, + 'garden_orientation': 'south', + 'state': 'new', + }) + + self.env['estate.property.offer'].create({ + 'property_id': property.id, + 'partner_id': self.env.ref('base.res_partner_2').id, + "validity": 7, + 'price': 1200, + }) + + with self.assertRaises(UserError): + property.action_sold() + + def test_reset_garden_area_and_orientation(self): + property = self.env["estate.property"].create( + { + "name": "Garden Test Property", + "expected_price": "789", + "garden": True, + "garden_area": 50, + "garden_orientation": "north", + } + ) + + with Form(property) as form: + form.garden = False + form.save() + + self.assertFalse(property.garden, "Garden checkbox should be unchecked.") + self.assertFalse( + property.garden_area, + "Garden area should be reset when the garden checkbox is unchecked.", + ) + self.assertFalse( + property.garden_orientation, + "Orientation should be reset when the garden checkbox is unchecked.", + ) diff --git a/estate/views/business_trip_views.xml b/estate/views/business_trip_views.xml new file mode 100644 index 00000000000..2122284f280 --- /dev/null +++ b/estate/views/business_trip_views.xml @@ -0,0 +1,35 @@ + + + + Business Trip + business.trip + list,form + + + business.trip.list + business.trip + + + + + + + + + business.trip.form + business.trip + +
+ + + + + + + + + + +
+
+
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..f775af16da3 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..12746c8e57c --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,21 @@ + + + + Property Offers + estate.property.offer + list + [('property_type_id', '=', active_id)] + + + estate.property.offer.list.view + 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..5fbe747e5ab --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,31 @@ + + + + Property Tag + estate.property.tag + list,form + + + estate.property.tag.list.view + estate.property.tag + + + + + + + + estate.property.tag.form.view + 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..9c68bfcca4e --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,47 @@ + + + + Property Types + estate.property.type + list,form + + + estate.property.type.list.view + estate.property.type + + + + + + + + + estate.property.type.form.view + 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..6313c57ef08 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,150 @@ + + + + Properties + estate.property + list,form,kanban + {'search_default_state': True, 'search_default_current': True} + + + estate.property.properties.list.view + estate.property + + + + + + + + + + + + + + + + estate.property.properties.form.view + estate.property + +
+
+
+ +
+ +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +