From 9658532b64f0d26faeaf53bf9ad8686719627e1c Mon Sep 17 00:00:00 2001 From: Astremy <43485551+Astremy@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:14:00 +0100 Subject: [PATCH 01/11] [ADD] Real Estate: Creation of the module definition --- estate/__init__.py | 0 estate/__manifest__.py | 25 +++++++++++++++++++++++++ 2 files changed, 25 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..9f9659f45eb --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Real Estate", + + 'summary': """ + Server framework 101: A New Application" + """, + + 'description': """ + Starting module for "Server framework 101: A New Application" + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base'], + + 'data': [], + 'assets': { + }, + 'license': 'LGPL-3' +} From 64e5ae64bdb731994f18d7f032022ed7dbf0fca4 Mon Sep 17 00:00:00 2001 From: "Astremy (luleg)" <43485551+Astremy@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:21:29 +0100 Subject: [PATCH 02/11] [IMP] Real Estate: Add basic property model --- estate/__init__.py | 1 + estate/models/__init__.py | 2 ++ estate/models/estate_property.py | 42 ++++++++++++++++++++++++++++++++ estate/models/estate_tag.py | 40 ++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/models/estate_tag.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..f835427dd99 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,2 @@ +from . import estate_tag +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..cd575efe598 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class Property(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + _order = "expecting_price, selling_price, sequence, id" + + name = fields.Char("Name", required=True, translate=True) + + property_type = fields.Selection( + [ + ("house", "House"), + ("apartment", "Apartment"), + ("land", "Land") + ], + required=True + ) + + currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) + expecting_price = fields.Monetary("Expecting Price", required=True) + selling_price = fields.Monetary("Selling Price", default=0) + + bedroom_number = fields.Integer("Bedrooms", default=0) + area = fields.Integer("Living Area (sqm)", required=True) + + sequence = fields.Integer(default=10) + + tag_ids = fields.Many2many("estate.tag", string="Tags") + + _check_bedroom_number = models.Constraint( + 'CHECK(bedroom_number >= 0)', + 'The number of bedrooms can\'t be negative.', + ) + + _check_area = models.Constraint( + 'CHECK(area >= 0)', + 'The area can\'t be negative.', + ) \ No newline at end of file diff --git a/estate/models/estate_tag.py b/estate/models/estate_tag.py new file mode 100644 index 00000000000..e8fcd7e05ad --- /dev/null +++ b/estate/models/estate_tag.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EstateTagCategory(models.Model): + _name = "estate.tag.category" + _description = "Estate Tag Category" + _order = "sequence" + + def _default_sequence(self): + """ + Here we use a _default method instead of ordering on 'sequence, id' to + prevent adding a new related stored field in the 'event.tag' model that + would hold the category id. + """ + return (self.search([], order="sequence desc", limit=1).sequence or 0) + 1 + + name = fields.Char("Name", required=True, translate=True) + sequence = fields.Integer('Sequence', default=_default_sequence) + tag_ids = fields.Many2many("estate.tag", "category_id", string="Tags") + +class EstateTag(models.Model): + _name = "estate.tag" + _description = "Estate Tag" + _order = "category_sequence, sequence, id" + + def _default_color(self): + return randint(1, 11) + + name = fields.Char("Name", required=True, translate=True) + sequence = fields.Integer("Sequence", default=0) + category_id = fields.Many2one("estate.tag.category", string="Category", required=True, index=True, ondelete='cascade') + category_sequence = fields.Integer(related="category_id.sequence", string='Category Sequence', store=True) + color = fields.Integer( + string='Color Index', default=lambda self: self._default_color(), + help='Tag color. No color means no display in kanban or front-end, to distinguish internal tags from public categorization tags.') + + From 95a55685ea00da401e0b91ec8884cfc1d7fa8dd2 Mon Sep 17 00:00:00 2001 From: "Astremy (luleg)" <43485551+Astremy@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:18:11 +0100 Subject: [PATCH 03/11] [IMP] real estate: Set base data and access rights --- estate/__init__.py | 2 +- estate/__manifest__.py | 7 +++++-- estate/data/estate.property.csv | 3 +++ estate/data/estate.tag.csv | 3 +++ estate/models/__init__.py | 2 +- estate/models/estate_property.py | 5 ++--- estate/models/estate_tag.py | 12 ++++++------ estate/security/ir.model.access.csv | 4 ++++ 8 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 estate/data/estate.property.csv create mode 100644 estate/data/estate.tag.csv create mode 100644 estate/security/ir.model.access.csv 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 9f9659f45eb..5c28bec050e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- { 'name': "Real Estate", @@ -18,7 +17,11 @@ 'installable': True, 'depends': ['base'], - 'data': [], + 'data': [ + "data/estate.tag.csv", + "data/estate.property.csv", + "security/ir.model.access.csv" + ], 'assets': { }, 'license': 'LGPL-3' diff --git a/estate/data/estate.property.csv b/estate/data/estate.property.csv new file mode 100644 index 00000000000..edefbd91839 --- /dev/null +++ b/estate/data/estate.property.csv @@ -0,0 +1,3 @@ +"id","name","expecting_price","area","property_type" +estate_tag_1,"Odoo Farm 2",300000,2000,"house" +estate_tag_2,"Odoo LLN",1000000,40000,"apartment" \ No newline at end of file diff --git a/estate/data/estate.tag.csv b/estate/data/estate.tag.csv new file mode 100644 index 00000000000..2632941d0cb --- /dev/null +++ b/estate/data/estate.tag.csv @@ -0,0 +1,3 @@ +"id","name","color" +estate_property_1,"Leased",5 +estate_property_2,"Empty",8 \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f835427dd99..1a1271f8598 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,2 @@ from . import estate_tag -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 cd575efe598..b5f01db6f1d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models @@ -10,7 +9,7 @@ class Property(models.Model): _order = "expecting_price, selling_price, sequence, id" name = fields.Char("Name", required=True, translate=True) - + property_type = fields.Selection( [ ("house", "House"), @@ -39,4 +38,4 @@ class Property(models.Model): _check_area = models.Constraint( 'CHECK(area >= 0)', 'The area can\'t be negative.', - ) \ No newline at end of file + ) diff --git a/estate/models/estate_tag.py b/estate/models/estate_tag.py index e8fcd7e05ad..9d629fc2827 100644 --- a/estate/models/estate_tag.py +++ b/estate/models/estate_tag.py @@ -1,14 +1,15 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models +from random import randint + class EstateTagCategory(models.Model): _name = "estate.tag.category" _description = "Estate Tag Category" _order = "sequence" - + def _default_sequence(self): """ Here we use a _default method instead of ordering on 'sequence, id' to @@ -21,20 +22,19 @@ def _default_sequence(self): sequence = fields.Integer('Sequence', default=_default_sequence) tag_ids = fields.Many2many("estate.tag", "category_id", string="Tags") + class EstateTag(models.Model): _name = "estate.tag" _description = "Estate Tag" _order = "category_sequence, sequence, id" - + def _default_color(self): return randint(1, 11) name = fields.Char("Name", required=True, translate=True) sequence = fields.Integer("Sequence", default=0) - category_id = fields.Many2one("estate.tag.category", string="Category", required=True, index=True, ondelete='cascade') + category_id = fields.Many2one("estate.tag.category", string="Category", index=True, ondelete='cascade') category_sequence = fields.Integer(related="category_id.sequence", string='Category Sequence', store=True) color = fields.Integer( string='Color Index', default=lambda self: self._default_color(), help='Tag color. No color means no display in kanban or front-end, to distinguish internal tags from public categorization tags.') - - diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..e5d581ea24f --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,4 @@ +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_tag,access_estate_tag,model_estate_tag,base.group_user,1,1,1,1 +access_estate_tag_category,access_estate_tag_category,model_estate_tag_category,base.group_user,1,1,1,1 \ No newline at end of file From 63a9396102d1eceb3868388ed493ab9f59388680 Mon Sep 17 00:00:00 2001 From: "Astremy (luleg)" <43485551+Astremy@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:15:29 +0100 Subject: [PATCH 04/11] [IMP] Real Estate: Add estate application page with list and form view. --- estate/__manifest__.py | 4 +- estate/models/estate_property.py | 12 ++++- estate/static/description/icon.png | Bin 0 -> 12880 bytes estate/views/estate_menu_views.xml | 19 +++++++ estate/views/estate_property_views.xml | 72 +++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 estate/static/description/icon.png create mode 100644 estate/views/estate_menu_views.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 5c28bec050e..1447e04f863 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -20,7 +20,9 @@ 'data': [ "data/estate.tag.csv", "data/estate.property.csv", - "security/ir.model.access.csv" + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_menu_views.xml", ], 'assets': { }, diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b5f01db6f1d..f0cb88f3a96 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -19,9 +19,19 @@ class Property(models.Model): required=True ) + active = fields.Boolean("Active", default=True) + + stage = fields.Selection([ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted","Offer Accepted"), + ("sold", "Sold"), + ("cancelled","Cancelled") + ], default="new") + currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) expecting_price = fields.Monetary("Expecting Price", required=True) - selling_price = fields.Monetary("Selling Price", default=0) + selling_price = fields.Monetary("Selling Price", default=0, readonly=True) bedroom_number = fields.Integer("Bedrooms", default=0) area = fields.Integer("Living Area (sqm)", required=True) diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..12da2f0de3c0395f06b44460dd393d9d0ecf3415 GIT binary patch literal 12880 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 w8Y)OQ*pZkGENK2 + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..f4d1f447b05 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,72 @@ + + + + estate.property.view.search + estate.property + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+ +
+ +
+
+
+

