From eae996b76df46492def5f04fbdc676ed184ecc32 Mon Sep 17 00:00:00 2001 From: SebVde Date: Tue, 21 Apr 2026 15:46:24 +0200 Subject: [PATCH 01/27] [ADD] estate: initialized Sevan Estate module --- estate/__init__.py | 0 estate/__manifest__.py | 8 ++++++++ 2 files changed, 8 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..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..6ebb4744f35 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': "Sevan Estate", + 'depends': ['base'], + 'summary': "Just a simple estate app for Sevan.", + 'application': True, + 'installable': True, + 'author': "Sevan Corp.", +} \ No newline at end of file From 59ab049db83ce77b12b649f5e59448953d2ceb46 Mon Sep 17 00:00:00 2001 From: SebVde Date: Tue, 21 Apr 2026 16:48:12 +0200 Subject: [PATCH 02/27] [ADD] estate: created Property model --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file 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..9bca5a4e477 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,26 @@ +from odoo import fields, models + +class Property(models.Model): + + # Model definition + _name = "estate.property" + _description = "Estate Property" + + # Fields + 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')] + ) + From 57edca3446b15c5b811b75625f99d6f5a1047f75 Mon Sep 17 00:00:00 2001 From: SebVde Date: Tue, 21 Apr 2026 17:26:16 +0200 Subject: [PATCH 03/27] [ADD] estate: add access rights to base.group-user --- estate/__manifest__.py | 4 +++- estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6ebb4744f35..3bdd5b265b8 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,5 +4,7 @@ 'summary': "Just a simple estate app for Sevan.", 'application': True, 'installable': True, + + 'data': ['security/ir.model.access.csv'], 'author': "Sevan Corp.", -} \ No newline at end of file +} diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..85de405deb2 --- /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 d6624078fbfc589056af7aac9c4c13706801b872 Mon Sep 17 00:00:00 2001 From: SebVde Date: Wed, 22 Apr 2026 09:02:21 +0200 Subject: [PATCH 04/27] [CLN] estate: clean code following coach's comments --- estate/__init__.py | 2 +- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) 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/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9bca5a4e477..8c7aabc1296 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import fields, models + class Property(models.Model): # Model definition @@ -21,6 +22,10 @@ class Property(models.Model): garden_area = fields.Integer() garden_orientation = fields.Selection( string='Orientation', - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], ) - From 47a9eac1da4eca4776e05162798238a7aaf6a8d0 Mon Sep 17 00:00:00 2001 From: SebVde Date: Wed, 22 Apr 2026 13:25:51 +0200 Subject: [PATCH 05/27] [ADD] estate: action and menus for the module's view --- estate/__manifest__.py | 7 ++++++- estate/models/estate_property.py | 27 +++++++++++++++++++++++--- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 13 +++++++++++++ 4 files changed, 51 insertions(+), 4 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 3bdd5b265b8..e62e5ac2426 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,11 @@ 'application': True, 'installable': True, - 'data': ['security/ir.model.access.csv'], + 'data': [ + 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_menus.xml', + ], 'author': "Sevan Corp.", } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 8c7aabc1296..59f2d816e2c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,6 @@ from odoo import fields, models +from datetime import datetime +from dateutil.relativedelta import relativedelta class Property(models.Model): @@ -11,10 +13,16 @@ class Property(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date( + default=datetime.today() + relativedelta(months=3), + copy=False, + ) 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() @@ -29,3 +37,16 @@ class Property(models.Model): ('west', 'West'), ], ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + required=True, + copy=False, + default='new', + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..b1ad99cda95 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..d8f0e79163e --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,13 @@ + + + + Property + estate.property + list,form + +

+ This is a new action created following the tutorial. +

+
+
+
From d260597b64682e57fa6cd4f122696c9e17e9caf8 Mon Sep 17 00:00:00 2001 From: SebVde Date: Wed, 22 Apr 2026 15:48:03 +0200 Subject: [PATCH 06/27] [ADD] estate: create list, form and search views --- estate/models/estate_property.py | 7 +- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 95 +++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 59f2d816e2c..406215ab813 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -10,10 +10,11 @@ class Property(models.Model): _description = "Estate Property" # Fields - name = fields.Char(required=True) + name = fields.Char('Title', required=True) description = fields.Text() postcode = fields.Char() date_availability = fields.Date( + 'Available From', default=datetime.today() + relativedelta(months=3), copy=False, ) @@ -23,11 +24,11 @@ class Property(models.Model): copy=False, ) bedrooms = fields.Integer(default=2) - living_area = fields.Integer() + living_area = fields.Integer('Living Area (sqm)') facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer('Garden Area (sqm)') garden_orientation = fields.Selection( string='Orientation', selection=[ diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index b1ad99cda95..1f8b0b9d48c 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,7 @@ - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d8f0e79163e..50b96454875 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,100 @@ + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ + +

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + - Property + Properties estate.property list,form From d09e0fa2848ec88214c8c6f55edde50f00e31161 Mon Sep 17 00:00:00 2001 From: SebVde Date: Wed, 22 Apr 2026 17:14:51 +0200 Subject: [PATCH 07/27] [ADD] estate: end-of-the-day draft for Chapter 7 --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property_type.py | 8 ++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menus.xml | 3 +++ estate/views/estate_property_type_views.xml | 18 ++++++++++++++++++ 6 files changed, 32 insertions(+) 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 e62e5ac2426..d5adb920e2d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,6 +10,7 @@ 'views/estate_property_views.xml', 'views/estate_menus.xml', + 'views/estate_property_type_views.xml', ], 'author': "Sevan Corp.", } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..40092a2d810 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ from . import estate_property +from . import estate_property_type diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..e7e8b829f03 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 85de405deb2..b28b50f65a1 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 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 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 1f8b0b9d48c..695c3214475 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,8 @@ + + +
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..244387fcc70 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,18 @@ + + + + estate.property.type.list + estate.property.type + + + + + + + + + Property Types + estate.property.type + list,form + + From 052c526f85d7791ff56f032339f3ff20a5e16854 Mon Sep 17 00:00:00 2001 From: "David Van Droogenbroeck (DROD)" Date: Thu, 23 Apr 2026 09:47:52 +0200 Subject: [PATCH 08/27] [FIX] estate: remove unlink permission for base users Base users were able to unlink records although they're just plebs. --- estate/security/ir.model.access.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index b28b50f65a1..ee1c76f186a 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,3 @@ 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,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,0 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,0 From ad84cd304baff03861ee706d6f7b29de945a2973 Mon Sep 17 00:00:00 2001 From: SebVde Date: Thu, 23 Apr 2026 10:19:32 +0200 Subject: [PATCH 09/27] [ADD] estate: add property type, salesman and buyer fields --- estate/models/estate_property.py | 3 +++ estate/views/estate_property_views.xml | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 406215ab813..cf905f48887 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -38,6 +38,9 @@ class Property(models.Model): ('west', 'West'), ], ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + buyer = fields.Many2one("res.partner", copy=False) + salesman = fields.Many2one("res.users", default=lambda self: self.env.user) active = fields.Boolean(default=True) state = fields.Selection( selection=[ diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 50b96454875..77844fa14c9 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -70,6 +70,14 @@ + + + + + + + + From a65fc53b932c4a6c4a6a64dacbf216965dad3838 Mon Sep 17 00:00:00 2001 From: SebVde Date: Thu, 23 Apr 2026 11:09:07 +0200 Subject: [PATCH 10/27] [ADD] estate: add tags to property --- estate/__manifest__.py | 4 ++- estate/models/__init__.py | 1 + estate/models/estate_property.py | 1 + estate/models/estate_property_tag.py | 8 ++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menus.xml | 1 + estate/views/estate_property_tag_views.xml | 32 ++++++++++++++++++++++ estate/views/estate_property_views.xml | 3 ++ 8 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/views/estate_property_tag_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index d5adb920e2d..ce731a1708f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,9 +8,11 @@ 'data': [ 'security/ir.model.access.csv', + # Be careful with the order! 'views/estate_property_views.xml', - 'views/estate_menus.xml', 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_menus.xml', ], 'author': "Sevan Corp.", } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 40092a2d810..c620ac481a3 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,3 @@ from . import estate_property from . import estate_property_type +from . import estate_property_tag diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cf905f48887..8e9c9d13e3a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -41,6 +41,7 @@ class Property(models.Model): property_type_id = fields.Many2one("estate.property.type", string="Property Type") buyer = fields.Many2one("res.partner", copy=False) salesman = fields.Many2one("res.users", default=lambda self: self.env.user) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") active = fields.Boolean(default=True) state = fields.Selection( selection=[ diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..20ce83232f9 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index ee1c76f186a..3f6f046abf5 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,4 @@ 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,0 estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,0 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,0 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 695c3214475..8b1d9acbdeb 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -6,6 +6,7 @@
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..b41ec8a05f7 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,32 @@ + + + + estate.property.tag.list + estate.property.tag + + + + + + + + + estate.property.tag.form + estate.property.tag + +
+ + + + + +
+
+
+ + + Property Tags + estate.property.tag + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 77844fa14c9..8faf4dba40a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,6 +6,7 @@ + @@ -27,6 +28,8 @@ + +

