From 667ac3525e52c42f26f0b369dd78faadc23ea6d6 Mon Sep 17 00:00:00 2001 From: joyep Date: Thu, 20 Nov 2025 14:33:10 +0100 Subject: [PATCH 01/17] [ADD] estate: Chapter 1 - debuger and formatter config --- estate/.vscode/launch.json | 13 +++++++++++++ estate/ruff.toml | 3 +++ 2 files changed, 16 insertions(+) create mode 100644 estate/.vscode/launch.json create mode 100644 estate/ruff.toml diff --git a/estate/.vscode/launch.json b/estate/.vscode/launch.json new file mode 100644 index 00000000000..11f1a73f047 --- /dev/null +++ b/estate/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "debug current file", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} diff --git a/estate/ruff.toml b/estate/ruff.toml new file mode 100644 index 00000000000..4713f8a60fe --- /dev/null +++ b/estate/ruff.toml @@ -0,0 +1,3 @@ +# ~/Codes/tutorials/ruff.toml + +extend = "../../odoo/ruff.toml" From eb8ba9e8eb1e118aa5fd6c26a17078ae252b6b5c Mon Sep 17 00:00:00 2001 From: joyep Date: Tue, 18 Nov 2025 13:54:24 +0100 Subject: [PATCH 02/17] [ADD] estate: Chapter 2 - initialize estate module with manifest and init file --- estate/__init__.py | 0 estate/__manifest__.py | 6 ++++++ 2 files changed, 6 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..df893391c04 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +{ + "name": "Estate", + "depends": [ + "base", + ], +} From d5afe7f73bebd81c34cebfd48608cbfe4037314b Mon Sep 17 00:00:00 2001 From: joyep Date: Tue, 18 Nov 2025 15:02:59 +0100 Subject: [PATCH 03/17] [ADD] estate: Chapter 3 - add estate module with models and field --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..62e6b3c073a --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,33 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property" + _order = "sequence" + + # Default fields + name = fields.Char("Title", required=True) + sequence = fields.Integer("Sequence", default=10) + + # Basic fields + description = fields.Text("Description") + postcode = fields.Char("Postcode") + date_availability = fields.Date("Available From") + expected_price = fields.Float("Expected Price", required=True) + selling_price = fields.Float("Selling Price") + bedrooms = fields.Integer("Bedrooms", default=2) + living_area = fields.Integer("Living Area (sqm)") + facades = fields.Integer("Number of Facades") + garage = fields.Boolean("Garage") + garden = fields.Boolean("Garden") + garden_area = fields.Integer("Garden Area (sqm)") + garden_orientation = fields.Selection( + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + string="Garden Orientation", + ) From 1a272d67adc26e88fe2b52b63c7b3acabc2734e4 Mon Sep 17 00:00:00 2001 From: joyep Date: Tue, 18 Nov 2025 15:42:44 +0100 Subject: [PATCH 04/17] [ADD] estate: Chapter 4 - add access control for estate properties --- estate/__manifest__.py | 9 +++++++-- estate/data/ir.model.access.csv | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 estate/data/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index df893391c04..9ebc23ad08f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,11 @@ { "name": "Estate", - "depends": [ - "base", + "version": "1.0", + "depends": ["base"], + "data": [ + "data/ir.model.access.csv", ], + "installable": True, + "application": True, + "auto_install": False, } diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv new file mode 100644 index 00000000000..84b3c59f2c5 --- /dev/null +++ b/estate/data/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_user,Estate Property User,model_estate_property,base.group_user,1,1,1,1 From eb628066292800d7b544aeef07705f4703690d66 Mon Sep 17 00:00:00 2001 From: joyep Date: Wed, 19 Nov 2025 09:39:37 +0100 Subject: [PATCH 05/17] [ADD] estate: Chapter 5 - add fields, menus, and views --- estate/__manifest__.py | 4 ++++ estate/models/estate_property.py | 21 +++++++++++++++++++-- estate/views/estate_menus.xml | 15 +++++++++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9ebc23ad08f..9b2e87eab6e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,9 +1,13 @@ { "name": "Estate", "version": "1.0", + "author": "Joyep", + "license": "LGPL-3", "depends": ["base"], "data": [ "data/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_menus.xml", ], "installable": True, "application": True, diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 62e6b3c073a..f78c10f312f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import fields, models +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model): @@ -13,9 +14,11 @@ class EstateProperty(models.Model): # Basic fields description = fields.Text("Description") postcode = fields.Char("Postcode") - date_availability = fields.Date("Available From") + date_availability = fields.Date( + "Available From", default=lambda self: fields.Date.today() + relativedelta(months=3), copy=False + ) expected_price = fields.Float("Expected Price", required=True) - selling_price = fields.Float("Selling Price") + selling_price = fields.Float("Selling Price", copy=False, readonly=True) bedrooms = fields.Integer("Bedrooms", default=2) living_area = fields.Integer("Living Area (sqm)") facades = fields.Integer("Number of Facades") @@ -31,3 +34,17 @@ class EstateProperty(models.Model): ], string="Garden Orientation", ) + active = fields.Boolean("Active", default=True) + status = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), + ], + string="Status", + default="new", + required=True, + copy=False, + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..1aecc1cb0e2 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..1d2a3aaa4cd --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + From 68ea866f76acbbe6058e25b31ab333edca75b0f4 Mon Sep 17 00:00:00 2001 From: joyep Date: Wed, 19 Nov 2025 11:08:10 +0100 Subject: [PATCH 06/17] [ADD] estate: Chapter 6 - add form, tree, and search layouts --- estate/views/estate_property_views.xml | 78 ++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1d2a3aaa4cd..23f65eba4b3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,82 @@ estate.property list,form + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + From 244ea1fe4dba4c0a37f8f5bdc07a9f4dcac1ac31 Mon Sep 17 00:00:00 2001 From: joyep Date: Wed, 19 Nov 2025 16:37:35 +0100 Subject: [PATCH 07/17] [ADD] estate: Chapter 7 - add property types, tags, and offers Add property types, tags, and offers with corresponding views and access rights --- estate/__manifest__.py | 2 ++ estate/data/ir.model.access.csv | 3 ++ estate/models/__init__.py | 7 +++- estate/models/estate_property.py | 7 ++++ estate/models/estate_property_offer.py | 18 ++++++++++ estate/models/estate_property_tag.py | 8 +++++ estate/models/estate_property_type.py | 8 +++++ estate/views/estate_menus.xml | 21 +++++------ estate/views/estate_property_tag_views.xml | 30 ++++++++++++++++ estate/views/estate_property_type_views.xml | 40 +++++++++++++++++++++ estate/views/estate_property_views.xml | 23 +++++++++++- 11 files changed, 153 insertions(+), 14 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 create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9b2e87eab6e..9c0000bfb0a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,8 @@ "data": [ "data/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", "views/estate_menus.xml", ], "installable": True, diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv index 84b3c59f2c5..0104d1eadf3 100644 --- a/estate/data/ir.model.access.csv +++ b/estate/data/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_user,Estate Property User,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type_user,Estate Property Type User,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag_user,Estate Property Tag User,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer_user,Estate Property Offer User,model_estate_property_offer,base.group_user,1,1,1,1 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 f78c10f312f..452ef6d521b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -48,3 +48,10 @@ class EstateProperty(models.Model): required=True, copy=False, ) + salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) + partner_id = fields.Many2one("res.partner", string="Partner", copy=False) + + # Relational fields + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + 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..e21c254d061 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + + price = fields.Float("Price") + status = fields.Selection( + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + string="Status", + copy=False, + ) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..3ebec85000c --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + + name = fields.Char("Name", required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..54459e3c267 --- /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 = "Estate Property Type" + + name = fields.Char("Name", required=True) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 1aecc1cb0e2..29029540af4 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,15 +1,12 @@ - - - - - + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..0a23dc14a8f --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,30 @@ + + + + Property Tags + estate.property.tag + list,form + + + estate.property.tag.list + estate.property.tag + + + + + + + + estate.property.tag.form + estate.property.tag + +
+ + + + + +
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..c830df588c2 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,40 @@ + + + + Property Types + estate.property.type + list,form,search + + + estate.property.type.list + estate.property.type + + + + + + + + estate.property.type.form + estate.property.type + +
+ + + + + +
+
+
+ + + estate.property.type.search + estate.property.type + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 23f65eba4b3..4f364a4450e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Properties estate.property - list,form + list,form,search @@ -12,6 +12,8 @@ + + @@ -32,9 +34,11 @@