+ +

+
+ + + + + + + + + + + +
+
+
+
+ + + estate.property.view.list + estate.property + + + + + + + + + + + + + + + Properties + estate.property + list,form + +
From 68e374f996e5d7172debde7c036368b3f9ed400d Mon Sep 17 00:00:00 2001 From: "Astremy (luleg)" <43485551+Astremy@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:04:41 +0100 Subject: [PATCH 05/11] [IMP] Real estate: Improvement of the property model, addition of the notebook to the form with more infos. --- estate/data/estate.property.csv | 6 ++--- estate/models/estate_property.py | 34 ++++++++++++++++++++------ estate/views/estate_property_views.xml | 30 +++++++++++++++++------ 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/estate/data/estate.property.csv b/estate/data/estate.property.csv index edefbd91839..2623bfdde4d 100644 --- a/estate/data/estate.property.csv +++ b/estate/data/estate.property.csv @@ -1,3 +1,3 @@ -"id","name","expecting_price","area","property_type" -estate_tag_1,"Odoo Farm 2",300000,2000,"house" -estate_tag_2,"Odoo LLN",1000000,40000,"apartment" \ No newline at end of file +"id","name","expecting_price","living_area","total_area","property_type" +estate_tag_1,"Odoo Farm 2",300000,1900,2000,"house" +estate_tag_2,"Odoo LLN",1000000,0,40000,"apartment" \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f0cb88f3a96..63b5e1de213 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -19,23 +19,36 @@ class Property(models.Model): required=True ) - active = fields.Boolean("Active", default=True) + description = fields.Char("Description") stage = fields.Selection([ ("new", "New"), ("offer_received", "Offer Received"), - ("offer_accepted","Offer Accepted"), + ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), - ("cancelled","Cancelled") + ("cancelled", "Cancelled") ], default="new") currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) expecting_price = fields.Monetary("Expecting Price", required=True) + best_offer = fields.Monetary("Best Offer", default=True) selling_price = fields.Monetary("Selling Price", default=0, readonly=True) - + + postcode = fields.Integer("Postcode") bedroom_number = fields.Integer("Bedrooms", default=0) - area = fields.Integer("Living Area (sqm)", required=True) + facade_number = fields.Integer("Facades", default=0) + garage = fields.Boolean("Garage", default=False) + garden = fields.Boolean("Garden", default=False) + + living_area = fields.Integer("Living Area (sqm)", default=0) + total_area = fields.Integer("Total Area (sqm)", default=0) + + def _current_date(self): + return fields.Date.today() + available_from = fields.Date("Date", default=lambda self: self._current_date()) + + active = fields.Boolean("Active", default=True) sequence = fields.Integer(default=10) tag_ids = fields.Many2many("estate.tag", string="Tags") @@ -45,7 +58,12 @@ class Property(models.Model): 'The number of bedrooms can\'t be negative.', ) - _check_area = models.Constraint( - 'CHECK(area >= 0)', - 'The area can\'t be negative.', + _check_living_area = models.Constraint( + 'CHECK(living_area >= 0)', + 'The living_area can\'t be negative.', + ) + + _check_total_area = models.Constraint( + 'CHECK(total_area >= 0)', + 'The total_area can\'t be negative.', ) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index f4d1f447b05..530cdad6fc2 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -25,9 +25,6 @@
-
-

@@ -35,14 +32,31 @@ - - + + - - + + + + + + + + + + + + + + + + + + + @@ -58,7 +72,7 @@ - + From adf087f61db4319674cff6c0b5117d724094a905 Mon Sep 17 00:00:00 2001 From: "Astremy (luleg)" <43485551+Astremy@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:43:54 +0100 Subject: [PATCH 06/11] [IMP] Real Estate: Change of property types from a selection to a many2one, addition of property offer and completion of the property page --- estate/__manifest__.py | 4 ++ estate/data/estate.property.csv | 6 +- estate/data/estate.property.type.csv | 4 ++ estate/data/estate.tag.csv | 2 +- estate/models/__init__.py | 2 + estate/models/estate_property.py | 25 +++++++-- estate/models/estate_property_offer.py | 25 +++++++++ estate/models/estate_property_type.py | 12 ++++ estate/security/ir.model.access.csv | 2 + estate/views/estate_menu_views.xml | 15 +++++ estate/views/estate_property_offer.xml | 61 +++++++++++++++++++++ estate/views/estate_property_type_views.xml | 51 +++++++++++++++++ estate/views/estate_property_views.xml | 40 +++++++++----- estate/views/estate_tag_views.xml | 18 ++++++ 14 files changed, 243 insertions(+), 24 deletions(-) create mode 100644 estate/data/estate.property.type.csv create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/estate_tag_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 1447e04f863..9eda80827b7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -19,9 +19,13 @@ 'data': [ "data/estate.tag.csv", + "data/estate.property.type.csv", "data/estate.property.csv", "security/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_offer.xml", + "views/estate_tag_views.xml", "views/estate_menu_views.xml", ], 'assets': { diff --git a/estate/data/estate.property.csv b/estate/data/estate.property.csv index 2623bfdde4d..77e77dd510f 100644 --- a/estate/data/estate.property.csv +++ b/estate/data/estate.property.csv @@ -1,3 +1,3 @@ -"id","name","expecting_price","living_area","total_area","property_type" -estate_tag_1,"Odoo Farm 2",300000,1900,2000,"house" -estate_tag_2,"Odoo LLN",1000000,0,40000,"apartment" \ No newline at end of file +"id","name","expecting_price","living_area","total_area","property_type_id:id" +estate_tag_1,"Odoo Farm 2",300000,1900,2000,estate_property_type_1 +estate_tag_2,"Odoo LLN",1000000,0,40000,estate_property_type_3 diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv new file mode 100644 index 00000000000..fdd166bc44b --- /dev/null +++ b/estate/data/estate.property.type.csv @@ -0,0 +1,4 @@ +"id","name","livable" +estate_property_type_1,House,1 +estate_property_type_2,Apartment,1 +estate_property_type_3,Office,0 diff --git a/estate/data/estate.tag.csv b/estate/data/estate.tag.csv index 2632941d0cb..ac50db43319 100644 --- a/estate/data/estate.tag.csv +++ b/estate/data/estate.tag.csv @@ -1,3 +1,3 @@ "id","name","color" estate_property_1,"Leased",5 -estate_property_2,"Empty",8 \ No newline at end of file +estate_property_2,"Empty",8 diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 1a1271f8598..e74970af685 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,4 @@ from . import estate_tag +from . import estate_property_type +from . import estate_property_offer from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 63b5e1de213..7e08cfcbc55 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class Property(models.Model): @@ -10,14 +10,14 @@ class Property(models.Model): name = fields.Char("Name", required=True, translate=True) - property_type = fields.Selection( + """property_type = fields.Selection( [ ("house", "House"), ("apartment", "Apartment"), ("land", "Land") ], required=True - ) + )""" description = fields.Char("Description") @@ -27,13 +27,16 @@ class Property(models.Model): ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled") - ], default="new") + ], default="new", copy=False) currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) expecting_price = fields.Monetary("Expecting Price", required=True) - best_offer = fields.Monetary("Best Offer", default=True) + best_offer = fields.Monetary("Best Offer", default=0) selling_price = fields.Monetary("Selling Price", default=0, readonly=True) - + + seller_id = fields.Many2one("res.users", string="Salesperson", index=True, default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", string="Buyer", index=True) + postcode = fields.Integer("Postcode") bedroom_number = fields.Integer("Bedrooms", default=0) facade_number = fields.Integer("Facades", default=0) @@ -51,8 +54,18 @@ def _current_date(self): active = fields.Boolean("Active", default=True) sequence = fields.Integer(default=10) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + property_livable = fields.Boolean("Livable", compute="_compute_property_livable") + tag_ids = fields.Many2many("estate.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + + @api.depends("property_type_id.livable") + def _compute_property_livable(self): + for property in self: + property.property_livable = property.property_type_id.livable + _check_bedroom_number = models.Constraint( 'CHECK(bedroom_number >= 0)', 'The number of bedrooms can\'t be negative.', diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..a53a92dcc6c --- /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 = "Offer" + _order = "sequence, id" + + currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) + price = fields.Monetary("Price", required=True) + + status = fields.Selection([ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], copy=False) + + property_id = fields.Many2one("estate.property", string="Property", required=True) + partner_id = fields.Many2one("res.partner", string="Partner", index=True, required=True) + + def _seven_days_from_now_date(self): + return fields.Date.add(fields.Date.today(), days=7) + + deadline = fields.Date("Deadline", default=lambda self: self._seven_days_from_now_date()) + + sequence = fields.Integer("Sequence", default=0) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..db8a446a3b6 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Property Type" + _order = "sequence, id" + + name = fields.Char("Name", required=True, translate=True) + livable = fields.Boolean("Livable", default=True) + + sequence = fields.Integer("Sequence", default=0) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index e5d581ea24f..b0da371ed14 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,4 +1,6 @@ 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_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 access_estate_tag,access_estate_tag,model_estate_tag,base.group_user,1,1,1,1 access_estate_tag_category,access_estate_tag_category,model_estate_tag_category,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menu_views.xml b/estate/views/estate_menu_views.xml index b7e754dfff6..e50f1db8a36 100644 --- a/estate/views/estate_menu_views.xml +++ b/estate/views/estate_menu_views.xml @@ -14,6 +14,21 @@ sequence="10" action="estate.estate_property_action" /> + + + + diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml new file mode 100644 index 00000000000..88f8fee0dd6 --- /dev/null +++ b/estate/views/estate_property_offer.xml @@ -0,0 +1,61 @@ + + + + estate.property.offer.view.search + estate.property.offer + + + + + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ +
+

+ +

+
+ + + + + + +
+
+
+
+ + + estate.property.offer.view.list + estate.property.offer + + + + + + + + + + + + + + Offers + estate.property.offer + list,form + +
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..4793b3a275b --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,51 @@ + + + + estate.property.type.view.search + estate.property.type + + + + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+

+ +

+
+ + + +
+
+
+
+ + + estate.property.type.view.list + estate.property.type + + + + + + + + + + Property Types + estate.property.type + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 530cdad6fc2..6ee72203b57 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,10 +7,7 @@ - - - +
@@ -25,36 +22,51 @@
-

- -

+ + +

+ +

+
+ + + +
+ - + - - - + + + - - + + + + + + + + +
@@ -68,7 +80,7 @@ - + diff --git a/estate/views/estate_tag_views.xml b/estate/views/estate_tag_views.xml new file mode 100644 index 00000000000..039de1984de --- /dev/null +++ b/estate/views/estate_tag_views.xml @@ -0,0 +1,18 @@ + + + + estate.tag.form + estate.tag + +
+ +
+

+ +

+
+
+
+
+
+
From 48de96791d21152d7ff0440da22427d09447df9c Mon Sep 17 00:00:00 2001 From: "Astremy (luleg)" <43485551+Astremy@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:04:04 +0100 Subject: [PATCH 07/11] [IMP] Real Estate: Added offers and automatic calculations Automatic calculation of validity/deadline, total area/living area/garden area, and best price --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 81 +++++++++++++++---- estate/models/estate_property_offer.py | 21 ++++- ...er.xml => estate_property_offer_views.xml} | 17 ++-- estate/views/estate_property_views.xml | 13 +-- 5 files changed, 103 insertions(+), 31 deletions(-) rename estate/views/{estate_property_offer.xml => estate_property_offer_views.xml} (82%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9eda80827b7..e1f2a2fdd4b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -24,7 +24,7 @@ "security/ir.model.access.csv", "views/estate_property_views.xml", "views/estate_property_type_views.xml", - "views/estate_property_offer.xml", + "views/estate_property_offer_views.xml", "views/estate_tag_views.xml", "views/estate_menu_views.xml", ], diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 7e08cfcbc55..65c134290f0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -10,15 +10,6 @@ class Property(models.Model): name = fields.Char("Name", required=True, translate=True) - """property_type = fields.Selection( - [ - ("house", "House"), - ("apartment", "Apartment"), - ("land", "Land") - ], - required=True - )""" - description = fields.Char("Description") stage = fields.Selection([ @@ -29,9 +20,10 @@ class Property(models.Model): ("cancelled", "Cancelled") ], default="new", copy=False) - currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) + currency_id = fields.Many2one("res.currency", "Currency") expecting_price = fields.Monetary("Expecting Price", required=True) - best_offer = fields.Monetary("Best Offer", default=0) + best_offer = fields.Monetary("Best Offer", default=0, compute="_compute_best_offer", store=True) + best_offer_currency_id = fields.Many2one("res.currency", "Best Offer Currency", readonly=True) selling_price = fields.Monetary("Selling Price", default=0, readonly=True) seller_id = fields.Many2one("res.users", string="Salesperson", index=True, default=lambda self: self.env.user) @@ -44,12 +36,20 @@ class Property(models.Model): garden = fields.Boolean("Garden", default=False) living_area = fields.Integer("Living Area (sqm)", default=0) - total_area = fields.Integer("Total Area (sqm)", default=0) + garden_area = fields.Integer("Garden Area (sqm)", default=0) + total_area = fields.Integer("Total Area (sqm)", default=0, compute="_compute_total_area", inverse="_inverse_total_area", store=True) + + garden_orientation = fields.Selection([ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West") + ]) def _current_date(self): return fields.Date.today() - available_from = fields.Date("Date", default=lambda self: self._current_date()) + available_from = fields.Date("Date", default=_current_date) active = fields.Boolean("Active", default=True) sequence = fields.Integer(default=10) @@ -61,11 +61,57 @@ def _current_date(self): offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for property in self: + property.total_area = property.living_area + property.garden_area + + @api.onchange("total_area") + def _inverse_total_area(self): + for property in self: + temp_total_area = property.total_area + property.living_area = max(0, temp_total_area - property.garden_area) + property.garden_area = temp_total_area - property.living_area + + @api.onchange("garden") + def _onchange_garden(self): + for property in self: + if property.garden: + property.garden_orientation = "north" + property.garden_area = 10 + else: + property.garden_orientation = "" + property.garden_area = 0 + @api.depends("property_type_id.livable") def _compute_property_livable(self): for property in self: property.property_livable = property.property_type_id.livable + def _compute_currency(self, offer): + if offer.currency_id == self.currency_id: + return offer.price + return offer.currency_id._convert(offer.price, self.currency_id) + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for property in self: + temp_best = 0 + + best_offer_currency_id = None + best_offer = 0 + # property.best_offer = max(property.offer_ids.mapped("price")) + # Could do it if we weren't checking the currencies + for offer in property.offer_ids: + val = property._compute_currency(offer) + if val > temp_best: + temp_best = val + best_offer_currency_id = offer.currency_id + best_offer = offer.price + + property.best_offer = best_offer + property.best_offer_currency_id = best_offer_currency_id + _check_bedroom_number = models.Constraint( 'CHECK(bedroom_number >= 0)', 'The number of bedrooms can\'t be negative.', @@ -73,10 +119,15 @@ def _compute_property_livable(self): _check_living_area = models.Constraint( 'CHECK(living_area >= 0)', - 'The living_area can\'t be negative.', + 'The living area can\'t be negative.', + ) + + _check_garden_area = models.Constraint( + 'CHECK(garden_area >= 0)', + 'The garden area can\'t be negative.', ) _check_total_area = models.Constraint( 'CHECK(total_area >= 0)', - 'The total_area can\'t be negative.', + 'The total area can\'t be negative.', ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a53a92dcc6c..a9272214a8f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class PropertyOffer(models.Model): @@ -6,7 +6,7 @@ class PropertyOffer(models.Model): _description = "Offer" _order = "sequence, id" - currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) + currency_id = fields.Many2one('res.currency', 'Currency') price = fields.Monetary("Price", required=True) status = fields.Selection([ @@ -17,9 +17,24 @@ class PropertyOffer(models.Model): property_id = fields.Many2one("estate.property", string="Property", required=True) partner_id = fields.Many2one("res.partner", string="Partner", index=True, required=True) + def _current_date(self): + return fields.Date.today() + def _seven_days_from_now_date(self): return fields.Date.add(fields.Date.today(), days=7) - deadline = fields.Date("Deadline", default=lambda self: self._seven_days_from_now_date()) + deadline = fields.Date("Deadline", default=_seven_days_from_now_date) + creation_date = fields.Date("Creation Date", default=_current_date) + validity = fields.Integer("Validity (days)", store=True, compute="_compute_validity", inverse="_inverse_validity") + + @api.depends("deadline") + def _compute_validity(self): + for offer in self: + offer.validity = (offer.deadline - offer.creation_date).days + + @api.onchange("validity") + def _inverse_validity(self): + for offer in self: + offer.deadline = fields.Date.add(offer.creation_date, days=offer.validity) sequence = fields.Integer("Sequence", default=0) diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer_views.xml similarity index 82% rename from estate/views/estate_property_offer.xml rename to estate/views/estate_property_offer_views.xml index 88f8fee0dd6..971652fb83e 100644 --- a/estate/views/estate_property_offer.xml +++ b/estate/views/estate_property_offer_views.xml @@ -22,14 +22,20 @@
-
+

- + + +

-
+ + + + + @@ -44,11 +50,10 @@ - - - + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6ee72203b57..f4304622f17 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -39,11 +39,13 @@ + - - + + + @@ -54,13 +56,12 @@ - + + - - - + From 03a702a97a3503496eec8240ee47d365eba7fcd1 Mon Sep 17 00:00:00 2001 From: "Astremy (luleg)" <43485551+Astremy@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:47:43 +0100 Subject: [PATCH 08/11] [IMP] Real Estate: Setup of validation --- estate/models/estate_property.py | 39 +++++------- estate/models/estate_property_offer.py | 67 +++++++++++++++++--- estate/views/estate_property_offer_views.xml | 6 +- estate/views/estate_property_views.xml | 12 ++-- 4 files changed, 87 insertions(+), 37 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 65c134290f0..dc93ad3713d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError class Property(models.Model): @@ -23,7 +24,6 @@ class Property(models.Model): currency_id = fields.Many2one("res.currency", "Currency") expecting_price = fields.Monetary("Expecting Price", required=True) best_offer = fields.Monetary("Best Offer", default=0, compute="_compute_best_offer", store=True) - best_offer_currency_id = fields.Many2one("res.currency", "Best Offer Currency", readonly=True) selling_price = fields.Monetary("Selling Price", default=0, readonly=True) seller_id = fields.Many2one("res.users", string="Salesperson", index=True, default=lambda self: self.env.user) @@ -88,29 +88,24 @@ def _compute_property_livable(self): for property in self: property.property_livable = property.property_type_id.livable - def _compute_currency(self, offer): - if offer.currency_id == self.currency_id: - return offer.price - return offer.currency_id._convert(offer.price, self.currency_id) - @api.depends("offer_ids.price") def _compute_best_offer(self): for property in self: - temp_best = 0 - - best_offer_currency_id = None - best_offer = 0 - # property.best_offer = max(property.offer_ids.mapped("price")) - # Could do it if we weren't checking the currencies - for offer in property.offer_ids: - val = property._compute_currency(offer) - if val > temp_best: - temp_best = val - best_offer_currency_id = offer.currency_id - best_offer = offer.price - - property.best_offer = best_offer - property.best_offer_currency_id = best_offer_currency_id + property.best_offer = max([0, *property.offer_ids.mapped("translated_price")]) + + def action_set_as_cancelled(self): + for property in self: + if property.stage in ["sold", "cancelled"]: + raise UserError(_("This property was already set as '%s'", Property.stage._selection[property.stage])) + property.stage = "cancelled" + return True + + def action_set_as_sold(self): + for property in self: + if property.stage in ["sold", "cancelled"]: + raise UserError(_("This property was already set as '%s'", Property.stage._selection[property.stage])) + property.stage = "sold" + return True _check_bedroom_number = models.Constraint( 'CHECK(bedroom_number >= 0)', diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a9272214a8f..c01a72b80c3 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 import _, api, fields, models +from odoo.exceptions import UserError class PropertyOffer(models.Model): @@ -6,17 +7,11 @@ class PropertyOffer(models.Model): _description = "Offer" _order = "sequence, id" - currency_id = fields.Many2one('res.currency', 'Currency') - price = fields.Monetary("Price", required=True) - - status = fields.Selection([ - ("accepted", "Accepted"), - ("refused", "Refused"), - ], copy=False) - property_id = fields.Many2one("estate.property", string="Property", required=True) partner_id = fields.Many2one("res.partner", string="Partner", index=True, required=True) + + # Beginning of the deadline part def _current_date(self): return fields.Date.today() @@ -32,9 +27,63 @@ def _compute_validity(self): for offer in self: offer.validity = (offer.deadline - offer.creation_date).days + # Reverse from _compute_validity, with real-time update because otherwise it's only after closing the form @api.onchange("validity") def _inverse_validity(self): for offer in self: offer.deadline = fields.Date.add(offer.creation_date, days=offer.validity) + # End of the deadline part + + + # Beginning of the currency part + currency_id = fields.Many2one("res.currency", "Currency") + property_currency_id = fields.Many2one("res.currency", "Partner Currency", compute="_compute_property_currency_id") + price = fields.Monetary("Original Price", required=True) + translated_price = fields.Monetary("Price", store=True, compute="_compute_translated_price") + + # Translate currency to the one of the property so it's easier to compare + # Also, the webpage doesn't like showing multiple currency signs (as $ and €), + # so we put everything in the base currency for display + + def _compute_currency(self): + if self.property_currency_id == self.currency_id: + return self.price + return self.currency_id._convert(self.price, self.property_currency_id) + + @api.depends("property_currency_id") + def _compute_translated_price(self): + for offer in self: + offer.translated_price = offer._compute_currency() + + @api.depends("property_id.currency_id") + def _compute_property_currency_id(self): + for offer in self: + offer.property_currency_id = offer.property_id.currency_id + + # End of the currency part + + + # Beginning of the state / validation part + status = fields.Selection([ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], copy=False) + + def action_confirm(self): + for offer in self: + if offer.property_id.stage in ["offer_accepted", "sold", "cancelled"]: + raise UserError(_("You can't accept new offers")) + offer.property_id.selling_price = offer.translated_price + offer.property_id.buyer_id = offer.partner_id + offer.property_id.stage = "offer_accepted" + offer.status = "accepted" + + def action_refuse(self): + for offer in self: + offer.status = "refused" + + # End of the state / validation part + + sequence = fields.Integer("Sequence", default=0) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 971652fb83e..966dd1543c0 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -49,11 +49,15 @@ estate.property.offer - + + + +

@@ -27,6 +35,18 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5830ec8183c..a319ada59ff 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,7 +7,8 @@ - + @@ -38,7 +39,7 @@

- + @@ -63,7 +64,18 @@ - + + + + + + + + +