diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index 31406e8addb..c50b9e0c067 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -1,30 +1,28 @@
# -*- coding: utf-8 -*-
{
- 'name': "Awesome Dashboard",
-
- 'summary': """
+ "name": "Awesome Dashboard",
+ "summary": """
Starting module for "Discover the JS framework, chapter 2: Build a dashboard"
""",
-
- 'description': """
+ "description": """
Starting module for "Discover the JS framework, chapter 2: Build a dashboard"
""",
-
- 'author': "Odoo",
- 'website': "https://www.odoo.com/",
- 'category': 'Tutorials/AwesomeDashboard',
- 'version': '0.1',
- 'application': True,
- 'installable': True,
- 'depends': ['base', 'web', 'mail', 'crm'],
-
- 'data': [
- 'views/views.xml',
+ "author": "Odoo",
+ "website": "https://www.odoo.com/",
+ "category": "Tutorials/AwesomeDashboard",
+ "version": "0.1",
+ "application": True,
+ "installable": True,
+ "depends": ["base", "web", "mail", "crm"],
+ "data": [
+ "views/views.xml",
],
- 'assets': {
- 'web.assets_backend': [
- 'awesome_dashboard/static/src/**/*',
+ "assets": {
+ "web.assets_backend": [
+ "awesome_dashboard/static/src/**/*",
+ ("remove", "awesome_dashboard/static/src/dashboard/**/*"),
],
+ "awesome_dashboard.dashboard": ["awesome_dashboard/static/src/dashboard/**/*"],
},
- 'license': 'AGPL-3'
+ "license": "AGPL-3",
}
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..54dec402fda
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,101 @@
+/** @odoo-module **/
+
+import { _t } from "@web/core/l10n/translation";
+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 { DashboardItem } from "./dashboard_item/dashboard_item";
+import { Dialog } from "@web/core/dialog/dialog";
+import { CheckBox } from "@web/core/checkbox/checkbox";
+import { browser } from "@web/core/browser/browser";
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem };
+
+ setup() {
+ this.display = {
+ controlPanel: {},
+ };
+ this.action = useService("action");
+
+ this.orders_details = useState(useService("awesome_dashboard.statistics"));
+
+ this.items = registry.category("awesome_dashboard").getAll();
+
+ this.dialog = useService("dialog");
+
+ this.state = useState({
+ disabledItems:
+ browser.localStorage.getItem("disabledDashboardItems")?.split(",") ||
+ [],
+ });
+ }
+
+ openConfiguration() {
+ this.dialog.add(ConfigurationDialog, {
+ items: this.items,
+ disabledItems: this.state.disabledItems,
+ onUpdateConfiguration: this.updateConfiguration.bind(this),
+ });
+ }
+
+ updateConfiguration(newDisabledItems) {
+ this.state.disabledItems = newDisabledItems;
+ }
+
+ showCustomers() {
+ this.action.doAction("base.action_partner_form");
+ }
+
+ showLeads() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: _t("Leads"),
+ target: "current",
+ res_model: "crm.lead",
+ views: [
+ [false, "list"],
+ [false, "form"],
+ ],
+ });
+ }
+}
+
+class ConfigurationDialog extends Component {
+
+ static template = "awesome_dashboard.ConfigurationDialog";
+ static components = { Dialog, CheckBox };
+ static props = ["close", "items", "disabledItems", "onUpdateConfiguration"];
+
+ setup() {
+ this.items = useState(this.props.items.map((item) => {
+ return {
+ ...item,
+ enabled: !this.props.disabledItems.includes(item.id),
+ }
+ }));
+ }
+
+ done() {
+ this.props.close();
+ }
+
+ onChange(checked, changedItem) {
+ changedItem.enabled = checked;
+ const newDisabledItems = Object.values(this.items).filter(
+ (item) => !item.enabled
+ ).map((item) => item.id)
+
+ browser.localStorage.setItem(
+ "disabledDashboardItems",
+ newDisabledItems,
+ );
+
+ this.props.onUpdateConfiguration(newDisabledItems);
+ }
+
+}
+
+registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss
new file mode 100644
index 00000000000..6be5e0f83c9
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.scss
@@ -0,0 +1,3 @@
+.o_dashboard {
+ background-color: gray;
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..c67bbfe6b83
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..4644048af21
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js
@@ -0,0 +1,18 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.DashboardItem";
+ static props = {
+ slots: {
+ type: Object,
+ shape: {
+ default: Object,
+ },
+ },
+ size: {
+ type: Number,
+ default: 1,
+ optional: true,
+ },
+ };
+}
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..56b1b832a0f
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
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..300e8bbd53e
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,65 @@
+import { NumberCard } from "./number_card/number_card";
+import { PieChartCard } from "./pie_chart_card/pie_chart_card";
+import { registry } from "@web/core/registry";
+
+const items = [
+ {
+ id: "average_quantity",
+ description: "Average amount of t-shirt",
+ Component: NumberCard,
+ 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",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ value: data.average_time,
+ })
+ },
+ {
+ id: "number_new_orders",
+ description: "New orders this month",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Number of new orders this month",
+ value: data.nb_new_orders,
+ })
+ },
+ {
+ id: "cancelled_orders",
+ description: "Cancelled orders this month",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Number of cancelled orders this month",
+ value: data.nb_cancelled_orders,
+ })
+ },
+ {
+ id: "amount_new_orders",
+ description: "amount orders this month",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Total amount of new orders this month",
+ value: data.total_amount,
+ })
+ },
+ {
+ id: "pie_chart",
+ description: "Shirt orders by size",
+ Component: PieChartCard,
+ size: 2,
+ props: (data) => ({
+ title: "Shirt orders by size",
+ values: data.orders_by_size,
+ })
+ }
+]
+
+items.forEach(item => {
+ registry.category("awesome_dashboard").add(item.id, item);
+});
diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js
new file mode 100644
index 00000000000..d3bd9c0e4ef
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js
@@ -0,0 +1,13 @@
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.NumberCard";
+ static props = {
+ title: {
+ type: String,
+ },
+ value: {
+ type: Number,
+ }
+ }
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml
new file mode 100644
index 00000000000..3a0713623fa
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
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..63b0870e620
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js
@@ -0,0 +1,37 @@
+import { Component, onWillStart, useState, useRef, onMounted, onWillUnmount } from "@odoo/owl";
+import { loadJS } from "@web/core/assets";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.PieChart";
+ static props = {
+ 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);
+ this.chart = new Chart(this.canvasRef.el, {
+ type: "pie",
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: this.props.label,
+ data: data,
+ },
+ ],
+ },
+ });
+ }
+}
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..4f3c54a6c15
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js
new file mode 100644
index 00000000000..3faac175fed
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js
@@ -0,0 +1,15 @@
+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: {
+ type: String,
+ },
+ values: {
+ type: Object,
+ },
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml
new file mode 100644
index 00000000000..58a6811c83a
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
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..95c9b53d3e2
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/statistics_service.js
@@ -0,0 +1,36 @@
+/** @odoo-module **/
+import { rpc } from "@web/core/network/rpc";
+import { registry } from "@web/core/registry";
+import { reactive } from "@odoo/owl";
+
+
+export const statisticsService = {
+
+ start() {
+ const statistics = reactive({ isLoading: false });
+
+ const loadStatistics = async () => {
+ statistics.isLoading = true;
+ try {
+ const updated_value = await rpc("/awesome_dashboard/statistics");
+ Object.assign(statistics, updated_value, {isLoading: false});
+ } catch (error) {
+ console.error("Failed to load statistics:", error);
+ statistics.isLoading = false;
+ }
+ };
+
+ // Initial Load
+ loadStatistics();
+
+ // Auto-refresh every 10 seconds
+ setInterval(loadStatistics, 100000);
+
+
+ return statistics;
+ },
+};
+
+registry
+ .category("services")
+ .add("awesome_dashboard.statistics", statisticsService);
diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js
new file mode 100644
index 00000000000..a5bdc15e1e9
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_loader.js
@@ -0,0 +1,13 @@
+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);
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..3e85928e555
--- /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: { type: String, optional: true },
+ slots: {
+ type: Object,
+ optional: true,
+ },
+ };
+
+ setup() {
+ this.state = useState({
+ isCounterOpen: false,
+ });
+ }
+
+ toggleCounter() {
+ this.state.isCounterOpen = !this.state.isCounterOpen;
+ }
+}
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..c1816078162
--- /dev/null
+++ b/awesome_owl/static/src/components/card/card.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
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..1bb8a03a4cf
--- /dev/null
+++ b/awesome_owl/static/src/components/counter/counter.js
@@ -0,0 +1,20 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "awesome_owl.counter";
+
+ static props = {
+ callbackIncrement: { type: Function, optional: true },
+ };
+
+ setup() {
+ this.state = useState({ value: 0 });
+ }
+
+ increment() {
+ this.state.value += 1;
+ if (this.props.callbackIncrement) {
+ this.props.callbackIncrement();
+ }
+ }
+}
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..68c4ec1f15f
--- /dev/null
+++ b/awesome_owl/static/src/components/counter/counter.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/components/todo/todo_item.js b/awesome_owl/static/src/components/todo/todo_item.js
new file mode 100644
index 00000000000..b478e66bbfc
--- /dev/null
+++ b/awesome_owl/static/src/components/todo/todo_item.js
@@ -0,0 +1,19 @@
+import { Component } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.todo_item";
+
+ static props = {
+ todo_item: { type: Object },
+ callbackToggleState: { type: Function },
+ callbackRemoveTodo: { type: Function },
+ };
+
+ removeTodo = (removeTodoId) => {
+ this.props.callbackRemoveTodo(removeTodoId);
+ };
+
+ toggleState = (todoId) => {
+ this.props.callbackToggleState(todoId);
+ };
+}
diff --git a/awesome_owl/static/src/components/todo/todo_item.xml b/awesome_owl/static/src/components/todo/todo_item.xml
new file mode 100644
index 00000000000..8a922365afc
--- /dev/null
+++ b/awesome_owl/static/src/components/todo/todo_item.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/components/todo/todo_list.js b/awesome_owl/static/src/components/todo/todo_list.js
new file mode 100644
index 00000000000..84d57842609
--- /dev/null
+++ b/awesome_owl/static/src/components/todo/todo_list.js
@@ -0,0 +1,49 @@
+import { Component, useState, useRef } 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 };
+ static props = {};
+
+ setup() {
+ this.todos = useState([]);
+ this.todoCounterId = 0;
+
+ this.inputRef = useRef("inputRef");
+
+ useAutofocus(this.inputRef);
+ }
+
+ addTodo(event) {
+ if (event.keyCode == 13) {
+ const newTask = event.target.value.trim();
+
+ if (newTask) {
+ this.todos.push({
+ id: this.todoCounterId,
+ description: newTask,
+ isCompleted: false,
+ });
+ this.todoCounterId++;
+ event.target.value = "";
+ }
+ }
+ this.todos.push();
+ }
+
+ removeTodo = (removeTodoId) => {
+ const index = this.todos.findIndex((todo) => todo.id === removeTodoId);
+ if (index !== -1) {
+ this.todos.splice(index, 1);
+ }
+ };
+
+ toggleTodoState(todoId) {
+ const todo = this.todos.find((t) => t.id === todoId);
+ if (todo) {
+ todo.isCompleted = !todo.isCompleted;
+ }
+ }
+}
diff --git a/awesome_owl/static/src/components/todo/todo_list.xml b/awesome_owl/static/src/components/todo/todo_list.xml
new file mode 100644
index 00000000000..aa190a7088c
--- /dev/null
+++ b/awesome_owl/static/src/components/todo/todo_list.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
Todo List
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 657fb8b07bb..8a782a6c624 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,7 +1,22 @@
/** @odoo-module **/
-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/todo_list";
export class Playground extends Component {
- static template = "awesome_owl.playground";
+ static template = "awesome_owl.playground";
+ static components = { Counter, Card, TodoList };
+ static props = {};
+
+ setup() {
+ this.state = useState({ sum: 0 });
+ }
+
+ card1ContentValue = markup("Some Content
");
+
+ incrementSum() {
+ this.state.sum += 1;
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..f25370ccf37 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -1,10 +1,36 @@
-
+
-
-
- hello world
+
+
+
Counter
+
+
+
+
+
+
+
+
Total Sum:
+
+
+
+
+
+
Cards
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..7c0fceee1ee
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,15 @@
+import { onMounted } from "@odoo/owl";
+
+/**
+ * Automatically focuses on the provided reference element when the component is mounted.
+ *
+ * @param {Object} ref - The reference object to the DOM element.
+ * @returns {void}
+ */
+export const useAutofocus = (ref) => {
+ onMounted(() => {
+ if (ref?.el) {
+ ref.el.focus();
+ }
+ });
+};
diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 00000000000..720587b971a
--- /dev/null
+++ b/estate/__init__.py
@@ -0,0 +1,4 @@
+from . import models
+from . import controllers
+from . import tests
+from . import wizard
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
new file mode 100644
index 00000000000..377e313d96b
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,33 @@
+{
+ 'name': 'Estate',
+ 'description': 'Estate Module',
+ 'version': '1.0',
+ 'depends': ['base','mail', 'website'],
+ 'author': 'Shiv Bhadaniya',
+ 'application': True,
+ 'installable': True,
+ 'license': 'LGPL-3',
+ 'category': 'Real Estate/Brokerage',
+ 'data': [
+ 'security/estate_security.xml',
+ 'security/ir.model.access.csv',
+ 'data/estate.property.type.csv',
+ 'wizard/estate_property_offer_wizard_views.xml',
+ 'views/estate_property_views.xml',
+ 'report/estate_property_offer_subtemplate.xml',
+ 'report/estate_property_templates.xml',
+ 'report/estate_property_offer_res_user_template.xml',
+ 'report/estate_property_reports.xml',
+ 'views/estate_available_property_view.xml',
+ 'views/estate_property_tags_views.xml',
+ 'views/estate_property_offers_views.xml',
+ 'views/estate_property_types_views.xml',
+ 'views/estate_res_users_views.xml',
+ 'demo/estate_property_demo.xml',
+ 'demo/estate_property_website.xml',
+ 'views/estate_menus.xml',
+ ],
+ 'demo': [
+ 'demo/estate_property_demo.xml',
+ ],
+}
diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py
new file mode 100644
index 00000000000..59d782585f3
--- /dev/null
+++ b/estate/controllers/__init__.py
@@ -0,0 +1 @@
+from . import estate_available_property
\ No newline at end of file
diff --git a/estate/controllers/estate_available_property.py b/estate/controllers/estate_available_property.py
new file mode 100644
index 00000000000..70d0f5e7f86
--- /dev/null
+++ b/estate/controllers/estate_available_property.py
@@ -0,0 +1,50 @@
+from odoo import http
+from odoo.http import request
+
+class EstateAvailableProperty(http.Controller):
+
+ @http.route(['/estate/available_property', '/estate/available_property/page/
'], type='http', auth='public', methods=['GET'], website=True)
+ def available_property(self, page=1, **kw):
+ """
+ Render a list of available properties with pagination.
+ """
+ Property = request.env['estate.property']
+
+ domain = [('state', 'in', ['new', 'offer_received'])]
+ listed_after = kw.get('listed_after')
+ if listed_after:
+ domain.append(('create_date', '>=', listed_after))
+
+ property_count = Property.search_count(domain)
+ url_args = {'listed_after': listed_after} if listed_after else {}
+
+ pager = request.website.pager(
+ url="/estate/available_property",
+ total=property_count,
+ page=page,
+ step=6,
+ url_args=url_args
+ )
+
+ properties = Property.search(domain, limit=6, offset=pager['offset'])
+
+ return request.render('estate.available_property_listing', {
+ 'properties': properties,
+ 'page_name': 'properties',
+ 'default_url': '/estate/available_property',
+ 'pager': pager,
+ 'listed_after': listed_after or False,
+ })
+
+
+ @http.route('/estate/available_property_details/', type='http', auth='public', website=True)
+ def property_details(self, id, **kw):
+ Property = request.env['estate.property']
+ property = Property.browse(id)
+
+ if not property:
+ return request.not_found()
+
+ return request.render('estate.available_property_details', {
+ 'property': property,
+ })
diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv
new file mode 100644
index 00000000000..15fab584b56
--- /dev/null
+++ b/estate/data/estate.property.type.csv
@@ -0,0 +1,5 @@
+"id","name"
+estate_property_type_1,"Residential"
+estate_property_type_2,"Commercial"
+estate_property_type_3,"Industrial",
+estate_property_type_4,"Land"
diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml
new file mode 100644
index 00000000000..245eef85359
--- /dev/null
+++ b/estate/demo/estate_property_demo.xml
@@ -0,0 +1,276 @@
+
+
+
+
+
+ Big Villa
+ A nice and big villa
+ 12345
+ new
+ True
+ 10
+ 6
+ 100
+ 4
+ True
+ True
+ 100000
+ north
+
+
+
+
+ Luxury Mansion
+ An elegant and luxurious mansion
+ 54321
+ new
+ True
+ 25
+ 8
+ 250
+ 6
+ True
+ True
+ 50000
+ south
+
+
+
+
+ Modern Apartment
+ A stylish and modern apartment
+ 67890
+ new
+ True
+ 15
+ 3
+ 90
+ 2
+ False
+ False
+ 0
+ east
+
+
+
+
+ Countryside Cottage
+ A cozy cottage in the countryside
+ 13579
+ new
+ True
+ 8
+ 4
+ 120
+ 3
+ True
+ True
+ 20000
+ west
+
+
+
+
+ Skyline Penthouse
+ A penthouse with a breathtaking skyline view
+ 24680
+ new
+ True
+ 30
+ 5
+ 180
+ 2
+ False
+ False
+ 0
+ north
+
+
+
+
+ Seaside Bungalow
+ A relaxing bungalow by the sea
+ 11223
+ new
+ True
+ 12
+ 4
+ 110
+ 3
+ True
+ True
+ 15000
+ south
+
+
+
+
+ Mountain Retreat
+ A peaceful retreat in the mountains
+ 33445
+ new
+ True
+ 20
+ 6
+ 160
+ 4
+ True
+ True
+ 40000
+ west
+
+
+
+
+ Lake House
+ A beautiful house by the lake
+ 55667
+ new
+ True
+ 18
+ 5
+ 140
+ 3
+ True
+ True
+ 30000
+ east
+
+
+
+
+ Urban Loft
+ A modern loft in the city
+ 66778
+ new
+ True
+ 22
+ 2
+ 85
+ 1
+ False
+ False
+ 0
+ north
+
+
+
+
+ Desert Villa
+ A luxurious villa in the desert
+ 77889
+ new
+ True
+ 35
+ 7
+ 200
+ 5
+ True
+ True
+ 70000
+ south
+
+
+
+
+ Eco Cabin
+ A sustainable eco-friendly cabin
+ 88990
+ new
+ True
+ 14
+ 3
+ 95
+ 2
+ False
+ True
+ 25000
+ west
+
+
+
+
+ Cliffside Retreat
+ A serene retreat on the cliffs
+ 99001
+ new
+ True
+ 28
+ 4
+ 150
+ 3
+ True
+ True
+ 35000
+ north
+
+
+
+
+ Seaside Bungalow
+ A cozy bungalow by the sea
+ 10101
+ new
+ True
+ 20
+ 3
+ 120
+ 2
+ True
+ True
+ 28000
+ east
+
+
+
+
+ Hilltop Mansion
+ A grand mansion on the hills
+ 20202
+ new
+ True
+ 50
+ 8
+ 300
+ 6
+ True
+ True
+ 80000
+ south
+
+
+
+
+ Trailer home
+ Home in a tariler park
+ 54321
+ True
+ new
+ 10
+ 1
+ 10
+ 4
+ False
+ True
+ 3
+ south
+
+
+
+
diff --git a/estate/demo/estate_property_demo_offers.xml b/estate/demo/estate_property_demo_offers.xml
new file mode 100644
index 00000000000..6116ea3b036
--- /dev/null
+++ b/estate/demo/estate_property_demo_offers.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ 10000
+ 14
+
+
+
+
+
+
+ 1500000
+ 14
+
+
+
+
+
+
+ 1500001
+ 14
+
+
+
\ No newline at end of file
diff --git a/estate/demo/estate_property_website.xml b/estate/demo/estate_property_website.xml
new file mode 100644
index 00000000000..6999efd8c06
--- /dev/null
+++ b/estate/demo/estate_property_website.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..41393ab1e39
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,5 @@
+from . import estate_property
+from . import estate_property_offer
+from . import estate_property_tag
+from . import estate_property_type
+from . import res_users
\ No newline at end of file
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..8a19571d904
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,166 @@
+from datetime import datetime, 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 Model"
+ _inherit = ["mail.thread"]
+ _order = "id desc"
+
+ name = fields.Char(required=True, tracking=True)
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date(copy=False, default= datetime.now() + timedelta(days=90))
+ 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'),
+ ])
+ active = fields.Boolean(default=False)
+ state = fields.Selection([
+ ('new', 'New'),
+ ('offer_received', 'Offer Received'),
+ ('offer_accepted', 'Offer Accepted'),
+ ('sold', 'Sold'),
+ ('canceled', 'Canceled'),
+ ], copy=False, default='new', required=True, tracking=True)
+ image = fields.Image(string="Property Image")
+
+ # Many2one relationship
+ property_type_id = fields.Many2one('estate.property.type', string="Property Type")
+ buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False)
+ user_id = fields.Many2one('res.users', string="Salesperson", default=lambda self: self.env.user)
+
+ # Many2many relationship
+ tag_ids = fields.Many2many('estate.property.tag', string="Property Tags")
+
+ # One2many relationship
+ offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offer")
+
+ # Computed fields
+ total_area = fields.Integer(compute='_compute_total_area', store=True)
+
+ best_price = fields.Float(compute='_compute_best_price', store=True)
+
+ company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id)
+
+ # -------------------------------------------------------------------------
+ # COMPUTE METHODS
+ # -------------------------------------------------------------------------
+
+ @api.depends('living_area', 'garden_area')
+ def _compute_total_area(self):
+ for data in self:
+ data.total_area = data.living_area + data.garden_area
+
+ @api.depends('offer_ids')
+ def _compute_best_price(self):
+ for data in self:
+ prices = data.offer_ids.mapped('price')
+ if prices:
+ data.best_price = max(prices)
+ else:
+ data.best_price = 0.0
+
+
+
+ # -------------------------------------------------------------------------
+ # ONCHANGE METHODS
+ # -------------------------------------------------------------------------
+
+ @api.onchange('garden')
+ def _onchange_garden(self):
+ """ Set garden area to 10 and orientation to North when garden is True.
+ Reset them to empty when garden is False.
+ """
+ if self.garden:
+ self.garden_area = 10
+ self.garden_orientation = 'north'
+ else:
+ self.garden_area = 0
+ self.garden_orientation = False
+
+
+
+ # ------------------------------------------------------------
+ # ACTIONS
+ # ------------------------------------------------------------
+
+ def action_property_sold(self):
+ if self.state == 'canceled':
+ raise UserError("You cannot sell a canceled property")
+ else:
+ self.state = 'sold'
+
+ offers = self.offer_ids
+ any_offer_accept = False
+ for offer in offers:
+ if offer.status == 'accepted':
+ any_offer_accept = True
+ continue
+ if any_offer_accept == False:
+ raise ValidationError("At least one offer must be accepted before selling the property.")
+
+ self.active = False
+ return True
+
+ def action_property_cancel(self):
+ if self.state == 'sold':
+ raise UserError("You cannot cancel a sold property")
+ else:
+ self.state = 'canceled'
+
+ self.active = False
+ return True
+
+
+
+ # -------------------------------------------------------------------------
+ # SQL CONSTRAINTS QUERIES
+ # -------------------------------------------------------------------------
+
+ _sql_constraints = [
+ ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must be positive'),
+ ]
+
+ _sql_constraints = [
+ ('check_selling_price', 'CHECK(selling_price > 0)', 'The selling price must be positive'),
+ ]
+
+
+
+ # -------------------------------------------------------------------------
+ # CONSTRAINTS METHODS
+ # -------------------------------------------------------------------------
+
+ @api.constrains('selling_price', 'expected_price')
+ def _check_selling_price(self):
+ for record in self:
+ if float_is_zero(record.selling_price, precision_digits=2):
+ continue
+
+ minimum_price = record.expected_price * 0.9
+ if float_compare(record.selling_price, minimum_price, precision_digits=2) == -1:
+ raise ValidationError(f"The selling price cannot be lower than 90% of the expected price.")
+
+
+ # -------------------------------------------------------------------------
+ # CRUD METHODS
+ # -------------------------------------------------------------------------
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_if_not_new_or_cancelled(self):
+ for record in self:
+ if record.state not in ('new', 'canceled'):
+ raise UserError("You may only delete properties in state 'New' or 'Canceled'")
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..c29fa66344e
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,99 @@
+from datetime import datetime, timedelta
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Estate Property Offer"
+
+ _order = "price desc"
+ price = fields.Float(string="Offer Price")
+ status = fields.Selection([
+ ('accepted', 'Accepted'),
+ ('refused', 'Refused'),
+ ], string="Offer Status", 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 (in days)", default=7)
+ date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_validity", store=True)
+ create_date = fields.Date(readonly=True, default=fields.Date.today)
+ property_type_id = fields.Many2one('estate.property.type', string="Property Type", related='property_id.property_type_id', store=True) # related field: Automatically fetches the property type.
+
+
+ # -------------------------------------------------------------------------
+ # COMPUTE METHODS
+ # -------------------------------------------------------------------------
+
+ @api.depends('validity')
+ def _compute_date_deadline(self):
+ for record in self:
+ if record.validity is not None:
+ record.date_deadline = datetime.now().date() + timedelta(days=record.validity)
+ else:
+ # If validity is not set, set the deadline to 7 days
+ record.date_deadline = datetime.now().date() + timedelta(days=7)
+
+ def _inverse_validity(self):
+ for record in self:
+ if record.create_date and record.date_deadline:
+ record.validity = (record.date_deadline - record.create_date).days
+
+
+
+ # ------------------------------------------------------------
+ # ACTIONS
+ # ------------------------------------------------------------
+
+ def action_offer_accept(self):
+ for record in self:
+ record.status = "accepted"
+ record.property_id.state = "offer_accepted"
+ record.property_id.selling_price = record.price
+ record.property_id.buyer_id = record.partner_id
+
+ other_offers = self.env['estate.property.offer'].search([
+ ('property_id', '=', record.property_id.id),
+ ('id', '!=', record.id),
+ ('status', '!=', 'refused')
+ ])
+ other_offers.write({'status': 'refused'})
+ return True
+
+ def action_offer_refuse(self):
+ self.status = "refused"
+ return True
+
+
+ # -------------------------------------------------------------------------
+ # SQL CONSTRAINTS QUERIES
+ # -------------------------------------------------------------------------
+
+ _sql_constraints = [
+ ('check_price', 'CHECK(price > 0)', 'The price must be positive'),
+ ]
+
+ # -------------------------------------------------------------------------
+ # CRUD METHODS
+ # -------------------------------------------------------------------------
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+
+ # Sold Property can't make a new offer
+ property = self.env['estate.property'].browse(vals.get('property_id'))
+ if not property.exists():
+ raise UserError("The property you are referring to doesn't exist.")
+ elif property.state == 'sold':
+ raise UserError("You cannot create an offer for a sold property.")
+
+ offer_price = vals.get('price')
+ current_maximum_offer = self.search([('property_id', '=', vals['property_id'])], order="price desc", limit=1) # Fetch the current maximum offer for the property, already stored in the descending order of price.
+
+ if offer_price < current_maximum_offer.price:
+ raise UserError(f"The offer price must be higher than {current_maximum_offer.price}")
+ else:
+ offer_received_property = self.env['estate.property'].browse(vals.get('property_id'))
+ offer_received_property.state = "offer_received"
+
+ return super().create(vals_list)
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..c25ec2e1da0
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,19 @@
+from odoo import fields, models
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _description = "Estate Property Tags"
+
+ _order = "name"
+
+ name = fields.Char(string="Tag Name")
+
+ color = fields.Integer()
+
+ # -------------------------------------------------------------------------
+ # SQL CONSTRAINTS QUERIES
+ # -------------------------------------------------------------------------
+
+ _sql_constraints = [
+ ('unique_tag_name', 'UNIQUE(name)', 'The property tag name must be unique.')
+ ]
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..5a5365627fc
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,32 @@
+from odoo import api, fields, models
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "Estate Property Type"
+ _order = "name"
+
+ name = fields.Char(string="Type Name")
+ property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties")
+
+ offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers")
+ offer_count = fields.Integer(compute='_compute_offer_count')
+ sequence = fields.Integer()
+
+
+ # -------------------------------------------------------------------------
+ # SQL CONSTRAINTS QUERIES
+ # -------------------------------------------------------------------------
+
+ _sql_constraints = [
+ ('unique_type_name', 'UNIQUE(name)', 'The property type name must be unique.')
+ ]
+
+
+ # -------------------------------------------------------------------------
+ # COMPUTE METHODS
+ # -------------------------------------------------------------------------
+
+ @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..cd5454b1aab
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+class ResUsers(models.Model):
+ _name = 'res.users'
+ _inherit = 'res.users'
+
+ property_ids = fields.One2many('estate.property', 'user_id', string='Properties', domain = [("state" , "in" , ["new" , "offer_received"])])
diff --git a/estate/report/estate_property_offer_res_user_template.xml b/estate/report/estate_property_offer_res_user_template.xml
new file mode 100644
index 00000000000..e83a7da6500
--- /dev/null
+++ b/estate/report/estate_property_offer_res_user_template.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+ Salesman:
+
+
+
+
+
+
+ Expected Price:
+
+
+
+ Status:
+
+
+
+
+
+
+
+
+ There are no offers yet!!
+
+
+
+
+
+
+
+
+
diff --git a/estate/report/estate_property_offer_subtemplate.xml b/estate/report/estate_property_offer_subtemplate.xml
new file mode 100644
index 00000000000..ffa1f8e3f22
--- /dev/null
+++ b/estate/report/estate_property_offer_subtemplate.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+ Price |
+ Partner |
+ Validity (days) |
+ Deadline |
+ State |
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+ No offers |
+
+
+
+
+
+
diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml
new file mode 100644
index 00000000000..5131a111b6d
--- /dev/null
+++ b/estate/report/estate_property_reports.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ Print
+ estate.property
+ estate.report_property_offers
+ estate.report_property_offers
+ '%s_offers' % (object.name or 'Property')
+
+ report
+ qweb-pdf
+
+
+
+
+ Report
+ res.users
+ estate.estate_property_offer_res_user_template
+ estate.estate_property_offer_res_user_template
+
+ report
+ qweb-pdf
+
+
diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml
new file mode 100644
index 00000000000..c820f5a0a22
--- /dev/null
+++ b/estate/report/estate_property_templates.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+
+ State:
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml
new file mode 100644
index 00000000000..a4f944dbf41
--- /dev/null
+++ b/estate/security/estate_security.xml
@@ -0,0 +1,29 @@
+
+
+
+ Agent
+
+
+
+
+ Manager
+
+
+
+
+
+
+
+
+ ['|', ('user_id', '=', user.id), ('user_id', '=', False),]
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..7c2482dd165
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,11 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1
+access_estate_property_offer_wizard,access_estate_property_offer_wizard,estate.model_estate_property_offer_wizard,base.group_user,1,1,1,1
+access_estate_property_manager,access_estate_property_manager,estate.model_estate_property,estate.estate_group_manager,1,1,1,0
+access_estate_property_type_manager,access_estate_property_type_manager,estate.model_estate_property_type,estate.estate_group_manager,1,1,1,1
+access_estate_property_tag_manager,access_estate_property_tag_manager,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,1
+access_estate_property_offer_manager,access_estate_property_offer_manager,estate.model_estate_property_offer,estate.estate_group_manager,1,1,1,1
+access_estate_property_type_agent,access_estate_property_type_agent,estate.model_estate_property_type,estate.estate_group_user,1,0,0,0
+access_estate_property_tag_agent,access_estate_property_tag_agent,estate.model_estate_property_tag,estate.estate_group_user,1,0,0,0
+access_estate_property_agent,access_estate_property_agent,estate.model_estate_property,estate.estate_group_user,1,1,1,0
+access_estate_property_offer_agent,access_estate_property_offer_agent,estate.model_estate_property_offer,estate.estate_group_user,1,1,1,0
diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png
new file mode 100644
index 00000000000..cf7dd4d86ce
Binary files /dev/null and b/estate/static/description/icon.png differ
diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py
new file mode 100644
index 00000000000..a42df2e12ba
--- /dev/null
+++ b/estate/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_property_sold
diff --git a/estate/tests/test_property_sold.py b/estate/tests/test_property_sold.py
new file mode 100644
index 00000000000..15c265090b9
--- /dev/null
+++ b/estate/tests/test_property_sold.py
@@ -0,0 +1,18 @@
+from odoo.tests.common import TransactionCase
+from odoo.exceptions import ValidationError
+from odoo.tests import tagged
+
+@tagged('post_install', '-at_install')
+class TestEstateProperty(TransactionCase):
+
+ def test_sell_property_without_accepted_offer(self):
+ estate_property = self.env['estate.property']
+
+ property = estate_property.create({
+ "name": "Test Property Without Offer",
+ "expected_price": "100",
+ "state": "new",
+ })
+
+ with self.assertRaises(ValidationError):
+ property.action_property_sold()
diff --git a/estate/views/estate_available_property_view.xml b/estate/views/estate_available_property_view.xml
new file mode 100644
index 00000000000..89ff3d97249
--- /dev/null
+++ b/estate/views/estate_available_property_view.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+ Available Properties
+
+
+
+
+
+
+
+
+
+
+
+ Modern Apartment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Property Details
+
+ Description:
+
+
+
+ Price:
+
+
+
+ Bedrooms:
+
+
+
+ Living Area:
+
+ sqm
+
+
+
+ Garden Area:
+
+ sqm
+
+
+ Garden Orientation:
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..8cee18839dd
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml
new file mode 100644
index 00000000000..4eca9ff6928
--- /dev/null
+++ b/estate/views/estate_property_offers_views.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ Property Offer
+ estate.property.offer
+ list,form
+
+
+
+ estate.property.offer.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml
new file mode 100644
index 00000000000..8c3e408897a
--- /dev/null
+++ b/estate/views/estate_property_tags_views.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
diff --git a/estate/views/estate_property_types_views.xml b/estate/views/estate_property_types_views.xml
new file mode 100644
index 00000000000..453b18e29a9
--- /dev/null
+++ b/estate/views/estate_property_types_views.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..5d944c23009
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,162 @@
+
+
+
+ Estate
+ estate.property
+ list,form,kanban
+ {'search_default_available_property': 1}
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+ Expected Price:
+
+ Best Price:
+
+ Selling Price:
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_res_users_views.xml b/estate/views/estate_res_users_views.xml
new file mode 100644
index 00000000000..8854f841128
--- /dev/null
+++ b/estate/views/estate_res_users_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ res.users.view.form.inherit.estate
+ res.users
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/wizard/__init__.py b/estate/wizard/__init__.py
new file mode 100644
index 00000000000..e9926bcd3ec
--- /dev/null
+++ b/estate/wizard/__init__.py
@@ -0,0 +1 @@
+from . import estate_property_offer_wizard
diff --git a/estate/wizard/estate_property_offer_wizard.py b/estate/wizard/estate_property_offer_wizard.py
new file mode 100644
index 00000000000..cb8cedb209c
--- /dev/null
+++ b/estate/wizard/estate_property_offer_wizard.py
@@ -0,0 +1,34 @@
+from datetime import timedelta
+
+from odoo import fields, models
+from odoo.exceptions import UserError
+
+class EstatePropertyOfferWizard(models.TransientModel):
+ _name = "estate.property.offer.wizard"
+ _description = "Estate Property Offer Wizard"
+
+ price = fields.Float(string="Price", required=True)
+ buyer_id = fields.Many2one("res.partner", string="Partner", required=True)
+ date_deadline = fields.Date("Deadline", default=lambda self: fields.Date.today() + timedelta(days=7), required=True)
+
+
+
+ def add_offers_to_multiple_properties(self):
+ selected_properties = self.env['estate.property'].browse(self.env.context.get('active_ids', []))
+
+ offer_price = self.price
+ offer_buyer_id = self.buyer_id
+ offer_date_deadline = self.date_deadline
+
+ for property in selected_properties:
+
+ if property.state == 'sold':
+ raise UserError(f"The property '{property.name}' is alredy sold ")
+
+ self.env['estate.property.offer'].create({
+ 'price' : offer_price,
+ 'partner_id' : offer_buyer_id.id,
+ 'date_deadline':offer_date_deadline,
+ 'property_id' : property.id
+ })
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/estate/wizard/estate_property_offer_wizard_views.xml b/estate/wizard/estate_property_offer_wizard_views.xml
new file mode 100644
index 00000000000..8773fa7b90d
--- /dev/null
+++ b/estate/wizard/estate_property_offer_wizard_views.xml
@@ -0,0 +1,29 @@
+
+
+
+ estate.property.offer.wizard.form
+ estate.property.offer.wizard
+
+
+
+
+
+
+ Estate Property Offer Wizard
+ estate.property.offer.wizard
+ form
+ new
+
+
+
+
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..29cf8d5dcc6
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,14 @@
+{
+ 'name': 'estate_account',
+ 'description': 'Estate Account Module',
+ 'sequence': 1,
+ 'version': '1.0',
+ 'depends': ['estate', 'account'],
+ 'author': 'Shiv Bhadaniya',
+ "installable": True,
+ "application": True,
+ 'license': 'LGPL-3',
+ 'data': [
+ 'report/estate_property_inherit.xml',
+ ]
+}
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..0ba4bfa3468
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,32 @@
+from odoo import Command, models
+
+class EstateAccount(models.Model):
+ _inherit = "estate.property"
+
+ def action_property_sold(self):
+
+ self.check_access('write')
+
+ self.env["account.move"].sudo().create(
+ {
+ "move_type": "out_invoice",
+ "partner_id": self.buyer_id.id,
+ "invoice_line_ids": [
+ Command.create(
+ {
+ "name": "Property Sale",
+ "quantity": 1,
+ "price_unit": 1.06 * self.selling_price,
+ }
+ ),
+ Command.create(
+ {
+ "name": "Additional Charges",
+ "quantity": 1,
+ "price_unit": self.selling_price + 100,
+ }
+ ),
+ ],
+ }
+ )
+ return super().action_property_sold()
diff --git a/estate_account/report/estate_property_inherit.xml b/estate_account/report/estate_property_inherit.xml
new file mode 100644
index 00000000000..bd3712546a2
--- /dev/null
+++ b/estate_account/report/estate_property_inherit.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+ !!! Invoice has already been created !!!
+
+
+
+
+
+
+