From a741e76e6fa6763daec6201d7c73576febd900b6 Mon Sep 17 00:00:00 2001 From: odoo Date: Tue, 21 Apr 2026 15:59:32 +0200 Subject: [PATCH 1/3] [ADD] estate: build estate module --- estate/__init__.py | 1 + estate/__manifest__.py | 28 +++ estate/models/__init__.py | 5 + estate/models/estate_property.py | 112 ++++++++++++ estate/models/estate_propery_offer.py | 90 ++++++++++ estate/models/estate_propery_tag.py | 15 ++ estate/models/estate_propery_type.py | 31 ++++ estate/models/res_users.py | 6 + estate/security/ir.model.access.csv | 5 + estate/static/description/icon.png | Bin 0 -> 1582 bytes estate/static/description/icon.svg | 1 + estate/static/description/icon_hi.png | Bin 0 -> 9081 bytes estate/tests/__init__.py | 1 + estate/tests/test_estate_property.py | 45 +++++ estate/views/estate_property_menu.xml | 48 +++++ estate/views/estate_property_offers_views.xml | 19 ++ estate/views/estate_property_tags_views.xml | 30 ++++ estate/views/estate_property_type_views.xml | 62 +++++++ estate/views/estate_property_users_views.xml | 17 ++ estate/views/estate_property_views.xml | 166 ++++++++++++++++++ 20 files changed, 682 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/models/estate_propery_offer.py create mode 100644 estate/models/estate_propery_tag.py create mode 100644 estate/models/estate_propery_type.py create mode 100644 estate/models/res_users.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/static/description/icon.png create mode 100644 estate/static/description/icon.svg create mode 100644 estate/static/description/icon_hi.png create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate_property.py create mode 100644 estate/views/estate_property_menu.xml create mode 100644 estate/views/estate_property_offers_views.xml create mode 100644 estate/views/estate_property_tags_views.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/estate_property_users_views.xml create mode 100644 estate/views/estate_property_views.xml 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..d5570b13b92 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,28 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Real Estate', + 'version': '19.0.0.1.0', + 'category': 'Real Estate/Properties', + 'sequence': 15, + 'summary': 'Track leads and close opportunities', + 'website': 'https://www.odoo.com/app/estate', + 'depends': [ + 'base', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_type_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_users_views.xml', + 'views/estate_property_offers_views.xml', + 'views/estate_property_views.xml', + 'views/estate_property_menu.xml', + ], + 'demo': [], + 'installable': True, + 'application': True, + 'assets': {}, + 'author': 'Odoo S.A.', + 'license': 'LGPL-3', +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..d16f45eccd6 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_propery_type +from . import estate_propery_tag +from . import estate_propery_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..508f9ca895b --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,112 @@ +from odoo import fields, models, api +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" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_avaibility = fields.Date(string="Date Avaibility") + expected_price = fields.Float() + selling_price = fields.Float() + 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( + selection=[ + ("new", "New"), + ("offer received", "Offer Received"), + ("offer accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled") + ], + default="new" + ) + + partner_id = fields.Many2one("res.partner", string="Partner") + user_id = fields.Many2one("res.users", string="Users") + property_type_id = fields.Many2one("estate.property.type", + string="PropType") + property_tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many( + "estate.property.offer", "property_id", string="offers") + total_area = fields.Float(compute="_compute_total") + best_price = fields.Float(compute="_compute_best_price") + + # define constrain + _check_expected_price = models.Constraint( + 'CHECK(expected_price > 0)', + 'The expected price must be strictly positive', + ) + + @api.depends("living_area", "garden_area") + def _compute_total(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price", "expected_price") + def _compute_best_price(self): + for record in self: + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped("price")) + else: + record.best_price = 0 + + @api.onchange("garden") + def _onchange_garden(self): + for record in self: + if record.garden: + record.garden_area = 10 + record.garden_orientation = "north" + else: + record.garden_area = 0 + record.garden_orientation = "" + + @api.constrains("expected_price", "selling_price") + def _check_price_offer(self): + for record in self: + if record.selling_price > 0: + if float_is_zero(record.selling_price, precision_rounding=2): + continue + min_price = 0.9 * record.expected_price + # if offer percentage lower then 90% + if float_compare(record.selling_price, min_price, precision_digits=2) == -1: + raise ValidationError( + "The selling price cannot be lower than 90% of the expected price\n" + "You must reduce the expected price if you want to accept this offer" + ) + + def action_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError("Cancelled properties cannot be sold") + record.state = "sold" + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("Sold properties cannot be cancelled") + record.state = "cancelled" + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_canceled(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise UserError("You can only delete new or canceled properties.") diff --git a/estate/models/estate_propery_offer.py b/estate/models/estate_propery_offer.py new file mode 100644 index 00000000000..a3ab31c87b3 --- /dev/null +++ b/estate/models/estate_propery_offer.py @@ -0,0 +1,90 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError, ValidationError +from datetime import datetime, timedelta + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused") + ], + ) + partner_id = fields.Many2one( + "res.partner", string="Partner", required=True) + property_id = fields.Many2one( + "estate.property", required=True) + property_type_id = fields.Many2one("estate.property.type") + deadline = fields.Date( + string="Deadline", + default=datetime.today(), + compute="_compute_validity_days", + inverse="_inverse_deadline", + store=True + ) + validity_days = fields.Integer(default=7) + + _check_expected_price = models.Constraint( + 'CHECK(price > 0)', + 'The price must be strictly positive', + ) + + @api.depends("validity_days") + def _compute_validity_days(self): + for record in self: + if record.deadline and record.validity_days > 0: + record.deadline = record.deadline + timedelta( + days=record.validity_days) + else: + record.deadline = fields.Date.today() + + @api.depends("deadline") + def _inverse_deadline(self): + for record in self: + if record.deadline: + diff = record.deadline - fields.Date.today() + record.validity_days = diff.days + + def accept_offer(self): + for record in self: + if record.status == "accepted": + raise ValidationError("the offer already accepted") + + if record.property_id.garden_orientation == "south" and record.property_id.expected_price > record.price: + raise ValidationError("The south-facing garden can only be accepted if above expected price") + + if record.property_id.selling_price == 0: + record.status = "accepted" + record.property_id.selling_price = record.price + record.property_id.partner_id = record.partner_id + record.property_id.state = "offer accepted" + + def action_refuse_offer(self): + for record in self: + if record.status == "accepted": + raise ValidationError( + "You're not eligible to refused an accepted offer" + ) + record.status = "refused" + + @api.model + def create(self, val_list): + for vals in val_list: + property_id = vals.get('property_id') + + existing_offers = self.search([ + ('property_id', '=', property_id)], + order='price desc', limit=1) + + if existing_offers and vals.get('price', 0) < existing_offers.price: + raise UserError(f"You cannot create an offer lower than {existing_offers.price}.") + + property_record = self.env['estate.property'].browse(property_id) + property_record.state = 'offer received' + + return super().create(val_list) diff --git a/estate/models/estate_propery_tag.py b/estate/models/estate_propery_tag.py new file mode 100644 index 00000000000..e78e66125aa --- /dev/null +++ b/estate/models/estate_propery_tag.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + _order = "name asc" + + name = fields.Char(required=True) + + color = fields.Integer() + + _check_unique_property_tags = models.Constraint( + 'unique(name)' + ) diff --git a/estate/models/estate_propery_type.py b/estate/models/estate_propery_type.py new file mode 100644 index 00000000000..a2ec0c12e6e --- /dev/null +++ b/estate/models/estate_propery_type.py @@ -0,0 +1,31 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + _order = "name asc" + + sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.") + name = fields.Char(required=True) + + property_ids = fields.One2many("estate.property", "property_type_id") + offer_ids = fields.One2many("estate.property.offer", "property_id") + expected_price = fields.Float() + state = fields.Char() + offer_count = fields.Char(compute="_compute_offer") + + @api.depends("offer_ids") + def _compute_offer(self): + for record in self: + record.offer_count = len(record.offer_ids) + + def action_offer(self): + return { + "type": "ir.actions.act_window", + "name": "Propery Offers", + "res_model": "estate.property.offer", + "domain": [("property_id", "=", self.id)], + "view_mode": "list,form", + "context": {"default_property_id": self.id}, + } diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..a387e729ad4 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,6 @@ +from odoo import fields, models + + +class Users(models.Model): + _inherit = "res.users" + property_ids = fields.One2many("estate.property", 'user_id') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0c0b62b7fee --- /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 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a02a465d8ccd4415eddb61e169674676f3cfdc44 GIT binary patch literal 1582 zcmV+}2GRM6P)S6203e`$lH;o`oNL!lF>~5y>HYBI!KmXmG$-E)&2miLTvrC!Z zzIiimX0iYb!!QiPFbu;mTZ;fg)MqB@Yohs=A^^P*5uG4KD}bzr?S-6cKNNu~h(VP+ zl9^}~vHXC5;xXlSO@Q3^g(;JYgE>DWMe@qm(_~ zHDx9`Q3goa3XE~e5`vK-Qcx3RwksIGS~nwON^`1Jaxzk8J_$J>>$-%$t9o;IUzXC zPha^9O!M`{pS{>0Avw!RojqeT0pDD{<4p;kq>!BYB%e~7OD4eFF*r8r&F&%Db;z*V z={sfMQ;qL2?9r|v=^evC6(zTpm(|W;`Q`YVYRJRa`y>M)zo(8JI<96%xJMGUERR#? z)Y%a}PXo zE)s)<5QBvfgM|>otE7%>+yxfp|70mCW$Ca((BI>qUE+lh?5afLLu$JF1RQ8;fu6Sg zYRX${e7hHABvNmHOGpbr5q(|V@QPXxZg%qBBox*!WQAZ9;Az9HJOY{{u(Bs71W&DF zndG>G1JV>qEu@6BsAw4Of79yOtj)kwKJ}0Jjj?heBLroff9s^XQ|cHER7GjVCu^qZ5+8^P{LJV?HBSZjP1r0335O_l7?Olz!M%r7aEW`jPqkm@u zG)Ln>&D1PkTmTI$q)Jc*=8l0n{X#do68%oshJ{qYdSskyNWh**a90WeXGA)sP3mD5 zhEnsNGWPYYzL1E-;c!hI9NMu9x*wT*8@X4qA6`#SV+#yfLKX^A4 zrZEp$){!$|WgslXDsm>eQY`C84GXb`oLQCdvPi#owVYRZcVWi^4nZdx);+D*{1;+5i9m07*qoM6N<$g7g*6umAu6 literal 0 HcmV?d00001 diff --git a/estate/static/description/icon.svg b/estate/static/description/icon.svg new file mode 100644 index 00000000000..12e8ec084cc --- /dev/null +++ b/estate/static/description/icon.svg @@ -0,0 +1 @@ + diff --git a/estate/static/description/icon_hi.png b/estate/static/description/icon_hi.png new file mode 100644 index 0000000000000000000000000000000000000000..7a3f54851c93c81dd1fc02791aa70b0df903f873 GIT binary patch literal 9081 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 M*FB$i*6QB>0zn? + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..eae2502042a --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,19 @@ + + + + + estate.property_offer.list + estate.property.offer + + + + + + + + + + +
+ +

