diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index e57ef4d5bb0..7528d8a1931 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 31406e8addb..17e48f7c1fd 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 457bae27e11..b0f26a9a602 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 56d4a051287..b5aab01be32 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 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 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<templates xml:space="preserve"> - - <t t-name="awesome_dashboard.AwesomeDashboard"> - hello dashboard - </t> - -</templates> diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..683bc4d23c7 --- /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 00000000000..32862ec0d82 --- /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..9c48b463423 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,19 @@ + +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_dashboard.AwesomeDashboard"> + <Layout display="{controlPanel: {} }" className="'o_dashboard h-100'"> + <t t-set-slot="layout-buttons"> + <div class="o_cp_buttons" role="toolbar" aria-label="Control Panel Buttons" t-ref="buttons"> + <button class="btn btn-primary" t-on-click="openCustomers">Customers</button> + <button class="btn btn-primary" style="margin-left: 0.5em" t-on-click="openLeads">Leads</button> + </div> + </t> + <t t-slot="content"> + <DashboardItem>Item 1</DashboardItem> + <DashboardItem size="2">Item 2</DashboardItem> + </t> + </Layout> + </t> +</templates> + 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 00000000000..aef49ee9bfa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_items/dahboard_item.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_dashboard.DashboardItem"> + <div class="o_dashboard_item" t-att-style="'width: ' + (props.size * 18) + 'rem'"> + <t t-slot="default" /> + </div> + </t> +</templates> 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 00000000000..0bed5a9b365 --- /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 00000000000..f6063ade108 --- /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 624766dca89..9410319978e 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 eae20acbf5c..45dc4a2bb71 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 0c11b8298ac..555008c371f 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 affef78bb12..e31a4d7189b 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 457bae27e11..b0f26a9a602 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 77abad510ef..a381f2e7ad6 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 457bae27e11..b0f26a9a602 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 bccfd6fe283..4da2f03a9f3 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 00000000000..34bccfc468c --- /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 00000000000..cc0b04323cb --- /dev/null +++ b/awesome_owl/static/src/components/TodoItem/todoitem.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_owl.todoitem"> + <div t-att-class="{'text-muted text-decoration-line-through': props.todo.isCompleted}"> + <input type="checkbox" t-att-checked="props.todo.isCompleted" t-on-change="toggleState"/> + <t t-esc="props.todo.id"/>. + <t t-esc="props.todo.description"/> + <span class="fa fa-remove ms-3 text-danger" t-on-click="removeTodo"/> + </div> + </t> +</templates> 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 00000000000..f797986f039 --- /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 00000000000..cd82af93974 --- /dev/null +++ b/awesome_owl/static/src/components/TodoList/todolist.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_owl.todolist"> + <div class="todo-list"> + <h3>Todo List</h3> + <div class="card shadow-sm mb-4"> + <input t-ref= "todo_input" placeholder="Enter a new to-do item." t-on-keyup="addTodo"/> + </div> + <div t-foreach="this.todos" t-as="todo" t-key="todo.id"> + <TodoItem t-props="{'todo': todo}" toggleState.bind="toggleState" removeTodo.bind="removeTodo"/> + </div> + </div> + </t> +</templates> 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..9ed5c0761c8 --- /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 00000000000..6d85d12b8a8 --- /dev/null +++ b/awesome_owl/static/src/components/card/card.xml @@ -0,0 +1,12 @@ +<templates xml:space="preserve"> + <t t-name="awesome_owl.card"> + <div class="card d-inline-block m-2" style="width: 18rem;"> + <div class="card-body"> + <h5 class="card-title"> <t t-esc="props.title"/> + <button class="btn btn-primary" t-on-click="toggle">Show Counter</button> + </h5> + <t t-slot="default" t-if="!state.hidden"/> + </div> + </div> + </t> +</templates> 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..aee34b8e3c0 --- /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 00000000000..a36a112e324 --- /dev/null +++ b/awesome_owl/static/src/components/counter/counter.xml @@ -0,0 +1,10 @@ +<templates xml:space="preserve"> + <t t-name="awesome_owl.counter"> + <div class="counter text-center"> + <h1 class="display-4 mb-3">Counter: <t t-esc="state.count"/></h1> + <div class="d-flex justify-content-center gap-2"> + <button class="btn btn-success" t-on-click="increment">+</button> + </div> + </div> + </t> +</templates> diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..f2d641c051f 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= "<div>Card with no markup</div>"; + content2 = markup("<div>Card with markup content</div>"); } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..37172310067 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,45 @@ <?xml version="1.0" encoding="UTF-8" ?> <templates xml:space="preserve"> - <t t-name="awesome_owl.playground"> - <div class="p-3"> - hello world + <div class="container py-5"> + <div class="card shadow-sm mb-4"> + <div class="card-body"> + <h2 class="card-title">Counters</h2> + <div class="d-flex gap-3"> + <div class="col"> + <Counter onChange.bind="incrementSum"/> + </div> + <div class="col"> + <Counter onChange.bind="incrementSum"/> + </div> + </div> + <div class="mt-3"> + <strong>Sum:</strong> <t t-esc="state.sum"/> + </div> + </div> + </div> + <div class="card shadow-sm mb-4"> + <div class="card-body"> + <h1 class="card-title mb-4">Cards</h1> + <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"> + <div class="col"> + <Card title="'Card 1'"> + <Counter/> + </Card> + </div> + <div class="col"> + <Card title="'Card 2'"> + <Counter/> + </Card> + </div> + </div> + </div> + </div> + <div class="card shadow-sm"> + <div class="card-body"> + <ToDoList /> + </div> + </div> </div> </t> - </templates> diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..0b0690f0e94 --- /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 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..32931c038cf --- /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 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..5fce905e259 --- /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 00000000000..3718bd0086d --- /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 00000000000..1cf18028229 --- /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 00000000000..278cf102ce4 --- /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 00000000000..676c2d06c88 --- /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 00000000000..5d6ae781d33 --- /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 00000000000..855e960b03f --- /dev/null +++ b/estate/views/estate_menues.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<odoo> + <menuitem id="estate_property_menu_root" name="Real Estate Property"> + <menuitem id="estate_property_first_level_menu" name="Advertisements"> + <menuitem id="estate_property_second_level_menu" name="Properties"> + <menuitem id="estate_property_menu_action" action="estate_property_action"/> + </menuitem> + </menuitem> + <menuitem id="settings_menue" name="Settings"> + <menuitem id="estate_property_type_menu_action" action="estate_property_type_action"/> + <menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action"/> + </menuitem> + </menuitem> + + +</odoo> diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..f7ba3769f61 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<odoo> + + <record id="estate_property_offer_action" model="ir.actions.act_window"> + <field name="name">Offers</field> + <field name="res_model">estate.property.offer</field> + <field name="view_mode">list,form</field> + <field name="domain">[('property_type_id', '=', active_id)]</field> + </record> + + <record id="estate_property_offer_view_tree" model="ir.ui.view"> + <field name="name">estate.property.offer.list</field> + <field name="model">estate.property.offer</field> + <field name="arch" type="xml"> + <list string="Offers" editable="bottom" decoration-danger="status == 'refused'" decoration-success="status == 'accepted'"> + <field name="price" string="Price" width="120px"/> + <field name="partner_id" string="Partner" width="170px"/> + <field name="validity" /> + <field name="date_deadline" string="Deadline"/> + <field name="property_type_id" string="Property Type"/> + <button name="action_accept" string="Accept" type="object" icon="fa-check" invisible="status"/> + <button name="action_refuse" string="Refuse" type="object" icon="fa-times" invisible="status"/> + </list> + </field> + </record> + + <record id="estate_property_offer_view_form" model="ir.ui.view"> + <field name="name">estate.property.offer.form</field> + <field name="model">estate.property.offer</field> + <field name="arch" type="xml"> + <form string="Create a new Offer"> + <group> + <field name="price" string="Price" /> + <field name="partner_id" string="Partner" /> + <field name="validity" /> + <field name="date_deadline" string="Deadline"/> + </group> + </form> + </field> + </record> +</odoo> diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..5c5f8da7d61 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<odoo> + + <record id="estate_property_tag_action" model="ir.actions.act_window"> + <field name="name">Tags</field> + <field name="res_model">estate.property.tag</field> + <field name="view_mode">list,form</field> + </record> + <record id="estate_property_tag_view_list" model="ir.ui.view"> + <field name="name">estate.property.tag.list</field> + <field name="model">estate.property.tag</field> + <field name="arch" type="xml"> + <list string="Tags" editable="bottom"> + <field name="name" string="Tag"/> + </list> + </field> + </record> + <record id="estate_property_type_view_form" model="ir.ui.view"> + <field name="name">estate.property.tag.form</field> + <field name="model">estate.property.tag</field> + <field name="arch" type="xml"> + <form string="Tags"> + <sheet> + <h1> + <field name="name" string="Property Tag" placeholder="Property Tag"/> + </h1> + </sheet> + </form> + </field> + </record> +</odoo> diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..550a9800348 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<odoo> + + <record id="estate_property_type_action" model="ir.actions.act_window"> + <field name="name">Types</field> + <field name="res_model">estate.property.type</field> + <field name="view_mode">list,form</field> + </record> + + <record id="estate_property_types_view_tree" model="ir.ui.view"> + <field name="name">estate.property.type.list</field> + <field name="model">estate.property.type</field> + <field name="arch" type="xml"> + <list string="Property Types"> + <field name="name" string="Title" width="120px"/> + <field name="sequence" widget="handle"/> + </list> + </field> + </record> + + <record id="estate_property_types_view_form" model="ir.ui.view"> + <field name="name">estate.property.type.form</field> + <field name="model">estate.property.type</field> + <field name="arch" type="xml"> + <form string="Property Types"> + <sheet> + <div name="button_box" class="oe_button_box"> + <button name="%(estate.estate_property_offer_action)d" + class="oe_stat_button" + icon="fa-money" + type="action"> + <field name="offer_count" widget="statinfo" string="Offers"/> + </button> + </div> + <header> + <h1> + <field name="name"/> + </h1> + </header> + <notebook> + <page string="Properties"> + <field name="property_ids"> + <list> + <field name="name" string="Title"/> + <field name="expected_price" string="Expected Price"/> + <field name="state" string="Status"/> + </list> + </field> + </page> + </notebook> + </sheet> + </form> + </field> + </record> +</odoo> diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..920ae6ca5c6 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,146 @@ +<?xml version="1.0"?> +<odoo> + + <record id="estate_property_action" model="ir.actions.act_window"> + <field name="name">Properties</field> + <field name="res_model">estate.property</field> + <field name="view_mode">list,form,kanban</field> + <field name="context">{'search_default_available': True}</field> + + </record> + + <record id="estate_property_view_search" model="ir.ui.view"> + <field name="name">estate.property.search</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <search string="Properties"> + <field name="name" string="Ttile"/> + <field name="postcode" string="Postcode"/> + <field name="expected_price" string="Expected Price"/> + <field name="bedrooms" string="Bedrooms"/> + <field name="living_area" string="Living Area (sqm)" filter_domain="[('living_area', '>=', self)]"/> + <field name="selling_price" string="Selling Price"/> + <field name="date_availability" string="Availabe From"/> + <separator/> + <filter string="Available" name="available" domain="[('state', 'in', ('new','offer_received'))]"/> + <group expand="1" string="Postcode"> + <filter string="Postcode" name="postcode" context="{'group_by':'postcode'}"/> + </group> + </search> + </field> + </record> + <record id="estate_property_view_tree" model="ir.ui.view"> + <field name="name">estate.property.list</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <list string="Properties" decoration-success="state in ('offer_received', 'offer_accepted')" decoration-bf="state == 'offer_accepted'" decoration-muted="state == 'sold'"> + <field name="name" string="Title" width="120px"/> + <field name="postcode" string="Postcode"/> + <field name="property_tag_ids" string="Tags" widget="many2many_tags" options="{'color_field': 'color'}"/> + <field name="expected_price" string="Expected Price"/> + <field name="bedrooms" string="Bedrooms"/> + <field name="living_area" string="Living Area (sqm)"/> + <field name="facades" string="Facades"/> + <field name="property_type_id" string="Types"/> + <field name="date_availability" string="Available From" optional="hide"/> + </list> + </field> + </record> + <record id="estate_property_view_kanban" model="ir.ui.view"> + <field name="name">estate.property.kanban</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <kanban default_group_by="property_type_id" records_draggable='false'> + <field name="state"/> + <templates> + <t t-name="card"> + <div> + <main class="ms-2"> + <div class="o_kanban_record_headings mt5"> + <span> + <field name="name"/> + </span> + </div> + <div> + <span>Expected Price:</span> + <field name="expected_price"/> + </div> + <div t-if="record.state.raw_value == 'offer_received'"> + <span>Best Offer:</span> + <field name="best_price"/> + </div> + <div t-if="record.state.raw_value == 'offer_accepted'"> + <span>Selling Price:</span> + <field name="selling_price"/> + </div> + <div> + <field name="property_tag_ids" options="{'color_field': 'color'}"/> + </div> + </main> + </div> + </t> + </templates> + </kanban> + </field> + </record> + <record id="estate_property_view_form" model="ir.ui.view"> + <field name="name">estate.property.form</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <form string="Properties"> + <sheet> + <header> + <button name="action_set_cancelled" string="Cancel" type="object" invisible="state in ('sold', 'cancelled')"/> + <button name="action_set_sold" string="Sold" class="oe_highlight" type="object" invisible="state in ('sold', 'cancelled')"/> + <field name="state" widget="statusbar" statusbar_visible="new,offer_received,offer_accepted, sold"/> + </header> + <group> + <h1> + <field name="name" string="Title" placeholder="Property Title"/> + </h1> + <field name="property_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/> + </group> + <group> + <group> + <field name="property_type_id" string="Property Type" options="{'no_create': true}"/> + <field name="postcode" string="Postcode"/> + <field name="date_availability" string="Availabe From"/> + </group> + <group> + <field name="expected_price" string="Expected Price"/> + <field name="best_price" string="Best Offer"/> + <field name="selling_price" string="Selling Price"/> + </group> + </group> + <notebook> + <page string="Description"> + <group> + <field name="bedrooms" string="Bedrooms"/> + <field name="living_area" string="Living Area (sqm)"/> + <field name="facades" string="Facades"/> + <field name="garage" string="Garage"/> + <field name="garden" string="Garden"/> + <field name="garden_area" string="Garden Area" invisible="not garden"/> + <field name="garden_orientation" string="Garden Orientation" invisible="not garden"/> + <field name="total_area" string="Total Area"/> + <field name="description" string="Description"/> + </group> + </page> + <page string="Other Info"> + <group> + <field name="partner_id" string="Salesman"/> + <field name="user_id" string="Buyer"/> + </group> + </page> + <page string="Offers"> + <field name="property_offer_ids" readonly="state in ('offer_accepted', 'sold', 'cancelled')"/> + </page> + </notebook> + </sheet> + </form> + </field> + </record> + + + +</odoo> diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..7e8857c7e3a --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="res_users_inherit_property_view_form" model="ir.ui.view"> + <field name="name">res.users.view.form.inherit.estate</field> + <field name="model">res.users</field> + <field name="inherit_id" ref="base.view_users_form"/> + <field name="arch" type="xml"> + <notebook position="inside"> + <page string="Properties"> + <field name="property_ids" /> + </page> + </notebook> + </field> + </record> +</odoo> 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..50dfac7539f --- /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 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..e06211186f9 --- /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()