diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py
index e57ef4d5bb..7528d8a193 100644
--- a/awesome_clicker/__manifest__.py
+++ b/awesome_clicker/__manifest__.py
@@ -1,29 +1,24 @@
# -*- coding: utf-8 -*-
{
- 'name': "Awesome Clicker",
-
- 'summary': """
+ "name": "Awesome Clicker",
+ "summary": """
Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game"
""",
-
- 'description': """
+ "description": """
Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game"
""",
-
- 'author': "Odoo",
- 'website': "https://www.odoo.com/",
- 'category': 'Tutorials/AwesomeClicker',
- 'version': '0.1',
- 'application': True,
- 'installable': True,
- 'depends': ['base', 'web'],
-
- 'data': [],
- 'assets': {
- 'web.assets_backend': [
- 'awesome_clicker/static/src/**/*',
+ "author": "Odoo",
+ "website": "https://www.odoo.com/",
+ "category": "Tutorials/AwesomeClicker",
+ "version": "0.1",
+ "application": True,
+ "installable": True,
+ "depends": ["base", "web"],
+ "data": [],
+ "assets": {
+ "web.assets_backend": [
+ "awesome_clicker/static/src/**/*",
],
-
},
- 'license': 'AGPL-3'
+ "license": "AGPL-3",
}
diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index 31406e8add..17e48f7c1f 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -1,30 +1,27 @@
# -*- 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/**/*",
+ "awesome_dashboard/static/src/scss/dashboard.scss",
],
},
- 'license': 'AGPL-3'
+ "license": "AGPL-3",
}
diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py
index 457bae27e1..b0f26a9a60 100644
--- a/awesome_dashboard/controllers/__init__.py
+++ b/awesome_dashboard/controllers/__init__.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
-from . import controllers
\ No newline at end of file
+from . import controllers
diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py
index 56d4a05128..b5aab01be3 100644
--- a/awesome_dashboard/controllers/controllers.py
+++ b/awesome_dashboard/controllers/controllers.py
@@ -8,8 +8,9 @@
logger = logging.getLogger(__name__)
+
class AwesomeDashboard(http.Controller):
- @http.route('/awesome_dashboard/statistics', type='json', auth='user')
+ @http.route("/awesome_dashboard/statistics", type="json", auth="user")
def get_statistics(self):
"""
Returns a dict of statistics about the orders:
@@ -22,15 +23,14 @@ def get_statistics(self):
"""
return {
- 'average_quantity': random.randint(4, 12),
- 'average_time': random.randint(4, 123),
- 'nb_cancelled_orders': random.randint(0, 50),
- 'nb_new_orders': random.randint(10, 200),
- 'orders_by_size': {
- 'm': random.randint(0, 150),
- 's': random.randint(0, 150),
- 'xl': random.randint(0, 150),
+ "average_quantity": random.randint(4, 12),
+ "average_time": random.randint(4, 123),
+ "nb_cancelled_orders": random.randint(0, 50),
+ "nb_new_orders": random.randint(10, 200),
+ "orders_by_size": {
+ "m": random.randint(0, 150),
+ "s": random.randint(0, 150),
+ "xl": random.randint(0, 150),
},
- 'total_amount': random.randint(100, 1000)
+ "total_amount": random.randint(100, 1000),
}
-
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js
deleted file mode 100644
index 637fa4bb97..0000000000
--- 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 1a2ac9a2fe..0000000000
--- 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 0000000000..683bc4d23c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,26 @@
+import { useService } from "@web/core/utils/hooks";
+import { Layout } from "@web/search/layout";
+import { DashboardItem } from "../dashboard_items/dashboard_items";
+
+export class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem };
+
+
+ setup() {
+ this.action = useService("action");
+ }
+
+ openCustomers() {
+ this.action.doAction("base.res_partner_action_kanban");
+ }
+
+ openLeads() {
+ this.action.doAction({
+ name: "Leads",
+ type: "ir.actions.act_window",
+ res_model: "crm.lead",
+ views: [[false, "list"], [false, "form"]],
+ });
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss
new file mode 100644
index 0000000000..32862ec0d8
--- /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 0000000000..9c48b46342
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Item 1
+ Item 2
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_items/dahboard_item.xml b/awesome_dashboard/static/src/dashboard_items/dahboard_item.xml
new file mode 100644
index 0000000000..aef49ee9bf
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_items/dahboard_item.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_items/dashboard_item.scss b/awesome_dashboard/static/src/dashboard_items/dashboard_item.scss
new file mode 100644
index 0000000000..0bed5a9b36
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_items/dashboard_item.scss
@@ -0,0 +1,7 @@
+.o_dashboard_item {
+ background-color: #f4f4f4;
+ border: 1px solid #ddd;
+ padding: 1rem;
+ margin: 0.5rem;
+ border-radius: 0.5rem;
+}
diff --git a/awesome_dashboard/static/src/dashboard_items/dashboard_items.js b/awesome_dashboard/static/src/dashboard_items/dashboard_items.js
new file mode 100644
index 0000000000..f6063ade10
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_items/dashboard_items.js
@@ -0,0 +1,6 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.DashboardItem";
+ static props = { size: { type: Number, optional: true, default: 1 } };
+}
diff --git a/awesome_gallery/__manifest__.py b/awesome_gallery/__manifest__.py
index 624766dca8..9410319978 100644
--- a/awesome_gallery/__manifest__.py
+++ b/awesome_gallery/__manifest__.py
@@ -1,26 +1,24 @@
# -*- coding: utf-8 -*-
{
- 'name': "Gallery View",
- 'summary': """
+ "name": "Gallery View",
+ "summary": """
Starting module for "Master the Odoo web framework, chapter 3: Create a Gallery View"
""",
-
- 'description': """
+ "description": """
Starting module for "Master the Odoo web framework, chapter 3: Create a Gallery View"
""",
-
- 'version': '0.1',
- 'application': True,
- 'category': 'Tutorials/AwesomeGallery',
- 'installable': True,
- 'depends': ['web', 'contacts'],
- 'data': [
- 'views/views.xml',
+ "version": "0.1",
+ "application": True,
+ "category": "Tutorials/AwesomeGallery",
+ "installable": True,
+ "depends": ["web", "contacts"],
+ "data": [
+ "views/views.xml",
],
- 'assets': {
- 'web.assets_backend': [
- 'awesome_gallery/static/src/**/*',
+ "assets": {
+ "web.assets_backend": [
+ "awesome_gallery/static/src/**/*",
],
},
- 'license': 'AGPL-3'
+ "license": "AGPL-3",
}
diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py
index eae20acbf5..45dc4a2bb7 100644
--- a/awesome_gallery/models/ir_action.py
+++ b/awesome_gallery/models/ir_action.py
@@ -3,8 +3,8 @@
class ActWindowView(models.Model):
- _inherit = 'ir.actions.act_window.view'
+ _inherit = "ir.actions.act_window.view"
- view_mode = fields.Selection(selection_add=[
- ('gallery', "Awesome Gallery")
- ], ondelete={'gallery': 'cascade'})
+ view_mode = fields.Selection(
+ selection_add=[("gallery", "Awesome Gallery")], ondelete={"gallery": "cascade"}
+ )
diff --git a/awesome_gallery/models/ir_ui_view.py b/awesome_gallery/models/ir_ui_view.py
index 0c11b8298a..555008c371 100644
--- a/awesome_gallery/models/ir_ui_view.py
+++ b/awesome_gallery/models/ir_ui_view.py
@@ -3,6 +3,6 @@
class View(models.Model):
- _inherit = 'ir.ui.view'
+ _inherit = "ir.ui.view"
- type = fields.Selection(selection_add=[('gallery', "Awesome Gallery")])
+ type = fields.Selection(selection_add=[("gallery", "Awesome Gallery")])
diff --git a/awesome_kanban/__manifest__.py b/awesome_kanban/__manifest__.py
index affef78bb1..e31a4d7189 100644
--- a/awesome_kanban/__manifest__.py
+++ b/awesome_kanban/__manifest__.py
@@ -1,26 +1,24 @@
# -*- coding: utf-8 -*-
{
- 'name': "Awesome Kanban",
- 'summary': """
+ "name": "Awesome Kanban",
+ "summary": """
Starting module for "Master the Odoo web framework, chapter 4: Customize a kanban view"
""",
-
- 'description': """
+ "description": """
Starting module for "Master the Odoo web framework, chapter 4: Customize a kanban view.
""",
-
- 'version': '0.1',
- 'application': True,
- 'category': 'Tutorials/AwesomeKanban',
- 'installable': True,
- 'depends': ['web', 'crm'],
- 'data': [
- 'views/views.xml',
+ "version": "0.1",
+ "application": True,
+ "category": "Tutorials/AwesomeKanban",
+ "installable": True,
+ "depends": ["web", "crm"],
+ "data": [
+ "views/views.xml",
],
- 'assets': {
- 'web.assets_backend': [
- 'awesome_kanban/static/src/**/*',
+ "assets": {
+ "web.assets_backend": [
+ "awesome_kanban/static/src/**/*",
],
},
- 'license': 'AGPL-3'
+ "license": "AGPL-3",
}
diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py
index 457bae27e1..b0f26a9a60 100644
--- a/awesome_owl/__init__.py
+++ b/awesome_owl/__init__.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
-from . import controllers
\ No newline at end of file
+from . import controllers
diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py
index 77abad510e..a381f2e7ad 100644
--- a/awesome_owl/__manifest__.py
+++ b/awesome_owl/__manifest__.py
@@ -1,42 +1,37 @@
# -*- coding: utf-8 -*-
{
- 'name': "Awesome Owl",
-
- 'summary': """
+ "name": "Awesome Owl",
+ "summary": """
Starting module for "Discover the JS framework, chapter 1: Owl components"
""",
-
- 'description': """
+ "description": """
Starting module for "Discover the JS framework, chapter 1: Owl components"
""",
-
- 'author': "Odoo",
- 'website': "https://www.odoo.com",
-
+ "author": "Odoo",
+ "website": "https://www.odoo.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
- 'category': 'Tutorials/AwesomeOwl',
- 'version': '0.1',
-
+ "category": "Tutorials/AwesomeOwl",
+ "version": "0.1",
# any module necessary for this one to work correctly
- 'depends': ['base', 'web'],
- 'application': True,
- 'installable': True,
- 'data': [
- 'views/templates.xml',
+ "depends": ["base", "web"],
+ "application": True,
+ "installable": True,
+ "data": [
+ "views/templates.xml",
],
- 'assets': {
- 'awesome_owl.assets_playground': [
- ('include', 'web._assets_helpers'),
- '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'),
- ('include', 'web._assets_core'),
- 'web/static/src/libs/fontawesome/css/font-awesome.css',
- 'awesome_owl/static/src/**/*',
+ "assets": {
+ "awesome_owl.assets_playground": [
+ ("include", "web._assets_helpers"),
+ "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"),
+ ("include", "web._assets_core"),
+ "web/static/src/libs/fontawesome/css/font-awesome.css",
+ "awesome_owl/static/src/**/*",
],
},
- 'license': 'AGPL-3'
+ "license": "AGPL-3",
}
diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py
index 457bae27e1..b0f26a9a60 100644
--- a/awesome_owl/controllers/__init__.py
+++ b/awesome_owl/controllers/__init__.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
-from . import controllers
\ No newline at end of file
+from . import controllers
diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py
index bccfd6fe28..4da2f03a9f 100644
--- a/awesome_owl/controllers/controllers.py
+++ b/awesome_owl/controllers/controllers.py
@@ -1,10 +1,11 @@
from odoo import http
from odoo.http import request, route
+
class OwlPlayground(http.Controller):
- @http.route(['/awesome_owl'], type='http', auth='public')
+ @http.route(["/awesome_owl"], type="http", auth="public")
def show_playground(self):
"""
Renders the owl playground page
"""
- return request.render('awesome_owl.playground')
+ return request.render("awesome_owl.playground")
diff --git a/awesome_owl/static/src/components/TodoItem/todoitem.js b/awesome_owl/static/src/components/TodoItem/todoitem.js
new file mode 100644
index 0000000000..34bccfc468
--- /dev/null
+++ b/awesome_owl/static/src/components/TodoItem/todoitem.js
@@ -0,0 +1,19 @@
+import { Component} from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.todoitem";
+ static props = {
+ todo: { type: Object },
+ toggleState: {type: Function},
+ removeTodo: {type: Function}
+ };
+
+ toggleState() {
+ this.props.toggleState(this.props.todo.id);
+ }
+
+ removeTodo() {
+ this.props.removeTodo(this.props.todo.id);
+ }
+
+}
diff --git a/awesome_owl/static/src/components/TodoItem/todoitem.xml b/awesome_owl/static/src/components/TodoItem/todoitem.xml
new file mode 100644
index 0000000000..cc0b04323c
--- /dev/null
+++ b/awesome_owl/static/src/components/TodoItem/todoitem.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ .
+
+
+
+
+
diff --git a/awesome_owl/static/src/components/TodoList/todolist.js b/awesome_owl/static/src/components/TodoList/todolist.js
new file mode 100644
index 0000000000..f797986f03
--- /dev/null
+++ b/awesome_owl/static/src/components/TodoList/todolist.js
@@ -0,0 +1,43 @@
+import { Component, useState} from "@odoo/owl";
+import { TodoItem } from "../TodoItem/todoitem";
+import { useAutofocus } from "../../utils";
+
+
+export class ToDoList extends Component {
+ static template = "awesome_owl.todolist";
+ static components = { TodoItem };
+ setup() {
+ this.todos = useState([]);
+ this.todoItemsNr =0;
+ useAutofocus("todo_input");
+ }
+ addTodo(inp) {
+ if (inp.keyCode === 13) {
+ let content = inp.target.value;
+ if(content){
+ const _newTodo = {
+ id: ++this.todoItemsNr,
+ description: content,
+ isCompleted: false
+ };
+ this.todos.push(_newTodo);
+
+ }
+ inp.target.value = '';
+ this.render();
+ }
+ }
+
+ toggleState(id) {
+ const index = this.todos.findIndex((item) => item.id === id);
+ this.todos[index].isCompleted = !this.todos[index].isCompleted;
+ }
+
+ removeTodo(id) {
+ const index = this.todos.findIndex((item) => item.id === id);
+ if (index >= 0) {
+ this.todos.splice(index, 1);
+ }
+ }
+
+}
diff --git a/awesome_owl/static/src/components/TodoList/todolist.xml b/awesome_owl/static/src/components/TodoList/todolist.xml
new file mode 100644
index 0000000000..cd82af9397
--- /dev/null
+++ b/awesome_owl/static/src/components/TodoList/todolist.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
Todo List
+
+
+
+
+
+
+
+
+
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 0000000000..9ed5c0761c
--- /dev/null
+++ b/awesome_owl/static/src/components/card/card.js
@@ -0,0 +1,18 @@
+import { Component, useState} from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.card";
+ static props = {
+ title: {type: String},
+ slots: { type: Object }
+ }
+
+ setup() {
+ this.state = useState({ hidden: false });
+ }
+
+ toggle() {
+ this.state.hidden = !this.state.hidden;
+ }
+
+}
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 0000000000..6d85d12b8a
--- /dev/null
+++ b/awesome_owl/static/src/components/card/card.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
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 0000000000..aee34b8e3c
--- /dev/null
+++ b/awesome_owl/static/src/components/counter/counter.js
@@ -0,0 +1,17 @@
+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({
+ count: 1
+ });
+ };
+ increment() {
+ this.state.count++;
+ this.props.onChange?.();
+ }
+}
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 0000000000..a36a112e32
--- /dev/null
+++ b/awesome_owl/static/src/components/counter/counter.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 657fb8b07b..f2d641c051 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,7 +1,23 @@
/** @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/TodoList/todolist"
export class Playground extends Component {
static template = "awesome_owl.playground";
+ static components = { Counter, Card, ToDoList};
+ setup() {
+ this.state = useState({
+ sum: 2
+ });
+ };
+
+ incrementSum(){
+ this.state.sum++;
+ }
+
+ content1= "
Card with no markup
";
+ content2 = markup("Card with markup content
");
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f..3717231006 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -1,10 +1,45 @@
-
-
- hello world
+
+
+
+
Counters
+
+
+ Sum:
+
+
+
+
+
-
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 0000000000..0b0690f0e9
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,9 @@
+import { useRef, useEffect } from "@odoo/owl"
+
+export function useAutofocus(itemName) {
+ let ref = useRef(itemName);
+ useEffect(
+ (el) => el?.focus(),
+ () => [ref.el]
+ )
+}
diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 0000000000..0650744f6b
--- /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 0000000000..32931c038c
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,21 @@
+{
+ "name": "Real Estate",
+ "version": "1.0",
+ "author": "Abdelrhman Fawzy",
+ "category": "Services/Real Estate",
+ "summary": "Manage real estate properties, offers, and advertisements.",
+ "license": "LGPL-3",
+ "application": True,
+ "installable": True,
+ "auto_install": False,
+ "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_offer_views.xml",
+ 'views/res_users_views.xml',
+ "views/estate_menues.xml",
+ ],
+ "description": "This module allows you to manage real estate properties, property types, tags, offers, and advertisements. It also includes tools for organizing and displaying data effectively.",
+}
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 0000000000..9a2189b638
--- /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 0000000000..5fce905e25
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,139 @@
+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 = "Real Estate Property Options"
+ _order = "id desc"
+
+
+ name = fields.Char(required=True)
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date(
+ copy=False,
+ default=lambda x: fields.Datetime.add(fields.Datetime.today(), months=3),
+ )
+ expected_price = fields.Float(required=True)
+ selling_price = fields.Float(readonly=True, copy=False)
+ bedrooms = fields.Integer()
+ 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(
+ required=True,
+ copy=False,
+ default="new",
+ selection=[
+ ("new", "NEW"),
+ ("offer_received", "OFFER RECEIVED"),
+ ("offer_accepted", "OFFER ACCEPTED"),
+ ("sold", "SOLD"),
+ ("cancelled", "CANCELLED"),
+ ],
+ )
+
+ property_type_id = fields.Many2one(comodel_name="estate.property.type")
+ user_id = fields.Many2one(
+ comodel_name="res.users",
+ string="Salesperson",
+ default=lambda self: self.env.user,
+ )
+ partner_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False)
+ property_tag_ids = fields.Many2many(comodel_name="estate.property.tag")
+ property_offer_ids = fields.One2many("estate.property.offer", "property_id")
+
+ total_area = fields.Integer(compute="_compute_total_area")
+ best_price = fields.Float(compute="_compute_best_price")
+
+ _sql_constraints = [
+ (
+ "check_expected_price",
+ "CHECK(expected_price >= 0)",
+ "Property expected price MUST be postive.",
+ ),
+ (
+ "check_selling_price",
+ "CHECK(selling_price >= 0)",
+ "Property selling price MUST be postive.",
+ ),
+ ]
+
+ @api.constrains("expected_price", "selling_price")
+ def _check_selling_price(self):
+ for record in self:
+ if float_is_zero(record.selling_price, precision_digits=2):
+ continue
+ if (
+ float_compare(
+ value1=record.selling_price,
+ value2=(0.9 * record.expected_price),
+ precision_digits=2,
+ )
+ == -1
+ ):
+ raise ValidationError(
+ "Property selling price MUST be 90% at least of the expected price."
+ )
+
+ @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("property_offer_ids.price")
+ def _compute_best_price(self):
+ for record in self:
+ record.best_price = max(
+ record.property_offer_ids.mapped("price"), default=0
+ )
+
+ @api.onchange("garden")
+ def _onchange_garden(self):
+ self.garden_area = 10 if self.garden else 0
+ self.garden_orientation = "north" if self.garden else ""
+
+ def action_set_cancelled(self):
+ self.ensure_one()
+ if self.state == "cancelled":
+ raise UserError("Cancelled Items cannot be sold.")
+ self.state = "sold"
+ return True
+
+
+ def action_set_sold(self):
+ for record in self:
+ if record.state == "sold":
+ raise UserError("Sold Items cannot be cancelled.")
+ record.state = "cancelled"
+ return True
+
+
+ def action_process_accept(self, offer):
+ self.ensure_one()
+ if self.state == "offer_accepted":
+ raise UserError("this property has already an accepted offer!!")
+ self.state = "offer_accepted"
+ self.selling_price = offer.price
+ self.partner_id = offer.partner_id
+ return True
+
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_property(self):
+ for record in self:
+ if record.state not in ('new','cancelled'):
+ raise UserError("Error, It CANNOT be deleted")
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 0000000000..3718bd0086
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,69 @@
+from datetime import timedelta
+from odoo import api, fields, models
+from odoo.exceptions import UserError, ValidationError
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.offer"
+ _description = "Offers for real state properties"
+ _order = "price desc"
+
+
+ validity = fields.Integer(string="Validity (Days)", default=7)
+
+ price = fields.Float()
+ status = fields.Selection(
+ selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False
+ )
+ property_id = fields.Many2one(comodel_name="estate.property")
+ partner_id = fields.Many2one(comodel_name="res.partner", string="Buyer")
+
+ date_deadline = fields.Date(
+ compute="_compute_date_deadline", inverse="_inverse_date_deadline"
+ )
+ property_type_id = fields.Many2one(
+ comodel_name="estate.property.type",
+ related="property_id.property_type_id",
+ store=True,
+ )
+
+ _sql_constraints = [
+ ("check_price", "CHECK(price >= 0)", "Offer prices MUST be postive."),
+ ]
+
+ def action_accept(self):
+ self.ensure_one()
+ self.property_id.action_process_accept(self)
+ self.status = "accepted"
+ return True
+
+
+ def action_refuse(self):
+ self.ensure_one()
+ self.status = "refused"
+ return True
+
+ @api.depends("validity", "create_date")
+ def _compute_date_deadline(self):
+ for record in self:
+ if not record.create_date:
+ record.create_date = fields.Date.today()
+ record.date_deadline = record.create_date + timedelta(days=record.validity)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ record.validity = (record.date_deadline - record.create_date.date()).days
+
+
+ @api.model_create_multi
+ def create(self, vals):
+ property_record = self.env['estate.property'].browse(vals['property_id'])
+ existing_offers = self.search([
+ ('property_id', '=', vals['property_id'])
+ ])
+ if any(offer.price >= vals['price'] for offer in existing_offers):
+ raise ValidationError(
+ "Error, You CANNOT create an offer with a lower price than an existing one."
+ )
+ property_record.state = 'offer_received'
+ return super().create(vals)
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 0000000000..1cf1802822
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,15 @@
+from odoo import fields, models
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.tag"
+ _description = "Tags for real state properties"
+ _order = "name"
+
+
+ name = fields.Char(required=True)
+ color = fields.Integer(string='Color', default=0)
+
+ _sql_constraints = [
+ ("check_name", "UNIQUE(name)", "Tag names MUST be unique."),
+ ]
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 0000000000..278cf102ce
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,26 @@
+from odoo import api, fields, models
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "Types for real state properties"
+ _order = "sequence, name"
+
+
+ name = fields.Char(required=True)
+ property_ids = fields.One2many('estate.property','property_type_id', string= 'Properties')
+ sequence = fields.Integer('Sequence', default=1, help="Used to order types. Higher is more used.")
+ offer_ids = fields.One2many(
+ comodel_name="estate.property.offer", inverse_name="property_type_id"
+ )
+ offer_count = fields.Integer(compute="_compute_offer_count")
+
+ @api.depends("offer_ids")
+ def _compute_offer_count(self):
+ for record in self:
+ record.offer_count = len(record.offer_ids)
+
+
+ _sql_constraints = [
+ ("check_name", "UNIQUE(name)", "Type Names MUST be unique."),
+ ]
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 0000000000..676c2d06c8
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,12 @@
+from odoo import fields, models
+
+
+class Users(models.Model):
+ _inherit = "res.users"
+
+ property_ids = fields.One2many(
+ comodel_name="estate.property",
+ inverse_name="user_id",
+ string='Available Properties',
+ domain=["|", ("state", "=", "new"), ("state", "=", "offer_received")],
+ )
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 0000000000..5d6ae781d3
--- /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
+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_menues.xml b/estate/views/estate_menues.xml
new file mode 100644
index 0000000000..855e960b03
--- /dev/null
+++ b/estate/views/estate_menues.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
new file mode 100644
index 0000000000..f7ba3769f6
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ Offers
+ estate.property.offer
+ list,form
+ [('property_type_id', '=', active_id)]
+
+
+
+ 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 0000000000..5c5f8da7d6
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ Tags
+ estate.property.tag
+ list,form
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
+ estate.property.tag.form
+ estate.property.tag
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 0000000000..550a980034
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,55 @@
+
+
+
+
+ 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 0000000000..920ae6ca5c
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,146 @@
+
+
+
+
+ Properties
+ estate.property
+ list,form,kanban
+ {'search_default_available': True}
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+
+ Best Offer:
+
+
+
+ Selling Price:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
new file mode 100644
index 0000000000..7e8857c7e3
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ res.users.view.form.inherit.estate
+ res.users
+
+
+
+
+
+
+
+
+
+
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
new file mode 100644
index 0000000000..0650744f6b
--- /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 0000000000..50dfac7539
--- /dev/null
+++ b/estate_account/manifest.py
@@ -0,0 +1,9 @@
+{
+ "name": "Sales Accounting",
+ "depends": ["estate", "account"],
+ "installable": True,
+ "license": "LGPL-3",
+ "data": [
+ "security/ir.model.access.csv",
+ ],
+}
diff --git a/estate_account/models/__init_.py b/estate_account/models/__init_.py
new file mode 100644
index 0000000000..5e1963c9d2
--- /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 0000000000..e06211186f
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,30 @@
+from odoo import models, Command
+
+class EstateProperty(models.Model):
+ _inherit = "estate.property"
+
+ def action_set_sold(self):
+ self.ensure_one()
+ self.env["account.move"].create(
+ {
+ "partner_id": self.buyer_id.id,
+ "move_type": "out_invoice",
+ "invoice_line_ids": [
+ Command.create(
+ {
+ "name": self.name,
+ "quantity": 1,
+ "price_unit": 0.06*self.selling_price,
+ }
+ ),
+ Command.create(
+ {
+ "name": "Administrative Fees",
+ "quantity": 1,
+ "price_unit": 100,
+ }
+ ),
+ ],
+ }
+ )
+ return super().action_set_sold()