From 6152077482572a8f174cb7fa56ebd880a739f7b2 Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Wed, 22 Jul 2026 16:12:00 +0200 Subject: [PATCH 01/15] [IMP] estate: Initial setup with models, views, and access rights --- estate/__init__.py | 1 + estate/__manifest__.py | 15 +++++++++++++++ estate/models/__init__.py | 1 + estate/models/estate.py | 20 ++++++++++++++++++++ estate/security/ir.model.access.csv | 2 ++ estate/views/estate_menus.xml | 7 +++++++ estate/views/estate_property_views.xml | 7 +++++++ 7 files changed, 53 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.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..3820b34f0c1 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': "Real Estate", + 'version': '1.0.0', + 'depends': ['base'], + 'author': "Amr (amgom)", + 'category': 'Real Estate', + 'description': """ + This is a real estate management module that allows users to manage properties, agents, and clients. + """, + 'application': True, + 'data': ['security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' + ], +} \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..912e8d6a61f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate \ No newline at end of file diff --git a/estate/models/estate.py b/estate/models/estate.py new file mode 100644 index 00000000000..bd40c630c68 --- /dev/null +++ b/estate/models/estate.py @@ -0,0 +1,20 @@ +from odoo import api, fields, models + +class Estate(models.Model): + _name = 'estate_property' + _description = 'Real Estate' + + name = fields.Char('Name', required=True) + description = fields.Text('Description') + postcode = fields.Char('Postcode') + date_availability = fields.Date("Date Available") + 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("Garage") + garden = fields.Boolean("Garden") + garden_area = fields.Integer("Garden Area") + garden_orientation = fields.Selection(string="Garden Orientation", + selection=[('north', 'North'), ('east', 'East'), ('west', 'West'), ('south', 'South')]) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..d61e9105329 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..ddf873cbd42 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..ce63c087cd3 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,7 @@ + + + Estate Properties + estate_property + list,form + + \ No newline at end of file From 5bd12fdf0d23588356a02f2d2838d7178a3cfbf9 Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Wed, 22 Jul 2026 17:28:48 +0200 Subject: [PATCH 02/15] [IMP] estate: Finish chapter 5 --- estate/__init__.py | 2 +- estate/__manifest__.py | 2 +- estate/models/__init__.py | 2 +- estate/models/estate.py | 14 ++++++++++---- 4 files changed, 13 insertions(+), 7 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/__manifest__.py b/estate/__manifest__.py index 3820b34f0c1..16da7c3252f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,4 +12,4 @@ 'views/estate_property_views.xml', 'views/estate_menus.xml' ], -} \ No newline at end of file +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 912e8d6a61f..e4f59229d23 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate \ No newline at end of file +from . import estate diff --git a/estate/models/estate.py b/estate/models/estate.py index bd40c630c68..dea9c600005 100644 --- a/estate/models/estate.py +++ b/estate/models/estate.py @@ -1,4 +1,6 @@ -from odoo import api, fields, models +from odoo import fields, models +from dateutil.relativedelta import relativedelta + class Estate(models.Model): _name = 'estate_property' @@ -7,10 +9,10 @@ class Estate(models.Model): name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Postcode') - date_availability = fields.Date("Date Available") + date_availability = fields.Date("Date Available", default=fields.Date.today() + relativedelta(months=3), copy=False) 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") garage = fields.Boolean("Garage") @@ -18,3 +20,7 @@ class Estate(models.Model): garden_area = fields.Integer("Garden Area") garden_orientation = fields.Selection(string="Garden Orientation", selection=[('north', 'North'), ('east', 'East'), ('west', 'West'), ('south', 'South')]) + active = fields.Boolean("Active", default=True) + state = fields.Selection(string="State", selection=[('new', 'New'), ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('canceled', 'Canceled')], + required=True, copy=False, default='new') \ No newline at end of file From 8a2518a0f107f836664677393ee58507a46468bf Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Thu, 23 Jul 2026 10:00:02 +0200 Subject: [PATCH 03/15] [IMP] add ruff linter and formatter --- .vscode/settings.json | 3 ++ estate/__manifest__.py | 23 +++++------ estate/models/estate.py | 47 ++++++++++++++++------- ruff.toml | 84 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 ruff.toml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..e12938ac80f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "Odoo.selectedProfile": "default" +} \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 16da7c3252f..85b2f4a1a9c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,15 +1,16 @@ { - 'name': "Real Estate", - 'version': '1.0.0', - 'depends': ['base'], - 'author': "Amr (amgom)", - 'category': 'Real Estate', - 'description': """ + "name": "Real Estate", + "version": "1.0.0", + "depends": ["base"], + "author": "Amr (amgom)", + "category": "Real Estate", + "description": """ This is a real estate management module that allows users to manage properties, agents, and clients. """, - 'application': True, - 'data': ['security/ir.model.access.csv', - 'views/estate_property_views.xml', - 'views/estate_menus.xml' - ], + "application": True, + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_menus.xml", + ], } diff --git a/estate/models/estate.py b/estate/models/estate.py index dea9c600005..b2d059b1b34 100644 --- a/estate/models/estate.py +++ b/estate/models/estate.py @@ -3,24 +3,45 @@ class Estate(models.Model): - _name = 'estate_property' - _description = 'Real Estate' + _name = "estate_property" + _description = "Real Estate" - name = fields.Char('Name', required=True) - description = fields.Text('Description') - postcode = fields.Char('Postcode') - date_availability = fields.Date("Date Available", default=fields.Date.today() + relativedelta(months=3), copy=False) - expected_price = fields.Float('Expected Price', required=True) - selling_price = fields.Float('Selling price', readonly=True, copy=False) + name = fields.Char("Name", required=True) + description = fields.Text("Description") + postcode = fields.Char("Postcode") + date_availability = fields.Date( + "Date Available", + default=fields.Date.today() + relativedelta(months=3), + copy=False, + ) + 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("Garage") garden = fields.Boolean("Garden") garden_area = fields.Integer("Garden Area") - garden_orientation = fields.Selection(string="Garden Orientation", - selection=[('north', 'North'), ('east', 'East'), ('west', 'West'), ('south', 'South')]) + garden_orientation = fields.Selection( + string="Garden Orientation", + selection=[ + ("north", "North"), + ("east", "East"), + ("west", "West"), + ("south", "South"), + ], + ) active = fields.Boolean("Active", default=True) - state = fields.Selection(string="State", selection=[('new', 'New'), ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('canceled', 'Canceled')], - required=True, copy=False, default='new') \ No newline at end of file + state = fields.Selection( + string="State", + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), + ], + required=True, + copy=False, + default="new", + ) diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000000..26df8ffc370 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,84 @@ +# automatically generated file by the runbot nightly ruff checks, do not modify +# for ruff version 0.15.0 (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", "RUF102", "TRY002", "UP006", "UP007"] + + +target-version = "py310" + +[lint] +preview = true +external = ["OLS"] +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 + "RUF067", # non-empty-init-module + "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 +] + +[lint.per-file-ignores] +"**/__init__.py" = [ + "F401", # unused-import +] + +[lint.isort] +# https://www.odoo.com/documentation/latest/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 8242bb92d32df944435273ea4b6b9a9ca384cb13 Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Thu, 23 Jul 2026 11:59:23 +0200 Subject: [PATCH 04/15] [IMP] estate: finish chapter 6 --- estate/__manifest__.py | 1 + estate/views/estate_menus.xml | 7 +++ estate/views/estate_views.xml | 81 +++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 estate/views/estate_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 85b2f4a1a9c..9ba653f9d75 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,5 +12,6 @@ "security/ir.model.access.csv", "views/estate_property_views.xml", "views/estate_menus.xml", + "views/estate_views.xml", ], } diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index ddf873cbd42..8823ea61abd 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -2,6 +2,13 @@ + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_views.xml b/estate/views/estate_views.xml new file mode 100644 index 00000000000..036dc360de5 --- /dev/null +++ b/estate/views/estate_views.xml @@ -0,0 +1,81 @@ + + + estate.property.list + estate_property + + + + + + + + + + + + + estate.property.form + estate_property + form + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + estate.property.search + estate_property + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file From 9e501f53d4832b9748866595d44de62afb2bf2b9 Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Thu, 23 Jul 2026 17:12:47 +0200 Subject: [PATCH 05/15] [IMP] estate: Finish chapter 7 --- estate/models/__init__.py | 2 +- estate/models/estate.py | 45 ++++++++++++------ estate/models/offer.py | 19 ++++++++ estate/models/property_tag.py | 8 ++++ estate/models/property_type.py | 8 ++++ estate/security/ir.model.access.csv | 3 ++ estate/views/estate_menus.xml | 13 ++--- estate/views/estate_property_views.xml | 14 +++++- estate/views/estate_views.xml | 66 +++++++++++++++++--------- 9 files changed, 130 insertions(+), 48 deletions(-) create mode 100644 estate/models/offer.py create mode 100644 estate/models/property_tag.py create mode 100644 estate/models/property_type.py diff --git a/estate/models/__init__.py b/estate/models/__init__.py index e4f59229d23..5a3ae53ae4f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate +from . import estate, offer, property_tag, property_type diff --git a/estate/models/estate.py b/estate/models/estate.py index b2d059b1b34..e1183ceebfb 100644 --- a/estate/models/estate.py +++ b/estate/models/estate.py @@ -1,27 +1,28 @@ -from odoo import fields, models from dateutil.relativedelta import relativedelta +from odoo import fields, models + class Estate(models.Model): _name = "estate_property" _description = "Real Estate" - name = fields.Char("Name", required=True) - description = fields.Text("Description") - postcode = fields.Char("Postcode") + name = fields.Char(string="Name", required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") date_availability = fields.Date( - "Date Available", + string="Available From", default=fields.Date.today() + relativedelta(months=3), copy=False, ) - 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("Garage") - garden = fields.Boolean("Garden") - garden_area = fields.Integer("Garden Area") + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling price", readonly=True, copy=False) + bedrooms = fields.Integer(string="Bedrooms", default=2) + living_area = fields.Integer(string="Living Area") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area") garden_orientation = fields.Selection( string="Garden Orientation", selection=[ @@ -31,7 +32,7 @@ class Estate(models.Model): ("south", "South"), ], ) - active = fields.Boolean("Active", default=True) + active = fields.Boolean(string="Active", default=True) state = fields.Selection( string="State", selection=[ @@ -45,3 +46,19 @@ class Estate(models.Model): copy=False, default="new", ) + type_id = fields.Many2one( + string="Property Type", + comodel_name="estate_property_type", + ) + salesman_id = fields.Many2one( + string="Salesman", + comodel_name="res.users", + default=lambda self: self.env.user, + ) + buyer_id = fields.Many2one(string="Buyer", comodel_name="res.partner", copy=False) + tag_ids = fields.Many2many(string="Tags", comodel_name="estate_property_tag") + offer_ids = fields.One2many( + string="Offers", + comodel_name="estate_property_offer", + inverse_name="property_id", + ) diff --git a/estate/models/offer.py b/estate/models/offer.py new file mode 100644 index 00000000000..3eb4ee7c4d6 --- /dev/null +++ b/estate/models/offer.py @@ -0,0 +1,19 @@ +from odoo import fields, models + + +class Offer(models.Model): + _name = "estate_property_offer" + _description = "Offer made to some estate (property)" + + price = fields.Float(string="Price") + status = fields.Selection( + string="Status", + selection=[("accepted", "Accepted"), ("refused", "Refused")], + copy=False, + ) + property_id = fields.Many2one( + string="Property", comodel_name="estate_property", required=True + ) + partner_id = fields.Many2one( + string="Partner", comodel_name="res.partner", required=True + ) diff --git a/estate/models/property_tag.py b/estate/models/property_tag.py new file mode 100644 index 00000000000..eee8acde474 --- /dev/null +++ b/estate/models/property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = "estate_property_tag" + _description = "A list of tags that categorize the properities" + + name = fields.Char(string="Name", required=True) diff --git a/estate/models/property_type.py b/estate/models/property_type.py new file mode 100644 index 00000000000..b0fe27e46c8 --- /dev/null +++ b/estate/models/property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyType(models.Model): + _name = "estate_property_type" + _description = "The type of the property to be sold such as House, apartment, ..." + + name = fields.Char(string="Name", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index d61e9105329..cf8c677a39a 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" access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 8823ea61abd..a43bd806b73 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,14 +1,9 @@ - - - - - - - - - + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ce63c087cd3..5960c177e96 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,19 @@ - Estate Properties + Properties estate_property list,form + + + Property Types + estate_property_type + list,form + + + + Property Tags + estate_property_tag + list,form + \ No newline at end of file diff --git a/estate/views/estate_views.xml b/estate/views/estate_views.xml index 036dc360de5..77b9aec0417 100644 --- a/estate/views/estate_views.xml +++ b/estate/views/estate_views.xml @@ -4,11 +4,13 @@ estate_property - - - - - + + + + + + + @@ -21,34 +23,52 @@
- +

