From ff5d6b325c89ad0c2fc6aba7e839f093a6a3394d Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Mon, 19 Jan 2026 16:48:59 +0100 Subject: [PATCH 01/18] [ADD] real estate module - Created the estate module. - Created the EstateProperty model. --- estate/__init__.py | 3 +++ estate/__manifest__.py | 16 ++++++++++++++++ estate/models/__init__.py | 3 +++ estate/models/estate_property.py | 26 ++++++++++++++++++++++++++ 4 files changed, 48 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/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e4300b98a79 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,3 @@ +# pylint: disable=missing-module-docstring + +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..75b52c0ea87 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,16 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# pylint: disable-next=pointless-statement +{ + 'name': 'Estate', + 'version': '1.9', + 'category': 'Real Estates', + 'summary': 'Manage real estate operations', + 'author': 'Haroune Hassine', + 'license': 'LGPL-3', + 'depends': [ + 'base_setup' + ], + 'application': True, + 'installable': True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..2cc6621a14b --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,3 @@ +# pylint: disable=missing-module-docstring + +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..185dadcbbd6 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,26 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property." + _order = "sequence" + + name = fields.Char('Real Estate Name', required=True) + description = fields.Text('Description') + postcode = fields.Char('Postcode') + date_availability = fields.Date('Availability Date') + expected_price = fields.Float('Expected Price', required=True) + selling_price = fields.Float('Selling Price') + bedrooms = fields.Integer('# Bedrooms') + living_area = fields.Integer('Living Area') + facades = fields.Integer('#Facades') + garage = fields.Boolean('With Garage') + garden = fields.Boolean('With Garden') + garden_area = fields.Integer('Garden Area') + garden_orientation = fields.Selection( + string='Garden Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + ) From c2f73b8f80acb630fa892ebc2b9e7b7a77438cff Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Tue, 20 Jan 2026 13:30:45 +0100 Subject: [PATCH 02/18] [IMP] Setup ruff & remove pylint - Remvoed pylint error-ingoring commands. - Added Ruff config file for style check. - Reformated some files. --- estate/__init__.py | 2 - estate/__manifest__.py | 3 +- estate/models/__init__.py | 2 - estate/models/estate_property.py | 2 +- ruff.toml | 83 ++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 ruff.toml diff --git a/estate/__init__.py b/estate/__init__.py index e4300b98a79..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1,3 +1 @@ -# pylint: disable=missing-module-docstring - from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 75b52c0ea87..03e50f2d0ca 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -# pylint: disable-next=pointless-statement { 'name': 'Estate', 'version': '1.9', @@ -9,7 +8,7 @@ 'author': 'Haroune Hassine', 'license': 'LGPL-3', 'depends': [ - 'base_setup' + 'base_setup', ], 'application': True, 'installable': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2cc6621a14b..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +1 @@ -# pylint: disable=missing-module-docstring - from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 185dadcbbd6..5e3f36c788a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -22,5 +22,5 @@ class EstateProperty(models.Model): garden_area = fields.Integer('Garden Area') garden_orientation = fields.Selection( string='Garden Orientation', - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], ) diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000000..543fa0290c0 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,83 @@ +# automatically generated file by the runbot nightly ruff checks, do not modify +# for ruff version 0.11.4 (or higher) +# note: 'E241', 'E272', 'E201', 'E221' are ignored on runbot in test files when formating a table like structure (more than two space) +# some rules present here are not enabled on runbot (yet) but are still advised to follow when possible : ["B904", "COM812", "E741", "EM101", "I001", "RET", "RUF021", "TRY002", "UP006", "UP007"] + + +target-version = "py310" + +[lint] +preview = true +select = [ + "BLE", # flake8-blind-except + "C", # flake8-comprehensions + "COM", # flake8-commas + "E", # pycodestyle Error + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # Pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PLC", # Pylint Convention + "PLE", # Pylint Error + "PLW", # Pylint Warning + "PYI", # flake8-pyi + "RET", # flake8-return + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "T", # flake8-print + "TC", # flake8-type-checking + "TID", # flake8-tidy-imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle Warning + "YTT", # flake8-2020 +] +ignore = [ + "C408", # unnecessary-collection-call + "C420", # unnecessary-dict-comprehension-for-iterable + "C901", # complex-structure + "E266", # multiple-leading-hashes-for-block-comment + "E501", # line-too-long + "E713", # not-in-test + "EM102", # f-string-in-exception + "FA100", # future-rewritable-type-annotation + "PGH003", # blanket-type-ignore + "PIE790", # unnecessary-placeholder + "PIE808", # unnecessary-range-start + "PLC2701", # import-private-name + "PLW2901", # redefined-loop-name + "RUF001", # ambiguous-unicode-character-string + "RUF005", # collection-literal-concatenation + "RUF012", # mutable-class-default + "RUF100", # unused-noqa + "SIM102", # collapsible-if + "SIM108", # if-else-block-instead-of-if-exp + "SIM117", # multiple-with-statements + "TID252", # relative-imports + "TRY003", # raise-vanilla-args + "TRY300", # try-consider-else + "TRY400", # error-instead-of-exception + "UP031", # printf-string-formatting + "UP038", # non-pep604-isinstance +] + +[lint.per-file-ignores] +"**/__init__.py" = [ + "F401", # unused-import +] + +[lint.isort] +# https://www.odoo.com/documentation/master/contributing/development/coding_guidelines.html#imports +section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] +known-first-party = ["odoo"] +known-local-folder = ["odoo.addons"] From f56cb74c378ad8ca81d8174f0525952daff40d96 Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Tue, 20 Jan 2026 13:34:33 +0100 Subject: [PATCH 03/18] [ADD] security configuration for estate - Added security configuration for the estate module. - Fixes the warning message about the access rules. --- 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 03e50f2d0ca..3bcd3322ead 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,4 +12,7 @@ ], 'application': True, 'installable': 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..db6b1d5444e --- /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_property_model","estate_property_model","model_estate_property","base.group_user",1,1,1,1 \ No newline at end of file From a2089f1b07547c18c1dfbdcef33ffa0cc481c13e Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Tue, 20 Jan 2026 14:36:27 +0100 Subject: [PATCH 04/18] [ADD] Add action to estate module - Added a simple test action to the estate module. --- estate/__manifest__.py | 1 + estate/views/estate_property_views.xml | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 3bcd3322ead..39358e15663 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,5 +14,6 @@ 'installable': True, 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', ], } diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..ee25ef6963d --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,10 @@ + + + + + Test action + estate.property + list,form + + + \ No newline at end of file From 3f06a7a31ac5c5f37bcd2274bf450c64ce4de437 Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Tue, 20 Jan 2026 16:36:07 +0100 Subject: [PATCH 05/18] [IMP] Added ui views Set up the general user interface for the estate app/module. - Added estate menu - Added action window - Set up the fields properties (required, default, etc..) - Added the `active` reserved field to the view --- estate/__manifest__.py | 1 + estate/models/estate_property.py | 21 +++++++++++++++------ estate/views/estate_menus.xml | 11 +++++++++++ estate/views/estate_property_views.xml | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 estate/views/estate_menus.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 39358e15663..e6570379b5b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -15,5 +15,6 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5e3f36c788a..2a3f9ebd2f6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import date +from dateutil.relativedelta import relativedelta from odoo import fields, models @@ -6,17 +7,18 @@ class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property." - _order = "sequence" + + active = fields.Boolean('Active', default=True) name = fields.Char('Real Estate Name', required=True) description = fields.Text('Description') postcode = fields.Char('Postcode') - date_availability = fields.Date('Availability Date') + date_availability = fields.Date('Availability Date', copy=False, default=date.today() + relativedelta(months=+3)) expected_price = fields.Float('Expected Price', required=True) - selling_price = fields.Float('Selling Price') - bedrooms = fields.Integer('# Bedrooms') + selling_price = fields.Float('Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer('# Bedrooms', default=2) living_area = fields.Integer('Living Area') - facades = fields.Integer('#Facades') + facades = fields.Integer('# Facades') garage = fields.Boolean('With Garage') garden = fields.Boolean('With Garden') garden_area = fields.Integer('Garden Area') @@ -24,3 +26,10 @@ class EstateProperty(models.Model): string='Garden Orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], ) + state = fields.Selection( + string='State', + 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..ce3b6df1471 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ee25ef6963d..461a8ad0a98 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,7 +2,7 @@ - Test action + Estate Action estate.property list,form From 72a1ba1992c68fd250e5ea6a275bc9c94ad0cc8e Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Wed, 21 Jan 2026 14:36:02 +0100 Subject: [PATCH 06/18] [IMP] estate: add basic views the estate model Adds three more views to the `estate` model: - `list`: allows the display of the records in a tabular form. - `form`: allows the create/edit of the record in a more sturctured way. - `search`: allows the search for records using filters, and grouping the results by some chosen fields. - We also defined a default filter: `Available`, which filters the results by `state = New` or `state = Offer Received`. Also adds some refactoring to align with the coding guidelines (new line, import order, etc..). --- estate/__manifest__.py | 2 + estate/models/estate_property.py | 14 ++++--- estate/security/ir.model.access.csv | 2 +- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_form_views.xml | 41 +++++++++++++++++++ estate/views/estate_property_search_views.xml | 25 +++++++++++ estate/views/estate_property_views.xml | 30 ++++++++++++-- 7 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 estate/views/estate_property_form_views.xml create mode 100644 estate/views/estate_property_search_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e6570379b5b..d5d963b9635 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -16,5 +16,7 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_menus.xml', + 'views/estate_property_form_views.xml', + 'views/estate_property_search_views.xml', ], } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2a3f9ebd2f6..1a6db98ca5b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from datetime import date + from dateutil.relativedelta import relativedelta from odoo import fields, models @@ -16,15 +17,16 @@ class EstateProperty(models.Model): date_availability = fields.Date('Availability Date', copy=False, default=date.today() + relativedelta(months=+3)) expected_price = fields.Float('Expected Price', required=True) selling_price = fields.Float('Selling Price', readonly=True, copy=False) - bedrooms = fields.Integer('# Bedrooms', default=2) - living_area = fields.Integer('Living Area') - facades = fields.Integer('# Facades') - garage = fields.Boolean('With Garage') - garden = fields.Boolean('With Garden') - garden_area = fields.Integer('Garden Area') + bedrooms = fields.Integer('Bedrooms', default=2) + living_area = fields.Integer('Living Area (sqm)') + facades = fields.Integer('Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden Area (sqm)') garden_orientation = fields.Selection( string='Garden Orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + default='north', ) state = fields.Selection( string='State', diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index db6b1d5444e..bc90d1448fb 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"estate_property_model","estate_property_model","model_estate_property","base.group_user",1,1,1,1 \ No newline at end of file +"estate_property_model","estate_property_model","model_estate_property","base.group_user",1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index ce3b6df1471..a6a051d913d 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,7 +4,7 @@ - + diff --git a/estate/views/estate_property_form_views.xml b/estate/views/estate_property_form_views.xml new file mode 100644 index 00000000000..98c8adc3465 --- /dev/null +++ b/estate/views/estate_property_form_views.xml @@ -0,0 +1,41 @@ + + + + + estate.property.form + estate.property + +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
diff --git a/estate/views/estate_property_search_views.xml b/estate/views/estate_property_search_views.xml new file mode 100644 index 00000000000..b3bdd02bf14 --- /dev/null +++ b/estate/views/estate_property_search_views.xml @@ -0,0 +1,25 @@ + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 461a8ad0a98..5559e5a24d3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,10 +1,34 @@ - - Estate Action + + Properties estate.property list,form - \ No newline at end of file + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + + + + + +
From 950727c55237e66fc064dee7cb73d14f085591fb Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Thu, 22 Jan 2026 09:55:00 +0100 Subject: [PATCH 07/18] [IMP] estate: add relations between models Added three more features for the real estate: `type`, `tag`, and `offers`. - A property can have one type, and a type can be assigned to multiple properties. - A property can have multiple tags, and each tag can be assinged to multiple properties. - A property can have multiple offers, and an offer can be assigned to multiple properties. In addition to the `fields.One2many('estate.property.offer')` defined in the `estate.property` model, we had to add `fields.Many2one('estate.property')` in the `estate,property.offer`, because the `One2many` is a virtual relationship. --- estate/__manifest__.py | 4 +- estate/models/__init__.py | 7 +- estate/models/estate_property.py | 10 ++ estate/models/estate_property_offer.py | 16 +++ estate/models/estate_property_tag.py | 8 ++ estate/models/estate_property_type.py | 8 ++ estate/security/ir.model.access.csv | 3 + estate/views/estate_menus.xml | 8 +- estate/views/estate_property_form_views.xml | 41 ------- estate/views/estate_property_search_views.xml | 25 ---- estate/views/estate_property_views.xml | 115 +++++++++++++++++- 11 files changed, 172 insertions(+), 73 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py delete mode 100644 estate/views/estate_property_form_views.xml delete mode 100644 estate/views/estate_property_search_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index d5d963b9635..057235601fc 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,7 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. { - 'name': 'Estate', + 'name': 'Real Estate', 'version': '1.9', 'category': 'Real Estates', 'summary': 'Manage real estate operations', @@ -16,7 +16,5 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_menus.xml', - 'views/estate_property_form_views.xml', - 'views/estate_property_search_views.xml', ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..3683ff97b61 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,6 @@ -from . import estate_property +from . import ( + estate_property, + estate_property_offer, + estate_property_tag, + estate_property_type, +) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 1a6db98ca5b..67bdf63a3c3 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -35,3 +35,13 @@ class EstateProperty(models.Model): copy=False, default='new', ) + + # res.users: the users of the system. Users can be 'internal', i.e. they have access to the Odoo backend. Or they can be + # 'portal', i.e. they cannot access the backend, only the frontend (e.g. to access their previous orders in eCommerce). + sales_person_id = fields.Many2one('res.users', string='Salesman', index=True, default=lambda self: self.env.user) + # res.partner: a partner is a physical or legal entity. It can be a company, an individual or even a contact address. + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + + 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..647e3edc004 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class EstateProperOffer(models.Model): + _name = "estate.property.offer" + _description = "Property Offer." + + price = fields.Float('Price') + status = fields.Selection( + string='Status', + selection=[('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False, + ) + partner_id = fields.Many2one('res.partner', required=True) + # Because a One2many is a virtual relationship, there must be a Many2one field defined in the comodel. + property_id = fields.Many2one('estate.property', required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..ae32722bb4f --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstateProperTag(models.Model): + _name = "estate.property.tag" + _description = "Property Tag." + + name = fields.Char('Property Tag', required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..0213c1b3c1f --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Property Type." + + name = fields.Char('Property Type', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index bc90d1448fb..992440e42e8 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" "estate_property_model","estate_property_model","model_estate_property","base.group_user",1,1,1,1 +"estate_property_type_model","estate_property_type_model","model_estate_property_type","base.group_user",1,1,1,1 +"estate_property_tag_model","estate_property_tag_model","model_estate_property_tag","base.group_user",1,1,1,1 +"estate_property_offer_model","estate_property_offer_model","model_estate_property_offer","base.group_user",1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index a6a051d913d..e5b5496158d 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -2,10 +2,14 @@ - - + + + + + + diff --git a/estate/views/estate_property_form_views.xml b/estate/views/estate_property_form_views.xml deleted file mode 100644 index 98c8adc3465..00000000000 --- a/estate/views/estate_property_form_views.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - estate.property.form - estate.property - -
- -

- - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- -
diff --git a/estate/views/estate_property_search_views.xml b/estate/views/estate_property_search_views.xml deleted file mode 100644 index b3bdd02bf14..00000000000 --- a/estate/views/estate_property_search_views.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - estate.property.search - estate.property - - - - - - - - - - - - - - - - - - - diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5559e5a24d3..d03eeac702e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,7 +7,19 @@ list,form - + + Property Types + estate.property.type + list,form + + + + Property Tags + estate.property.tag + list,form + + + estate.property.list estate.property @@ -28,7 +40,108 @@ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + estate.type.property.list + estate.property.type + + + + + + + + + estate.tag.property.list + estate.property.tag + + + + + + + + + estate.offer.property.list + estate.property.offer + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
From d1f1fadc19cc38807f1ccee5e0eeae238b3d90f6 Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Thu, 22 Jan 2026 17:21:21 +0100 Subject: [PATCH 08/18] [CLN] estate: remove `ruff.toml` and add it to `.gitignore` --- .gitignore | 3 ++ ruff.toml | 83 ------------------------------------------------------ 2 files changed, 3 insertions(+), 83 deletions(-) delete mode 100644 ruff.toml diff --git a/.gitignore b/.gitignore index b6e47617de1..ba2ff690412 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Ruff linter +ruff.toml diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 543fa0290c0..00000000000 --- a/ruff.toml +++ /dev/null @@ -1,83 +0,0 @@ -# automatically generated file by the runbot nightly ruff checks, do not modify -# for ruff version 0.11.4 (or higher) -# note: 'E241', 'E272', 'E201', 'E221' are ignored on runbot in test files when formating a table like structure (more than two space) -# some rules present here are not enabled on runbot (yet) but are still advised to follow when possible : ["B904", "COM812", "E741", "EM101", "I001", "RET", "RUF021", "TRY002", "UP006", "UP007"] - - -target-version = "py310" - -[lint] -preview = true -select = [ - "BLE", # flake8-blind-except - "C", # flake8-comprehensions - "COM", # flake8-commas - "E", # pycodestyle Error - "EM", # flake8-errmsg - "EXE", # flake8-executable - "F", # Pyflakes - "FA", # flake8-future-annotations - "FLY", # flynt - "G", # flake8-logging-format - "I", # isort - "ICN", # flake8-import-conventions - "INT", # flake8-gettext - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PLC", # Pylint Convention - "PLE", # Pylint Error - "PLW", # Pylint Warning - "PYI", # flake8-pyi - "RET", # flake8-return - "RUF", # Ruff-specific rules - "SIM", # flake8-simplify - "SLOT", # flake8-slots - "T", # flake8-print - "TC", # flake8-type-checking - "TID", # flake8-tidy-imports - "TRY", # tryceratops - "UP", # pyupgrade - "W", # pycodestyle Warning - "YTT", # flake8-2020 -] -ignore = [ - "C408", # unnecessary-collection-call - "C420", # unnecessary-dict-comprehension-for-iterable - "C901", # complex-structure - "E266", # multiple-leading-hashes-for-block-comment - "E501", # line-too-long - "E713", # not-in-test - "EM102", # f-string-in-exception - "FA100", # future-rewritable-type-annotation - "PGH003", # blanket-type-ignore - "PIE790", # unnecessary-placeholder - "PIE808", # unnecessary-range-start - "PLC2701", # import-private-name - "PLW2901", # redefined-loop-name - "RUF001", # ambiguous-unicode-character-string - "RUF005", # collection-literal-concatenation - "RUF012", # mutable-class-default - "RUF100", # unused-noqa - "SIM102", # collapsible-if - "SIM108", # if-else-block-instead-of-if-exp - "SIM117", # multiple-with-statements - "TID252", # relative-imports - "TRY003", # raise-vanilla-args - "TRY300", # try-consider-else - "TRY400", # error-instead-of-exception - "UP031", # printf-string-formatting - "UP038", # non-pep604-isinstance -] - -[lint.per-file-ignores] -"**/__init__.py" = [ - "F401", # unused-import -] - -[lint.isort] -# https://www.odoo.com/documentation/master/contributing/development/coding_guidelines.html#imports -section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] -known-first-party = ["odoo"] -known-local-folder = ["odoo.addons"] From 7c86e25eac517313dee0e45b823890fa745136da Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Thu, 22 Jan 2026 17:24:01 +0100 Subject: [PATCH 09/18] [IMP] estate: add computed fields and onchanges Adds computed fields such as the `total area`. The `validity` can also be updated automatically when the `total_area` is explicitly set. Adds `onchange` listeners to update the offer form's fields `Garden Area` and `Garden Orientation` automatically when the `Garden` field is set. When the latter field is set, the `Garden Area` is given a default value of 10, and the `Garden Orientation` is set to North. --- estate/models/__init__.py | 10 ++++----- estate/models/estate_property.py | 31 +++++++++++++++++++++----- estate/models/estate_property_offer.py | 23 +++++++++++++++++-- estate/views/estate_property_views.xml | 4 ++++ 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 3683ff97b61..8f2187ee09e 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,6 +1,4 @@ -from . import ( - estate_property, - estate_property_offer, - estate_property_tag, - estate_property_type, -) +from . import estate_property +from . import estate_property_offer +from . import estate_property_tag +from . import estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 67bdf63a3c3..90879255d3d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -2,8 +2,9 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models +DEFAULT_GARDEN_AREA = 10 class EstateProperty(models.Model): _name = "estate.property" @@ -26,7 +27,6 @@ class EstateProperty(models.Model): garden_orientation = fields.Selection( string='Garden Orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], - default='north', ) state = fields.Selection( string='State', @@ -35,13 +35,34 @@ class EstateProperty(models.Model): copy=False, default='new', ) + total_area = fields.Integer(compute='_compute_total_area', string='Total Area (sqm)') + best_price = fields.Float(compute='_compute_best_price', string='Best Price') - # res.users: the users of the system. Users can be 'internal', i.e. they have access to the Odoo backend. Or they can be - # 'portal', i.e. they cannot access the backend, only the frontend (e.g. to access their previous orders in eCommerce). sales_person_id = fields.Many2one('res.users', string='Salesman', index=True, default=lambda self: self.env.user) - # res.partner: a partner is a physical or legal entity. It can be a company, an individual or even a contact address. buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) tag_ids = fields.Many2many('estate.property.tag', string="Tags") offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + + @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.0 + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = DEFAULT_GARDEN_AREA + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 647e3edc004..1b5de486b15 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,8 @@ -from odoo import fields, models +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import fields, models, api class EstateProperOffer(models.Model): @@ -12,5 +16,20 @@ class EstateProperOffer(models.Model): copy=False, ) partner_id = fields.Many2one('res.partner', required=True) - # Because a One2many is a virtual relationship, there must be a Many2one field defined in the comodel. property_id = fields.Many2one('estate.property', required=True) + + validity = fields.Integer('Validity (days)', default=7) + date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string='Deadline') + + + @api.depends('create_date', 'validity') + def _compute_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = record.create_date + relativedelta(days=+record.validity) + else: + record.date_deadline = date.today() + relativedelta(days=+record.validity) + + def _inverse_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d03eeac702e..70396e46387 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -90,6 +90,8 @@ + +
@@ -112,6 +114,7 @@ + @@ -127,6 +130,7 @@ + From d82e9b7b3b01ea5ebd7fc1e3c9840f181c6f595a Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Fri, 23 Jan 2026 08:57:08 +0100 Subject: [PATCH 10/18] [LINT] estate: add missing line before class definition --- estate/models/estate_property.py | 1 + 1 file changed, 1 insertion(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 90879255d3d..3c553042de2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -6,6 +6,7 @@ DEFAULT_GARDEN_AREA = 10 + class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property." From 6754e8f54d351bd13d93bb19835f19e100536fd1 Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Fri, 23 Jan 2026 09:00:17 +0100 Subject: [PATCH 11/18] [LINT] estate: remove unnecessary blank line --- estate/models/estate_property_offer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 1b5de486b15..21512957643 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -21,7 +21,6 @@ class EstateProperOffer(models.Model): validity = fields.Integer('Validity (days)', default=7) date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string='Deadline') - @api.depends('create_date', 'validity') def _compute_deadline(self): for record in self: From 0bda7e7833453c80121f067c4c27fae1e1eb25f4 Mon Sep 17 00:00:00 2001 From: "Haroune Hassine (Hahas)" Date: Fri, 23 Jan 2026 11:09:18 +0100 Subject: [PATCH 12/18] [IMP] estate: add actions to the property form Adds `accept` and `cancel` buttons to the property form, and links them to the correct python methods (actions). When an offer is accepted, the property's buyer info, as well as the selling price are automatically updated. --- estate/models/estate_property.py | 38 ++++++++++++++++++++++++-- estate/models/estate_property_offer.py | 16 ++++++++++- estate/views/estate_property_views.xml | 8 +++++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3c553042de2..d5ccc222c38 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -3,6 +3,8 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.translate import _ DEFAULT_GARDEN_AREA = 10 @@ -18,7 +20,7 @@ class EstateProperty(models.Model): postcode = fields.Char('Postcode') date_availability = fields.Date('Availability Date', copy=False, default=date.today() + relativedelta(months=+3)) expected_price = fields.Float('Expected Price', required=True) - selling_price = fields.Float('Selling Price', readonly=True, copy=False) + selling_price = fields.Float('Selling Price', readonly=True, copy=False, compute='_compute_selling_price') bedrooms = fields.Integer('Bedrooms', default=2) living_area = fields.Integer('Living Area (sqm)') facades = fields.Integer('Facades') @@ -40,7 +42,7 @@ class EstateProperty(models.Model): best_price = fields.Float(compute='_compute_best_price', string='Best Price') sales_person_id = fields.Many2one('res.users', string='Salesman', index=True, default=lambda self: self.env.user) - buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False, readonly=True, compute='_compute_buyer') tag_ids = fields.Many2many('estate.property.tag', string="Tags") @@ -59,7 +61,7 @@ def _compute_best_price(self): else: record.best_price = 0.0 - @api.onchange("garden") + @api.onchange('garden') def _onchange_garden(self): if self.garden: self.garden_area = DEFAULT_GARDEN_AREA @@ -67,3 +69,33 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = None + + @api.depends('offer_ids.status') + def _compute_buyer(self): + for record in self: + for offer in record.offer_ids: + if offer.status == 'accepted': + record.buyer_id = offer.partner_id + return + record.buyer_id = None + + @api.depends('offer_ids.status') + def _compute_selling_price(self): + for record in self: + for offer in record.offer_ids: + if offer.status == 'accepted': + record.selling_price = offer.price + return + record.selling_price = None + + def action_cancel_property(self): + if self.state == 'sold': + raise UserError(_('Sold properties cannot be canceled.')) + self.state = 'cancelled' + return True + + def action_sold_property(self): + if self.state == 'cancelled': + raise UserError(_('Cancelled properties cannot be sold.')) + self.state = 'sold' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 21512957643..45714a8848c 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -2,7 +2,9 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models, api +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.translate import _ class EstateProperOffer(models.Model): @@ -32,3 +34,15 @@ def _compute_deadline(self): def _inverse_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days + + def action_accept(self): + if self.status != 'accepted': + for offer in self.property_id.offer_ids: + if offer.status == 'accepted': + raise UserError(_('Only one offer can be accepted.')) + self.status = 'accepted' + return True + + def action_refuse(self): + self.status = 'refused' + return True diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 70396e46387..f5fd2712cd2 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -88,10 +88,12 @@ - + + +

+ + + + + + + + + + + + + + + +
+ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index bc72c2440ab..a11232b672f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,29 +5,20 @@ Properties estate.property list,form - - - - Property Types - estate.property.type - list,form - - - - Property Tags - estate.property.tag - list,form + + {'search_default_available': True} estate.property.list estate.property - + - + @@ -51,7 +42,7 @@ - + @@ -62,59 +53,25 @@ - - estate.type.property.list - estate.property.type - - - - - - - - - estate.tag.property.list - estate.property.tag - - - - - - - - - estate.offer.property.list - estate.property.offer - - - - - - -