From 42962d93f472524861cfc1ffc302d54e39031052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Mon, 17 Aug 2026 13:26:41 +0200 Subject: [PATCH 01/15] [IMP] estate: Chapter 2 --- estate/__init__.py | 2 ++ estate/__manifest__.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..a0fdc10fe11 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..a0824b7787d --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Estate", + + 'summary': """ + Tutorial module for estate management + """, + + 'description': """ + Tutorial module for estate management + """, + + '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', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': [], + 'application': True, + 'installable': True, + 'data': [ + ], + 'assets': { + }, + 'license': 'AGPL-3' +} From d1496f3f10f17caf810926c9401b3ded9f7e7241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Mon, 17 Aug 2026 13:26:50 +0200 Subject: [PATCH 02/15] [IMP] estate: Chapter 3 --- estate/models/__init__.py | 3 +++ estate/models/estate_property.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9e791e66f27 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# import filename_python_file_within_folder_or_subfolder +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..af065caff83 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,21 @@ +from typing_extensions import Required + +from odoo import models, fields + +class EstateProperty(models.Model): + _name = "estate_property" + _description = "Estate property" + + name = fields.Char("Property Name", required = True) + description = fields.Text("Description") + postcode = fields.Char("Zip Code") + date_availability = fields.Date("Availability Date") + expected_price = fields.Float("Expected Price", required = True) + selling_price = fields.Float("Selling Price") + bedrooms = fields.Integer("Number of Bedrooms") + living_area = fields.Integer("Number of distinct Living Areas") + facades = fields.Integer("Number of Facades") + garage = fields.Boolean("Garage") + garden = fields.Boolean("Garden") + garden_area = fields.Integer("Garden Area") + garden_orientation = fields.Selection(string="Garden Orientation", selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')]) From c7da25c4049a7d54fd84708edc52e8daebad9dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Mon, 17 Aug 2026 13:43:13 +0200 Subject: [PATCH 03/15] [IMP] estate: Chapter 4 --- estate/__manifest__.py | 1 + estate/security/ir.model.access.csv | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index a0824b7787d..6a85242d53e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -24,6 +24,7 @@ 'application': True, 'installable': True, 'data': [ + 'security/ir.model.access.csv', ], 'assets': { }, diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..98f4671fb0d --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +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 From 70c5959eab3e52dcaac0a2249966abf8ff32ad23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Mon, 17 Aug 2026 14:06:51 +0200 Subject: [PATCH 04/15] [IMP] estate: Chapter 5 --- estate/__init__.py | 1 - estate/__manifest__.py | 4 +++- estate/models/__init__.py | 1 - estate/models/estate_property.py | 19 +++++++++++-------- estate/views/estate_property_menu_views.xml | 10 ++++++++++ estate/views/estate_property_views.xml | 15 +++++++++++++++ 6 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 estate/views/estate_property_menu_views.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py index a0fdc10fe11..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6a85242d53e..e125d8f472d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- { 'name': "Estate", @@ -25,6 +24,9 @@ 'installable': True, 'data': [ 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_property_menu_views.xml' ], 'assets': { }, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 9e791e66f27..e6ebe16945c 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +1,2 @@ -# -*- coding: utf-8 -*- # import filename_python_file_within_folder_or_subfolder from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index af065caff83..bf8949d89c6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,21 +1,24 @@ -from typing_extensions import Required - from odoo import models, fields +from datetime import timedelta + class EstateProperty(models.Model): + _name = "estate_property" _description = "Estate property" - name = fields.Char("Property Name", required = True) + name = fields.Char("Property Name", required=True) description = fields.Text("Description") postcode = fields.Char("Zip Code") - date_availability = fields.Date("Availability Date") - expected_price = fields.Float("Expected Price", required = True) - selling_price = fields.Float("Selling Price") - bedrooms = fields.Integer("Number of Bedrooms") - living_area = fields.Integer("Number of distinct Living Areas") + date_availability = fields.Date("Availability Date", copy=False, default=fields.Date.to_date(fields.Date.today()+timedelta(days=3*30))) + expected_price = fields.Float("Expected Price", required=True) + selling_price = fields.Float("Selling Price", readonly=True, copy=False) + bedrooms = fields.Integer("Number of Bedrooms", default=2) + living_area = fields.Integer("Living Area") facades = fields.Integer("Number of Facades") garage = fields.Boolean("Garage") garden = fields.Boolean("Garden") garden_area = fields.Integer("Garden Area") garden_orientation = fields.Selection(string="Garden Orientation", selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')]) + state = fields.Selection(string="State", required=True, copy=False, selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], default='new') + active = fields.Boolean("Active", default=True) diff --git a/estate/views/estate_property_menu_views.xml b/estate/views/estate_property_menu_views.xml new file mode 100644 index 00000000000..562a794b9b2 --- /dev/null +++ b/estate/views/estate_property_menu_views.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..3dd0cd62414 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,15 @@ + + + + + + Test Action + estate_property + list,form + +

+ This is a simple window action test. +

+
+
+
From e7254fd5fc97b5eec092f76b9c0d7e378c4c677a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Tue, 18 Aug 2026 09:23:49 +0200 Subject: [PATCH 05/15] [IMP] estate: Chapter 6 [FIX] estate: estate_property to estate.property --- estate/models/estate_property.py | 6 +- estate/views/estate_property_views.xml | 85 +++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index bf8949d89c6..17517c0bbde 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -4,13 +4,13 @@ class EstateProperty(models.Model): - _name = "estate_property" + _name = "estate.property" _description = "Estate property" name = fields.Char("Property Name", required=True) description = fields.Text("Description") - postcode = fields.Char("Zip Code") - date_availability = fields.Date("Availability Date", copy=False, default=fields.Date.to_date(fields.Date.today()+timedelta(days=3*30))) + postcode = fields.Char("Postcode") + date_availability = fields.Date("Availability Date", copy=False, default=fields.Date.to_date(fields.Date.today() + timedelta(days=3*30))) expected_price = fields.Float("Expected Price", required=True) selling_price = fields.Float("Selling Price", readonly=True, copy=False) bedrooms = fields.Integer("Number of Bedrooms", default=2) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3dd0cd62414..3e3ea6e9b61 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,13 +3,92 @@ - Test Action - estate_property - list,form + Estate Property + estate.property + list,form,search

This is a simple window action test.

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

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + From 37390b678ac3ea31f84baadb96c5a25d5276f734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Tue, 18 Aug 2026 10:50:35 +0200 Subject: [PATCH 06/15] [IMP] estate: Chapter 7 --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 2 +- estate/models/estate_property.py | 14 +++++++-- estate/models/estate_property_offer.py | 12 ++++++++ estate/models/estate_property_tag.py | 9 ++++++ estate/models/estate_property_type.py | 9 ++++++ estate/security/ir.model.access.csv | 3 ++ estate/views/estate_property_menu_views.xml | 9 ++++-- estate/views/estate_property_offer_views.xml | 32 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 16 ++++++++++ estate/views/estate_property_type_views.xml | 30 ++++++++++++++++++ estate/views/estate_property_views.xml | 15 +++++++++ 12 files changed, 148 insertions(+), 6 deletions(-) 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/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 diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e125d8f472d..2e93b55bd25 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -26,6 +26,9 @@ '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/estate_property_menu_views.xml' ], 'assets': { diff --git a/estate/models/__init__.py b/estate/models/__init__.py index e6ebe16945c..a81a7748ee0 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,2 @@ # import filename_python_file_within_folder_or_subfolder -from . import estate_property +from . import estate_property_type, estate_property_tag, estate_property_offer, estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 17517c0bbde..73a3628e76a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -10,15 +10,23 @@ class EstateProperty(models.Model): name = fields.Char("Property Name", required=True) description = fields.Text("Description") postcode = fields.Char("Postcode") - date_availability = fields.Date("Availability Date", copy=False, default=fields.Date.to_date(fields.Date.today() + timedelta(days=3*30))) + date_availability = fields.Date("Availability Date", copy=False, default=fields.Date.to_date(fields.Date.today() + timedelta(days=3 * 30))) expected_price = fields.Float("Expected Price", required=True) selling_price = fields.Float("Selling Price", readonly=True, copy=False) bedrooms = fields.Integer("Number of Bedrooms", default=2) - living_area = fields.Integer("Living Area") + living_area = fields.Integer("Living Area m²") facades = fields.Integer("Number of Facades") garage = fields.Boolean("Garage") garden = fields.Boolean("Garden") - garden_area = fields.Integer("Garden Area") + garden_area = fields.Integer("Garden Area m²") garden_orientation = fields.Selection(string="Garden Orientation", selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')]) state = fields.Selection(string="State", required=True, copy=False, selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], default='new') active = fields.Boolean("Active", default=True) + + type = fields.Many2one(string="Type", comodel_name="estate.property.type") + buyer = fields.Many2one(string="Buyer", comodel_name="res.partner", copy=False) + seller = fields.Many2one(string="Salesperson", comodel_name="res.users", default=lambda self: self.env.user) + + tags = fields.Many2many(string="Tags", comodel_name="estate.property.tag") + + offers = fields.One2many(string="Offers", comodel_name="estate.property.offer", inverse_name="property") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..37ed33c9e98 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class EstatePropertyOffer(models.Model): + + _name = "estate.property.offer" + _description = "Estate property offer" + + price = fields.Float("Offer Price") + status = fields.Selection(string="Status", copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + partner = fields.Many2one(string="Buyer", comodel_name="res.partner",required=True) + property = fields.Many2one(string="Property", comodel_name="estate.property", required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..c28da10ef3c --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,9 @@ +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + + _name = "estate.property.tag" + _description = "Estate property tag" + + name = fields.Char("Property Tag Name", required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..de51c6b7833 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,9 @@ +from odoo import models, fields + + +class EstatePropertyType(models.Model): + + _name = "estate.property.type" + _description = "Estate property type" + + name = fields.Char("Property Type Name", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 98f4671fb0d..0c0b62b7fee 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +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/views/estate_property_menu_views.xml b/estate/views/estate_property_menu_views.xml index 562a794b9b2..2d458829099 100644 --- a/estate/views/estate_property_menu_views.xml +++ b/estate/views/estate_property_menu_views.xml @@ -2,9 +2,14 @@ - - + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..d3e11f2fd33 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,32 @@ + + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+ +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..8e7f6d18605 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,16 @@ + + + + + + Property Tag + estate.property.tag + list,form + +

+ This is a simple window action test. +

+
+
+ +
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..512cad4195a --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,30 @@ + + + + + + Property Types + estate.property.type + list,form + +

+ This is a simple window action test. +

+
+
+ + + estate.property.type.form + estate.property.type + +
+ +

+ +

+
+
+
+
+ +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3e3ea6e9b61..3bf87a27996 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -19,6 +19,8 @@ + + @@ -37,10 +39,13 @@ + +
+ @@ -63,6 +68,15 @@ + + + + + + + + + @@ -82,6 +96,7 @@ + From 21d7f60ef0b905cf93832e184724c650088b1db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Tue, 18 Aug 2026 13:48:01 +0200 Subject: [PATCH 07/15] [IMP] estate: Chapter 8 --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 64 +++++++++++++++----- estate/models/estate_property_offer.py | 27 +++++++-- estate/views/estate_property_menu_views.xml | 1 - estate/views/estate_property_offer_views.xml | 6 +- estate/views/estate_property_views.xml | 8 +-- 6 files changed, 81 insertions(+), 27 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 2e93b55bd25..c2fed3af20d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -19,7 +19,7 @@ 'version': '0.1', # any module necessary for this one to work correctly - 'depends': [], + 'depends': ['base'], 'application': True, 'installable': True, 'data': [ diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 73a3628e76a..2586fa6caff 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ -from odoo import models, fields -from datetime import timedelta +from odoo import models, fields, api +from dateutil import relativedelta class EstateProperty(models.Model): @@ -10,23 +10,59 @@ class EstateProperty(models.Model): name = fields.Char("Property Name", required=True) description = fields.Text("Description") postcode = fields.Char("Postcode") - date_availability = fields.Date("Availability Date", copy=False, default=fields.Date.to_date(fields.Date.today() + timedelta(days=3 * 30))) - expected_price = fields.Float("Expected Price", required=True) - selling_price = fields.Float("Selling Price", readonly=True, copy=False) + date_availability = fields.Date("Availability Date", copy=False, default=lambda self:fields.Date.today() + relativedelta(months=3)) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer("Number of Bedrooms", default=2) living_area = fields.Integer("Living Area m²") facades = fields.Integer("Number of Facades") - garage = fields.Boolean("Garage") - garden = fields.Boolean("Garden") + garage = fields.Boolean() + garden = fields.Boolean() garden_area = fields.Integer("Garden Area m²") - garden_orientation = fields.Selection(string="Garden Orientation", selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')]) - state = fields.Selection(string="State", required=True, copy=False, selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], default='new') - active = fields.Boolean("Active", default=True) + garden_orientation = fields.Selection(selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')]) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), ('cancelled', 'Cancelled'), + ], + default='new', + required=True, + copy=False,) - type = fields.Many2one(string="Type", comodel_name="estate.property.type") - buyer = fields.Many2one(string="Buyer", comodel_name="res.partner", copy=False) + active = fields.Boolean(default=True) + + type = fields.Many2one(comodel_name="estate.property.type") + buyer = fields.Many2one(comodel_name="res.partner", copy=False) seller = fields.Many2one(string="Salesperson", comodel_name="res.users", default=lambda self: self.env.user) - tags = fields.Many2many(string="Tags", comodel_name="estate.property.tag") + tags = fields.Many2many(comodel_name="estate.property.tag") + + offers = fields.One2many(comodel_name="estate.property.offer", inverse_name="property") + + total_area = fields.Integer(compute="_compute_total_area") + + best_price = fields.Float("Best offer price", compute="_compute_best_price") + + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for property in self: + property.total_area = property.living_area + property.garden_area + + @api.depends("offers") + def _compute_best_price(self): + for property in self: + best_offer = max(property.offers, key=lambda offer: offer.price) + property.best_price = best_offer.price - offers = fields.One2many(string="Offers", comodel_name="estate.property.offer", inverse_name="property") + @api.onchange("garden") + def _onchange_garden(self): + for property in self: + if property.garden: + property.garden_area = 10 + property.garden_orientation = 'north' + else: + property.garden_area = None + property.garden_orientation = None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 37ed33c9e98..ef4258a7959 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,9 @@ -from odoo import models, fields +from odoo import models, fields, api +from datetime import timedelta + +import logging + +_logger = logging.getLogger(__name__) class EstatePropertyOffer(models.Model): @@ -7,6 +12,20 @@ class EstatePropertyOffer(models.Model): _description = "Estate property offer" price = fields.Float("Offer Price") - status = fields.Selection(string="Status", copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) - partner = fields.Many2one(string="Buyer", comodel_name="res.partner",required=True) - property = fields.Many2one(string="Property", comodel_name="estate.property", required=True) + status = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + partner = fields.Many2one(string="Buyer", comodel_name="res.partner", required=True) + property = fields.Many2one(comodel_name="estate.property", required=True) + + validity = fields.Integer(default=7) + date_deadline = fields.Date("Offer Deadline", compute="_compute_deadline", inverse="_inverse_deadline") + + + @api.depends("validity") + def _compute_deadline(self): + for offer in self: + compare_date = offer.create_date.date() if offer.create_date else fields.Date.today() + offer.date_deadline = compare_date + timedelta(days=offer.validity) + + def _inverse_deadline(self): + for offer in self: + offer.validity = (offer.date_deadline - offer.create_date.date()).days diff --git a/estate/views/estate_property_menu_views.xml b/estate/views/estate_property_menu_views.xml index 2d458829099..4ea0cb6beb0 100644 --- a/estate/views/estate_property_menu_views.xml +++ b/estate/views/estate_property_menu_views.xml @@ -5,7 +5,6 @@ - diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index d3e11f2fd33..ba417787397 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,8 +7,10 @@ - + + + @@ -22,6 +24,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3bf87a27996..41c055259cc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -38,11 +38,8 @@

- -
- @@ -51,10 +48,10 @@ + - @@ -66,6 +63,7 @@ + @@ -78,7 +76,6 @@
- @@ -97,7 +94,6 @@ - From 56af061237e14764d3fe0bf6a1d4632f19d29849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Tue, 18 Aug 2026 15:08:52 +0200 Subject: [PATCH 08/15] [IMP] estate: Chapter 9 --- estate/models/estate_property.py | 21 ++++++++++++--- estate/models/estate_property_offer.py | 27 +++++++++++++++++--- estate/views/estate_property_offer_views.xml | 6 +++-- estate/views/estate_property_views.xml | 6 ++++- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2586fa6caff..621d568803d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import models, fields, api +from odoo import models, fields, api, exceptions from dateutil import relativedelta @@ -10,7 +10,7 @@ class EstateProperty(models.Model): name = fields.Char("Property Name", required=True) description = fields.Text("Description") postcode = fields.Char("Postcode") - date_availability = fields.Date("Availability Date", copy=False, default=lambda self:fields.Date.today() + relativedelta(months=3)) + date_availability = fields.Date("Availability Date", copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer("Number of Bedrooms", default=2) @@ -29,7 +29,7 @@ class EstateProperty(models.Model): ], default='new', required=True, - copy=False,) + copy=False) active = fields.Boolean(default=True) @@ -45,7 +45,6 @@ class EstateProperty(models.Model): best_price = fields.Float("Best offer price", compute="_compute_best_price") - @api.depends("living_area", "garden_area") def _compute_total_area(self): for property in self: @@ -66,3 +65,17 @@ def _onchange_garden(self): else: property.garden_area = None property.garden_orientation = None + + def action_cancel_property(self): + for property in self: + if property.state == "sold": + raise exceptions.UserError("Sold properties cannot be cancelled") + property.state = "cancelled" + return True + + def action_sell_property(self): + for property in self: + if property.state == "cancelled": + raise exceptions.UserError("Cancelled properties cannot be sold") + property.state = "sold" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ef4258a7959..7b5aec747f9 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import models, fields, api +from odoo import models, fields, api, exceptions from datetime import timedelta import logging @@ -12,14 +12,13 @@ class EstatePropertyOffer(models.Model): _description = "Estate property offer" price = fields.Float("Offer Price") - status = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + state = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) partner = fields.Many2one(string="Buyer", comodel_name="res.partner", required=True) property = fields.Many2one(comodel_name="estate.property", required=True) validity = fields.Integer(default=7) date_deadline = fields.Date("Offer Deadline", compute="_compute_deadline", inverse="_inverse_deadline") - @api.depends("validity") def _compute_deadline(self): for offer in self: @@ -29,3 +28,25 @@ def _compute_deadline(self): def _inverse_deadline(self): for offer in self: offer.validity = (offer.date_deadline - offer.create_date.date()).days + + def action_accept_offer(self): + # We can only accept one offer at a time + self.ensure_one() + + # Ensure there is no existing accepted offer + for offer in self.property.offers: + if offer.state == "accepted": + raise exceptions.UserError("Property already has an accepted offer.") + + self.state = "accepted" + + self.property.buyer = self.partner + self.property.selling_price = self.price + + return True + + def action_refuse_offer(self): + for offer in self: + # We could forbid refusing accepted offers + offer.state = "refused" + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index ba417787397..c3aee2fcf5e 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -10,7 +10,9 @@ - + +

@@ -39,10 +53,10 @@ - estate.property.type.form + estate.property.type.list estate.property.type - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8e1fa0f1f8f..ec448874fde 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,6 +6,7 @@ Estate Property estate.property list,form,search + {'search_default_available': True}

This is a simple window action test. @@ -17,14 +18,19 @@ estate.property.list estate.property - + - + + @@ -35,18 +41,19 @@

-

- + - + @@ -65,13 +72,13 @@ - - + + - + @@ -95,10 +102,11 @@ - + - + + From 892c7a81e36552a0335c3af900fae42259b59e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Wed, 19 Aug 2026 13:44:32 +0200 Subject: [PATCH 12/15] [IMP] estate: Chapter 12 --- estate/__manifest__.py | 4 +++- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 6 ++++++ estate/models/estate_property_offer.py | 8 ++++++++ estate/models/res_users.py | 24 ++++++++++++++++++++++++ estate/views/estate_property_views.xml | 1 - estate/views/res_user_views.xml | 17 +++++++++++++++++ 7 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_user_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 97275a66864..d4805a04d68 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -29,7 +29,9 @@ 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', - 'views/estate_property_menu_views.xml' + 'views/estate_property_menu_views.xml', + + 'views/res_user_views.xml', ], 'assets': { }, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index a81a7748ee0..6f4f8524e8a 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,2 @@ # import filename_python_file_within_folder_or_subfolder -from . import estate_property_type, estate_property_tag, estate_property_offer, estate_property +from . import estate_property_type, estate_property_tag, estate_property_offer, estate_property, res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0fde20e50a2..88ee378bcad 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -108,3 +108,9 @@ def action_sell_property(self): raise exceptions.UserError("Cancelled properties cannot be sold") property.state = "sold" return True + + @api.ondelete(at_uninstall=False) + def _unlink_only_new_cancelled(self): + for property in self: + if property.state != 'new' and property.state != 'cancelled': + raise exceptions.UserError("You can only delete new or cancelled properties") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 1d4af907f7e..9409a225333 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -54,3 +54,11 @@ def action_refuse_offer(self): # We could forbid refusing accepted offers offer.state = "refused" return True + + @api.model + def create(self, vals_list): + for vals in vals_list: + property = self.env['estate.property'].browse(vals['property']) + if property.state == 'new': + property.state = 'offer_received' + return super().create(vals_list) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..384f36251e6 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,24 @@ +from odoo import models, fields, api + + +class Users(models.Model): + + _inherit = 'res.users' + + property_ids = fields.One2many( + comodel_name="estate.property", + inverse_name="seller" + ) + + available_properties = fields.One2many( + comodel_name="estate.property", + compute="_compute_available_properties" + ) + + # Probably overkill but domains don't seem to work here or in the view ? + @api.depends("property_ids") + def _compute_available_properties(self): + for user in self: + user.available_properties = user.property_ids.filtered( + lambda property: property.state in ["new", "offer_received"] + ) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ec448874fde..d68a41787ff 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,6 @@ - Estate Property estate.property diff --git a/estate/views/res_user_views.xml b/estate/views/res_user_views.xml new file mode 100644 index 00000000000..27cbea41475 --- /dev/null +++ b/estate/views/res_user_views.xml @@ -0,0 +1,17 @@ + + + + + res.users.form.estate + res.users + + + + + + + + + + + From 9ccf6a634b31b5bd152c2c446f28321527b04a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Thu, 20 Aug 2026 09:17:53 +0200 Subject: [PATCH 13/15] [IMP] estate: Chapter 14 --- estate/models/estate_property.py | 2 +- estate/views/estate_property_views.xml | 30 +++++++++++++++++++- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 30 ++++++++++++++++++++ estate_account/models/__init__.py | 2 ++ estate_account/models/estate_property.py | 35 ++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 2 deletions(-) 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.py b/estate/models/estate_property.py index 88ee378bcad..479d84de262 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -81,7 +81,7 @@ def _compute_best_price(self): for property in self: if not property.offers: property.best_price = 0 - return + continue best_offer = max(property.offers, key=lambda offer: offer.price) property.best_price = best_offer.price diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d68a41787ff..1ba9452b9c0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,7 +4,7 @@ Estate Property estate.property - list,form,search + list,kanban,form,search {'search_default_available': True}

@@ -92,6 +92,34 @@ + + + estate.property.kanban + estate.property + + + + + +

+

+ +

+ Expected Price: +
+ Best Price: +
+
+ Selling Price: +
+ +
+ + + +
+
+ estate.property.search estate.property 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..ca384b15083 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,30 @@ +{ + 'name': "Estate Account", + + 'summary': """ + Tutorial module for estate account management + """, + + 'description': """ + Tutorial module for estate account management + """, + + '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', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base', 'estate', 'account'], + 'application': False, + 'installable': True, + 'data': [ + ], + 'assets': { + }, + 'license': 'AGPL-3' +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..e6ebe16945c --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1,2 @@ +# import filename_python_file_within_folder_or_subfolder +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..3c63e8a1462 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,35 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + + _inherit = 'estate.property' + + def action_sell_property(self): + + vals_list = [] + + for record in self: + vals = { + 'partner_id': record.buyer.id, + 'move_type': 'out_invoice', + 'line_ids': [ + Command.create({ + 'name': '6% of selling price', + 'quantity': 1, + 'price_unit': 0.06 * record.selling_price + }), + Command.create({ + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100 + }) + ] + } + vals_list.append(vals) + + print(vals_list) + + self.env['account.move'].create(vals_list) + + return super().action_sell_property() From 5c294f8ef9a5cd2d9fe147274093a2c6feb80fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9licien=20=28feraz=29?= Date: Thu, 20 Aug 2026 10:13:14 +0200 Subject: [PATCH 14/15] [CLN] estate: Chapter 15 --- estate/models/estate_property.py | 48 ++++---- estate/models/estate_property_offer.py | 32 ++--- estate/models/estate_property_type.py | 8 +- estate/models/res_users.py | 7 +- estate/tests/__init__.py | 1 + estate/tests/test_estate.py | 117 +++++++++++++++++++ estate/views/estate_property_menu_views.xml | 12 +- estate/views/estate_property_offer_views.xml | 13 +-- estate/views/estate_property_tag_views.xml | 4 +- estate/views/estate_property_type_views.xml | 8 +- estate/views/estate_property_views.xml | 24 ++-- estate/views/res_user_views.xml | 2 +- estate_account/models/estate_property.py | 4 +- 13 files changed, 200 insertions(+), 80 deletions(-) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 479d84de262..1836017fa75 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,7 @@ -from odoo import models, fields, api, exceptions from dateutil.relativedelta import relativedelta + +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError from odoo.tools.float_utils import float_is_zero, float_compare @@ -32,19 +34,13 @@ class EstateProperty(models.Model): default='new', required=True, copy=False) - active = fields.Boolean(default=True) - - type = fields.Many2one(comodel_name="estate.property.type") - buyer = fields.Many2one(comodel_name="res.partner", copy=False) - seller = fields.Many2one(string="Salesperson", comodel_name="res.users", default=lambda self: self.env.user) - - tags = fields.Many2many(comodel_name="estate.property.tag") - - offers = fields.One2many(comodel_name="estate.property.offer", inverse_name="property") - + type_id = fields.Many2one(string="Type", comodel_name="estate.property.type") + buyer_id = fields.Many2one("res.partner", copy=False) + seller_id = fields.Many2one(string="Salesperson", comodel_name="res.users", default=lambda self: self.env.user) + tag_ids = fields.Many2many(string="Tags", comodel_name="estate.property.tag") + offer_ids = fields.One2many(string="Offers", comodel_name="estate.property.offer", inverse_name="property_id") total_area = fields.Integer(compute="_compute_total_area") - best_price = fields.Float("Best offer price", compute="_compute_best_price") _expected_price_strictly_positive = models.Constraint( @@ -57,7 +53,8 @@ class EstateProperty(models.Model): ) def _no_accepted_offer(self): - return all(offer.state != "accepted" for offer in self.offers) + self.ensure_one() + return all(offer.state != "accepted" for offer in self.offer_ids) @api.constrains("selling_price") def _check_selling_price(self): @@ -67,7 +64,7 @@ def _check_selling_price(self): return if float_compare(property.selling_price, property.expected_price * 0.9, 2) == -1: - raise exceptions.ValidationError( + raise ValidationError( "Selling price must be at least 90% of expected price. Update expected price to accept offer." ) @@ -76,14 +73,14 @@ def _compute_total_area(self): for property in self: property.total_area = property.living_area + property.garden_area - @api.depends("offers") + @api.depends("offer_ids") def _compute_best_price(self): for property in self: - if not property.offers: + if not property.offer_ids: property.best_price = 0 continue - best_offer = max(property.offers, key=lambda offer: offer.price) - property.best_price = best_offer.price + best_offer_price = max(property.offer_ids.mapped('price')) + property.best_price = best_offer_price @api.onchange("garden") def _onchange_garden(self): @@ -98,19 +95,26 @@ def _onchange_garden(self): def action_cancel_property(self): for property in self: if property.state == "sold": - raise exceptions.UserError("Sold properties cannot be cancelled") + raise UserError("Sold properties cannot be cancelled") property.state = "cancelled" return True def action_sell_property(self): for property in self: if property.state == "cancelled": - raise exceptions.UserError("Cancelled properties cannot be sold") + raise UserError("Cancelled properties cannot be sold") + + if not property.offer_ids: + raise UserError("Cannot sell property with no offer") + + if property._no_accepted_offer(): + raise UserError("Cannot sell property with no accepted offer") + property.state = "sold" return True @api.ondelete(at_uninstall=False) def _unlink_only_new_cancelled(self): for property in self: - if property.state != 'new' and property.state != 'cancelled': - raise exceptions.UserError("You can only delete new or cancelled properties") + if property.state not in ('new', 'cancelled'): + raise UserError("You can only delete new or cancelled properties") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9409a225333..b494e6544a9 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,7 @@ -from odoo import models, fields, api, exceptions from datetime import timedelta +from odoo import models, fields, api, exceptions + class EstatePropertyOffer(models.Model): @@ -10,9 +11,9 @@ class EstatePropertyOffer(models.Model): price = fields.Float("Offer Price") state = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) - partner = fields.Many2one(string="Buyer", comodel_name="res.partner", required=True) - property = fields.Many2one(comodel_name="estate.property", required=True) - property_type = fields.Many2one(related="property.type", store=True) + partner_id = fields.Many2one(string="Buyer", comodel_name="res.partner", required=True) + property_id = fields.Many2one(comodel_name="estate.property", required=True) + property_type_id = fields.Many2one(related="property_id.type_id", store=True) validity = fields.Integer(default=7) date_deadline = fields.Date("Offer Deadline", compute="_compute_deadline", inverse="_inverse_deadline") @@ -33,19 +34,18 @@ def _inverse_deadline(self): offer.validity = (offer.date_deadline - offer.create_date.date()).days def action_accept_offer(self): - # We can only accept one offer at a time - self.ensure_one() - - # Ensure there is no existing accepted offer - for offer in self.property.offers: - if offer.state == "accepted": + for offer_to_accept in self: + # Ensure there is no existing accepted offer + if not offer_to_accept.property_id._no_accepted_offer(): raise exceptions.UserError("Property already has an accepted offer.") - self.state = "accepted" + offer_to_accept.state = "accepted" - self.property.buyer = self.partner - self.property.selling_price = self.price - self.property.state = "offer_accepted" + offer_to_accept.property_id.write({ + 'buyer_id': offer_to_accept.partner_id.id, + 'state': 'offer_accepted', + 'selling_price': offer_to_accept.price, + }) return True @@ -58,7 +58,9 @@ def action_refuse_offer(self): @api.model def create(self, vals_list): for vals in vals_list: - property = self.env['estate.property'].browse(vals['property']) + property = self.env['estate.property'].browse(vals['property_id']) if property.state == 'new': property.state = 'offer_received' + elif property.state == 'sold': + raise exceptions.UserError("Property already has been sold, new offers can not be created for it.") return super().create(vals_list) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 7760bf6b1bf..b281c1407cf 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -9,8 +9,8 @@ class EstatePropertyType(models.Model): name = fields.Char("Property Type Name", required=True) sequence = fields.Integer(default=1, help="Used to order stages. Lower is ranked higher.") - properties = fields.One2many(comodel_name="estate.property", inverse_name="type") - offers = fields.One2many(related="properties.offers", inverse_name="property_type") + property_ids = fields.One2many(comodel_name="estate.property", inverse_name="type_id") + offer_ids = fields.One2many(related="property_ids.offer_ids", inverse_name="property_type_id") offer_count = fields.Integer(compute="_compute_offer_count") @@ -19,7 +19,7 @@ class EstatePropertyType(models.Model): 'A property type with the same name already exists.', ) - @api.depends("properties.offers") + @api.depends("property_ids.offer_ids") def _compute_offer_count(self): for type in self: - type.offer_count = sum(len(property.offers) for property in type.properties) + type.offer_count = self.env['estate.property.offer'].search_count([('property_id', 'in', type.property_ids.ids)]) diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 384f36251e6..fa3361ef315 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -7,18 +7,17 @@ class Users(models.Model): property_ids = fields.One2many( comodel_name="estate.property", - inverse_name="seller" + inverse_name="seller_id" ) - available_properties = fields.One2many( + available_property_ids = fields.One2many( comodel_name="estate.property", compute="_compute_available_properties" ) - # Probably overkill but domains don't seem to work here or in the view ? @api.depends("property_ids") def _compute_available_properties(self): for user in self: - user.available_properties = user.property_ids.filtered( + user.available_property_ids = user.property_ids.filtered( lambda property: property.state in ["new", "offer_received"] ) diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..dfd37f0be11 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate diff --git a/estate/tests/test_estate.py b/estate/tests/test_estate.py new file mode 100644 index 00000000000..14a4eb07ce8 --- /dev/null +++ b/estate/tests/test_estate.py @@ -0,0 +1,117 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tests.form import Form + + +@tagged('post_install', '-at_install') +class EstateTestCase(TransactionCase): + + @classmethod + def setUpClass(cls): + # add env on cls and many other things + super().setUpClass() + + # create the data for each tests. By doing it in the setUpClass instead + # of in a setUp or in each test case, we reduce the testing time and + # the duplication of code. + cls.properties = cls.env['estate.property'].create([ + {'name': 'property1', 'expected_price': 1000}, + {'name': 'property2', 'expected_price': 10000, 'garden': True, 'garden_area': 40}, + ]) + + cls.property = cls.properties[0] + cls.property_with_garden = cls.properties[1] + + cls.partner = cls.env['res.partner'].create({'name': 'partner1'}) + + def test_creation_area(self): + """Test that the total_area is computed like it should.""" + self.properties.living_area = 20 + self.assertRecordValues(self.properties, [ + {'total_area': 20}, + {'total_area': 60}, + ]) + + def test_action_sell(self): + """Test that everything behaves like it should when selling a property.""" + + # Cannot sell property with no offer + with self.assertRaises(UserError): + self.properties.action_sell_property() + + offers = self.env['estate.property.offer'].create([ + {'price': 2000, 'partner_id': self.partner.id, 'property_id': self.property.id}, + {'price': 10000, 'partner_id': self.partner.id, 'property_id': self.property_with_garden.id}, + ]) + + offers.action_accept_offer() + + self.properties.action_sell_property() + + self.assertRecordValues(self.properties, [ + {'state': 'sold', 'selling_price': 2000}, + {'state': 'sold', 'selling_price': 10000}, + ]) + + # Cannot cancel sold properties + with self.assertRaises(UserError): + self.properties.action_cancel_property() + + def test_action_cancel(self): + """Test that everything behaves like it should when cancelling a property.""" + self.properties.action_cancel_property() + self.assertRecordValues(self.properties, [ + {'state': 'cancelled'}, + {'state': 'cancelled'}, + ]) + + # Cannot sell cancelled properties + with self.assertRaises(UserError): + self.properties.action_sell_property() + + def test_offer_creation(self): + """ + Test that everything behaves like it should when creating an offer. + - The user can create one or several offers for a property + - The user can not sell properties with no accepted offers + - The user can sell properties with an accepted offer + - Offers can't be created for sold properties + """ + offers = self.env['estate.property.offer'].create([ + {'price': 2000, 'partner_id': self.partner.id, 'property_id': self.property.id}, + {'price': 1500, 'partner_id': self.partner.id, 'property_id': self.property.id}, + {'price': 12200, 'partner_id': self.partner.id, 'property_id': self.property_with_garden.id}, + ]) + + # Cannot sell property with no accepted offer + with self.assertRaises(UserError): + self.properties.action_sell_property() + + offers[0].action_accept_offer() + offers[2].action_accept_offer() + + self.properties.action_sell_property() + + # Cannot create offer on sold property + with self.assertRaises(UserError): + self.env['estate.property.offer'].create([ + {'price': 2000, 'partner_id': self.partner.id, 'property_id': self.property.id}, + {'price': 8500, 'partner_id': self.partner.id, 'property_id': self.property_with_garden.id}, + ]) + + def test_garden_reset(self): + """Test that the garden area and orientation correctly reset when garden is set to False in a form""" + with Form(self.property) as property: + property.garden = True + + self.assertRecordValues(self.property, [ + {'name': 'property1', 'garden': True, 'garden_area': 10, 'garden_orientation': 'north'}, + ]) + + with Form(self.property) as property: + property.garden = False + + self.assertRecordValues(self.property, [ + {'name': 'property1', 'garden': False, 'garden_area': 0, 'garden_orientation': None}, + ]) diff --git a/estate/views/estate_property_menu_views.xml b/estate/views/estate_property_menu_views.xml index 4ea0cb6beb0..1788eb7a5d3 100644 --- a/estate/views/estate_property_menu_views.xml +++ b/estate/views/estate_property_menu_views.xml @@ -2,13 +2,13 @@ - - - + + + - - - + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index ad65c6d3a4c..7e1f02ef6ff 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,14 +1,14 @@ - + Estate Property Offer estate.property.offer list,form,search - [('property_type', '=', active_id)] + [('property_type_id', '=', active_id)]

- This is a simple window action test. + Create estate property offers to view them here !

@@ -22,13 +22,12 @@ decoration-danger="state=='refused'" > - + - +