From 1da68edadfef1060be914098d38af9ef3bd84e7f Mon Sep 17 00:00:00 2001 From: SebVrc Date: Tue, 21 Apr 2026 15:19:36 +0200 Subject: [PATCH 1/4] [ADD] estate: real estate module from server framework tutorial --- estate/__init__.py | 1 + estate/__manifest__.py | 18 +++ estate/models/__init__.py | 5 + estate/models/estate_property.py | 110 +++++++++++++++++++ estate/models/estate_property_offer.py | 79 +++++++++++++ estate/models/estate_property_tag.py | 15 +++ estate/models/estate_property_type.py | 16 +++ estate/models/user_property.py | 9 ++ estate/security/ir.model.access.csv | 5 + estate/tests/__init__.py | 1 + estate/tests/test_estate_property.py | 44 ++++++++ estate/views/estate_menus.xml | 11 ++ estate/views/estate_property_offer_views.xml | 35 ++++++ estate/views/estate_property_tag_views.xml | 8 ++ estate/views/estate_property_type_views.xml | 44 ++++++++ estate/views/estate_property_users_view.xml | 14 +++ estate/views/estate_property_views.xml | 100 +++++++++++++++++ 17 files changed, 515 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_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/models/user_property.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate_property.py create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/estate_property_users_view.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..b407910104d --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Estate", + "author": "sever", + "depends": [ + "base", + ], + "application": True, + "license": "LGPL-3", + "data": [ + "views/estate_property_tag_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_views.xml", + "views/estate_property_users_view.xml", + "views/estate_menus.xml", + "security/ir.model.access.csv", + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..e408ee51abc --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property # noqa: I001 +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import user_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..c7ca9461bf2 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,110 @@ +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 = "Estate Property Module" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + default=fields.Date.add(fields.Date.today(), months=3), + copy=False, + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_areas = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + [("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")], + ) + active = fields.Boolean(default=True) + state = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + default="new", + required=True, + copy=False, + ) + property_type_id = fields.Many2one("estate.property.type") + buyer_id = fields.Many2one("res.partner", copy=False) + seller_id = fields.Many2one("res.users", default=lambda self: self.env.user) + property_tag_ids = fields.Many2many("estate.property.tag") + 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") + + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", + "The expected price must be strictly positive.", + ) + _check_selling_price = models.Constraint( + "CHECK(selling_price >= 0)", + "The selling price must be positive", + ) + + @api.depends("living_areas", "garden_area") + def _compute_total_area(self): + for property in self: + property.total_area = property.garden_area + property.living_areas + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for property in self: + property.best_price = ( + max(property.offer_ids.mapped("price")) if len(property.offer_ids) > 0 else 0 + ) + + @api.constrains("expected_price", "selling_price") + def _check_selling_price(self): + for property in self: + too_low = float_compare(property.selling_price, 0.9 * property.expected_price, 3) == -1 + if not float_is_zero(property.selling_price, 3) and too_low: + msg = "The selling price cannot be lower than 90 percents of the expected price." + raise ValidationError(msg) + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = "north" + else: + self.garden_area = 0 + self.garden_orientation = None + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_cancelled_property(self): + for property in self: + if property.state != "new" and property.state != "cancelled": + msg = "Only new or cancelled properties can be deleted" + raise UserError(msg) + return super().unlink() + + def action_sold_property(self): + for property in self: + if property.state == "cancelled": + msg = "Sold properties cannot be cancelled" + raise UserError(msg) + property.state = "sold" + return True + + def action_cancel_property(self): + for property in self: + if property.state == "sold": + msg = "Cancelled properties cannot be sold" + raise UserError(msg) + property.state = "cancelled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..fb0da936291 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,79 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_compare + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + [("accepted", "Accepted"), ("refused", "Refused")], + copy=False, + ) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + ) + + _check_offer_price = models.Constraint( + "CHECK(price > 0)", + "The offer price must be strictly positive", + ) + + @api.depends("validity") + def _compute_date_deadline(self): + for offer in self: + if offer.create_date: + offer.date_deadline = fields.Date.add( + offer.create_date, + days=offer.validity, + ) + else: + offer.date_deadline = fields.Date.add( + fields.Date.today(), + days=offer.validity, + ) + + def _inverse_date_deadline(self): + for offer in self: + if offer.create_date: + offer.validity = int((offer.date_deadline - offer.create_date.date()).days) + else: + offer.validity = int((offer.date_deadline - fields.Date.today()).days) + + @api.model_create_multi + def create(self, vals): + for offer in vals: + linked_property = self.env["estate.property"].browse(offer["property_id"]) + if float_compare(offer["price"], linked_property.best_price, 3) == -1: + msg = "Offer price cannot be lower than an existing offer" + raise ValidationError(msg) + linked_property.state = "offer_received" + return super().create(vals) + + @api.constrains("status", "property_id") + def _check_south_facing_garden_accept_offer(self): + for offer in self: + price_below_expected = float_compare(offer.price, offer.property_id.expected_price, 3) == -1 + if offer.status == "accepted" and offer.property_id.garden_orientation == "south" and price_below_expected: + msg = "Offers for properties with south facing garden can only be accepted if the price of the offer is above the expected price of the property" + raise ValidationError(msg) + + def action_accept_offer(self): + for offer in self: + offer.status = "accepted" + offer.property_id.selling_price = offer.price + offer.property_id.buyer_id = offer.partner_id + offer.property_id.state = "offer_accepted" + return True + + def action_refuse_offer(self): + for offer in self: + offer.status = "refused" + return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..8126d87e215 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer() + + _check_name_unique = models.Constraint( + "UNIQUE(name)", + "The tag name 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..d0823a9acb8 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _order = "sequence, name" + + name = fields.Char(required=True) + property_ids = fields.One2many("estate.property", "property_type_id") + sequence = fields.Integer("Sequence", default=1) + + _check_name_unique = models.Constraint( + "UNIQUE(name)", + "The type name must be unique", + ) diff --git a/estate/models/user_property.py b/estate/models/user_property.py new file mode 100644 index 00000000000..7d763b37113 --- /dev/null +++ b/estate/models/user_property.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class UserProperties(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", "seller_id", domain=[('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..49bca99cac8 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..576617cccff --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..2727af40584 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,44 @@ +from odoo.exceptions import ValidationError +from odoo.tests import TransactionCase +from odoo import Command + + +class TestEstateProperty(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.estate = cls.env['estate.property'].create({ + 'name': 'Super test estate', + 'expected_price': 100000.0, + 'state': 'new', + }) + cls.test_partner = cls.env['res.partner'].create({ + 'name': 'Maman ours', + }) + + def test_estate_best_price(self): + ''' + Ensure best price is correctly updated when an offer is received. + ''' + self.assertEqual(self.estate.best_price, 0.0) + self.estate.offer_ids = [Command.create({ + 'price': 125000.0, + 'partner_id': self.test_partner.id, + })] + self.assertEqual(self.estate.best_price, 125000.0) + + def test_accept_offer_south_facing_garden(self): + ''' + Ensure offers for estates with south-facing gardens can only be accepted if above expected + price. + ''' + self.estate.expected_price = 500000 + self.estate.offer_ids = [Command.create({ + 'price': 475000.0, + 'partner_id': self.test_partner.id, + })] + self.estate.garden = True + self.estate.garden_orientation = 'south' + with self.assertRaises(ValidationError): + self.estate.offer_ids.action_accept_offer() diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..0bd8bc84734 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..6733f0e6e91 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,35 @@ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..cd4be6e3c20 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,15 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter }; + + setup() { + this.state = useState({ value: 0}); + } + + increment() { + this.state.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..03b7243b2a7 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,9 @@ -
- hello world -
+ First Counter: + +
From 51daf3a74fb7082dd6612e09f4f9de28e9311893 Mon Sep 17 00:00:00 2001 From: SebVrc Date: Thu, 30 Apr 2026 10:48:25 +0200 Subject: [PATCH 4/4] [ADD] awesome_owl: added card component --- awesome_owl/static/src/card/card.js | 9 +++++++++ awesome_owl/static/src/card/card.xml | 14 ++++++++++++++ awesome_owl/static/src/playground.js | 7 +++++-- awesome_owl/static/src/playground.xml | 3 +++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..80a6829afbf --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: String, + content: String, + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..cbc12638419 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,14 @@ + + +
+
+
+ +
+

+ +

+
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index cd4be6e3c20..c5c497ec6c4 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,12 +1,15 @@ -import { Component, useState } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; import { Counter } from "./counter/counter"; +import { Card } from "./card/card" export class Playground extends Component { static template = "awesome_owl.playground"; - static components = { Counter }; + static components = { Counter, Card }; setup() { this.state = useState({ value: 0}); + this.str1 = "
some content
"; + this.str2 = markup("
some content
"); } increment() { diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 03b7243b2a7..f2cc10d52af 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -5,6 +5,9 @@ First Counter: + + +