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 @@ + + +
+

Counter:

+
+ +
+
+
+
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: +
+
+
+
+
+

Cards

+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
- 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 + + + + + + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + 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()