diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..bd10cc6fc0c 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -24,6 +24,10 @@ 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', + ('remove', 'awesome_dashboard/static/src/dashboard/**/*'), + ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.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/components/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..160a1b9b4c7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.js @@ -0,0 +1,19 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + slots: { + type: Object, + shape: { + default: true + } + }, + size: { type: Number, optional: true }, + }; + + static defaultProps = { + size: 1, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..72cccf9b119 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.xml @@ -0,0 +1,12 @@ + + + + +
+
+ +
+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/components/filter_dialog/filter_dialog.js b/awesome_dashboard/static/src/dashboard/components/filter_dialog/filter_dialog.js new file mode 100644 index 00000000000..91f4e58eea9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/filter_dialog/filter_dialog.js @@ -0,0 +1,45 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; + +export class FilterDialogue extends Component { + static template = "awesome_dashboard.FilterDialogue"; + + static components = { Dialog, CheckBox }; + + static props = { + close: Function, + items: Object, + disabledItems: Object, + onUpdate: Function, + }; + + setup() { + const configuration = this.props.items.map((item) => { + return { + ...item, + enabled: !this.props.disabledItems.includes(item.id), + }; + }); + this.state = useState({ items: configuration }); + } + + toggleItem(id) { + const item = this.state.items.find(i => i.id === id); + if (item) { + item.enabled = !item.enabled; + } + } + + applyFilters() { + const disabledIds = this.state.items + .filter(item => !item.enabled) + .map(item => item.id); + + browser.localStorage.setItem("disabledDashboardItems", JSON.stringify(disabledIds)); + + this.props.onUpdate(disabledIds); + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/components/filter_dialog/filter_dialog.xml b/awesome_dashboard/static/src/dashboard/components/filter_dialog/filter_dialog.xml new file mode 100644 index 00000000000..4d2df2e6a2d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/filter_dialog/filter_dialog.xml @@ -0,0 +1,19 @@ + + + + +
+ + + + + +
+ + + +
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/components/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.js new file mode 100644 index 00000000000..62de1c24bb4 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.js @@ -0,0 +1,11 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + + static props = { + title: String, + value: String, + }; + +} diff --git a/awesome_dashboard/static/src/dashboard/components/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.xml new file mode 100644 index 00000000000..4e0c9f35514 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.xml @@ -0,0 +1,13 @@ + + + + +
+
+
+

+

+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.js new file mode 100644 index 00000000000..d903e3cd7ec --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.js @@ -0,0 +1,74 @@ +import { Component, onWillStart, useRef, onMounted } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + ordersBySize: Object, + }; + + setup() { + this.chartRef = useRef("chart"); + + // Load chartjs in onWillStart using loadjs function to load /web/static/lib/Chart/Chart.js + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + onMounted(() => { + this.renderChart(); + }); + } + + renderChart() { + const ctx = this.chartRef.el.getContext('2d'); + + // Prepare data from props + const labels = Object.keys(this.props.ordersBySize); + const data = Object.values(this.props.ordersBySize); + + // Create pie chart + new Chart(ctx, { + type: 'pie', + data: { + labels: labels, + datasets: [{ + label: 'Orders by Size', + data: data, + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)', + 'rgba(255, 159, 64, 0.8)', + 'rgba(201, 203, 207, 0.8)', + ], + borderColor: [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)', + 'rgba(201, 203, 207, 1)', + ], + borderWidth: 1 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + }, + title: { + display: true, + text: 'Orders by Size' + } + } + } + }); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..8362d2dba65 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/components/pie_chart_card/pie_chart_card.js new file mode 100644 index 00000000000..924abf3134c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/pie_chart_card/pie_chart_card.js @@ -0,0 +1,13 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + + static components = { PieChart }; + + static props = { + title: String, + ordersBySize: Object, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/components/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..f195afbe4b6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,13 @@ + + + + +
+
+
+ +
+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..891daca29e9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,55 @@ +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout" +import { useService } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; +import { DashboardItem } from "./components/dashboard_item/dashboard_item"; +import { PieChart } from "./components/pie_chart/pie_chart"; +import { NumberCard } from "./components/number_card/number_card"; +import { PieChartCard } from "./components/pie_chart_card/pie_chart_card"; +import { browser } from "@web/core/browser/browser"; +import { FilterDialogue } from "./components/filter_dialog/filter_dialog"; + + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { Layout, DashboardItem, PieChart, NumberCard, PieChartCard }; + + setup() { + this.items = registry.category("awesome_dashboard").getAll(); + this.action = useService("action"); + const statisticsService = useService("statistics"); + this.statistics = useState(statisticsService.statistics); + this.dialog = useService("dialog"); + this.state = useState({ + disabledItems: JSON.parse(browser.localStorage.getItem("disabledDashboardItems") || "[]"), + }); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads(e) { + this.action.doAction({ + type: 'ir.actions.act_window', // The Action we will do + name: _t('Leads'), // The name that will be in the breadcrumb + res_model: 'crm.lead', // The model that we will open the view of + views: [[false, 'list'], [false, 'form']], // [choose if their is a specific view id we want to choose, the type of view we are choosing] + search_view_id: [false], // chooses the search view we want to use. + }); + } + + openFilterDialog() { + this.dialog.add(FilterDialogue, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdate: (disabledIds) => { + this.state.disabledItems = disabledIds; + }, + }); + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..769fc1e72f9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} \ 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..0b6b36ddc6b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + +
+

Awesome Dashboard

+
+ + + + + + +
+
+
+
+
\ No newline at end of file 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..b2c065b6d85 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,67 @@ +import { registry } from "@web/core/registry"; +import { NumberCard } from "./components/number_card/number_card"; +import { PieChart } from "./components/pie_chart/pie_chart"; + +const awesomeDashboardRegistry = registry.category("awesome_dashboard"); + +const items = [ + { + id: "average_quantity", + description: "Average Quantity", + Component: NumberCard, + props: (data) => ({ + title: "Average Quantity", + value: data.average_quantity + }), + }, + { + id: "average_time", + description: "Average Time", + Component: NumberCard, + props: (data) => ({ + title: "Average Time", + value: data.average_time + }), + }, + { + id: "nb_cancelled_orders", + description: "Cancelled Orders", + Component: NumberCard, + props: (data) => ({ + title: "Cancelled Orders", + value: data.nb_cancelled_orders + }), + }, + { + id: "nb_new_orders", + description: "New Orders", + Component: NumberCard, + props: (data) => ({ + title: "New Orders", + value: data.nb_new_orders + }), + }, + { + id: "total_amount", + description: "Total Amount", + Component: NumberCard, + props: (data) => ({ + title: "Total Amount", + value: data.total_amount + }), + }, + { + id: "orders_by_size", + description: "Orders by Size", + Component: PieChart, + size: 2, + props: (data) => ({ + title: "Orders by Size", + ordersBySize: data.orders_by_size + }), + }, +]; + +items.forEach(item => { + awesomeDashboardRegistry.add(item.id, item); +}); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..59bd191e44d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,12 @@ +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); \ No newline at end of file diff --git a/awesome_dashboard/static/src/network/statistics_service.js b/awesome_dashboard/static/src/network/statistics_service.js new file mode 100644 index 00000000000..be57b73cb45 --- /dev/null +++ b/awesome_dashboard/static/src/network/statistics_service.js @@ -0,0 +1,23 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +export const statisticsService = { + start() { + const statistics = reactive({}); + + async function loadStatistics() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updates); + } + + setInterval(loadStatistics, 10000); + loadStatistics(); + + return { + statistics + }; + } +}; + +registry.category("services").add("statistics", statisticsService); \ No newline at end of file diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 55002ab81de..beb6457de4c 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -33,6 +33,7 @@ 'web/static/src/scss/pre_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_maps.scss', + ('include', 'web._assets_bootstrap_backend'), ('include', 'web._assets_bootstrap'), ('include', 'web._assets_core'), 'web/static/src/libs/fontawesome/css/font-awesome.css', diff --git a/awesome_owl/static/src/components/card/card.js b/awesome_owl/static/src/components/card/card.js new file mode 100644 index 00000000000..446001901cd --- /dev/null +++ b/awesome_owl/static/src/components/card/card.js @@ -0,0 +1,23 @@ +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({renderContent: true}); + } + + toggleContent(){ + this.state.renderContent = !this.state.renderContent; + } + +} \ No newline at end of file diff --git a/awesome_owl/static/src/components/card/card.xml b/awesome_owl/static/src/components/card/card.xml new file mode 100644 index 00000000000..cb5e9a6ad47 --- /dev/null +++ b/awesome_owl/static/src/components/card/card.xml @@ -0,0 +1,14 @@ + + + + +
+
+ +
+ +
+
+
+ +
diff --git a/awesome_owl/static/src/components/counter/counter.js b/awesome_owl/static/src/components/counter/counter.js new file mode 100644 index 00000000000..568a6ac7f2f --- /dev/null +++ b/awesome_owl/static/src/components/counter/counter.js @@ -0,0 +1,21 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + static props = { + onChange: {type: Function, optional: true}, + }; + + static defaultProps = { + onChange: () => {}, + }; + + setup () { + this.state = useState({value: 0}); + } + + increment () { + this.state.value++; + this.props.onChange(); + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/components/counter/counter.xml b/awesome_owl/static/src/components/counter/counter.xml new file mode 100644 index 00000000000..f05774d994f --- /dev/null +++ b/awesome_owl/static/src/components/counter/counter.xml @@ -0,0 +1,11 @@ + + + + +
+ Counter: + +
+
+ +
diff --git a/awesome_owl/static/src/components/todo_list/todo_item.js b/awesome_owl/static/src/components/todo_list/todo_item.js new file mode 100644 index 00000000000..66eab045225 --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_item.js @@ -0,0 +1,27 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + + static props = { + todo: { + type: Object, + shape: { + id: Number, + title: String, + isCompleted: Boolean, + } + }, + toggleTodo: Function, + deleteTodo: Function + }; + + onToggle() { + this.props.toggleTodo(this.props.todo.id); + } + + onDelete() { + this.props.deleteTodo(this.props.todo.id); + } + +} diff --git a/awesome_owl/static/src/components/todo_list/todo_item.xml b/awesome_owl/static/src/components/todo_list/todo_item.xml new file mode 100644 index 00000000000..e20447a4800 --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_item.xml @@ -0,0 +1,16 @@ + + + + +
+ +
+
+ +
diff --git a/awesome_owl/static/src/components/todo_list/todo_list.js b/awesome_owl/static/src/components/todo_list/todo_list.js new file mode 100644 index 00000000000..a2be5285ca8 --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_list.js @@ -0,0 +1,42 @@ +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.TodoList"; + + static components = { TodoItem }; + + setup () { + this.todos = useState([]); + this.ids = useState({"last_id": 0}); + useAutoFocus("input"); + } + + addTodo (ev) { + if (ev.keyCode === 13 && ev.target.value) { + this.ids.last_id += 1 + this.todos.push({ + id: this.ids.last_id, + title: ev.target.value, + isCompleted: false, + }); + ev.target.value = ""; + } + } + + toggleTodo(todo_id) { + const index = this.todos.findIndex(t => t.id === todo_id); + if (index !== -1) { + this.todos[index].isCompleted = !this.todos[index].isCompleted; + } + } + + deleteTodo(todo_id){ + const index = this.todos.findIndex(t => t.id === todo_id); + if (index !== -1) { + this.todos.splice(index, 1); + } + } + +} diff --git a/awesome_owl/static/src/components/todo_list/todo_list.xml b/awesome_owl/static/src/components/todo_list/todo_list.xml new file mode 100644 index 00000000000..d176f06d25f --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_list.xml @@ -0,0 +1,14 @@ + + + + +
+

