From 5b20e6f51155f3a1bff4cb96e8fe22d7285f1682 Mon Sep 17 00:00:00 2001 From: joemo-odoo Date: Tue, 21 Apr 2026 14:02:59 +0200 Subject: [PATCH 1/5] [ADD] estate: create an Real estate Advertisement app make it possible to handle all requests for this [ADD] estate: create placeholder for Real Estate Advertisement make basic setup [IMP] estate: convert module in application convert module in application [IMP] estate: add property in database make it possible to store properties in the database [IMP] estate: add description to property database model make it easier to understand what the estate property means [IMP] estate: add fields in property model make sure the basic information can be stored in the estate property [FIX] estate: in property some fields are not optional some fields should not be required [IMP] estate: add access rights for properties make sure that data is only accessible for the predefined usergroup [IMP] estate: create view for properties making it possible to have a view the model of property [IMP] estate: make app visible in UI make it possible to see the properties in the userinterface [LINT] estate: fix end of file keep code inline with the python standard [FIX] estate: make UI labels in line with wireframe change the labels so the labes in the UI stay inline with the wireframe [CLN] estate: resolve problems with coding guidelines improve consistency with rest of the codebase [IMP] estate: make property more resilient during copy make sure property specific information is not copied [IMP] estate: default values for property reducing work by filling in fields op properties with sensible default values [IMP] estate: add option to archive a property reducing work by filling in fields op properties with sensible default values [FIX] estate: stop archiving new properties when creating new properties they should not be archived immediately [FIX] estate: add different states for property making sure the current state of a property can be properly represented [IMP] estate: add listview for property making it possible to view more information in the list view [IMP] estate: add formview for property make the detail view of a property more appealing [IMP] estate: add search options improve the possible options for searching [IMP] estate: default filter and grouping for property improve the options for searching properties by having defaults [IMP] estate: add type of properties make it possible to distinguish between multiple types of properties [IMP] estate: add buyer and salesperson on property keep track of who bought the property and the salesperson [IMP] estate: add tags for properties adding an option to some custom tag to buildings so you don't need to search for important information about the building [FIX] estate: wrong naming in records of tag and type make naming consistent [IMP] estate: add option to put offers in allow the system to store offers on a property, in preparation to be able to really sell [IMP] estate: add total area of property help the realtor, precalculate the total area [IMP] estate: show best offer help the realtor with showing the price of the best offer [IMP] estate: add validity and deadline to offer make sure an offer is not valid for an infinite amount of time [IMP] estate: defaults for garden attributes based on availability help users when garden is selected to fill in some details and remove them if no garden is there [CLN] estate: use blank end of line follow coding guidelines [CLN] estate: double quotes for language specific texts follow coding guidelines [FIX] estate: make default value based on moment of invocation default availability needs to be calculated when creating the property, not at startup [IMP] estate: rename field make field in line with naming standaard [IMP] estate: state changes make sure transitions are set to reduce users making impossible actions [CLN] estate: add missing fields in module manifest make sure the module is compliant with the testing tools [LINT] estate: blank line at end of file make sure the module is compliant with the coding style [CLN] estate: make _description technical term the _description is used in technical locations so keep it useful for that environment over an explanation [CLN] estate: code guidelines fix naming compute and inverse make sure to follow coding guidelines [FIX] estate: make it possible to delete offers for admins make sure offers can be deleted, but only if you know what you do [CLN] estate: change naming to match coding style for views follow coding rules [REF] estate: improve readability of search filter improve readability [REF] estate: move the security file to load before the views the content of the security can be used in the views but not the opposite [LINT] estate: use linter to fix codestyle follow style defined by team [FIX] estate: security duplicate entry make sure the id's are unique for security rules [IMP] estate: add database constraints on prices, tags and types make sure no invalid data or duplicate can be entered in the database [IMP] estate: add code constraint on selling price make sure the property can not be sold if it is not at least 90% of the expected price [IMP] estate: view property on type page make it possible to view a minimum of property information that is linked to one type of property [IMP] estate: add status bar on property details make the status visually appealing [IMP] estate: add default ordering making sure there is a logical default ordering for each model [IMP] estate: add manual sorting for properties make sure that it is possible to manually sort the properties [IMP] estate: add widget options making sure property tags can be easily distinguished from each-other based on color avoid accidentally creating new property types when creating or editing properties [IMP] estate: hide action buttons if not possible to use help user by not showing buttons they can not use op properties [IMP] estate: improve user workflow help user by disabling or hiding buttons or items they do not need [IMP] estate: list views editable there is no need for detailed form_view if the data is limited and can easily be changed in the listview [IMP] estate: hide field improve usability by making fields optional if they are not adding value all the time [IMP] estate: use decoration to indicate state make the state visually appealing so less space is wasted on the screen and have a quick overview [IMP] estate: improve property search and listview make sure search has some sensible defaults and make sure searching is not exact but at least add some missing information to the listview [IMP] estate: add offers to be shown by type when looking at a type of building see the amount of offers and also see the detailed list of the offers [IMP] estate: improve error handling and fix error message making sure to use the correct type of errors make output to the user more consistent [IMP] estate: add validation on CRUD operations limit creation offer to useful situations do not allow delete of properties that are currently active [IMP] estate: add properties to user page help the user to access properties by adding them to there userpage [IMP] estate: add kanban view for properties have a secondary way to show the different properties, imporve readablility and make sure values are stored correctly [IMP] estate: add kanban view for properties have a secondary way to show the different properties [IMP] estate: follow up on coding advice improve readability of code [IMP] estate: runbot message fix make sure value is stored --- estate/__init__.py | 1 + estate/__manifest__.py | 16 +++ estate/models/__init__.py | 5 + estate/models/estate_property.py | 111 +++++++++++++++ estate/models/estate_property_offer.py | 48 +++++++ estate/models/estate_property_tag.py | 15 +++ estate/models/estate_property_type.py | 22 +++ estate/models/res_users.py | 7 + estate/security/ir.model.access.csv | 6 + estate/views/estate_menus.xml | 11 ++ estate/views/estate_property_offer_views.xml | 25 ++++ estate/views/estate_property_tag_views.xml | 17 +++ estate/views/estate_property_type_views.xml | 42 ++++++ estate/views/estate_property_views.xml | 135 +++++++++++++++++++ estate/views/res_user_views.xml | 15 +++ 15 files changed, 476 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/res_users.py create mode 100644 estate/security/ir.model.access.csv 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_views.xml create mode 100644 estate/views/res_user_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..9fc7a8b9117 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Real estate", + "author": "Odoo", + "license": "LGPL-3", + "depends": ["base"], + "application": True, + "data": [ + "security/ir.model.access.csv", + "views/estate_property_offer_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_views.xml", + "views/res_user_views.xml", + "views/estate_menus.xml", + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..fea9f441d6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_offer +from . import estate_property_tag +from . import estate_property_type +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..a197d1ccb77 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,111 @@ +from dateutil.relativedelta import relativedelta + +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" + _order = "sequence, id desc" + + name = fields.Char("Title", required=True) + sequence = fields.Integer(default=1) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + "Available From", default=lambda self: fields.Date.today() + relativedelta(months=3), copy=False + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer("Living Area (sqm)") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer("Garden Area (sqm)") + garden_orientation = fields.Selection([("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]) + active = fields.Boolean(default=True) + state = fields.Selection( + [ + ("new", "New"), + ("offer", "Offer Received"), + ("accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Cancelled"), + ], + default="new", + readonly=True, + ) + property_type_id = fields.Many2one("estate.property.type") + salesman_id = fields.Many2one("res.users", copy=False, default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", copy=False, readonly=True) + tag_ids = fields.Many2many("estate.property.tag") + offer_ids = fields.One2many("estate.property.offer", "property_id") + total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area") + best_offer = fields.Float(compute="_compute_best_offer") + + _check_expected_price = models.Constraint("CHECK(expected_price>0)", "The expected price needs to be bigger then 0") + _check_selling_price = models.Constraint("CHECK(selling_price>=0)", "The selling price needs to be positive") + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for estate_property in self: + estate_property.total_area = estate_property.living_area + estate_property.garden_area + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for estate_property in self: + estate_property.best_offer = max(estate_property.offer_ids.mapped("price"), default=None) + + @api.onchange("garden") + def _onchange_garden(self): + self.garden_area = 10 if self.garden else 0 + self.garden_orientation = "north" if self.garden else None + + @api.constrains("selling_price", "expected_price") + def _check_selling_price(self): + for estate_property in self: + if ( + not float_is_zero(estate_property.selling_price, 2) + and float_compare(estate_property.selling_price, estate_property.expected_price * 0.9, 2) < 0 + ): + raise ValidationError("The property can not be sold for a price lower than 90% of the expected price") + + @api.ondelete(at_uninstall=False) + def _unlink_property(self): + if any(estate_property.state not in ["new", "canceled"] for estate_property in self): + raise UserError("Only properties with state new or canceled can be removed!") + + def action_state_to_sold(self): + self.ensure_one() + if self.state == "canceled": + raise UserError("Canceled properties can not be sold") + self.state = "sold" + + def action_state_to_canceled(self): + self.ensure_one() + if self.state == "sold": + raise UserError("Sold properties can not be canceled") + self.state = "canceled" + + def accept_offer(self, price, buyer): + self.ensure_one() + if self.state not in ["new", "offer"]: + raise UserError( + "An offer can can only be accepted when the building is still for sale and no other offer is already accepted" + ) + self.selling_price = price + self.buyer_id = buyer + self.state = "accepted" + + def offer_made(self, price): + self.ensure_one() + if self.state not in ["new", "offer"]: + raise UserError( + "An offer can can only be accepted when the building is still for sale and no other offer is already accepted" + ) + if float_compare(price, self.best_offer, 2) <= 0: + raise UserError("Only offers that are over the current best offer can be accepted") + self.state = "offer" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..ee7c5d0bf2e --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,48 @@ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +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("Validity (days)", default=7) + date_deadline = fields.Date("Deadline", compute="_calculate_date_deadline", inverse="_inverse_date_deadline") + property_type_id = fields.Many2one(related="property_id.property_type_id", stored=True) + + _check_price = models.Constraint("CHECK(price>0)", "The selling price needs to be bigger then 0") + + @api.depends("validity", "create_date") + def _calculate_date_deadline(self): + for offer in self: + offer.date_deadline = (offer.create_date or fields.Date.today()) + relativedelta(days=offer.validity) + + def _inverse_date_deadline(self): + for offer in self: + offer.validity = (offer.date_deadline - fields.Date.today()).days + + @api.model + def create(self, vals_list): + for vals in vals_list: + self.env["estate.property"].browse(vals["property_id"]).offer_made(vals["price"]) + return super().create(vals_list) + + def action_accept_offer(self): + self.ensure_one() + if self.status: + raise UserError("The offer was already revised") + self.property_id.accept_offer(self.price, self.partner_id) + self.status = "accepted" + + def action_refuse_offer(self): + self.ensure_one() + if self.status: + raise UserError("The offer was already revised") + self.status = "refused" diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..cb82933c8c1 --- /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() + + _unique_name = models.UniqueIndex( + "(name)", + "Another entry with the same name already exists.", + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..8f675ddaa63 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,22 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate property type" + _order = "name" + + name = fields.Char(required=True) + property_ids = fields.One2many("estate.property", "property_type_id") + offer_ids = fields.One2many("estate.property.offer", "property_type_id") + offer_count = fields.Integer(compute="_compute_offer_count") + + _unique_name = models.UniqueIndex( + "(name)", + "Another entry with the same name already exists.", + ) + + @api.depends("offer_ids") + def _compute_offer_count(self): + for property_type in self: + property_type.offer_count = len(property_type.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..320617c0ce6 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "salesman_id") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..3338f9c89b7 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,6 @@ +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,0 +access_estate_property_offer_admin,access_estate_property_offer_admin,model_estate_property_offer,base.group_system,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..9a1d5e3fd43 --- /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..9d0a2598f67 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,25 @@ + + + estate.property.offer.view.list + estate.property.offer + + + + + + + +

+ +

+ + + + + + + + + +
+
+ + + + Property Types + estate.property.type + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..a00fb804631 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,135 @@ + + + + estate.property.view.search + estate.property + + + + + + + + + + + + + + + + + estate.property.view.list + estate.property + + + + + + + + + + + + + + + + + + estate.property.view.kanban + estate.property + + + + +
+ +
Expected price: + +
+
Best Price: + +
+
Selling Price: + +
+ +
+
+
+
+
+
+ + + estate.property.view.form + estate.property + +
+
+
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Property + estate.property + list,form,kanban + { + 'search_default_available': 1, + } + + +
diff --git a/estate/views/res_user_views.xml b/estate/views/res_user_views.xml new file mode 100644 index 00000000000..d1f5c3c2f23 --- /dev/null +++ b/estate/views/res_user_views.xml @@ -0,0 +1,15 @@ + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + From ac225f6d819c2b24858f4543c6fc91fa4d222ab1 Mon Sep 17 00:00:00 2001 From: joemo-odoo Date: Fri, 24 Apr 2026 14:56:30 +0200 Subject: [PATCH 2/5] [ADD] estate_account: create invoices when property is sold make life easy for the accounting by creating an invoice directly from the property when sold [IMP] estate_account: use the correct error the error that is needed is a usererror not validation error [IMP] estate_account: change type of error errors should be of correct type --- estate/models/estate_property_offer.py | 2 +- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 6 ++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 21 +++++++++++++++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ee7c5d0bf2e..7386c36a902 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -15,7 +15,7 @@ class EstatePropertyOffer(models.Model): property_id = fields.Many2one("estate.property", required=True) validity = fields.Integer("Validity (days)", default=7) date_deadline = fields.Date("Deadline", compute="_calculate_date_deadline", inverse="_inverse_date_deadline") - property_type_id = fields.Many2one(related="property_id.property_type_id", stored=True) + property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) _check_price = models.Constraint("CHECK(price>0)", "The selling price needs to be bigger then 0") 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..6fcca4fd25f --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,6 @@ +{ + "name": "Real estate accounting", + "author": "Odoo", + "license": "LGPL-3", + "depends": ["estate", "account"], +} 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..22aa332f08b --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,21 @@ +from odoo import Command, models +from odoo.exceptions import UserError + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_state_to_sold(self): + if not self.buyer_id: + raise UserError("Properties can only be sold if the buyer is filled in") + self.env["account.move"].create( + { + "move_type": "out_invoice", + "partner_id": self.buyer_id.id, + "invoice_line_ids": [ + Command.create({"name": "commission", "quantity": 1, "price_unit": self.selling_price * 0.06}), + Command.create({"name": "Administrative fee", "quantity": 1, "price_unit": 100}), + ], + } + ) + return super().action_state_to_sold() From 44057da24e3b366ae59119ca1191b52d7319cb3d Mon Sep 17 00:00:00 2001 From: joemo-odoo Date: Thu, 30 Apr 2026 11:16:00 +0200 Subject: [PATCH 3/5] [IMP] awesome_dashboard: add basic tests make basic test cases to get known with the idea of setup testing and form testing --- estate/models/estate_property.py | 2 ++ estate/tests/__init__.py | 2 ++ estate/tests/test_estate_property.py | 33 +++++++++++++++++ estate/tests/test_estate_property_offer.py | 42 ++++++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate_property.py create mode 100644 estate/tests/test_estate_property_offer.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a197d1ccb77..f0f55dcce8d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -82,6 +82,8 @@ def action_state_to_sold(self): self.ensure_one() if self.state == "canceled": raise UserError("Canceled properties can not be sold") + if not [offer for offer in self.offer_ids if offer.status == 'accepted']: + raise UserError("Properties can not be marked as sold if there is no accepted offer") self.state = "sold" def action_state_to_canceled(self): diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..d6724ad4c71 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_estate_property +from . import test_estate_property_offer diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..f66a43c9f05 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,33 @@ +from odoo.exceptions import UserError +from odoo.tests import TransactionCase, tagged, Form + + +@tagged('post_install', '-at_install') +class TestEstateProperty(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_mark_as_sold_without_offer(self): + self.property = self.env['estate.property'].create({ + 'name': 'property1', + 'expected_price': 100, + 'state': 'offer' + }) + with self.assertRaises(UserError): + self.property.action_state_to_sold() + + def test_reset_garden(self): + self.property = self.env['estate.property'].create({ + 'name': 'property', + 'expected_price': 100, + 'garden': True, + 'garden_area': 20, + 'garden_orientation': 'north' + }) + with Form(self.property) as f1: + f1.garden = False + self.assertFalse(self.property.garden, 'the garden should not be there anymore') + self.assertEqual(self.property.garden_area, 0, "garden area should be reset") + self.assertFalse(self.property.garden_orientation, "garden orientation is expected to be unset") diff --git a/estate/tests/test_estate_property_offer.py b/estate/tests/test_estate_property_offer.py new file mode 100644 index 00000000000..90191b596a0 --- /dev/null +++ b/estate/tests/test_estate_property_offer.py @@ -0,0 +1,42 @@ +from odoo.exceptions import UserError +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class TestEstatePropertyOffer(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.buyer = cls.env['res.partner'].create({'name': 'buyer'}) + + def test_good_offer(self): + self.property = self.env['estate.property'].create({ + 'name': 'property1', + 'expected_price': 100, + }) + offer = self.env['estate.property.offer'].create({ + 'price': 90, + 'partner_id': self.buyer.id, + 'property_id': self.property.id, + }) + self.assertRecordValues(self.property.offer_ids, [ + {'price': 90, 'partner_id': self.buyer.id} + ]) + self.assertEqual(self.property.state, "offer") + + def test_offer_for_sold(self): + self.property = self.env['estate.property'].create({ + 'name': 'property1', + 'expected_price': 100, + 'state': 'sold' + }) + with self.assertRaises(UserError): + offer = self.env['estate.property.offer'].create({ + 'price': 90, + 'partner_id': self.buyer.id, + 'property_id': self.property.id, + }) + self.assertFalse(self.property.offer_ids, "after failure there should be no offer added") + self.assertEqual(self.property.state, "sold", "after failure the state should not change") From 5a214adaa9495a6d1a0bb9f14cd3fa3a329dac45 Mon Sep 17 00:00:00 2001 From: joemo-odoo Date: Tue, 5 May 2026 09:35:30 +0200 Subject: [PATCH 4/5] [CLN] estate: delete unused variable remove unused variable, there is a need to create an item, to see there are no the side effects when failing to create --- estate/tests/test_estate_property_offer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/tests/test_estate_property_offer.py b/estate/tests/test_estate_property_offer.py index 90191b596a0..563efee37f4 100644 --- a/estate/tests/test_estate_property_offer.py +++ b/estate/tests/test_estate_property_offer.py @@ -33,7 +33,7 @@ def test_offer_for_sold(self): 'state': 'sold' }) with self.assertRaises(UserError): - offer = self.env['estate.property.offer'].create({ + self.env['estate.property.offer'].create({ 'price': 90, 'partner_id': self.buyer.id, 'property_id': self.property.id, From 03e5a751ae84e0e79d02ad923b8ae91d1f316fcf Mon Sep 17 00:00:00 2001 From: joemo-odoo Date: Tue, 5 May 2026 09:51:47 +0200 Subject: [PATCH 5/5] [CLN] estate: delete unused variable remove unused variable, there is a need to create an item, to see there are no the side effects when failing to create --- estate/tests/test_estate_property_offer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/tests/test_estate_property_offer.py b/estate/tests/test_estate_property_offer.py index 563efee37f4..57c3fef776e 100644 --- a/estate/tests/test_estate_property_offer.py +++ b/estate/tests/test_estate_property_offer.py @@ -16,7 +16,7 @@ def test_good_offer(self): 'name': 'property1', 'expected_price': 100, }) - offer = self.env['estate.property.offer'].create({ + self.env['estate.property.offer'].create({ 'price': 90, 'partner_id': self.buyer.id, 'property_id': self.property.id,