From 58cc017ce5574e585fd17703ac75e0757b85b5f1 Mon Sep 17 00:00:00 2001 From: SebVde Date: Thu, 23 Apr 2026 10:19:32 +0200 Subject: [PATCH 11/27] [ADD] estate: add property type, salesman and buyer fields --- estate/models/estate_property_type.py | 2 +- estate/views/estate_property_views.xml | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index e7e8b829f03..3519fec3eaf 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -3,6 +3,6 @@ class PropertyType(models.Model): _name = "estate.property.type" - _description = "Estate Property" + _description = "Estate Property Type" name = fields.Char(required=True) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8faf4dba40a..4dc5aecf4f7 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,6 +7,7 @@ + @@ -32,7 +33,7 @@

- + @@ -40,12 +41,17 @@ - + + + + + + @@ -81,6 +87,14 @@ + + + + + + + + @@ -93,6 +107,7 @@ + From d22ebc0ec62e3f4ee9f73fd17c1517b7e9b227b0 Mon Sep 17 00:00:00 2001 From: SebVde Date: Thu, 23 Apr 2026 11:09:07 +0200 Subject: [PATCH 12/27] [ADD] estate: add tags to property --- estate/views/estate_property_views.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4dc5aecf4f7..36f389336b0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -8,6 +8,7 @@ + From cff0075f92a11d33279e3117502fb68cd26391ff Mon Sep 17 00:00:00 2001 From: SebVde Date: Thu, 23 Apr 2026 11:45:25 +0200 Subject: [PATCH 13/27] [ADD] estate: add offers to property --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 1 + estate/models/estate_property_offer.py | 25 +++++++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_property_offer_views.xml | 38 ++++++++++++++++++++ estate/views/estate_property_views.xml | 3 ++ 7 files changed, 70 insertions(+) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/views/estate_property_offer_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ce731a1708f..a4d44b52853 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,7 @@ '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', ], 'author': "Sevan Corp.", diff --git a/estate/models/__init__.py b/estate/models/__init__.py index c620ac481a3..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +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 8e9c9d13e3a..db7b61b58c5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -42,6 +42,7 @@ class Property(models.Model): buyer = fields.Many2one("res.partner", copy=False) salesman = fields.Many2one("res.users", default=lambda self: self.env.user) tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id") active = fields.Boolean(default=True) state = fields.Selection( selection=[ diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..3fd3b8423ed --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + + price = fields.Float() + status = fields.Selection( + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], + copy=False, + ) + partner_id = fields.Many2one( + "res.partner", + string="Partner", + required=True, + ) + property_id = fields.Many2one( + "estate.property", + string="Property", + required=True, + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 3f6f046abf5..ff68a56c044 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -2,3 +2,4 @@ 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,0 estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,0 estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,0 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,0 diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..ed6f3780a63 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,38 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + + + + + + + + + +
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 36f389336b0..9eecfd70e61 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -80,6 +80,9 @@
+ + + From 96e43bdaa072258e05249b88b018552c86ef425a Mon Sep 17 00:00:00 2001 From: SebVde Date: Thu, 23 Apr 2026 13:36:35 +0200 Subject: [PATCH 14/27] [ADD] estate: compute total area of property --- estate/models/estate_property.py | 8 +++++++- estate/views/estate_property_views.xml | 12 +++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index db7b61b58c5..135141b1eee 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models from datetime import datetime from dateutil.relativedelta import relativedelta @@ -38,6 +38,7 @@ class Property(models.Model): ('west', 'West'), ], ) + total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area") property_type_id = fields.Many2one("estate.property.type", string="Property Type") buyer = fields.Many2one("res.partner", copy=False) salesman = fields.Many2one("res.users", default=lambda self: self.env.user) @@ -56,3 +57,8 @@ class Property(models.Model): copy=False, default='new', ) + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9eecfd70e61..baf0865b9c2 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,7 +6,6 @@ - @@ -79,6 +78,9 @@ + + + @@ -91,14 +93,6 @@
- - - - - - - - From 39728f4675f8178bf069893cdca5be85785f781b Mon Sep 17 00:00:00 2001 From: SebVde Date: Thu, 23 Apr 2026 17:27:53 +0200 Subject: [PATCH 15/27] [ADD] estate: compute best offer, deadline and garden values --- estate/models/estate_property.py | 11 +++++++++++ estate/models/estate_property_offer.py | 19 ++++++++++++++++++- estate/views/estate_property_offer_views.xml | 10 ++++++++++ estate/views/estate_property_views.xml | 5 ++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 135141b1eee..d5c34086bcc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -44,6 +44,7 @@ class Property(models.Model): salesman = fields.Many2one("res.users", default=lambda self: self.env.user) tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id") + best_price = fields.Float(compute="_compute_best_price") active = fields.Boolean(default=True) state = fields.Selection( selection=[ @@ -62,3 +63,13 @@ class Property(models.Model): def _compute_total_area(self): for record in self: record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids") + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped("price")) if record.offer_ids else 0.0 + + @api.onchange("garden") + def _onchange_garden_values(self): + self.garden_area = 10 if self.garden else 0 + self.garden_orientation = 'north' if self.garden else None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 3fd3b8423ed..81eb8dc69c7 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from odoo import api, fields, models +from datetime import datetime +from dateutil.relativedelta import relativedelta class PropertyOffer(models.Model): @@ -23,3 +25,18 @@ class PropertyOffer(models.Model): string="Property", required=True, ) + validity = fields.Integer("Validity (days)", default=7) + date_deadline = fields.Date("Deadline", compute="_compute_deadline", inverse="_inverse_deadline") + + @api.depends("validity") + def _compute_deadline(self): + for record in self: + if not record.create_date: + record.create_date = datetime.today() + record.date_deadline = record.create_date + relativedelta(days=record.validity) + + def _inverse_deadline(self): + for record in self: + if not record.create_date: + record.create_date = datetime.today() + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index ed6f3780a63..21b82dc5878 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,6 +7,8 @@ + +
@@ -26,6 +28,14 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index baf0865b9c2..e1d009c649b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -44,13 +44,16 @@ - + + + + From 2cfe9bfd1ae239ee6161b3265f741ed894b9faa9 Mon Sep 17 00:00:00 2001 From: SebVde Date: Fri, 24 Apr 2026 10:32:02 +0200 Subject: [PATCH 16/27] [ADD] estate: buttons linked to actions for property and offer --- estate/models/estate_property.py | 14 ++++- estate/models/estate_property_offer.py | 16 +++++ estate/views/estate_property_offer_views.xml | 15 +---- estate/views/estate_property_views.xml | 65 +++++++------------- 4 files changed, 53 insertions(+), 57 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d5c34086bcc..3b8c1c00e36 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,10 +1,10 @@ from odoo import api, fields, models +from odoo.exceptions import UserError from datetime import datetime from dateutil.relativedelta import relativedelta class Property(models.Model): - # Model definition _name = "estate.property" _description = "Estate Property" @@ -73,3 +73,15 @@ def _compute_best_price(self): def _onchange_garden_values(self): self.garden_area = 10 if self.garden else 0 self.garden_orientation = 'north' if self.garden else None + + def action_cancel_property(self): + if self.state == 'sold': + raise UserError("You can't cancel a property that is already sold.") + self.state = 'cancelled' + return True + + def action_sold_property(self): + if self.state == 'cancelled': + raise UserError("You can't sell a property that is cancelled.") + self.state = 'sold' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 81eb8dc69c7..36256850250 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError from datetime import datetime from dateutil.relativedelta import relativedelta @@ -40,3 +41,18 @@ def _inverse_deadline(self): if not record.create_date: record.create_date = datetime.today() record.validity = (record.date_deadline - record.create_date.date()).days + + def action_accept_offer(self): + if any(record != self and record.status == 'accepted' for record in self.property_id.offer_ids): + raise UserError("Only one offer can be accepted") + self.status = 'accepted' + self.property_id.buyer = self.partner_id + self.property_id.selling_price = self.price + return True + + def action_refuse_offer(self): + for record in self: + if record.status == 'accepted': + record.property_id.buyer = None + record.property_id.selling_price = 0.00 + record.status = 'refused' diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 21b82dc5878..c50f8d642c0 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + +

