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/dash_item_dialog/dashboard_items_dialog.js b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js
new file mode 100644
index 00000000000..40637b0ac54
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js
@@ -0,0 +1,33 @@
+import { Dialog } from "@web/core/dialog/dialog";
+import { Component } from "@odoo/owl";
+import { useService } from "@web/core/utils/hooks";
+
+export class DashboardItemDialog extends Component {
+ static template = "awesome_dashboard.dashboard_items_dialog";
+ static components = {
+ Dialog,
+ };
+
+ setup() {
+ this.service = useService("dashboard_items");
+ this.allItems = this.service.getAllItems();
+ this.usedIds = this.service.getUsedIds();
+ }
+
+ isSelected(id) {
+ return this.usedIds.includes(id);
+ }
+
+ select(id) {
+ return () => {
+ let found = this.usedIds.indexOf(id);
+ if(found >= 0) this.usedIds.splice(found, 1);
+ else this.usedIds.push(id);
+ }
+ }
+
+ apply() {
+ this.service.setUsedIds(this.usedIds);
+ this.props?.close()
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml
new file mode 100644
index 00000000000..36f9e7d3a96
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..a207a442bed
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,40 @@
+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 { DashboardItemDialog } from "./dash_item_dialog/dashboard_items_dialog";
+
+export class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+
+ static components = {
+ Layout,
+ DashboardItem
+ }
+
+ setup() {
+ this.items = useState(useService("dashboard_items").getUsedItems());
+ this.action = useService("action")
+ this.stats = useState(useService("statistics").loadStatistics());
+ this.dialog = useService("dialog");
+ }
+
+ openCustomers() {
+ this.action.doAction("base.action_partner_form")
+ }
+
+ openLeads() {
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'crm.lead',
+ views: [[false, 'list'], [false, 'form']]
+ })
+ }
+
+ openItemsDialog() {
+ this.dialog.add(DashboardItemDialog)
+ }
+}
+
+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..6af4f389665
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.scss
@@ -0,0 +1,13 @@
+.o_dashboard {
+ background-color: azure;
+ display: flex;
+ width: 100%;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-content: start;
+}
+
+.green_value {
+ color: green;
+ font-size: 3rem;
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..7e68772189f
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..ee138c0d99c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js
@@ -0,0 +1,10 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.dashboard_item";
+
+ static props = {
+ size: {type:Number, default: 1},
+ slots: {type: Object, optional: true}
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss
new file mode 100644
index 00000000000..450ca3e0105
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss
@@ -0,0 +1,11 @@
+.card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: white;
+ border-radius: 0.5rem;
+ border: none;
+ padding: 1rem;
+ margin: 0.5rem;
+ height: fit-content;
+}
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..12604f4ecc3
--- /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..92e0d45a272
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,68 @@
+import { PieChartCard } from "./dashboard_items/pie_char_card"
+import { NumberCard } from "./dashboard_items/number_card"
+import { registry } from "@web/core/registry";
+
+const items = [
+ {
+ id: "average_quantity",
+ description: "Average amount of t-shirt",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Average amount of t-shirt by order this month",
+ value: data.average_quantity
+ }),
+ },
+ {
+ id: "average_time",
+ description: "Average time",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ value: data.average_time
+ }),
+ },
+ {
+ id: "nb_new_orders",
+ description: "Number of new orders",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Number of new orders this month",
+ value: data.nb_new_orders
+ }),
+ },
+ {
+ id: "nb_cancelled_orders",
+ description: "Number of cancelled orders",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Number of cancelled orders this month",
+ value: data.nb_cancelled_orders
+ }),
+ },
+ {
+ id: "total_amount",
+ description: "Total amount of new orders",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Total amount of new orders this month",
+ value: data.total_amount
+ }),
+ },
+ {
+ id: "orders_by_size",
+ description: "Shirt orders by size",
+ Component: PieChartCard,
+ size: 2,
+ props: (data) => ({
+ title: "Shirt orders by size",
+ value: data.orders_by_size
+ }),
+ },
+ ]
+
+registry.category("awesome_dashboard").add("items", items);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js
new file mode 100644
index 00000000000..ad2a98f9160
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js
@@ -0,0 +1,10 @@
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.NumberCard"
+
+ static props = {
+ title: String,
+ value: Number
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml
new file mode 100644
index 00000000000..45454142519
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js
new file mode 100644
index 00000000000..96bb730e390
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js
@@ -0,0 +1,43 @@
+import { Component, onWillStart, useRef, onMounted, onWillPatch, useState } from "@odoo/owl";
+import { loadJS } from "@web/core/assets"
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.PieChartCard"
+
+ static props = {
+ title: String,
+ value: {type: Object, optional: true}
+ }
+
+ chart = null;
+
+ drawChart() {
+ if(!this.props.value) return;
+
+ if(this.chart) {
+ this.chart.data.datasets[0].data = Object.values(this.props.value);
+ this.chart.update();
+ return;
+ }
+
+ this.chart = new Chart(this.canvasRef.el, {
+ type: 'pie',
+ data: {
+ labels: Object.keys(this.props.value),
+ datasets: [
+ {
+ label: 'Shirts',
+ data: Object.values(this.props.value)
+ }
+ ]
+ }
+ })
+ }
+
+ setup() {
+ this.canvasRef = useRef("canvas");
+ onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
+ onMounted(() => this.drawChart())
+ onWillPatch(() => this.drawChart())
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml
new file mode 100644
index 00000000000..c6b5c12d7a3
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js b/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js
new file mode 100644
index 00000000000..77cab8c07b3
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js
@@ -0,0 +1,43 @@
+import { registry } from "@web/core/registry";
+import { reactive } from "@odoo/owl";
+
+const storageKey = "owl-dashboard-used-dashboard-items"
+
+function setUsedItems(result, allItems, usedIds) {
+ result.splice(0, result.length);
+
+ for(let id of usedIds) {
+ let found = allItems.find((i) => i.id == id);
+ result.push(found);
+ }
+}
+
+const dashboardItemsService = {
+ start() {
+ let fromStorage = localStorage.getItem(storageKey);
+ let usedIds = fromStorage ? JSON.parse(fromStorage) : [];
+ let allItems = registry.category("awesome_dashboard").get("items");
+ let usedItems = reactive([]);
+
+ setUsedItems(usedItems, allItems, usedIds);
+
+ return {
+ getUsedItems() {
+ return usedItems;
+ },
+ getAllItems() {
+ return allItems;
+ },
+ getUsedIds() {
+ return usedIds.slice();
+ },
+ setUsedIds(ids) {
+ usedIds = ids;
+ localStorage.setItem(storageKey, JSON.stringify(ids));
+ setUsedItems(usedItems, allItems, usedIds);
+ }
+ }
+ }
+}
+
+registry.category("services").add("dashboard_items", dashboardItemsService);
diff --git a/awesome_dashboard/static/src/dashboard/services/statistics.service.js b/awesome_dashboard/static/src/dashboard/services/statistics.service.js
new file mode 100644
index 00000000000..f1a9f55b38d
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/services/statistics.service.js
@@ -0,0 +1,30 @@
+import { registry } from "@web/core/registry";
+import { rpc } from "@web/core/network/rpc";
+import { reactive } from "@odoo/owl";
+
+const fetchInterval = 3000;
+
+async function fetchStatistics(stats) {
+ const result = await rpc("/awesome_dashboard/statistics");
+
+ for(let entry of Object.entries(result)) {
+ if(stats[entry[0]] !== undefined) stats[entry[0]] = entry[1];
+ }
+}
+
+const statisticsService = {
+ start() {
+ let stats = reactive({average_quantity: 0, average_time: 0, nb_cancelled_orders: 0, nb_new_orders: 0, total_amount: 0, orders_by_size: {}});
+
+ fetchStatistics(stats)
+ setInterval(() => fetchStatistics(stats), fetchInterval);
+
+ return {
+ loadStatistics() {
+ return stats;
+ }
+ }
+ }
+}
+
+registry.category("services").add("statistics", statisticsService);
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js
new file mode 100644
index 00000000000..f09712a4459
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,18 @@
+import { Component, xml } from "@odoo/owl";
+import { LazyComponent } from "@web/core/assets";
+import { AwesomeDashboard } from "./dashboard/dashboard";
+import { registry } from "@web/core/registry";
+
+class AwesomeDashboardLoader extends Component {
+ static components = {
+ LazyComponent,
+ AwesomeDashboard
+ };
+ static template = xml`
+
+ `;
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader);
+
+
diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py
index 55002ab81de..e4fb0502d24 100644
--- a/awesome_owl/__manifest__.py
+++ b/awesome_owl/__manifest__.py
@@ -36,7 +36,7 @@
('include', 'web._assets_bootstrap'),
('include', 'web._assets_core'),
'web/static/src/libs/fontawesome/css/font-awesome.css',
- 'awesome_owl/static/src/**/*',
+ 'awesome_owl/static/src/**/*'
],
},
'license': 'AGPL-3'
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..303d31cd2de
--- /dev/null
+++ b/awesome_owl/static/src/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, validate: val => {
+ if(val.length === 0) return false;
+
+ let first = val.substring(0, 1);
+ return first === first.toUpperCase();
+ }},
+ slots: {type: Object, optional: true},
+ }
+
+ setup() {
+ this.state = useState({open: true});
+ }
+
+ toggle(){
+ this.state.open = !this.state.open;
+ }
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..fa2adf3bfa9
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..bfccd6ea4ad
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,18 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "awesome_owl.counter";
+
+ static props = {
+ onChange: {type: Function, optional: true}
+ }
+
+ setup() {
+ this.state = useState({ value: 1 });
+ }
+
+ increment() {
+ this.state.value++;
+ this.props?.onChange(this.state.value);
+ }
+}
diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml
new file mode 100644
index 00000000000..5e0ec2135c0
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ Counter:
+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 4ac769b0aa5..e2124b07784 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,5 +1,25 @@
-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 "./todo_list/todo_list";
export class Playground extends Component {
static template = "awesome_owl.playground";
+
+ someHtml = "
some content
"
+ markupedHtml = markup(this.someHtml);
+
+ static components = {
+ Counter,
+ Card,
+ TodoList
+ };
+
+ setup() {
+ this.state = useState({ sum: 2 });
+ }
+
+ incrementSum() {
+ this.state.sum++;
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..32b66270c7f 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -3,8 +3,25 @@
- hello world
+ hello word
+
+
+
+ This is working
+
+
+
+
+
+
+
+
+
+
+
+ The sum :
+
diff --git a/awesome_owl/static/src/todo_item/todo_item.js b/awesome_owl/static/src/todo_item/todo_item.js
new file mode 100644
index 00000000000..412bacc74a4
--- /dev/null
+++ b/awesome_owl/static/src/todo_item/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: {type: Object, shape: {id: Number, description: String, isCompleted: Boolean}},
+ toggleState: Function,
+ removeTodo: Function
+ }
+
+ complete() {
+ this.props.toggleState(this.props.todo.id)
+ }
+
+ remove() {
+ this.props.removeTodo(this.props.todo.id)
+ }
+}
diff --git a/awesome_owl/static/src/todo_item/todo_item.xml b/awesome_owl/static/src/todo_item/todo_item.xml
new file mode 100644
index 00000000000..68da38622b2
--- /dev/null
+++ b/awesome_owl/static/src/todo_item/todo_item.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ .
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js
new file mode 100644
index 00000000000..923b3468819
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.js
@@ -0,0 +1,38 @@
+import { Component, useState } from "@odoo/owl";
+import { TodoItem } from "../todo_item/todo_item";
+import { useAutofocus } from "../util";
+
+export class TodoList extends Component {
+ static template = "awesome_owl.todo_list";
+
+ counter = 1;
+
+ static components = {
+ TodoItem
+ }
+
+ setup() {
+ this.todos = useState([]);
+ useAutofocus("input");
+ }
+
+ change(id) {
+ let found = this.todos.findIndex(t => t.id === id);
+ if(found >= 0) this.todos[found] = {...this.todos[found], isCompleted: !this.todos[found].isCompleted};
+ }
+
+ remove(id) {
+ let found = this.todos.findIndex(t => t.id === id);
+ if(found >= 0) this.todos.splice(found, 1);
+ }
+
+ tryAdd(ev) {
+ if(ev.keyCode != 13) return;
+
+ let txt = ev.srcElement.value;
+ if(txt === "") return;
+
+ this.todos.push({id: this.counter++, description: txt, isCompleted: false});
+ ev.srcElement.value = "";
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml
new file mode 100644
index 00000000000..abc59016150
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/util.js b/awesome_owl/static/src/util.js
new file mode 100644
index 00000000000..c5a84892a3c
--- /dev/null
+++ b/awesome_owl/static/src/util.js
@@ -0,0 +1,8 @@
+import { useRef, onMounted } from "@odoo/owl";
+
+export function useAutofocus(refName) {
+ let ref = useRef(refName);
+ onMounted(() => {
+ ref.el?.focus()
+ })
+}
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..8271d22e15b
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,16 @@
+{
+ 'name': 'Real Estate',
+ 'author': 'zavan',
+ 'depends': ['base'],
+ 'application': True,
+ 'license': 'LGPL-3',
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/estate_property_type_views.xml',
+ 'views/estate_property_tag_views.xml',
+ 'views/estate_property_offer_views.xml',
+ 'views/estate_property_views.xml',
+ 'views/estate_menus.xml',
+ 'views/res_users_views.xml'
+ ]
+}
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..eaca1278bbd
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,92 @@
+from odoo import api, fields, models, exceptions
+from dateutil.relativedelta import relativedelta
+from odoo.tools.float_utils import float_compare, float_is_zero
+
+
+class EstateProperty(models.Model):
+ _name = 'estate.property'
+ _description = "Real estate property"
+ _order = "id desc"
+
+ _check_expected_price = models.Constraint('CHECK(expected_price > 0)', 'The expected price should always be positive')
+ _check_selling_price = models.Constraint('CHECK(selling_price >= 0)', 'The selling price should always be positive')
+
+ name = fields.Char(required=True)
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date("Available From", copy=False, default=fields.Date.today() + relativedelta(months=3))
+ 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(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')])
+ active = fields.Boolean(default=True)
+ state = fields.Selection(
+ selection=[('new', 'New'), ('offer-received', 'Offer Received'), ('offer-accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')],
+ copy=False, default="new",
+ required=True
+ )
+ property_type_id = fields.Many2one("estate.property.type", string="Type")
+ buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True)
+ salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user)
+ tag_ids = fields.Many2many("estate.property.tag", string="Tags")
+ offer_ids = fields.One2many("estate.property.offer", "property_id")
+ total_area = fields.Integer(compute="_compute_total_area")
+ best_price = fields.Float(string="Best Offer", compute="_compute_best_price")
+
+ @api.depends("garden_area", "living_area")
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = record.garden_area + record.living_area
+
+ @api.depends("offer_ids")
+ def _compute_best_price(self):
+ for prop in self:
+ prop.best_price = max((offer.price for offer in prop.offer_ids), default=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
+
+ def sell_property(self):
+ for prop in self:
+ if (prop.state == 'sold'):
+ continue
+ elif (prop.state == 'cancelled'):
+ raise exceptions.UserError('Cannot sell a cancelled property')
+ elif (prop.state != 'offer-accepted'):
+ raise exceptions.UserError('Cannot sell a property with no accepted offer')
+ else:
+ prop.state = 'sold'
+ return True
+
+ def cancel_property(self):
+ for prop in self:
+ if (prop.state == 'cancelled'):
+ continue
+ elif (prop.state == 'sold'):
+ raise exceptions.UserError('Cannot cancel a sold property')
+ else:
+ prop.state = 'cancelled'
+ return True
+
+ @api.constrains("selling_price", "expected_price")
+ def _check_price(self):
+ for prop in self:
+ if (not (prop.selling_price is None or float_is_zero(prop.selling_price, precision_digits=2)) and
+ float_compare(prop.selling_price, prop.expected_price * 9 / 10, precision_digits=2) < 0):
+ raise exceptions.ValidationError("The selling price cannot be below 90 percent of the expected price")
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_if_new_or_cancelled(self):
+ if any(prop.state != 'new' and prop.state != 'cancelled' for prop in self):
+ raise exceptions.UserError("Can only delete new or cancelled properties")
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..74cd4f9c969
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,64 @@
+from odoo import api, fields, models, exceptions
+from dateutil.relativedelta import relativedelta
+import datetime
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Estate property offer"
+ _order = "price desc"
+
+ _check_price = models.Constraint('CHECK(price > 0)', 'The price should always be positive')
+
+ price = fields.Float()
+ status = fields.Selection(readonly=True, selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False)
+ partner_id = fields.Many2one("res.partner", required=True)
+ property_id = fields.Many2one("estate.property", required=True)
+ validity = fields.Integer(default=7)
+ date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_validity", readonly=False)
+ property_type_id = fields.Many2one(related="property_id.property_type_id")
+
+ @api.model
+ def create(self, vals_list):
+ prop = self.env['estate.property'].browse(vals_list[0]['property_id'])
+
+ if (any(o.price > vals_list[0]['price'] for o in prop.offer_ids)):
+ raise exceptions.UserError("Cannot add an offer with a lower amount than an existing one")
+
+ prop.state = "offer-received"
+
+ return super().create(vals_list)
+
+ @api.depends("validity", "create_date")
+ def _compute_deadline(self):
+ for offer in self:
+ create = offer.create_date.date() if isinstance(offer.create_date, datetime.datetime) else fields.Date.today()
+ offer.date_deadline = create + relativedelta(days=offer.validity)
+
+ def _inverse_validity(self):
+ for offer in self:
+ create = offer.create_date.date() if isinstance(offer.create_date, datetime.datetime) else fields.Date.today()
+ offer.validity = (offer.date_deadline - create).days
+
+ def accept_offer(self):
+ for offer in self:
+ if (offer.status == 'accepted'):
+ continue
+
+ for other in offer.property_id.offer_ids:
+ if (other.status == 'accepted'):
+ raise exceptions.UserError("Cannot accept multiple offers for a single property")
+
+ offer.status = 'accepted'
+ offer.property_id.buyer_id = offer.partner_id
+ offer.property_id.selling_price = offer.price
+ offer.property_id.state = "offer-accepted"
+ return True
+
+ def refuse_offer(self):
+ for offer in self:
+ if (offer.status == 'accepted'):
+ offer.property_id.buyer_id = None
+ offer.property_id.selling_price = None
+ offer.status = 'refused'
+ return True
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..d2dbd0e6a0f
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,12 @@
+from odoo import fields, models
+
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _description = "Estate property tag"
+ _order = "name"
+
+ _unique_name = models.Constraint("UNIQUE (name)", "A tag should be unique")
+
+ name = fields.Char(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..bbad0fafb2d
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,20 @@
+from odoo import api, fields, models
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "Real estate property type"
+ _order = "name"
+
+ _unique_name = models.Constraint("UNIQUE (name)", "A property type should be unique")
+
+ name = fields.Char(required=True)
+ sequence = fields.Integer(default=1)
+ property_ids = fields.One2many("estate.property", "property_type_id")
+ offer_ids = fields.One2many("estate.property.offer", "property_type_id")
+ offer_count = fields.Integer(compute="_compute_offer_count")
+
+ @api.depends("offer_ids")
+ def _compute_offer_count(self):
+ for prop_type in self:
+ prop_type.offer_count = len(prop_type.offer_ids)
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..54cef1202b7
--- /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")
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..e20ec4dd90b
--- /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
+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
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..1081c03c37f
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
new file mode 100644
index 00000000000..61e6c51230c
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,35 @@
+
+
+
+ estate.property.offer.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..b043cee2bd0
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,18 @@
+
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
+
+ Estate Property Tag
+ estate.property.tag
+ list,form
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..db08a2a54ec
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,56 @@
+
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
+
+ Estate Property Offer
+ estate.property.offer
+ list
+ [('property_type_id', '=', active_id)]
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
+
+ Estate Property Type
+ estate.property.type
+ list,form
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..c9e13db954c
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,134 @@
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+ Expected Price :
+
+
+
+ Best Price :
+
+
+
+ Selling Price :
+
+
+
+
+
+
+
+
+
+
+ Estate Property
+ estate.property
+ kanban,list,form
+ {'search_default_available': True}
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
new file mode 100644
index 00000000000..1e0afe33495
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ res.users.view.form.inherit.gamification
+ res.users
+
+
+
+
+
+
+
+
+
+
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/estate_account/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py
new file mode 100644
index 00000000000..8a89d41ac35
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,6 @@
+{
+ 'name': 'Real Estate Accounting',
+ 'author': 'zavan',
+ 'depends': ['estate', 'account'],
+ 'license': 'LGPL-3'
+}
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..5e1963c9d2f
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_property
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..9d92998f846
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,27 @@
+from odoo import models, Command
+
+
+class EstateProperty(models.Model):
+ _inherit = ["estate.property"]
+
+ def sell_property(self):
+ for prop in self:
+ vals = {
+ 'partner_id': prop.buyer_id.id,
+ 'move_type': 'out_invoice',
+ 'journal_id': 1,
+ 'invoice_line_ids': [
+ Command.create({
+ 'name': '6 percent of selling price',
+ 'quantity': 1,
+ 'price_unit': (prop.selling_price * 6 / 100)
+ }),
+ Command.create({
+ 'name': 'Administrative fees',
+ 'quantity': 1,
+ 'price_unit': 100
+ })
+ ]
+ }
+ self.env['account.move'].create(vals)
+ return super().sell_property()