Todo List

+ + + + +
+
+ +
diff --git a/awesome_owl/static/src/components/utils.js b/awesome_owl/static/src/components/utils.js new file mode 100644 index 00000000000..19e0d7b8f79 --- /dev/null +++ b/awesome_owl/static/src/components/utils.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutoFocus (refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} \ No newline at end of file diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..6c108687e29 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -9,4 +9,3 @@ const config = { // Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); - diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..a18bb42c5a6 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,21 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./components/counter/counter"; +import { Card } from "./components/card/card"; +import { TodoList } from "./components/todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = { Counter, Card, TodoList }; + + value1 = "Some HTML content"; + value2 = markup("Some other HTML content"); + + setup () { + this.state = useState({sum: 0}); + } + + onCounterChange () { + this.state.sum += 1; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..c26120ccda1 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -3,7 +3,18 @@
- hello world +
+ + +

The sum is:

+
+ + Description for Card 1 + + + + +
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..83ad38c3a47 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,21 @@ + +{ + 'name': 'estate', + 'depends': [ + 'base', + ], + "author": "Karim Gamaleldin", + "license": "LGPL-3", + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offers_views.xml", + "views/res_users_views.xml", + "views/estate_property_menus.xml", + "views/estate_property_type_menus.xml", + "views/estate_property_tag_menus.xml", + ], + 'application': True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..cc8ae6435fb --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,122 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare, float_is_zero + + +def get_date_in_3_months() -> fields.Date: + ''' + This function calculates the date that is three months from the current date. + + returns: + A date object representing the date that is three months from today. + ''' + today_data = fields.Date.today() + three_months_later = fields.Date.add(today_data, months=3) + return three_months_later + + +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(copy=False, default=lambda self: get_date_in_3_months()) + 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() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ]) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled'), + ], default="new", required=True, copy=False) + active = fields.Boolean(default=True) + + total_area = fields.Integer(compute="_compute_total_area") + best_offer = fields.Float(compute="_compute_best_offer", string="Best Offer") + + 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") + + _check_expected_price_pos = models.Constraint( + 'CHECK(expected_price > 0)', + 'The expected price must be strictly positive.' + ) + + _check_selling_price_pos = models.Constraint( + 'CHECK(selling_price >= 0)', + 'The selling price cannot be negative.' + ) + + @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_offer(self): + for record in self: + # If the property has offers, the best offer is the maximum of the expected prices of the offers. Otherwise, it is 0. + if record.offer_ids: + record.best_offer = max(record.offer_ids.mapped("price")) + else: + record.best_offer = 0.0 + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False + + @api.constrains("selling_price", "expected_price") + def _check_selling_price_vs_expected_price(self): + for record in self: + if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0: + raise UserError("The selling price cannot be less than 90% of the expected price.") + + def action_set_sold(self): + for record in self: + if record.state == "canceled": + raise UserError("Canceled properties cannot be sold.") + record.state = "sold" + return True + + def action_set_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("Sold properties cannot be canceled.") + record.state = "canceled" + return True + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_canceled(self): + for record in self: + if record.state not in ("new", "canceled"): + raise UserError("Only properties in 'new' or 'canceled' state can be deleted.") + + def check_has_higher_offer(self, price): + for offer in self.offer_ids: + if float_compare(offer.price, price, precision_digits=2) > 0: + return True + return False diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..2c1b50971de --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,78 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + _order = 'price desc' + + price = fields.Float(string='Price') + status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False) + partner_id = fields.Many2one('res.partner', string='Partner') + property_id = fields.Many2one('estate.property', string='Property') + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') + + property_type_id = fields.Many2one( + 'estate.property.type', + related='property_id.property_type_id', + store=True + ) + + _check_offer_price = models.Constraint( + 'CHECK(price > 0)', + 'The offer price must be strictly positive.' + ) + + @api.depends('validity', 'create_date') + def _compute_date_deadline(self): + for offer in self: + start = fields.Date.to_date(offer.create_date) or fields.Date.today() + offer.date_deadline = fields.Date.add(start, days=offer.validity) + + def _inverse_date_deadline(self): + for offer in self: + start = fields.Date.to_date(offer.create_date) or fields.Date.today() + if offer.date_deadline and start: + delta = offer.date_deadline - start + offer.validity = delta.days + + def accept_offer(self): + for offer in self: + # Check no other offer has been accepted for the same property + if offer.property_id.offer_ids.filtered(lambda o: o.status == 'accepted'): + raise UserError('Another offer has already been accepted for this property.') + + # Accept the offer + offer.status = 'accepted' + offer.property_id.selling_price = offer.price + offer.property_id.state = 'offer_accepted' + offer.property_id.buyer_id = offer.partner_id + + # Refuse all other offers for the same property + other_offers = offer.property_id.offer_ids.filtered(lambda o: o.id != offer.id and o.status != 'refused') + for other_offer in other_offers: + other_offer.status = 'refused' + + def refuse_offer(self): + for offer in self: + offer.status = 'refused' + if offer.property_id.state != 'offer_accepted': + offer.property_id.state = 'offer_received' + + @api.model + def create(self, vals_list): + # Check that the offer price is higher than the current best offer for the property + for val in vals_list: + property_id = val.get('property_id') + has_higher_offer = self.env['estate.property'].browse(property_id).check_has_higher_offer(val.get('price', 0)) + if has_higher_offer: + raise UserError('The offer price must be higher than the current best offer for the property.') + + # Create the offer + offer = super().create(vals_list) + if offer.property_id.state == 'new': + offer.property_id.state = 'offer_received' + + return offer diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..2d2cbfe7219 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag Model" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer(default=1) + + _check_property_tag_name_unique = models.Constraint( + 'UNIQUE(name)', + 'The name of the property tag must be unique.' + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..de28038b838 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,25 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type Model" + _order = "sequence, name" + + name = fields.Char(required=True) + sequence = fields.Integer(default=1, help="Used to order the property types. Lower numbers are displayed first.") + + property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") + offers_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers") + + offers_count = fields.Integer(string="Offers Count", compute="_compute_offers_count") + + _check_property_type_name_unique = models.Constraint( + 'UNIQUE(name)', + 'The name of the property type must be unique.' + ) + + @api.depends("offers_ids") + def _compute_offers_count(self): + for record in self: + record.offers_count = len(record.offers_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..74cc2697b67 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "salesperson_id", string="Estate Properties", domain=[('state', 'in', ['new', 'offer_received'])]) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..c79331f2f1c --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml new file mode 100644 index 00000000000..0c17c9a9381 --- /dev/null +++ b/estate/views/estate_property_menus.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..cea3c589946 --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,39 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + + + estate.property.offer.list + estate.property.offer + + + + + + + + +

+ +

+ + + + + + + + + +
+
+ + + estate.property.type.list + estate.property.type + + + + + + + + + + + Property Types + estate.property.type + list,form + +

+ Define a new estate property type. +

+ You can specify the name and description of each property type. +

+
+
+ +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..edb2314e2d1 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,144 @@ + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+ +
+
+ Expected Price: +
+
+ Best Offer: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+ + + Properties + estate.property + list,form,kanban + {'search_default_available': 1} + +

+ Define a new estate property to sell or rent. +

+ You can specify the expected price and the selling/renting price, the buyer/tenant, the salesperson, and the status of the property. +

+
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..dd7b812809f --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,21 @@ + + + + estate.salesperson.form + res.users + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..089740e1bdf --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ + +{ + 'name': 'estate_account', + 'depends': [ + 'base', + 'estate', + 'account', + ], + "author": "Karim Gamaleldin", + "license": "LGPL-3", + "data": [ + ], + 'application': True, +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..15c9468a53e --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,26 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def action_set_sold(self): + partner_id = self.buyer_id.id + move_type = 'out_invoice' + property_fee_invoice = {'name': self.name, 'quantity': 1, 'price_unit': self.selling_price * 0.06} + adminstration_fee_invoice = { + 'name': "Administration fees", + 'quantity': 1, + 'price_unit': 100, + } + + self.env['account.move'].create({ + 'move_type': move_type, + 'partner_id': partner_id, + "line_ids": [ + Command.create(property_fee_invoice), + Command.create(adminstration_fee_invoice), + ] + }) + + return super().action_set_sold()