+ + @@ -57,6 +61,21 @@ + + + + + + + + + + + + + + + @@ -71,6 +90,8 @@ + + From 7a297425a8cb4e6b7a49b1056cac533edd2d6246 Mon Sep 17 00:00:00 2001 From: joyep Date: Thu, 20 Nov 2025 09:53:14 +0100 Subject: [PATCH 08/17] [ADD] estate: Chapter 8 - add computed fields and views Enhance property and offer models with computed fields and views --- estate/models/estate_property.py | 33 ++++++++++++++++++++++++-- estate/models/estate_property_offer.py | 21 +++++++++++++++- estate/views/estate_property_views.xml | 13 ++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 452ef6d521b..fbe5161d3f8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models from dateutil.relativedelta import relativedelta @@ -15,7 +15,9 @@ class EstateProperty(models.Model): description = fields.Text("Description") postcode = fields.Char("Postcode") date_availability = fields.Date( - "Available From", default=lambda self: fields.Date.today() + relativedelta(months=3), copy=False + "Available From", + default=lambda self: fields.Date.today() + relativedelta(months=3), + copy=False, ) expected_price = fields.Float("Expected Price", required=True) selling_price = fields.Float("Selling Price", copy=False, readonly=True) @@ -55,3 +57,30 @@ class EstateProperty(models.Model): property_type_id = fields.Many2one("estate.property.type", string="Property Type") tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + + # Computed fields + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area", store=True) + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for record in self: + prices = record.offer_ids.mapped("price") + record.best_offer = max(prices) if prices else 0.0 + + best_offer = fields.Float("Best Offer", compute="_compute_best_offer", store=True) + + @api.onchange("garden") + def _onchange_garden(self): + if not self.garden: + self.garden_area = 0 + self.garden_orientation = False + else: + if not self.garden_area: + self.garden_area = 10 + if not self.garden_orientation: + self.garden_orientation = "north" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e21c254d061..557573fa295 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from datetime import timedelta + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -16,3 +18,20 @@ class EstatePropertyOffer(models.Model): ) partner_id = fields.Many2one("res.partner", string="Partner", required=True) property_id = fields.Many2one("estate.property", string="Property", required=True) + validity = fields.Integer("Validity (days)", default=7) + date_deadline = fields.Date("Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") + + @api.depends("create_date", "validity") + def _compute_date_deadline(self): + for offer in self: + if offer.create_date: + offer.date_deadline = offer.create_date.date() + timedelta(days=offer.validity) + else: # Fallback if create_date is not set + offer.date_deadline = fields.Date.today() + timedelta(days=offer.validity) + + def _inverse_date_deadline(self): + for offer in self: + if offer.date_deadline and offer.create_date: + offer.validity = (offer.date_deadline - offer.create_date.date()).days + elif offer.date_deadline: # Fallback if create_date is not set + offer.validity = (offer.date_deadline - fields.Date.today()).days diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4f364a4450e..e8388ce25e8 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -44,6 +44,7 @@ + @@ -59,6 +60,7 @@ + @@ -66,8 +68,19 @@ + + +
+ + + + + + + +
From 526554ddb23e3063be16faee304972c241faea44 Mon Sep 17 00:00:00 2001 From: joyep Date: Thu, 20 Nov 2025 10:39:14 +0100 Subject: [PATCH 09/17] [ADD] estate: Chapter 9 - add buttons and correspoding actions Implement action methods for property status and offer status --- estate/models/estate_property.py | 14 ++++++++++++++ estate/models/estate_property_offer.py | 21 +++++++++++++++++++++ estate/views/estate_property_views.xml | 7 +++++++ 3 files changed, 42 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index fbe5161d3f8..9927b500e59 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError from dateutil.relativedelta import relativedelta @@ -84,3 +85,16 @@ def _onchange_garden(self): self.garden_area = 10 if not self.garden_orientation: self.garden_orientation = "north" + + # Action methods and other business logic + def action_set_sold(self): + for record in self: + if record.status == "canceled": + raise UserError("A cancelled property cannot be set as sold.") + record.status = "sold" + + def action_set_canceled(self): + for record in self: + if record.status == "sold": + raise UserError("A sold property cannot be cancelled.") + record.status = "canceled" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 557573fa295..0fcee7637a4 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,7 @@ from datetime import timedelta from odoo import api, fields, models +from odoo.exceptions import UserWarning class EstatePropertyOffer(models.Model): @@ -35,3 +36,23 @@ def _inverse_date_deadline(self): offer.validity = (offer.date_deadline - offer.create_date.date()).days elif offer.date_deadline: # Fallback if create_date is not set offer.validity = (offer.date_deadline - fields.Date.today()).days + + def action_accept_offer(self): + for offer in self: + # Check if there's already an accepted offer + other_offers = offer.property_id.offer_ids - offer + if any(other_offers.filtered(lambda o: o.status == "accepted")): + raise UserWarning("An offer has already been accepted for this property.") + offer.status = "accepted" + # Refuse other offers for the same property + other_offers.write({"status": "refused"}) + # Update the property status + offer.property_id.status = "offer_accepted" + offer.property_id.selling_price = offer.price + offer.property_id.partner_id = offer.partner_id + return True + + def action_refuse_offer(self): + for offer in self: + offer.status = "refused" + return True diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e8388ce25e8..d9dae4dcdf0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -30,6 +30,10 @@
+
+

@@ -38,6 +42,7 @@

+ @@ -70,6 +75,8 @@ + + + + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d9dae4dcdf0..05248304009 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,13 +4,16 @@ Properties estate.property list,form,search + + {'search_default_available': True}
- + estate.property.list estate.property - + + @@ -19,7 +22,7 @@ - + @@ -31,19 +34,19 @@
-

- +
- - + @@ -63,31 +66,21 @@ - - + + - - + + - diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml index aa54c1a7241..2dfb0428129 100644 --- a/awesome_owl/views/templates.xml +++ b/awesome_owl/views/templates.xml @@ -8,6 +8,7 @@ +