diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..0f8ed5b7edb 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_assets': [ + 'awesome_dashboard/static/src/dashboard/**/*', + ], }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 56d4a051287..9a00d99ad5b 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -8,7 +8,10 @@ logger = logging.getLogger(__name__) + class AwesomeDashboard(http.Controller): + _name = "awesome.dashboard" + @http.route('/awesome_dashboard/statistics', type='json', auth='user') def get_statistics(self): """ @@ -33,4 +36,3 @@ def get_statistics(self): }, 'total_amount': random.randint(100, 1000) } - diff --git a/awesome_dashboard/static/src/cards/number_card.js b/awesome_dashboard/static/src/cards/number_card.js new file mode 100644 index 00000000000..5d5efd029c5 --- /dev/null +++ b/awesome_dashboard/static/src/cards/number_card.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = xml` +
+
+
+ +
+
+ `; +} diff --git a/awesome_dashboard/static/src/cards/pie_chart_card.js b/awesome_dashboard/static/src/cards/pie_chart_card.js new file mode 100644 index 00000000000..9bb6aed2a8f --- /dev/null +++ b/awesome_dashboard/static/src/cards/pie_chart_card.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; +import { PieChart } from "../dashboard/pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = xml` +
+
+ +
+ `; + static components = { PieChart }; +} 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/dailog/Dialog.js b/awesome_dashboard/static/src/dashboard/dailog/Dialog.js new file mode 100644 index 00000000000..92d0d05a480 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dailog/Dialog.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class DashboardConfigDialog extends Component { + static template = "awesome_dashboard.DashboardConfigDialog"; + static components = { Dialog }; + static props = ["title", "items", "disabledItems", "close"]; + + setup() { + const savedConfig = JSON.parse(localStorage.getItem("dashboard_config")) || []; + this.state = useState({ + disabledItems: new Set(savedConfig.length ? savedConfig : this.props.disabledItems || []) + }); + } + + toggleItem = (itemId) => { + this.state.disabledItems = new Set(this.state.disabledItems); + if (this.state.disabledItems.has(itemId)) { + this.state.disabledItems.delete(itemId); + } else { + this.state.disabledItems.add(itemId); + } + }; + + saveConfig() { + const disabledItemsArray = [...this.state.disabledItems]; + localStorage.setItem("dashboard_hidden_items", JSON.stringify(disabledItemsArray)); + + if (this.props.onSave) { + this.props.onSave(disabledItemsArray); + } + + this.props.close(); + } +} + diff --git a/awesome_dashboard/static/src/dashboard/dailog/Dialog.xml b/awesome_dashboard/static/src/dashboard/dailog/Dialog.xml new file mode 100644 index 00000000000..286510b77cd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dailog/Dialog.xml @@ -0,0 +1,18 @@ + + + +
+ +
+ + +
+
+
+ +
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..ef470dc12ae --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,63 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useState, useEffect, reactive } 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 "./dashboard_item/dashboard_item"; +import { rpc } from "@web/core/network/rpc"; +import {PieChart} from "./pie_chart/pie_chart" +import { items } from "./dashboard_items"; +import { Dialog } from "@web/core/dialog/dialog"; +import { DashboardConfigDialog } from "./dailog/Dialog"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = {Layout, DashboardItem, PieChart, Dialog}; + + setup(){ + this.action = useService("action"); + this.statisticsService = useState(useService("awesome_dashboard.statistics")); + this.state = reactive({ statistics: {}, disabledItems: [] }); + this.items = registry.category("awesome_dashboard").getAll(); + + onWillStart(async () => { + this.state.statistics = await this.statisticsService.loadStatistics(); + this.loadConfig(); + }); + + useEffect(() => { + this.state.statistics = this.statisticsService.statistics.data; + }); + } + + loadConfig() { + const config = localStorage.getItem("dashboard_hidden_items"); + this.state.disabledItems = config ? JSON.parse(config) : []; + } + + openConfiguration() { + this.env.services.dialog.add(DashboardConfigDialog, { + items: this.items, + disabledItems: [...this.state.disabledItems], + onSave: (hiddenItems) => { + localStorage.setItem("dashboard_hidden_items", JSON.stringify(hiddenItems)); + this.state.disabledItems = hiddenItems; + }, + }); + } + + openCustomers(){ + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "crm.lead", + views: [[false, 'form'], [false, 'list']] + }) + } +} + +registry.category("lazy_components").add("awesome_dashboard.dashboard", AwesomeDashboard); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..2f097fbf098 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,8 @@ +.o_dashboard { + background-color: #d3d3d3; + padding: 20px; +} +.dashboard-item { + background-color: white; + border: 1px solid #ddd; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..8a6b27fcf48 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,28 @@ + + + + +
+ + +

Dashboard

+ + + + +
+
+ + + + + + +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..c959240c590 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + slot: { type: Object, optional: true }, + size: { type: Number, optional: true }, + }; + + static defaultProps = { + size: 1 + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..516de834f9f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,11 @@ + + + +
+
+ +
+ +
+
+
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..9e7cede258b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,72 @@ +/** @odoo-module **/ + +import { PieChartCard } from "../cards/pie_chart_card"; +import { NumberCard } from "../cards/number_card"; +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirts per order this month", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Average amount of t-shirts per 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, + props: (data) => ({ + title: "Avg. time from 'new' to '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, + 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, + props: (data) => ({ + title: "Total amount of new orders", + value: data.total_amount, + }), + }, + { + id: "orders_by_size", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + data: data.orders_by_size, + }), + }, +]; + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..29de2774d35 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,43 @@ +/** @odoo-module */ + +import { loadJS } from "@web/core/assets"; +import { getColor } from "@web/core/colors/colors"; +import { Component, onWillStart, useRef, onMounted, onWillUnmount } from "@odoo/owl"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + label: String, + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"])); + onMounted(() => { + this.renderChart(); + }); + onWillUnmount(() => { + this.chart.destroy(); + }); + } + + renderChart() { + const labels = Object.keys(this.props.data); + const data = Object.values(this.props.data); + const color = labels.map((_, index) => getColor(index)); + this.chart = new Chart(this.canvasRef.el, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + label: this.props.label, + data: data, + backgroundColor: color, + }, + ], + }, + }); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..18416e9a223 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..093791bef37 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,32 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { memoize } from "@web/core/utils/functions"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +class StatisticsService { + constructor() { + this.statistics = reactive({ data: null }); + + this._loadStatistics = async () => { + this.statistics.data = await rpc('/awesome_dashboard/statistics'); + }; + + this._loadStatistics(); + + setInterval(() => { + this._loadStatistics(); + }, 10000); + } + + async loadStatistics() { + return this.statistics.data; + } +} + +registry.category("services").add("awesome_dashboard.statistics", { + start() { + return new 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..954963f0b63 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py index 457bae27e11..dd15b5c32e5 100644 --- a/awesome_owl/controllers/__init__.py +++ b/awesome_owl/controllers/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import controllers \ No newline at end of file +from . import controllers diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py index bccfd6fe283..57d0d0ddae5 100644 --- a/awesome_owl/controllers/controllers.py +++ b/awesome_owl/controllers/controllers.py @@ -1,6 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import http from odoo.http import request, route + class OwlPlayground(http.Controller): @http.route(['/awesome_owl'], type='http', auth='public') def show_playground(self): diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..861bc7ca40d --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,19 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: { type: String, optional: false }, + slots: { type: Object, optional: true }, + }; + + setup() { + this.state = useState({ visibility: true }); + } + + toggleState() { + this.state.visibility = !this.state.visibility; + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..03a4ed0f05d --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,18 @@ + + + +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..684c42b96ba --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,20 @@ +/** @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); + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..5b00ba8cb56 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + +
+ hello world +

Counter: +

+ +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..c830d201201 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,27 @@ /** @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/todo_list"; export class Playground extends Component { - static template = "awesome_owl.playground"; + static template = "awesome_owl.playground"; + static components = { Card, Counter, TodoList }; + + setup() { + this.plainText = "
Plain text content
"; + this.htmlContent = markup("
Markup content
"); + this.state = useState({ + counter1: 0, + counter2: 0, + }); + this.incrementSum = (counterNumber, newValue) => { + this.state[counterNumber] = newValue; + }; + } + + get sum() { + return this.state.counter1 + this.state.counter2; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..1ef8886edd7 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,23 @@ -
- hello world +

Playground

+ + +

Total Sum: +

+
+
+ +

This is basic card.

+
+ + + +
+
+
- -
+ \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_item.js b/awesome_owl/static/src/todolist/todo_item.js new file mode 100644 index 00000000000..a792a446bef --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_item.js @@ -0,0 +1,20 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + static props = { + todo: { id: Number, description: String, isCompleted: Boolean }, + toggleState: { type: Function }, + removeTodo: { type: Function }, + }; + + toggleCompleted() { + this.props.toggleState(this.props.todo.id); + } + + deleteTodo() { + this.props.removeTodo(this.props.todo.id); + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_item.xml b/awesome_owl/static/src/todolist/todo_item.xml new file mode 100644 index 00000000000..770d4f6a310 --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_item.xml @@ -0,0 +1,11 @@ + + + +
+ + - + + +
+
+
diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js new file mode 100644 index 00000000000..56bb5ad6d58 --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_list.js @@ -0,0 +1,45 @@ +/** @odoo-module */ + +import { Component, useState, useRef, onMounted } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + + setup() { + this.todos = useState([]); + this.todoCounter = 1; + this.inputRef = useRef("taskInput"); + useAutofocus(this.inputRef); + } + + addTodo(ev) { + if (ev.keyCode === 13) { + const description = ev.target.value.trim(); + if (description) { + this.todos.push({ + id: this.todoCounter++, + description, + isCompleted: false, + }); + ev.target.value = ""; + } + } + } + + toggleState = (todoId) => { + const todo = this.todos.find((t) => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + }; + + removeTodo = (todoId) => { + const index = this.todos.findIndex((t) => t.id === todoId); + if (index >= 0) { + this.todos.splice(index, 1); + } + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_list.xml b/awesome_owl/static/src/todolist/todo_list.xml new file mode 100644 index 00000000000..78a60a6650e --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_list.xml @@ -0,0 +1,14 @@ + + + +
+

Todo List

+ +
+ + + +
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..f1338862301 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,9 @@ +import {onMounted} from "@odoo/owl"; + +export function useAutofocus(ref){ + onMounted(()=>{ + if(ref.el){ + ref.el.focus(); + } + }); +} \ No newline at end of file diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..1b1e1d510a6 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import wizard +from . import controllers diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..3144d8df050 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Real Estate', + 'version': '1.0', + 'depends': ['base', 'mail', 'website'], + 'author': 'Author Name', + 'category': 'Real Estate/Brokerage', + 'application': True, + 'installable': True, + 'sequence': 0, + 'license': 'LGPL-3', + + 'description': ''' + Description text + ''', + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'report/estate_property_reports.xml', + 'report/estate_property_templates.xml', + 'report/estate_property_user_reports.xml', + 'report/estate_property_user_templates.xml', + 'views/estate_offer_wizard_views.xml', + 'views/estate_property_views.xml', + 'views/estate_property_offer_view.xml', + 'views/estate_property_type_view.xml', + 'views/estate_property_tag_view.xml', + 'views/estate_salesperson.xml', + 'views/estate_menus.xml', + 'data/master_data.xml', + 'data/website_menu_data.xml', + 'views/estate_property_website_templates.xml', + ], + 'demo': [ + 'demo/demo_data.xml', + 'demo/demo_offer_data.xml' + ] +} diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py new file mode 100644 index 00000000000..dcd02bbfe9d --- /dev/null +++ b/estate/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import property_controller diff --git a/estate/controllers/property_controller.py b/estate/controllers/property_controller.py new file mode 100644 index 00000000000..6549040ad51 --- /dev/null +++ b/estate/controllers/property_controller.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request + + +class PropertyController(http.Controller): + + @http.route('/properties', type='http', auth='public', website=True) + def list_properties(self, **kwargs): + properties = request.env['estate.property'].sudo().search([]) + return request.render('estate.property_listing_template', {'properties': properties}) diff --git a/estate/data/master_data.xml b/estate/data/master_data.xml new file mode 100644 index 00000000000..57fa1b1740d --- /dev/null +++ b/estate/data/master_data.xml @@ -0,0 +1,20 @@ + + + + + Residential + + + + Commercial + + + + Industrial + + + + Land + + + \ No newline at end of file diff --git a/estate/data/website_menu_data.xml b/estate/data/website_menu_data.xml new file mode 100644 index 00000000000..044bcaa0225 --- /dev/null +++ b/estate/data/website_menu_data.xml @@ -0,0 +1,11 @@ + + + + + Properties + /properties + + + + + diff --git a/estate/demo/demo_data.xml b/estate/demo/demo_data.xml new file mode 100644 index 00000000000..4e1e7359bc5 --- /dev/null +++ b/estate/demo/demo_data.xml @@ -0,0 +1,70 @@ + + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 0 + 6 + 100 + 4 + True + True + 100000 + south + + + + + Trailer home + cancel + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + False + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 0 + 6 + 100 + 4 + True + True + 100000 + south + + + + + Demo Apartment + 2200 + 2000 + + + + diff --git a/estate/demo/demo_offer_data.xml b/estate/demo/demo_offer_data.xml new file mode 100644 index 00000000000..959f88d44c8 --- /dev/null +++ b/estate/demo/demo_offer_data.xml @@ -0,0 +1,38 @@ + + + + + + 10000 + 14 + + + + + + + 1500000 + 14 + + + + + + + 1500001 + 14 + + + + + + + + + + + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..2507d182e0f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import estate_property +from . import estate_property_type +from . import estate_property_offer +from . import estate_property_tag +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..68691806e9b --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + + +class Estateproperty(models.Model): + _name = 'estate.property' + _description = 'Estate property' + _inherit = ['mail.thread'] + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', + 'The expected price must be strictly positive.'), + ('check_selling_price', 'CHECK(selling_price >= 0)', + 'The selling price must be positive.'), + ] + + name = fields.Char(string='Property Name', + required=True, default='Property') + property_type_id = fields.Many2one( + 'estate.property.type', string='Property Type') + salesperson_id = fields.Many2one( + 'res.users', string='Salesperson', default=lambda self: self.env.user) + buyer_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') + description = fields.Text(string='Description', compute='_compute_desc') + bedrooms = fields.Integer(string='Bedrooms', default=2) + living_area = fields.Integer(string='Living Area') + facades = fields.Integer(string='Facades') + garage = fields.Boolean(string='Garage') + garden = fields.Boolean(string='Garden') + garden_area = fields.Integer(string='Garden Area') + garden_orientation = fields.Selection( + string='Type', + selection=[('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West')] + ) + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + selection=[('new', 'New'), + ('offer_received', 'Offer received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancel', 'Cancelled')], + required=True, + copy=False, + default='new', + tracking=True + ) + total_area = fields.Integer( + string='Total Area', compute='_compute_total_area') + best_price = fields.Float( + string='Best Price', compute='_compute_best_price') + offer_received = fields.Boolean( + compute='_compute_offer_received', store=True) + postcode = fields.Char('PostCode') + date_availability = fields.Date( + 'Available From', copy=False, default=lambda self: fields.Datetime.today() + timedelta(days=90)) + expected_price = fields.Float('Expected Price', required=True) + selling_price = fields.Float('Selling Price', readonly=True, copy=False) + company_id = fields.Many2one( + 'res.company', string='Company', required=True, default=lambda self: self.env.company) + + @api.depends('offer_ids') + def _compute_offer_received(self): + for record in self: + record.offer_received = bool(record.offer_ids) + + @api.depends('salesperson_id.name') + def _compute_desc(self): + for record in self: + record.description = 'For property information contact salesperson %s' % record.salesperson_id.name + + @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 property in self: + prices = property.offer_ids.mapped('price') + property.best_price = max(prices, default=0) + + @api.constrains('expected_price') + def _check_expected_price(self): + for record in self: + if record.expected_price < 0: + raise ValidationError('The expected price must be positive.') + + @api.constrains('expected_price', 'selling_price') + def _check_selling_price(self): + for record in self: + if not float_is_zero(record.selling_price, precision_digits=2): + min_acceptable_price = record.expected_price * 0.9 + if float_compare(record.selling_price, min_acceptable_price, precision_digits=2) == -1: + raise ValidationError( + 'The selling price cannot be lower than 90% of the expected price.') + + @api.onchange('garden') + def _onchange_garden(self): + self.garden_area = 10 if self.garden else 0 + self.garden_orientation = 'north' if self.garden else False + + @api.ondelete(at_uninstall=False) + def _unlink_if_property_in_cancel_or_new_state(self): + for record in self: + if record.state not in ('new', 'cancel'): + raise UserError( + 'You can only delete properties that are in new or cancel state.') + + def action_cancel(self): + for property in self: + if property.state == 'sold': + raise UserError('A sold property cannot be cancelled.') + else: + property.state = 'cancel' + + def action_sold(self): + for property in self: + if property.state == 'cancel': + raise UserError('A cancelled property cannot be sold.') + if not property.offer_ids: + raise UserError('Cannot sold without any offer.') + if property.state == "offer_accepted": + property.state = 'sold' + else: + raise UserError( + "Cannot sold property without offer being accepted.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..734f0596c81 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + _sql_constraints = [ + ('check_offer_price', 'CHECK(price > 0)', + 'The offer price must be strictly positive.'), + ] + + price = fields.Float(string='Price') + status = fields.Selection( + string='Status', + selection=[('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False, + ) + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one( + 'estate.property', required=True, ondelete='cascade') + create_date = fields.Date() + property_type_id = fields.Many2one( + comodel_name='estate.property.type', + related='property_id.property_type_id', + store=True + ) + validity = fields.Integer(string='Validity', default=7) + date_deadline = fields.Date( + string='Deadline', compute='_compute_deadline', inverse='_inverse_deadline') + + @api.depends('create_date', 'validity') + def _compute_deadline(self): + for offer in self: + if offer.create_date: + offer.date_deadline = offer.create_date + \ + timedelta(days=offer.validity) + else: + offer.date_deadline = False + + def _inverse_deadline(self): + for offer in self: + if offer.create_date and offer.date_deadline: + offer_days = (offer.date_deadline - offer.create_date).days + offer.validity = offer_days + + @api.model_create_multi + def create(self, vals_list): + for record in vals_list: + property_id = record.get('property_id') + if property_id: + existing_offer = self.search( + [('property_id', '=', property_id)], limit=1, order='price DESC') + if existing_offer and existing_offer.price >= vals_list[0]['price']: + raise UserError( + 'You cannot create an offer lower than or equal to an existing offer.') + records = super().create(vals_list) + for record in records: + if record.property_id.state == 'new': + record.property_id.state = 'offer_received' + + record.property_id.message_post( + body=f"A new offer of {record.price} has been received by {record.partner_id.name}.", + subject="New Offer Received", + message_type="notification", + subtype_xmlid="mail.mt_comment", + ) + return records + + def action_accept(self): + for offer in self: + if offer.property_id.state == 'sold': + raise UserError( + 'You cannot accept an offer for a sold property.') + existing_accepted_offer = offer.property_id.offer_ids.filtered( + lambda o: o.status == 'accepted') + if existing_accepted_offer: + raise UserError( + 'Only one offer can be accepted for a given property.') + offer.status = 'accepted' + offer.property_id.write({ + 'selling_price': offer.price, + 'buyer_id': offer.partner_id, + 'state': 'offer_accepted', + }) + offer.property_id.selling_price = offer.price + offer.property_id.buyer_id = offer.partner_id + offer.property_id.state = 'offer_accepted' + + def action_refuse(self): + for offer in self: + offer.status = 'refused' + offer.property_id.write({ + 'selling_price': 0.00, + 'buyer_id': False, + }) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..4b2ed31e5a6 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate Property Tag' + _sql_constraints = [ + ('unique_tag_name', 'UNIQUE(name)', + 'The property tag name must be unique.'), + ] + _order = 'name asc' + + name = fields.Char(string='Tags', required=True) + color = fields.Integer() diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..0275e3f9a1b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Estate Property Type' + _sql_constraints = [ + ('unique_type_name', 'UNIQUE(name)', + 'The property type name must be unique.'), + ] + + name = fields.Char(string='Property Type', required=True) + property_ids = fields.One2many( + 'estate.property', 'property_type_id', readonly=True) + sequence = fields.Integer(string='Sequence', default=1, + help='Used to order stages. Lower is better.') + offer_ids = fields.One2many( + string='Offers', + comodel_name='estate.property.offer', + inverse_name='property_type_id' + ) + offer_count = fields.Integer( + string='Offer Count', compute='_compute_offers', store=True) + + @api.depends('offer_ids') + def _compute_offers(self): + for record in self: + record.offer_count = len(record.offer_ids) + + def action_open_offers(self): + return { + 'name': 'Offers', + 'type': 'ir.actions.act_window', + 'res_model': 'estate.property.offer', + 'view_mode': 'list,form', + 'domain': [('property_type_id', '=', self.id)], + 'context': {'default_property_type_id': self.id}, + } diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..ab0d8d339ce --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', 'salesperson_id', string='Properties', domain="[('state', 'in', ['new', 'offer_received'])]") diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..efc956d4e24 --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,12 @@ + + + + Offer Report + estate.property + qweb-pdf + estate.report_property_offer + 'Property_Offers_%s' % (object.name) + + + + \ No newline at end of file diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml new file mode 100644 index 00000000000..08f760bb9dc --- /dev/null +++ b/estate/report/estate_property_templates.xml @@ -0,0 +1,72 @@ + + + + + + + diff --git a/estate/report/estate_property_user_reports.xml b/estate/report/estate_property_user_reports.xml new file mode 100644 index 00000000000..d84fc4ebbbd --- /dev/null +++ b/estate/report/estate_property_user_reports.xml @@ -0,0 +1,10 @@ + + + User Report + res.users + qweb-pdf + estate.report_user_properties + + action + + \ No newline at end of file diff --git a/estate/report/estate_property_user_templates.xml b/estate/report/estate_property_user_templates.xml new file mode 100644 index 00000000000..8ec237dc873 --- /dev/null +++ b/estate/report/estate_property_user_templates.xml @@ -0,0 +1,42 @@ + + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..9ecd4d53e2c --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,14 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,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_estate_offer_wizard,access_estate_offer_wizard,model_estate_offer_wizard,base.group_user,1,1,1,1 +access_estate_property_manager,access_estate_property_manager,model_estate_property,estate_group_manager,1,1,1,0 +access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate_group_manager,1,1,1,1 +access_estate_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate_group_manager,1,1,1,1 +access_estate_property_offer_manager,access_estate_property_offer_manager,model_estate_property_offer,estate_group_manager,1,1,1,1 +access_estate_property_agent,access_estate_property_agent,model_estate_property,estate_group_user,1,1,1,0 +access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate_group_user,1,0,0,0 +access_estate_property_tag_agent,access_estate_property_tag_agent,model_estate_property_tag,estate_group_user,1,0,0,0 +access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0 \ No newline at end of file diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 00000000000..0fe45b8fb6c --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,34 @@ + + + Agent + + + + + + Manager + + + + + + Agent Properties + + + ['|', ('salesperson_id', '=', False), ('salesperson_id', '=', user.id)] + + + + Manager Properties + + + [(1, '=', 1)] + + + + Estate Properties + + + ['|',('company_id', '=', 'company_ids'), ('company_id', '=', False)] + + \ No newline at end of file diff --git a/estate/static/description/estate_icon.png b/estate/static/description/estate_icon.png new file mode 100644 index 00000000000..dce74a4bd2e Binary files /dev/null and b/estate/static/description/estate_icon.png differ diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..ad693b01810 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_property diff --git a/estate/tests/test_property.py b/estate/tests/test_property.py new file mode 100644 index 00000000000..d6107c455cb --- /dev/null +++ b/estate/tests/test_property.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import Form, tagged +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + + +@tagged("post_install", "-at_install") +class EstateTestCase(TransactionCase): + + @classmethod + def setUpClass(cls): + super(EstateTestCase, cls).setUpClass() + + cls.properties = cls.env["estate.property"].create( + [ + { + "name": "Property1", + "living_area": 100, + "expected_price": 10000, + }, + { + "name": "Property2", + "living_area": 20, + "expected_price": 4000, + }, + ] + ) + + cls.offers = cls.env["estate.property.offer"].create({ + 'partner_id': cls.env.ref('base.res_partner_10').id, + 'property_id': cls.properties[0].id, + 'price': 1000000, + 'status': 'accepted' + }) + + cls.properties[0].offer_ids = [(6, 0, [cls.offers.id])] + + def test_creation_area(self): + self.assertRecordValues( + self.properties, + [ + { + "name": "Property1", + "total_area": 100, + "expected_price": 10000, + }, + { + "name": "Property2", + "total_area": 20, + "expected_price": 4000, + }, + ], + ) + + def test_sell_property(self): + self.properties[0].action_sold() + self.assertEqual(self.properties[0].state, "sold") + + def test_sell_property_without_offer(self): + with self.assertRaises(UserError): + self.properties[1].action_sold() + + def test_check_offer_for_sold_property(self): + self.properties[0].action_sold() + with self.assertRaises(UserError): + self.env["estate.property.offer"].create( + { + "partner_id": self.env.ref("base.res_partner_10").id, + "property_id": self.properties[0].id, + "price": 10000, + } + ) + + def test_garden_checkbox_area_orientation(self): + property = Form(self.properties[0]) + self.assertFalse(property.garden) + property.garden = True + self.assertEqual(property.garden_orientation, 'north') + self.assertEqual(property.garden_area, 10) + property.garden_orientation = 'east' + property.garden_area = 100 + property.save() + self.assertEqual(property.garden_orientation, 'east') + self.assertEqual(property.garden_area, 100) + property.garden = False + property.save() + property.garden = True + self.assertEqual(property.garden_orientation, 'north') + self.assertEqual(property.garden_area, 10) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..4e30f120bec --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_offer_wizard_views.xml b/estate/views/estate_offer_wizard_views.xml new file mode 100644 index 00000000000..22d527ba328 --- /dev/null +++ b/estate/views/estate_offer_wizard_views.xml @@ -0,0 +1,28 @@ + + + + estate.offer.wizard.form + estate.offer.wizard + +
+ + + + +
+
+
+
+
+ + + Add Offer + estate.offer.wizard + form + + new + {'default_property_ids': active_ids} + +
diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml new file mode 100644 index 00000000000..503318d8416 --- /dev/null +++ b/estate/views/estate_property_offer_view.xml @@ -0,0 +1,28 @@ + + + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + + + estate.property.offer.view.list + estate.property.offer + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..6dad4001a61 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,135 @@ + + + + + Properties + estate.property + list,form + {'search_default_state': True} + + + + + estate.property.view.list + estate.property + + +
+
+ + + + + + + + + +
+
+
+ + + + estate.property.view.form + estate.property + +
+
+ + + + +
+ + + +
+
+
+

+ +

+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+
+ + + + + + + + + + + + + + + +
Task DescriptionCompleted
+
+ + +
+
+ + + + +
+
+
+ + + + diff --git a/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view .scss b/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view .scss new file mode 100644 index 00000000000..66adf24fbbc --- /dev/null +++ b/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view .scss @@ -0,0 +1,3 @@ +.o_res_partner_kanban_view_sidebar { + flex: 0 0 300px; +} \ No newline at end of file diff --git a/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view .xml b/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view .xml new file mode 100644 index 00000000000..501cbf92d0c --- /dev/null +++ b/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view .xml @@ -0,0 +1,29 @@ + + + + + + + + + + + +
+

Customer Locations

+
+ + + + + + +
+ +
+
+ + model.useSampleModel ? 'o_view_sample_data' : '' + "d-flex" + +
+
\ No newline at end of file diff --git a/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view.js b/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view.js new file mode 100644 index 00000000000..7543e3ed9a8 --- /dev/null +++ b/owl_tutorials/static/src/components/view_inheritance/res_partner_kanban_view.js @@ -0,0 +1,45 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { kanbanView } from "@web/views/kanban/kanban_view"; +import { KanbanController } from "@web/views/kanban/kanban_controller"; +import { useService } from "@web/core/utils/hooks"; +import { onWillStart } from '@odoo/owl'; + +class ResPartnerKanbanController extends KanbanController{ + static template = "owl_tutorials.ResPartnerKanbanView" + setup(){ + super.setup() + console.log("This is res parnter controller") + this.action = useService("action") + this.orm = useService("orm") + + onWillStart(async () => { + this.customerLocation = await this.orm.readGroup("res.partner", [], ['state_id'], ['state_id']) + this.customerLocations = this.customerLocation.slice(0, -1) + console.log(this.customerLocations) + }) + } + + openSalesView(){ + console.log("opened sales view") + this.action.doAction({ + type: "ir.actions.act_window", + name: "Customer Sales", + res_model: "sale.order", + views: [[false, "list"], [false, "form"]] + }) + } + + selectLocations(state){ + this.model.load({ domain: [['state_id', '=', state[0]]] }); + } +} + +export const ResPartnerKanbanView = { + ...kanbanView, + Controller: ResPartnerKanbanController, + buttonTemplate: "owl_tutorials.ResPartnerKanbanView.Buttons" +} + +registry.category("views").add("res_partner_kanban_view", ResPartnerKanbanView ) diff --git a/owl_tutorials/static/src/components/view_inheritance/res_partner_list_view.js b/owl_tutorials/static/src/components/view_inheritance/res_partner_list_view.js new file mode 100644 index 00000000000..2d9fa689765 --- /dev/null +++ b/owl_tutorials/static/src/components/view_inheritance/res_partner_list_view.js @@ -0,0 +1,32 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { listView } from "@web/views/list/list_view"; +import { ListController } from "@web/views/list/list_controller"; +import { useService } from "@web/core/utils/hooks"; + +class ResPartnerListController extends ListController{ + setup(){ + super.setup() + console.log("This is res parnter controller") + this.action = useService("action") + } + + openSalesView(){ + console.log("opened sales view") + this.action.doAction({ + type: "ir.actions.act_window", + name: "Customer Sales", + res_model: "sale.order", + views: [[false, "list"], [false, "form"]] + }) + } +} + +export const ResPartnerListView = { + ...listView, + Controller: ResPartnerListController, + buttonTemplate: "owl_tutorials.ResPartnerListView.Buttons" +} + +registry.category("views").add("res_partner_list_view", ResPartnerListView ) diff --git a/owl_tutorials/static/src/components/view_inheritance/res_partner_list_view.xml b/owl_tutorials/static/src/components/view_inheritance/res_partner_list_view.xml new file mode 100644 index 00000000000..8fb4b49a077 --- /dev/null +++ b/owl_tutorials/static/src/components/view_inheritance/res_partner_list_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/owl_tutorials/views/res_partner.xml b/owl_tutorials/views/res_partner.xml new file mode 100644 index 00000000000..d3956d92ca6 --- /dev/null +++ b/owl_tutorials/views/res_partner.xml @@ -0,0 +1,23 @@ + + + + res.partner.list.view.inherit.owl.tutorials + res.partner + + + + res_partner_list_view + + + + + res.partner.kanban.view.inherit.owl.tutorials + res.partner + + + + res_partner_kanban_view + + + + diff --git a/owl_tutorials/views/views.xml b/owl_tutorials/views/views.xml new file mode 100644 index 00000000000..ec122f797bb --- /dev/null +++ b/owl_tutorials/views/views.xml @@ -0,0 +1,45 @@ + + + + todo.list.view.list + todo.list + + + + + + + + + + + todo.list.view.form + todo.list + +
+ + + + + + + +
+
+
+ + + Todo List + todo.list + list,form + + + + Todo List OWL + owl.action_todo_list_js + + + + + +