- - - - - - - - - - +

+
+ + + - + + - + + - + + + + + + + + + + + - - + + + + + + + + + + - - + + + + +
From b27f61215f2c029150f08964a214af1014ac169d Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Fri, 24 Jul 2026 10:47:57 +0200 Subject: [PATCH 06/15] [IMP] estate: Finish chapter 8 --- estate/__manifest__.py | 2 +- estate/models/estate.py | 20 +++++++++++- estate/models/offer.py | 29 +++++++++++++++-- ...te_menus.xml => estate_property_menus.xml} | 2 +- estate/views/estate_property_views.xml | 2 +- estate/views/estate_views.xml | 31 +++++++++++++++++-- 6 files changed, 76 insertions(+), 10 deletions(-) rename estate/views/{estate_menus.xml => estate_property_menus.xml} (92%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9ba653f9d75..6e687e8b926 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,7 +11,7 @@ "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", - "views/estate_menus.xml", + "views/estate_property_menus.xml", "views/estate_views.xml", ], } diff --git a/estate/models/estate.py b/estate/models/estate.py index e1183ceebfb..768e1882239 100644 --- a/estate/models/estate.py +++ b/estate/models/estate.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class Estate(models.Model): @@ -62,3 +62,21 @@ class Estate(models.Model): comodel_name="estate_property_offer", inverse_name="property_id", ) + total_area = fields.Float(string="Total Area", compute="_compute_total_area") + best_offer = fields.Float(string="Best Offer", compute="_compute_best_offer") + + @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_offer(self): + self.best_offer = ( + max(self.offer_ids.mapped("price")) if len(self.offer_ids) else 0.0 + ) + + @api.onchange("garden") + def _on_change_garden(self): + self.garden_area = 10 if self.garden else 0 + self.garden_orientation = "north" if self.garden else None diff --git a/estate/models/offer.py b/estate/models/offer.py index 3eb4ee7c4d6..a215feeda70 100644 --- a/estate/models/offer.py +++ b/estate/models/offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models class Offer(models.Model): @@ -12,8 +14,29 @@ class Offer(models.Model): copy=False, ) property_id = fields.Many2one( - string="Property", comodel_name="estate_property", required=True + string="Property", + comodel_name="estate_property", + required=True, ) partner_id = fields.Many2one( - string="Partner", comodel_name="res.partner", required=True + string="Partner", + comodel_name="res.partner", + required=True, + ) + validity = fields.Integer(string="Validity", default=7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_deadline", + inverse="_inverse_deadline", ) + + @api.depends("validity") + def _compute_deadline(self): + for offer in self: + curr_date = offer.create_date if offer.create_date else fields.Date.today() + offer.date_deadline = curr_date + relativedelta(days=offer.validity) + + def _inverse_deadline(self): + for offer in self: + date_diff = offer.date_deadline - (offer.create_date.date()) + offer.validity = date_diff.days diff --git a/estate/views/estate_menus.xml b/estate/views/estate_property_menus.xml similarity index 92% rename from estate/views/estate_menus.xml rename to estate/views/estate_property_menus.xml index a43bd806b73..6675224ebae 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -1,6 +1,6 @@ - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5960c177e96..1afb0b4a1f9 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,5 @@ - + Properties estate_property list,form diff --git a/estate/views/estate_views.xml b/estate/views/estate_views.xml index 77b9aec0417..7a0ceb7817e 100644 --- a/estate/views/estate_views.xml +++ b/estate/views/estate_views.xml @@ -20,7 +20,7 @@ estate_property form - +

@@ -31,11 +31,13 @@ + + @@ -50,7 +52,7 @@ - + @@ -58,8 +60,10 @@ - + + + @@ -76,6 +80,27 @@ + + estate.property.offer.form + estate_property_offer + form + + + + + + + + + + + + + + + + + estate.property.search estate_property From 7a5cd9b0181e2cd159c8bb9dd3fc2deeb9b68baf Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Fri, 24 Jul 2026 10:58:21 +0200 Subject: [PATCH 07/15] [REF] estate: reorganize views files --- estate/__manifest__.py | 7 +- estate/views/estate_property_menus.xml | 1 + estate/views/estate_property_offer_views.xml | 23 ++++ estate/views/estate_property_tag_views.xml | 8 ++ estate/views/estate_property_type_views.xml | 8 ++ estate/views/estate_property_views.xml | 111 ++++++++++++++-- estate/views/estate_views.xml | 126 ------------------- 7 files changed, 147 insertions(+), 137 deletions(-) create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml delete mode 100644 estate/views/estate_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6e687e8b926..b29121a3332 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,8 +10,11 @@ "application": True, "data": [ "security/ir.model.access.csv", - "views/estate_property_views.xml", + "views/estate_property_menus.xml", - "views/estate_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_views.xml", ], } diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 6675224ebae..b50193048e3 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -1,3 +1,4 @@ + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..b160eef6c1c --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,23 @@ + + + + estate.property.offer.form + estate_property_offer + form + +
+ + + + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..54a2e775ef9 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate_property_tag + list,form + + \ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..df6a52459a9 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate_property_type + list,form + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1afb0b4a1f9..dfb7c57e611 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,19 +1,112 @@ + Properties estate_property list,form - - - Property Types - estate_property_type - list,form + + + estate.property.list + estate_property + + + + + + + + + + + - - Property Tags - estate_property_tag - list,form + + estate.property.form + estate_property + form + +
+ + +

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate_property + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_views.xml b/estate/views/estate_views.xml deleted file mode 100644 index 7a0ceb7817e..00000000000 --- a/estate/views/estate_views.xml +++ /dev/null @@ -1,126 +0,0 @@ - - - estate.property.list - estate_property - - - - - - - - - - - - - - - estate.property.form - estate_property - form - -
- - -

