From c0139007978d2af947b8f67a244bdc99a3cac682 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Mon, 15 Dec 2025 13:15:50 +0100 Subject: [PATCH 01/14] [ADD] estate: create new estate module with property business object This create a new module to handle estate property. It add a new Property model with its base fields. --- .gitignore | 3 +++ estate/__init__.py | 1 + estate/__manifest__.py | 9 +++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 23 +++++++++++++++++++++++ 5 files changed, 37 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 diff --git a/.gitignore b/.gitignore index b6e47617de1..627f35a9e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Pycharm IDE +/.idea diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..9f375e47945 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,9 @@ +{ + "name": "estate", + "version": 1.0, + "depends": [ + "base", + ], + "installable": True, + "application": True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..a48986997d4 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,23 @@ +from odoo import models, fields + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Estate property' + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='Orientation', + selection=[('north','North'), ('south','South'), ('east', 'East'), ('west','West')], + ) + \ No newline at end of file From 4cf9fa897e2d70b64db2673a8a68702e4d2ff52d Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Mon, 15 Dec 2025 13:48:07 +0100 Subject: [PATCH 02/14] [IMP] estate: add access right to estate.property model This create the security/ir.model.access.csv file to give full access on the estate.property model for base.group_user group. --- estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9f375e47945..b4abdd54f20 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,4 +6,7 @@ ], "installable": True, "application": True, + "data": [ + "security/ir.model.access.csv" + ] } diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..976b61e8cb3 --- /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 +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 937f0fcf5b765745840bc2b4d0329db16f1e9a70 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Mon, 15 Dec 2025 14:46:30 +0100 Subject: [PATCH 03/14] [IMP] estate: create views for property model and update model fields This create an action to display the form and list views for the property model and link it to a new menu. This also updates existing fields with attributes and adds new fields to the model. --- estate/__manifest__.py | 8 ++++-- estate/models/estate_property.py | 35 ++++++++++++++++++++------ estate/views/estate_menus.xml | 7 ++++++ estate/views/estate_property_views.xml | 7 ++++++ 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b4abdd54f20..8a4142d3366 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,10 @@ "installable": True, "application": True, "data": [ - "security/ir.model.access.csv" - ] + # Access Rights + "security/ir.model.access.csv", + # Views + "views/estate_property_views.xml", + "views/estate_menus.xml", + ], } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a48986997d4..039954cc3e0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,23 +1,42 @@ from odoo import models, fields + class EstateProperty(models.Model): - _name = 'estate.property' - _description = 'Estate property' + _name = "estate.property" + _description = "Estate property" name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date( + copy=False, default=fields.Date.add(fields.Date.today(), month=3) + ) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( - string='Orientation', - selection=[('north','North'), ('south','South'), ('east', 'East'), ('west','West')], + string="Orientation", + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + ) + active = fields.Boolean(default=True) + state = fields.Selection( + string="Status", + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + default="new", ) - \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..d1fd435bf53 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..a81214e39f4 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,7 @@ + + + Properties + estate.property + list,form + + \ No newline at end of file From 34770d2e84a92afaf7ffb8ee644ccf32479f0cad Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Mon, 15 Dec 2025 16:12:16 +0100 Subject: [PATCH 04/14] [IMP] estate: add custom form and list view with custom filter and group by. This will updates the form view, grouping field and adding a notebook with tabs, the list view, adding more field into the list, and the search view, allowing the search to be made using more fields. This will also create a new available filter, to only show properties that are still purchasable and a new group by postcode. --- estate/models/estate_property.py | 17 +++-- estate/views/estate_property_views.xml | 99 ++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 039954cc3e0..37cea525b1d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,22 +5,28 @@ class EstateProperty(models.Model): _name = "estate.property" _description = "Estate property" - name = fields.Char(required=True) + name = fields.Char(required=True, string="Title") description = fields.Text() + postcode = fields.Char() date_availability = fields.Date( - copy=False, default=fields.Date.add(fields.Date.today(), month=3) + copy=False, + default=fields.Date.add(fields.Date.today(), month=3), + string="Available From", ) + 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 = fields.Integer(string="Living Area (sqm)") facades = fields.Integer() garage = fields.Boolean() + garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( - string="Orientation", + string="Garden Orientation", selection=[ ("north", "North"), ("south", "South"), @@ -28,6 +34,7 @@ class EstateProperty(models.Model): ("west", "West"), ], ) + active = fields.Boolean(default=True) state = fields.Selection( string="Status", diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index a81214e39f4..6852cdac5c5 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,4 +1,103 @@ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + Properties + estate.property + + + + + + + + + + + + + + + + + Properties + estate.property + + +
+ + + + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Properties estate.property From c72e20f642051ec16982990bf95188d32714af42 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Mon, 15 Dec 2025 17:05:51 +0100 Subject: [PATCH 05/14] [IMP] estate: create property type and link property to buyer and salesperson. This will allow for each property to select a property type, select a partner as buyer and a user as a salesperson. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 3 ++- estate/models/estate_property.py | 6 +++++ estate/models/estate_property_type.py | 8 +++++++ estate/security/ir.model.access.csv | 3 ++- estate/views/estate_menus.xml | 5 +++- estate/views/estate_property_type_views.xml | 26 +++++++++++++++++++++ estate/views/estate_property_views.xml | 11 +++++++-- 8 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8a4142d3366..afcc75f6b98 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,6 +11,7 @@ "security/ir.model.access.csv", # Views "views/estate_property_views.xml", + "views/estate_property_type_views.xml", "views/estate_menus.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..40092a2d810 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ -from . import estate_property \ No newline at end of file +from . import estate_property +from . import estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 37cea525b1d..1f40beae8e6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -47,3 +47,9 @@ class EstateProperty(models.Model): ], default="new", ) + + property_type_id = fields.Many2one("estate.property.type", string="Type") + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + salesperson_id = fields.Many2one( + "res.users", string="Salesperson", default=lambda self: self._uid + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..98c7d9f7303 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class EstatePropertyType(models.Model): + __name = "estate.property.type" + __description = "Type of a property" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 976b61e8cb3..ac245eddbb7 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,3 @@ 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 \ No newline at end of file +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 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index d1fd435bf53..c1246508c73 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,10 @@ - + + + + \ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..971ed53bb98 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,26 @@ + + + + Property Types + estate.property.type + + +
+ +
+

+ +

+
+
+
+
+
+ + + + Property Type + estate.property.type + list,form + +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6852cdac5c5..773966b701b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -47,8 +47,6 @@
- -

@@ -58,6 +56,7 @@ + @@ -90,6 +89,14 @@ + + + + + + + + From 801a3ce3210d209c79618ec1d13f80e319a4cb94 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 16 Dec 2025 13:20:30 +0100 Subject: [PATCH 06/14] [IMP] estate: add tags and offer on property model. This swill add the ability to create tags through the settings and link them to properties. It will also allow to create offers for a property directly from the property form. Each offer is linked to a partner, with a set price and a property. --- estate/__manifest__.py | 2 ++ estate/models/__init__.py | 2 ++ estate/models/estate_property.py | 2 ++ estate/models/estate_property_offer.py | 15 +++++++++++ estate/models/estate_property_tag.py | 8 ++++++ estate/models/estate_property_type.py | 4 +-- estate/security/ir.model.access.csv | 4 ++- estate/views/estate_menus.xml | 1 + estate/views/estate_property_offer_views.xml | 15 +++++++++++ estate/views/estate_property_tag_views.xml | 26 ++++++++++++++++++++ estate/views/estate_property_type_views.xml | 2 +- estate/views/estate_property_views.xml | 13 +++++----- 12 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index afcc75f6b98..eefc9d2489b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,8 @@ # Views "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_menus.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 40092a2d810..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,4 @@ from . import estate_property from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 1f40beae8e6..4f10281554f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -53,3 +53,5 @@ class EstateProperty(models.Model): salesperson_id = fields.Many2one( "res.users", string="Salesperson", default=lambda self: self._uid ) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..e2d12758fb4 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,15 @@ +from odoo import models, fields + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Offer made for a property by a customer. The offer can be lower or higher than the expected price." + + price = fields.Float() + status = fields.Selection( + string="Status", + selection=[("accepted", "Accepted"), ("refused", "Refused")], + copy=False, + ) + partner_id = fields.Many2one("res.partner", string="Partner") + property_id = fields.Many2one("estate.property", string="Property") diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..0ac762aaeda --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Tag to add to properties" + + name = fields.Char(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 98c7d9f7303..df476d29093 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -2,7 +2,7 @@ class EstatePropertyType(models.Model): - __name = "estate.property.type" - __description = "Type of a property" + _name = "estate.property.type" + _description = "Type of a property" name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index ac245eddbb7..4c593ed42e4 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +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 \ No newline at end of file +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 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index c1246508c73..3851b734f85 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -5,6 +5,7 @@ + \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..07854e3fb76 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,15 @@ + + + + Offers + estate.property.offer + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..dc6a765dcb1 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,26 @@ + + + + Property Tags + estate.property.tag + + +
+ +
+

+ +

+
+
+
+
+
+ + + + Property Tags + estate.property.tag + list,form + +
\ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 971ed53bb98..f7e9541231c 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -19,7 +19,7 @@ - Property Type + Property Types estate.property.type list,form diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 773966b701b..9345e2c62ac 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -54,6 +54,9 @@

+ + + @@ -63,6 +66,7 @@ + @@ -81,13 +85,8 @@ - - - - - - - + + From 8e44d2de63e0fc8265e93ddb3abc7b87b0aa334e Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 16 Dec 2025 14:26:57 +0100 Subject: [PATCH 07/14] [IMP] estate: add computed fields to property and offer models. This will add new computed fields in the models: - total_area in estate.property, computed as the sum of living_area and garden_area - best_price in estate.property, computed as the max price of the property offers (offer_ids) - date_deadline in estate.property.offer, computed as the sum of create_date and validity. As create_date is only set after creating the record, an alternative using fields.Date.today() function has been set to avoid crashes when creating an offer. This will also add an inverse function for date_deadline, allowing to set the deadline manually and computing the availibility value instead. --- estate/models/estate_property.py | 24 ++++++++++++++--- estate/models/estate_property_offer.py | 27 +++++++++++++++++++- estate/views/estate_property_offer_views.xml | 3 ++- estate/views/estate_property_views.xml | 4 ++- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4f10281554f..0644d592a60 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api class EstateProperty(models.Model): @@ -11,12 +11,13 @@ class EstateProperty(models.Model): postcode = fields.Char() date_availability = fields.Date( copy=False, - default=fields.Date.add(fields.Date.today(), month=3), + default=fields.Date.add(fields.Date.today(), months=3), string="Available From", ) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) + best_price = fields.Float(compute="_compute_best_price") bedrooms = fields.Integer(default=2) living_area = fields.Integer(string="Living Area (sqm)") @@ -35,6 +36,8 @@ class EstateProperty(models.Model): ], ) + total_area = fields.Integer(compute="_compute_total_area", readonly=True) + active = fields.Boolean(default=True) state = fields.Selection( string="Status", @@ -48,10 +51,25 @@ class EstateProperty(models.Model): default="new", ) + # Relations property_type_id = fields.Many2one("estate.property.type", string="Type") buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) salesperson_id = fields.Many2one( - "res.users", string="Salesperson", default=lambda self: self._uid + "res.users", string="Salesperson", default=lambda self: self.env.uid ) tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + + # Functions + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped("price")) + else: + record.best_price = 0 diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e2d12758fb4..5e13f440000 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api class EstatePropertyOffer(models.Model): @@ -11,5 +11,30 @@ class EstatePropertyOffer(models.Model): selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False, ) + validity = fields.Integer(default=7, string="Validity (days)") + date_deadline = fields.Date( + compute="_compute_deadline", inverse="_inverse_deadline", string="Deadline" + ) + + # Relations partner_id = fields.Many2one("res.partner", string="Partner") property_id = fields.Many2one("estate.property", string="Property") + + # Methods + @api.depends("validity") + def _compute_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 + ) # Use today() instead of create_date to avoid crashing at record creation + + def _inverse_deadline(self): + for offer in self: + offer.validity = ( + offer.date_deadline - fields.Date.to_date(offer.create_date) + ).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 07854e3fb76..83a7a6963e0 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -8,7 +8,8 @@ - + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9345e2c62ac..f4f6b4c0c07 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -66,7 +66,7 @@ - + @@ -75,6 +75,7 @@ + @@ -82,6 +83,7 @@ + From 094c99a322f193489ccfc78c0e5dba36e9653bfc Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 16 Dec 2025 15:01:02 +0100 Subject: [PATCH 08/14] [IMP] estate: add onchange function to estate.property model. This will add a new onchange function to the estate.property model to set the garden_area and garden_orientation fields value when changing the value of the garden field. --- estate/models/estate_property.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0644d592a60..31af93bda7f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -73,3 +73,12 @@ def _compute_best_price(self): record.best_price = max(record.offer_ids.mapped("price")) else: record.best_price = 0 + + @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 = "" From 86e6e5b36723c31f4908b6d016c538a2095fc6c8 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 16 Dec 2025 16:07:02 +0100 Subject: [PATCH 09/14] [IMP] estate: add button to estate.property form view and estate.property.offer list view. This will allow to quickly set a property as sold or cancelled, directly from the form view. The functions prevent to set a cancelled property as sold, and to set a sold property as cancelled by raising an UserError Exception. This will also allow to quickly set an offer as accepted or refused and update the linked property selling_price and buyer. The refuse action function will clear the selling_price and buyer of the property if it was in the accepted status before. --- estate/__init__.py | 2 +- estate/models/estate_property.py | 15 +++++++++++++++ estate/models/estate_property_offer.py | 15 +++++++++++++++ estate/views/estate_property_offer_views.xml | 3 +++ estate/views/estate_property_views.xml | 5 +++++ 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 31af93bda7f..2c2d05f76e2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import models, fields, api +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -82,3 +83,17 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = "" + + def action_sold_property(self): + for property in self: + if property.state == "cancelled": + raise UserError("Cancelled properties cannot be sold") + property.state = "sold" + return True + + def action_cancel_property(self): + for property in self: + if property.state == "sold": + raise UserError("Sold properties cannot be cancelled") + property.state = "cancelled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5e13f440000..be5f315c4a7 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -38,3 +38,18 @@ def _inverse_deadline(self): offer.validity = ( offer.date_deadline - fields.Date.to_date(offer.create_date) ).days + + 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 + return True + + def action_refuse_offer(self): + for offer in self: + if offer.status == "accepted": + offer.property_id.selling_price = 0 + offer.property_id.buyer_id = "" + offer.status = "refused" + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 83a7a6963e0..4c3317bd602 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -10,6 +10,9 @@ + +

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 93ff61842e0..169178e9cdd 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -10,7 +10,8 @@ - + Properties estate.property list,form + {'search_default_available': True} \ No newline at end of file From f207648884983dd51af4bc178543e7fa3a3ee3c7 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Thu, 18 Dec 2025 11:25:58 +0100 Subject: [PATCH 13/14] [IMP] estate: add property list view inside res.users form view. This will create a new users model that inherit from res.users with a relational field to estate.property. This way, the users can see the properties that are linked to each user inside the users form (Settings > Users). This field has a domain that allows to only display the available properties. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 14 +++++++++++--- estate/models/estate_property_offer.py | 11 +++++++++++ estate/models/users.py | 11 +++++++++++ estate/views/estate_users_views.xml | 14 ++++++++++++++ 6 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 estate/models/users.py create mode 100644 estate/views/estate_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 904a6f94daa..634c220197c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,6 +14,7 @@ "views/estate_property_views.xml", "views/estate_property_type_views.xml", "views/estate_property_tag_views.xml", + "views/estate_users_views.xml", "views/estate_menus.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..8ada7efccc7 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 660851dd674..0b1a3a3d430 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -12,9 +12,9 @@ class EstateProperty(models.Model): description = fields.Text() postcode = fields.Char() - date_availability = fields.Date( + date_availability = fields.Datetime( copy=False, - default=fields.Date.add(fields.Date.today(), months=3), + default=lambda self: fields.Datetime.add(fields.Datetime.today(), months=3), string="Available From", ) @@ -103,6 +103,12 @@ def action_cancel_property(self): property.state = "cancelled" return True + @api.ondelete(at_uninstall=False) + def _check_unlink(self): + for property in self: + if property.state != "new" and property.state != "cancelled": + raise UserError("Only new and cancelled properties can be deleted!") + # Constraints _check_positive_expected_price = models.Constraint( "CHECK(expected_price > 0)", @@ -117,7 +123,9 @@ def action_cancel_property(self): def _check_selling_price(self): for property in self: if ( - float_compare(property.selling_price, (0.9 * self.expected_price), 2) + float_compare( + property.selling_price, (0.9 * property.expected_price), 2 + ) == -1 ): raise ValidationError( diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 57e8752b22e..ccc2f148c1f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import models, fields, api +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -62,6 +63,16 @@ def action_refuse_offer(self): offer.status = "refused" return True + @api.model + def create(self, vals): + for record in vals: + best_price = ( + self.env["estate.property"].browse(record["property_id"]).best_price + ) + if record["price"] < best_price: + raise UserError(f"The offer must be greater than {best_price}€") + return super().create(vals) + # Constraints _check_positive_price = models.Constraint( "CHECK(price > 0)", diff --git a/estate/models/users.py b/estate/models/users.py new file mode 100644 index 00000000000..ee8df81e744 --- /dev/null +++ b/estate/models/users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class User(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson_id", + domain=["|", ("state", "=", "new"), ("state", "=", "offer_received")], + ) diff --git a/estate/views/estate_users_views.xml b/estate/views/estate_users_views.xml new file mode 100644 index 00000000000..cc36ab117ea --- /dev/null +++ b/estate/views/estate_users_views.xml @@ -0,0 +1,14 @@ + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + \ No newline at end of file From 6ea4f8417a32c02b6d08c9454fbaa06c8bc90307 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Thu, 18 Dec 2025 12:36:52 +0100 Subject: [PATCH 14/14] [FIX] estate: add missing key in manifest and fix a typo in a estate.property.offer field. --- estate/__manifest__.py | 2 ++ estate/models/estate_property_offer.py | 2 +- estate/views/estate_property_offer_views.xml | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 634c220197c..a243f2ba22e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,8 @@ { "name": "estate", "version": 1.0, + "author": "gicha", + "license": "LGPL-3", "depends": [ "base", ], diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ccc2f148c1f..09cdeaa2b67 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -22,7 +22,7 @@ class EstatePropertyOffer(models.Model): partner_id = fields.Many2one("res.partner", string="Partner") property_id = fields.Many2one("estate.property", string="Property") property_type_id = fields.Many2one( - related="property_id.property_type_id", stored=True + related="property_id.property_type_id", store=True ) # Methods diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index e6644f24fa8..66184f46521 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -12,9 +12,9 @@ -