+ +

+ +
+
+ + + + + + + + + + + + + +
+
+
+
diff --git a/estate/views/estate_property_users_views.xml b/estate/views/estate_property_users_views.xml new file mode 100644 index 00000000000..5cc378bd95a --- /dev/null +++ b/estate/views/estate_property_users_views.xml @@ -0,0 +1,17 @@ + + + + + inherited.res.users.view.form.estate + res.users + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..c5e52b27903 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,166 @@ + + + + + Property + estate.property + list,kanban,form + {'search_default_active': 1} + +

+ If you need help, ask yourself +

+

+ You need to be strong, go working out +

+

+ don't skip working out +

+
+
+ + estate.properties.list + estate.property + + + + + + + + + + + + + + + + + estate.property.form + estate.property + + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/awesome_owl/static/src/components/todo/todo_item.js b/awesome_owl/static/src/components/todo/todo_item.js new file mode 100644 index 00000000000..c218473d1fb --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_item.js @@ -0,0 +1,12 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + + static props = { + todo: { + type: Object, + shape: { id: Number, description: String, isCompleted: Boolean }, + }, + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/components/todo/todo_item.xml b/awesome_owl/static/src/components/todo/todo_item.xml new file mode 100644 index 00000000000..2e1badc803f --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_item.xml @@ -0,0 +1,9 @@ + + + +
+ . +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/components/todo/todo_list.js b/awesome_owl/static/src/components/todo/todo_list.js new file mode 100644 index 00000000000..91467e9d4a8 --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_list.js @@ -0,0 +1,38 @@ +import { Component, useState, onMounted, useRef} from "@odoo/owl" +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list" + static components = { TodoItem }; + + setup() { + // in TodoList + this.todos = useState([ + // { id: 2, description: "write tutorial", isCompleted: true }, + // { id: 3, description: "buy milk", isCompleted: false }, + ]); + this.nextId = 1; + + this.inputRef = useRef("todo-input"); + + onMounted(() => { + this.inputRef.el.focus(); + }) + } + + addTodo(ev) { + if (ev.keyCode == 13) { + const description = ev.target.value.trim(); + + if (description !== "") { + this.todos.push({ + id:this.nextId++, + description: description, + isCompleted: false, + }); + + ev.target.value = ""; + } + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/components/todo/todo_list.xml b/awesome_owl/static/src/components/todo/todo_list.xml new file mode 100644 index 00000000000..3dba6336ab1 --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_list.xml @@ -0,0 +1,22 @@ + + + +
+

My Tasks

+ +
+ +
+ +
+ + + +
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..c542ee41e4a 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,21 @@ -import { Component } from "@odoo/owl"; +import { Component, useState, markup } from "@odoo/owl"; +import { Counter } from "./components/counter/counter" +import { Card } from "./components/card/card"; +import { TodoList } from "./components/todo/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = { Counter, Card, TodoList }; + + setup() { + this.html = markup("
some content
"); + + this.state = useState({sum: 2}) + this.incrementSum = this.incrementSum.bind(this); + } + + incrementSum(){ + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..83c6b084f95 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,25 @@ -
- hello world +
+
+ hello world +
+ +
+ + + + + The sum is: +
+ + + + + +
- +