- -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - - estate.property.offer.form - estate_property_offer - form - -
- - - - - - - - - - - -
-
-
- - - estate.property.search - estate_property - - - - - - - - - - - - - - - - - - - -
\ No newline at end of file From 1bb53b342a3bccd51c55df345e52b5081d9d5ac4 Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Fri, 24 Jul 2026 13:31:25 +0200 Subject: [PATCH 08/15] [IMP] estate: Finish chapter 9 --- estate/__manifest__.py | 1 - estate/models/estate.py | 21 +++++++++++++++++++-- estate/models/offer.py | 16 ++++++++++++++++ estate/views/estate_property_views.xml | 7 +++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b29121a3332..c7a6c552933 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,7 +10,6 @@ "application": True, "data": [ "security/ir.model.access.csv", - "views/estate_property_menus.xml", "views/estate_property_offer_views.xml", "views/estate_property_tag_views.xml", diff --git a/estate/models/estate.py b/estate/models/estate.py index 768e1882239..ee683820e25 100644 --- a/estate/models/estate.py +++ b/estate/models/estate.py @@ -1,6 +1,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError class Estate(models.Model): @@ -34,13 +35,13 @@ class Estate(models.Model): ) active = fields.Boolean(string="Active", default=True) state = fields.Selection( - string="State", + string="Status", selection=[ ("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), - ("canceled", "Canceled"), + ("cancelled", "Cancelled"), ], required=True, copy=False, @@ -80,3 +81,19 @@ def _compute_best_offer(self): def _on_change_garden(self): self.garden_area = 10 if self.garden else 0 self.garden_orientation = "north" if self.garden else None + + def action_sell_property(self): + for property in self: + if property.state == "cancelled": + raise UserError("Cancelled properties cannot be sold") + property.state = "sold" + + return True + + def action_cancel_property(self): + for property in self: + if property.state == "sold": + raise UserError("Sold properties cannot be cancelled") + property.state = "cancelled" + + return True diff --git a/estate/models/offer.py b/estate/models/offer.py index a215feeda70..372285fae04 100644 --- a/estate/models/offer.py +++ b/estate/models/offer.py @@ -1,6 +1,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError class Offer(models.Model): @@ -40,3 +41,18 @@ def _inverse_deadline(self): for offer in self: date_diff = offer.date_deadline - (offer.create_date.date()) offer.validity = date_diff.days + + def action_accept_offer(self): + # check if there is no other accepted offers + if "accepted" in self.property_id.offer_ids.mapped("status"): + raise UserError("Only one offer can be accepted for a given property!") + + # update status, selling price, buyer + self.status = "accepted" + self.property_id.selling_price = self.price + self.property_id.buyer_id = self.partner_id + + def action_refuse_offer(self): + self.status = "refused" + self.property_id.selling_price = 0 + self.property_id.buyer_id = None diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index dfb7c57e611..382b0790535 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -28,6 +28,10 @@ form
+
+

@@ -38,6 +42,7 @@ + @@ -70,6 +75,8 @@ + + + +

+ +

+
+ + + + + + + + + + + + + + + + + +
+ \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 382b0790535..77d2102839a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,20 +4,26 @@ Properties estate_property list,form + {'search_default_available': True} estate.property.list estate_property - - - - - - - - + + + + + + + + + @@ -29,8 +35,9 @@
-
@@ -38,12 +45,11 @@

- + - - + @@ -62,22 +68,21 @@ - - + + - - + + - diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d88e77561b5..4823d9e8da2 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,14 +2,14 @@ Properties - estate_property + estate.estate list,form,kanban {'search_default_available': True} estate.property.view.kanban - estate_property + estate.estate @@ -36,16 +36,16 @@ - - estate.property.list - estate_property + + estate.property.view.list + estate.estate - + @@ -57,9 +57,9 @@ - - estate.property.form - estate_property + + estate.property.view.form + estate.estate form @@ -128,9 +128,9 @@ - - estate.property.search - estate_property + + estate.property.view.search + estate.estate diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml index 38019fd95ef..11db622f9aa 100644 --- a/estate/views/res_users_views.xml +++ b/estate/views/res_users_views.xml @@ -1,8 +1,8 @@ - - res.users.inherit.form + + res.users.view.form.inherit.add.properities res.users diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 2cca12c1cbb..67913097232 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -2,7 +2,7 @@ "name": "Estate Account Link", "version": "1.0.0", "depends": ["estate", "account"], - "author": "Amr (amgom)", + "author": "Odoo S.A.", "license": "LGPL-3", "category": "Real Estate", "summary": "Link module between Real Estate and Invoicing", diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py index 5e1963c9d2f..e4f59229d23 100644 --- a/estate_account/models/__init__.py +++ b/estate_account/models/__init__.py @@ -1 +1 @@ -from . import estate_property +from . import estate diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate.py similarity index 81% rename from estate_account/models/estate_property.py rename to estate_account/models/estate.py index 4bf1fa27e1e..929893956ee 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate.py @@ -3,7 +3,7 @@ class Estate(models.Model): - _inherit = "estate_property" + _inherit = "estate.estate" def action_sell_property(self): res = super().action_sell_property() @@ -11,7 +11,9 @@ def action_sell_property(self): journal = self.env["account.journal"].search([("type", "=", "sale")], limit=1) if not journal: raise UserError( - "Please configure an accounting sales journal before selling a property." + self.env._( + "Please configure an accounting sales journal before selling a property." + ), ) for property in self: @@ -21,7 +23,9 @@ def action_sell_property(self): or not property.selling_price ): raise UserError( - "To sell a property, it must be in 'sold' state, have a buyer and a selling price.", + self.env._( + "To sell a property, it must be in 'sold' state, have a buyer and a selling price." + ), ) commission_line = ( diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 26df8ffc370..00000000000 --- a/ruff.toml +++ /dev/null @@ -1,84 +0,0 @@ -# automatically generated file by the runbot nightly ruff checks, do not modify -# for ruff version 0.15.0 (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", "RUF102", "TRY002", "UP006", "UP007"] - - -target-version = "py310" - -[lint] -preview = true -external = ["OLS"] -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 - "RUF067", # non-empty-init-module - "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 -] - -[lint.per-file-ignores] -"**/__init__.py" = [ - "F401", # unused-import -] - -[lint.isort] -# https://www.odoo.com/documentation/latest/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 a9b3564df2731d4a4542de13b63fcb9c51e399c3 Mon Sep 17 00:00:00 2001 From: amgom-odoo Date: Fri, 31 Jul 2026 13:32:29 +0200 Subject: [PATCH 15/15] [IMP] estate: address comments --- .gitignore | 3 --- estate/models/estate.py | 4 ++-- estate/models/estate_offer.py | 4 ++-- estate/views/estate_property_menus.xml | 2 +- estate/views/estate_property_offer_views.xml | 2 +- estate/views/estate_property_tag_views.xml | 2 +- estate/views/estate_property_type_views.xml | 4 ++-- estate/views/estate_property_views.xml | 2 +- estate/views/res_users_views.xml | 2 +- estate_account/models/estate.py | 4 ++-- 10 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index c9b9dbc788f..b6e47617de1 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,3 @@ dmypy.json # Pyre type checker .pyre/ - -# Ruff -ruff.toml \ No newline at end of file diff --git a/estate/models/estate.py b/estate/models/estate.py index a8bfd5d81e9..e5fc321cf23 100644 --- a/estate/models/estate.py +++ b/estate/models/estate.py @@ -117,7 +117,7 @@ def _check_selling_price_limit(self): ): raise ValidationError( self.env._( - "The selling price should not be less than 90% of the expected price of the property" + "The selling price should not be less than 90% of the expected price of the property", ), ) @@ -126,5 +126,5 @@ def _unlink_if_new_or_cancelled(self): for state in self.mapped("state"): if state not in ["new", "cancelled"]: raise UserError( - self.env._("New or cancelled estates only can be deleted") + self.env._("New or cancelled estates only can be deleted"), ) diff --git a/estate/models/estate_offer.py b/estate/models/estate_offer.py index cd92b71970a..0a9eee99cf6 100644 --- a/estate/models/estate_offer.py +++ b/estate/models/estate_offer.py @@ -58,7 +58,7 @@ def action_accept_offer(self): # check if there is no other accepted offers if "accepted" in self.property_id.offer_ids.mapped("status"): raise UserError( - self.env._("Only one offer can be accepted for a given property!") + self.env._("Only one offer can be accepted for a given property!"), ) # update status, selling price, buyer @@ -79,7 +79,7 @@ def create(self, vals_list): if record["price"] < max_offer_price: raise UserError( self.env._( - "Cannot create an offer with price lower than an existing offer" + "Cannot create an offer with price lower than an existing offer", ), ) diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 8b026b857b7..ec7c08d31b1 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -7,4 +7,4 @@
-
\ No newline at end of file + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 44c0bfef83c..20b04d5d969 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -35,4 +35,4 @@
- \ No newline at end of file + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index c0672a925f1..742da5f270e 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -15,4 +15,4 @@
- \ No newline at end of file + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index e6868babfdc..5cb82c81562 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -10,7 +10,7 @@ Offers estate.offer list,form - [('property_id.type_id', '=', active_id)] + [('property_type_id', '=', active_id)] @@ -63,4 +63,4 @@
- \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4823d9e8da2..481bc696910 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -147,4 +147,4 @@
- \ No newline at end of file + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml index 11db622f9aa..8c82e10f103 100644 --- a/estate/views/res_users_views.xml +++ b/estate/views/res_users_views.xml @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/estate_account/models/estate.py b/estate_account/models/estate.py index 929893956ee..43f1c5b0a1e 100644 --- a/estate_account/models/estate.py +++ b/estate_account/models/estate.py @@ -12,7 +12,7 @@ def action_sell_property(self): if not journal: raise UserError( self.env._( - "Please configure an accounting sales journal before selling a property." + "Please configure an accounting sales journal before selling a property.", ), ) @@ -24,7 +24,7 @@ def action_sell_property(self): ): raise UserError( self.env._( - "To sell a property, it must be in 'sold' state, have a buyer and a selling price." + "To sell a property, it must be in 'sold' state, have a buyer and a selling price.", ), )