From 4209cd6dba9715b697a7f2775f161cd2b5479762 Mon Sep 17 00:00:00 2001 From: SebVde Date: Mon, 27 Apr 2026 14:25:03 +0200 Subject: [PATCH 20/27] [FIX] estate: add missing license --- estate/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 5f23af6bad0..b125aaefde2 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,6 @@ { 'name': "Sevan Estate", + 'license': 'LGPL-3', 'depends': ['base'], 'summary': "Just a simple estate app for Sevan.", 'application': True, From 7a8ea2007486459a94ae1bbf62b4a94f344c3d3e Mon Sep 17 00:00:00 2001 From: SebVde Date: Mon, 27 Apr 2026 17:21:23 +0200 Subject: [PATCH 21/27] [IMP] estate: override CRUD methods, and inherit models and views --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 6 ++++++ estate/models/estate_property_offer.py | 9 +++++++++ estate/models/res_users.py | 11 +++++++++++ estate/views/res_users_views.xml | 15 +++++++++++++++ 6 files changed, 43 insertions(+) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b125aaefde2..8c7af4fb8d8 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,6 +14,7 @@ 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', + 'views/res_users_views.xml', 'views/estate_menus.xml', ], 'author': "Sevan Corp.", diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 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 res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9f491aae49d..9a718df99bc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -108,3 +108,9 @@ def action_sold_property(self): raise UserError("You can't sell a property that is cancelled.") self.state = 'sold' return True + + @api.ondelete(at_uninstall=False) + def _unlink_except_new_or_cancelled(self): + if self.state in ('new', 'cancelled'): + raise UserError("Can't delete a new or cancelled property.") + return super().unlink() diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5f436d60c01..e0ff848d61b 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -64,3 +64,12 @@ def action_refuse_offer(self): record.property_id.buyer = None record.property_id.selling_price = 0.00 record.status = 'refused' + + @api.model + def create(self, vals_list): + for vals_dict in vals_list: + property_id = self.env['estate.property'].browse(vals_dict['property_id']) + offers = property_id.offer_ids + if (len(offers) > 0 and vals_dict['price'] < min(offers.mapped('price'))): + raise UserError("Can't create an offer with a lower value than an existing offer.") + 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..ea2b220d03b --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesman", + domain=[("state", "not in", ["sold", "cancelled"])] + ) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..84b69f7a03b --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.form.estate.extention + res.users + + + + + + + + + + From cd50a96c27ee86addfb162f8260ce11c492d975f Mon Sep 17 00:00:00 2001 From: SebVde Date: Tue, 28 Apr 2026 15:30:37 +0200 Subject: [PATCH 22/27] [ADD] estate account: add invoice creation for sold property --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 21 +++++++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_account.py | 34 +++++++++++++++++++++++++ 4 files changed, 57 insertions(+) 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_account.py 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..b273b0c6e7a --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': "Estate Account", + 'license': 'LGPL-3', + 'depends': ['estate', 'account'], + 'summary': "Link module to be able to create invoices for sold estate properties.", + 'application': True, + 'installable': True, + + # 'data': [ + # 'security/ir.model.access.csv', + + # # Be careful with the order! + # 'views/estate_property_views.xml', + # 'views/estate_property_offer_views.xml', + # 'views/estate_property_type_views.xml', + # 'views/estate_property_tag_views.xml', + # 'views/res_users_views.xml', + # 'views/estate_menus.xml', + # ], + 'author': "Sevan Corp.", +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..02b688798a3 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_account diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py new file mode 100644 index 00000000000..c271454dd27 --- /dev/null +++ b/estate_account/models/estate_account.py @@ -0,0 +1,34 @@ +from odoo import models, Command +from odoo.tools.float_utils import float_round + +class EstateAccount(models.Model): + _inherit = "estate.property" + + def action_sold_property(self): + invoice_vals_list = [] + for sold_property in self: + sold_to_partner_id = None + for offer in sold_property.offer_ids: + if offer.status == 'accepted': + sold_to_partner_id = offer.partner_id + break + current_section_vals = None + invoice_vals = { + 'move_type': 'out_invoice', + 'partner_id': sold_to_partner_id.id, + 'line_ids': [ + Command.create({ + 'name': "6% of the selling price", + 'quantity': 1, + 'price_unit': float_round(sold_property.selling_price * 0.06, precision_digits=2) + }), + Command.create({ + 'name': "Administrative fees", + 'quantity': 1, + 'price_unit': 100.00 + }), + ] + } + invoice_vals_list.append(invoice_vals) + self.env['account.move'].create(invoice_vals_list) + return super().action_sold_property() From e703a19d3514e7178829be5122b2a13039503581 Mon Sep 17 00:00:00 2001 From: "David Van Droogenbroeck (DROD)" Date: Tue, 28 Apr 2026 15:11:43 +0200 Subject: [PATCH 23/27] [IMP] estate: add basic test cases This commit is here to introduce the testing framework of Odoo. Try running the tests using `--test-tags :TestEstateProperty`. Doc: https://www.odoo.com/documentation/18.0/developer/reference/backend/testing.html?highlight=tests#invocation The tests were made such that the first one should work but the second one should fail. Your job is to ensure both tests pass in the end. You should update the behaviour of the appropriate models. If you want, you can also add a small test of your own to get a feel for it. --- estate/tests/__init__.py | 1 + estate/tests/test_estate_property.py | 42 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate_property.py diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..18f3a50c3e1 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate_property \ No newline at end of file diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..7202f8b67a0 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,42 @@ +from odoo.exceptions import ValidationError +from odoo.tests import TransactionCase +from odoo import Command + + +class TestEstateProperty(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.estate = cls.env['estate.property'].create({ + 'name': 'Super test estate', + 'expected_price': 100000.0, + 'state': 'new', + }) + cls.test_partner = cls.env['res.partner'].create({ + 'name': 'Maman ours', + }) + + def test_estate_best_price(self): + ''' + Ensure best price is correctly updated when an offer is received. + ''' + self.assertEqual(self.estate.best_price, 0.0) + self.estate.offer_ids = [Command.create({ + 'price': 125000.0, + 'partner_id': self.test_partner.id, + })] + self.assertEqual(self.estate.best_price, 125000.0) + + def test_accept_offer_south_facing_garden(self): + ''' + Ensure offers for estates with south-facing gardens can only be accepted if above expected + price. + ''' + self.estate.expected_price = 500000 + self.estate.offer_ids = [Command.create({ + 'price': 475000.0, + 'partner_id': self.test_partner.id, + })] + with self.assertRaises(ValidationError): + self.estate.offer_ids.accept_offer() From cf25e2bfaac044bb38ea291e46041736f16f0101 Mon Sep 17 00:00:00 2001 From: SebVde Date: Tue, 28 Apr 2026 16:12:48 +0200 Subject: [PATCH 24/27] [IMP] estate: modify behavious of offer accept action --- estate/models/estate_property_offer.py | 6 +++++- estate/tests/test_estate_property.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e0ff848d61b..77092844efc 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ from odoo import api, fields, models -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare from datetime import datetime from dateutil.relativedelta import relativedelta @@ -52,6 +53,9 @@ def _inverse_deadline(self): def action_accept_offer(self): if any(record != self and record.status == 'accepted' for record in self.property_id.offer_ids): raise UserError("Only one offer can be accepted") + if (self.property_id.garden_orientation == 'south' and + float_compare(self.property_id.expected_price, self.price, 2) == 1): + raise ValidationError("For properties South oriented gardens, only offers having a price higher than the expected value of the property can be accepted") self.status = 'accepted' self.property_id.buyer = self.partner_id self.property_id.selling_price = self.price diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py index 7202f8b67a0..5df181fb0a0 100644 --- a/estate/tests/test_estate_property.py +++ b/estate/tests/test_estate_property.py @@ -33,10 +33,12 @@ def test_accept_offer_south_facing_garden(self): Ensure offers for estates with south-facing gardens can only be accepted if above expected price. ''' + self.estate.garden = True + self.estate.garden_orientation = 'south' self.estate.expected_price = 500000 self.estate.offer_ids = [Command.create({ 'price': 475000.0, 'partner_id': self.test_partner.id, })] with self.assertRaises(ValidationError): - self.estate.offer_ids.accept_offer() + self.estate.offer_ids.action_accept_offer() From fae93ca7b91410edca7bc362f49fd8bc6ac0f3a4 Mon Sep 17 00:00:00 2001 From: SebVde Date: Tue, 28 Apr 2026 16:57:58 +0200 Subject: [PATCH 25/27] [IMP] estate: add kanban view for properties --- estate/views/estate_property_views.xml | 34 +++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e598c20c9a4..c21239efaba 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -105,10 +105,42 @@ + + estate.property.kanban + estate.property + + + + +
+ + + + +
+ Expected price: + +
+
+ Best Offer: + +
+
+ Selling Price: + +
+
+
+
+
+
+
+ Properties estate.property - list,form + list,form,kanban {'search_default_state': 1} From 478c5945c8c854901c37a5d0c8049ed6241c3cd4 Mon Sep 17 00:00:00 2001 From: SebVde Date: Tue, 28 Apr 2026 17:11:19 +0200 Subject: [PATCH 26/27] [CLN] estate: cleaning following coding guidelines --- estate/models/estate_property.py | 6 +++--- estate/models/estate_property_offer.py | 5 +++-- estate/models/estate_property_type.py | 2 +- estate/models/res_users.py | 2 +- estate/tests/__init__.py | 2 +- estate/tests/test_estate_property.py | 2 +- estate/views/estate_property_offer_views.xml | 4 ++-- estate/views/estate_property_tag_views.xml | 4 ++-- estate/views/estate_property_type_views.xml | 4 ++-- estate/views/estate_property_views.xml | 8 ++++---- estate_account/models/estate_account.py | 4 ++-- 11 files changed, 22 insertions(+), 21 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9a718df99bc..2dbd7ed7e6d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,9 +1,9 @@ -from odoo.tools.float_utils import float_compare, float_is_zero +from datetime import datetime +from dateutil.relativedelta import relativedelta from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError -from datetime import datetime -from dateutil.relativedelta import relativedelta +from odoo.tools.float_utils import float_compare, float_is_zero class Property(models.Model): diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 77092844efc..2da05ec5698 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,8 +1,9 @@ +from datetime import datetime +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 -from datetime import datetime -from dateutil.relativedelta import relativedelta class PropertyOffer(models.Model): diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 837c3c614b1..2ad9665851e 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,4 +1,4 @@ -from odoo import fields, models, api +from odoo import api, fields, models class PropertyType(models.Model): diff --git a/estate/models/res_users.py b/estate/models/res_users.py index ea2b220d03b..f8dbb75c218 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import fields, models class ResUsers(models.Model): diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py index 18f3a50c3e1..576617cccff 100644 --- a/estate/tests/__init__.py +++ b/estate/tests/__init__.py @@ -1 +1 @@ -from . import test_estate_property \ No newline at end of file +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py index 5df181fb0a0..efb1b93f5c8 100644 --- a/estate/tests/test_estate_property.py +++ b/estate/tests/test_estate_property.py @@ -1,6 +1,6 @@ +from odoo import Command from odoo.exceptions import ValidationError from odoo.tests import TransactionCase -from odoo import Command class TestEstateProperty(TransactionCase): diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 58a9116101e..efc6a63e006 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,7 +1,7 @@ - estate.property.offer.list + estate.property.offer.view.list estate.property.offer - estate.property.offer.form + estate.property.offer.view.form estate.property.offer diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index 6076e8cefc2..cbcf4909180 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -1,7 +1,7 @@ - estate.property.tag.list + estate.property.tag.view.list estate.property.tag @@ -11,7 +11,7 @@ - estate.property.tag.form + estate.property.tag.view.form estate.property.tag diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index e53bfe7cab0..2633a801543 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,7 +1,7 @@ - estate.property.type.list + estate.property.type.view.list estate.property.type @@ -12,7 +12,7 @@ - estate.property.type.form + estate.property.type.view.form estate.property.type diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index c21239efaba..2e609cc6bbc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,7 @@ - estate.property.list + estate.property.view.list estate.property - estate.property.form + estate.property.view.form estate.property @@ -87,7 +87,7 @@ - estate.property.search + estate.property.view.search estate.property @@ -106,7 +106,7 @@ - estate.property.kanban + estate.property.view.kanban estate.property diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py index c271454dd27..dc95644774e 100644 --- a/estate_account/models/estate_account.py +++ b/estate_account/models/estate_account.py @@ -1,6 +1,7 @@ -from odoo import models, Command +from odoo import Command, models from odoo.tools.float_utils import float_round + class EstateAccount(models.Model): _inherit = "estate.property" @@ -12,7 +13,6 @@ def action_sold_property(self): if offer.status == 'accepted': sold_to_partner_id = offer.partner_id break - current_section_vals = None invoice_vals = { 'move_type': 'out_invoice', 'partner_id': sold_to_partner_id.id, From d8542bd1bc01c34b10f7630db1db88f74d5fb678 Mon Sep 17 00:00:00 2001 From: SebVde Date: Wed, 29 Apr 2026 13:53:33 +0200 Subject: [PATCH 27/27] [CLN] estate + estate_account --- estate/models/estate_property.py | 98 +++++++++++++------------ estate/models/estate_property_offer.py | 68 +++++++++-------- estate/models/res_users.py | 2 +- estate/views/estate_property_views.xml | 4 +- estate_account/__manifest__.py | 12 --- estate_account/models/estate_account.py | 7 +- 6 files changed, 87 insertions(+), 104 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2dbd7ed7e6d..66ef822b20b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -11,13 +11,17 @@ class Property(models.Model): _name = "estate.property" _description = "Estate Property" _order = "id desc" + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", + "The expected price should be strictly positive.", + ) # Fields - name = fields.Char('Title', required=True) + name = fields.Char("Title", required=True) description = fields.Text() postcode = fields.Char() date_availability = fields.Date( - 'Available From', + "Available From", default=datetime.today() + relativedelta(months=3), copy=False, ) @@ -27,55 +31,43 @@ class Property(models.Model): copy=False, ) bedrooms = fields.Integer(default=2) - living_area = fields.Integer('Living Area (sqm)') + living_area = fields.Integer("Living Area (sqm)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer('Garden Area (sqm)') + garden_area = fields.Integer("Garden Area (sqm)") garden_orientation = fields.Selection( - string='Orientation', + string="Orientation", selection=[ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West'), + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), ], ) total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area") property_type_id = fields.Many2one("estate.property.type", string="Property Type") - buyer = fields.Many2one("res.partner", copy=False) - salesman = fields.Many2one("res.users", default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", copy=False) + salesman_id = fields.Many2one("res.users", default=lambda self: self.env.user) tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id") best_price = fields.Float(compute="_compute_best_price") active = fields.Boolean(default=True) state = fields.Selection( selection=[ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('cancelled', 'Cancelled'), + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), ], required=True, copy=False, - default='new', + default="new", store=True, - compute='_compute_if_offer', - ) - - _check_expected_price = models.Constraint( - 'CHECK(expected_price > 0)', - 'The expected price should be strictly positive.', + compute="_compute_state", ) - @api.constrains('expected_price', 'selling_price') - def _check_selling_price_above_90(self): - for record in self: - if (not float_is_zero(record.selling_price, precision_digits=0) and - float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=0) == -1): - raise ValidationError("The selling price should be at least 90% of the expected price.") - @api.depends("living_area", "garden_area") def _compute_total_area(self): for record in self: @@ -87,30 +79,40 @@ def _compute_best_price(self): record.best_price = max(record.offer_ids.mapped("price")) if record.offer_ids else 0.0 @api.depends("offer_ids") - def _compute_if_offer(self): + def _compute_state(self): + for record in self: + if (record.state == "new" and len(record.offer_ids) > 0): + record.state = "offer_received" + + @api.constrains("expected_price", "selling_price") + def _check_selling_price_to_expected_price_ratio(self): for record in self: - if (record.state == 'new' and len(record.offer_ids) > 0): - record.state = 'offer_received' + if (not float_is_zero(record.selling_price, precision_digits=0) and + float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=0) == -1): + raise ValidationError("The selling price should be at least 90% of the expected price.") @api.onchange("garden") def _onchange_garden_values(self): self.garden_area = 10 if self.garden else 0 - self.garden_orientation = 'north' if self.garden else None + self.garden_orientation = "north" if self.garden else None + + @api.ondelete(at_uninstall=False) + def _unlink(self): + for record in self: + if record.state in ("new", "cancelled"): + raise UserError("New or cancelled properties cannot be deleted.") + return super().unlink() def action_cancel_property(self): - if self.state == 'sold': - raise UserError("You can't cancel a property that is already sold.") - self.state = 'cancelled' - return True + if self.state == "sold": + raise UserError("Sold properties cannot be cancelled.") + return self.write({ + "state": "cancelled", + }) def action_sold_property(self): - if self.state == 'cancelled': - raise UserError("You can't sell a property that is cancelled.") - self.state = 'sold' - return True - - @api.ondelete(at_uninstall=False) - def _unlink_except_new_or_cancelled(self): - if self.state in ('new', 'cancelled'): - raise UserError("Can't delete a new or cancelled property.") - return super().unlink() + if self.state == "cancelled": + raise UserError("Cancelled properties cannot be sold.") + return self.write({ + "state": "sold", + }) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 2da05ec5698..362b12a33bb 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -10,12 +10,16 @@ class PropertyOffer(models.Model): _name = "estate.property.offer" _description = "Estate Property Offer" _order = "price desc" + _check_price = models.Constraint( + 'CHECK(price > 0.00)', + "The offer's amount should be strictly positive.", + ) price = fields.Float() status = fields.Selection( selection=[ - ('accepted', 'Accepted'), - ('refused', 'Refused'), + ("accepted", "Accepted"), + ("refused", "Refused"), ], copy=False, ) @@ -33,48 +37,42 @@ class PropertyOffer(models.Model): date_deadline = fields.Date("Deadline", compute="_compute_deadline", inverse="_inverse_deadline") property_type_id = fields.Many2one("estate.property.type", store=True, related="property_id.property_type_id") - _check_price = models.Constraint( - 'CHECK(price > 0.00)', - "The offer's amount should be strictly positive.", - ) - @api.depends("validity") def _compute_deadline(self): for record in self: - if not record.create_date: - record.create_date = datetime.today() - record.date_deadline = record.create_date + relativedelta(days=record.validity) + date = datetime.today() if not record.create_date else record.create_date.date() + record.date_deadline = date + relativedelta(days=record.validity) def _inverse_deadline(self): for record in self: - if not record.create_date: - record.create_date = datetime.today() - record.validity = (record.date_deadline - record.create_date.date()).days - - def action_accept_offer(self): - if any(record != self and record.status == 'accepted' for record in self.property_id.offer_ids): - raise UserError("Only one offer can be accepted") - if (self.property_id.garden_orientation == 'south' and - float_compare(self.property_id.expected_price, self.price, 2) == 1): - raise ValidationError("For properties South oriented gardens, only offers having a price higher than the expected value of the property can be accepted") - self.status = 'accepted' - self.property_id.buyer = self.partner_id - self.property_id.selling_price = self.price - self.property_id.state = 'offer_accepted' - return True - - def action_refuse_offer(self): - for record in self: - if record.status == 'accepted': - record.property_id.buyer = None - record.property_id.selling_price = 0.00 - record.status = 'refused' + date = datetime.today() if not record.create_date else record.create_date.date() + record.validity = (record.date_deadline - date).days @api.model def create(self, vals_list): for vals_dict in vals_list: - property_id = self.env['estate.property'].browse(vals_dict['property_id']) + property_id = self.env["estate.property"].browse(vals_dict["property_id"]) offers = property_id.offer_ids - if (len(offers) > 0 and vals_dict['price'] < min(offers.mapped('price'))): - raise UserError("Can't create an offer with a lower value than an existing offer.") + if (len(offers) > 0 and vals_dict["price"] < min(offers.mapped("price"))): + raise UserError("Offer with a lower value than an existing offer cannot be created.") return super().create(vals_list) + + def action_accept_offer(self): + for record in self: + if "accepted" in record.mapped("property_id.offer_ids.status"): + raise UserError("Only one offer can be accepted") + if (record.property_id.garden_orientation == "south" and + float_compare(record.property_id.expected_price, record.price, 2) == 1): + raise ValidationError("For properties with South oriented gardens, only offers having a price higher than the expected value of the property can be accepted") + return self.status.write({ + "status": "accepted", + }) and self.property_id.write({ + "state": "offer_accepted", + "selling_price": record.price, + "buyer_id": record.partner_id, + }) + + def action_refuse_offer(self): + return self.write({ + "status": "refused", + }) diff --git a/estate/models/res_users.py b/estate/models/res_users.py index f8dbb75c218..265d5b5729a 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -6,6 +6,6 @@ class ResUsers(models.Model): property_ids = fields.One2many( "estate.property", - "salesman", + "salesman_id", domain=[("state", "not in", ["sold", "cancelled"])] ) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 2e609cc6bbc..f18b2da7cc3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -75,8 +75,8 @@ - - + + diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index b273b0c6e7a..7b2b1f69d61 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -5,17 +5,5 @@ 'summary': "Link module to be able to create invoices for sold estate properties.", 'application': True, 'installable': True, - - # 'data': [ - # 'security/ir.model.access.csv', - - # # Be careful with the order! - # 'views/estate_property_views.xml', - # 'views/estate_property_offer_views.xml', - # 'views/estate_property_type_views.xml', - # 'views/estate_property_tag_views.xml', - # 'views/res_users_views.xml', - # 'views/estate_menus.xml', - # ], 'author': "Sevan Corp.", } diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py index dc95644774e..eebb6c9186e 100644 --- a/estate_account/models/estate_account.py +++ b/estate_account/models/estate_account.py @@ -8,14 +8,9 @@ class EstateAccount(models.Model): def action_sold_property(self): invoice_vals_list = [] for sold_property in self: - sold_to_partner_id = None - for offer in sold_property.offer_ids: - if offer.status == 'accepted': - sold_to_partner_id = offer.partner_id - break invoice_vals = { 'move_type': 'out_invoice', - 'partner_id': sold_to_partner_id.id, + 'partner_id': sold_property.buyer_id.id, 'line_ids': [ Command.create({ 'name': "6% of the selling price",