From 1c8c58e826bd347e4bd51c244e421df11c43d8d0 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Mon, 16 Feb 2026 17:26:41 +0100 Subject: [PATCH 1/9] [ADD] estate: property model with fields, views, and menu access - Add estate_property model with fields - Add active field with default True - Set date_availability default to 3 months from today - Create list, form, and search views for estate.property - Add search filters and group by postcode - Configure access rights in ir.model.access.csv --- estate/__init__.py | 1 + estate/__manifest__.py | 15 +++++ estate/models/__init__.py | 2 + estate/models/estate_property.py | 45 +++++++++++++ estate/security/ir.model.access.csv | 3 + estate/views/estate_property_menus.xml | 9 +++ estate/views/estate_property_views.xml | 92 ++++++++++++++++++++++++++ 7 files changed, 167 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_property_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..8f013a95959 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,15 @@ + +{ + 'name': 'estate', + 'depends': [ + 'base', + ], + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + # "views/estate_property_type_views.xml", + "views/estate_property_menus.xml", + # "views/estate_property_type_menus.xml", + ], + 'application': True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..8e433062639 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,2 @@ +from . import estate_property +# from . import estate_property_type \ 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..9a62783aa54 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,45 @@ +from odoo import fields, models + +def get_date_in_3_months(): + ''' + This function calculates the date that is three months from the current date. + + returns: + A date object representing the date that is three months from today. + ''' + today_data = fields.Date.today() + three_months_later = fields.Date.add(today_data, months=3) + return three_months_later + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property Model" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(copy=False, default=get_date_in_3_months()) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ]) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled'), + ], default="new", required=True, copy=False) + active = fields.Boolean(default=True) + + property_type_id = fields.Many2one("estate.property.type", string="Property Type") \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..db554fd92fd --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml new file mode 100644 index 00000000000..6ec80af05ab --- /dev/null +++ b/estate/views/estate_property_menus.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ 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..4bf84facc1c --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,92 @@ + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + + Properties + estate.property + list,form + +

+ Define a new estate property to sell or rent. +

+ You can specify the expected price and the selling/renting price, the buyer/tenant, the salesperson, and the status of the property. +

+
+
+
\ No newline at end of file From 6df9dac8363b816e8db83cb4ac741abd36202ffc Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Tue, 17 Feb 2026 10:10:42 +0100 Subject: [PATCH 2/9] [IMP] estate: property types, tags, and offers * Add estate.property.type model with form view and Settings menu * Add estate.property.tag model with form view and Settings menu * Add estate.property.offer model with form/list views for tracking offers * Update estate.property model with: - property_type_id: Many2one relation to property types - tag_ids: Many2many relation to property tags - offer_ids: One2many relation to offers - buyer_id and salesperson_id fields * Update property form view to display tags, type, offers, buyer, and salesperson * Reorganize menus: Advertisements and Settings submenus * Add security access rules for all new models --- estate/__manifest__.py | 7 ++-- estate/models/__init__.py | 4 ++- estate/models/estate_property.py | 8 +++-- estate/models/estate_property_offer.py | 10 ++++++ estate/models/estate_property_tag.py | 8 +++++ estate/models/estate_property_type.py | 8 +++++ estate/security/ir.model.access.csv | 4 ++- estate/views/estate_property_menus.xml | 2 +- estate/views/estate_property_offers_views.xml | 33 +++++++++++++++++++ estate/views/estate_property_tag_menus.xml | 4 +++ estate/views/estate_property_tag_views.xml | 31 +++++++++++++++++ estate/views/estate_property_type_menus.xml | 6 ++++ estate/views/estate_property_type_views.xml | 31 +++++++++++++++++ estate/views/estate_property_views.xml | 12 ++++++- 14 files changed, 160 insertions(+), 8 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_offers_views.xml create mode 100644 estate/views/estate_property_tag_menus.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_menus.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8f013a95959..fdea66cd571 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,9 +7,12 @@ "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", - # "views/estate_property_type_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offers_views.xml", "views/estate_property_menus.xml", - # "views/estate_property_type_menus.xml", + "views/estate_property_type_menus.xml", + "views/estate_property_tag_menus.xml", ], 'application': True, } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8e433062639..09b2099fe84 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,4 @@ from . import estate_property -# from . import estate_property_type \ No newline at end of file +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9a62783aa54..dd06aefaaa8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from odoo import fields, models -def get_date_in_3_months(): +def get_date_in_3_months() -> fields.Date: ''' This function calculates the date that is three months from the current date. @@ -42,4 +42,8 @@ class EstateProperty(models.Model): ], default="new", required=True, copy=False) active = fields.Boolean(default=True) - property_type_id = fields.Many2one("estate.property.type", string="Property Type") \ No newline at end of file + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..8caf5d371be --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,10 @@ +from odoo import models, fields + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + + price = fields.Float(string='Price') + status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False) + partner_id = fields.Many2one('res.partner', string='Partner') + property_id = fields.Many2one('estate.property', string='Property') \ No newline at end of file diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..e63d6d7a781 --- /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 Model" + + name = fields.Char(required=True) + \ No newline at end of file diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..228eb2b0e30 --- /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 Model" + + name = fields.Char(required=True) + \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index db554fd92fd..c79331f2f1c 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 -estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 6ec80af05ab..2e9e9014207 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -1,7 +1,7 @@ - + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..7d332d91e7c --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,33 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_tag_menus.xml b/estate/views/estate_property_tag_menus.xml new file mode 100644 index 00000000000..ba14c9674cd --- /dev/null +++ b/estate/views/estate_property_tag_menus.xml @@ -0,0 +1,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 new file mode 100644 index 00000000000..b40144216b4 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,31 @@ + + + + estate.property.tag.form + estate.property.tag + +
+ +

+ +

+
+
+
+
+ + + + + Property Tags + estate.property.tag + list,form + +

+ Define a new estate property tag. +

+ You can specify the name of each property tag. +

+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_type_menus.xml b/estate/views/estate_property_type_menus.xml new file mode 100644 index 00000000000..f542ad3e23c --- /dev/null +++ b/estate/views/estate_property_type_menus.xml @@ -0,0 +1,6 @@ + + + + + + \ 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..474ad7ef0fb --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,31 @@ + + + + estate.property.type.form + estate.property.type + +
+ +

+ +

+
+
+
+
+ + + + + Property Types + estate.property.type + list,form + +

+ Define a new estate property type. +

+ You can specify the name and description of each property type. +

+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4bf84facc1c..bd50918a48b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -30,8 +30,10 @@

+ + @@ -51,7 +53,15 @@ - + + + + + + + + + From 14d0e3e5a2bfe79438e8b2a793529d27d89deace Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Tue, 17 Feb 2026 11:13:05 +0100 Subject: [PATCH 3/9] [IMP] estate: computed fields and onchange methods * estate.property: - Add total_area computed field (living_area + garden_area) - Add best_offer computed field (max of offer prices) - Add onchange method for garden field to auto-populate garden_area and orientation * estate.property.offer: - Add validity field (days, default 7) - Add date_deadline computed field based on create_date + validity - Add inverse method to update validity when deadline is changed * Update views to display new computed fields (total_area, best_offer) * Update offer views to include validity and date_deadline fields --- estate/models/estate_property.py | 31 +++++++++++++++++-- estate/models/estate_property_offer.py | 19 ++++++++++-- estate/views/estate_property_offers_views.xml | 7 +++-- estate/views/estate_property_views.xml | 2 ++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index dd06aefaaa8..f33b93983c6 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 def get_date_in_3_months() -> fields.Date: ''' @@ -41,9 +41,36 @@ class EstateProperty(models.Model): ('canceled', 'Canceled'), ], default="new", required=True, copy=False) active = fields.Boolean(default=True) + + total_area = fields.Integer(compute="_compute_total_area") + best_offer = fields.Float(compute="_compute_best_offer", string="Best Offer") property_type_id = fields.Many2one("estate.property.type", string="Property Type") salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) tag_ids = fields.Many2many("estate.property.tag", string="Tags") - offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") \ No newline at end of file + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for record in self: + # If the property has offers, the best offer is the maximum of the expected prices of the offers. Otherwise, it is 0. + if record.offer_ids: + record.best_offer = max(record.offer_ids.mapped("price")) + else: + record.best_offer = 0.0 + + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 8caf5d371be..769f32d30bf 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api class EstatePropertyOffer(models.Model): _name = 'estate.property.offer' @@ -7,4 +7,19 @@ class EstatePropertyOffer(models.Model): price = fields.Float(string='Price') status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False) partner_id = fields.Many2one('res.partner', string='Partner') - property_id = fields.Many2one('estate.property', string='Property') \ No newline at end of file + property_id = fields.Many2one('estate.property', string='Property') + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') + + @api.depends('validity', 'create_date') + def _compute_date_deadline(self): + for offer in self: + start = fields.Date.to_date(offer.create_date) or fields.Date.today() + offer.date_deadline = fields.Date.add(start, days=offer.validity) + + def _inverse_date_deadline(self): + for offer in self: + start = fields.Date.to_date(offer.create_date) or fields.Date.today() + if offer.date_deadline and start: + delta = offer.date_deadline - start + offer.validity = delta.days \ No newline at end of file diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index 7d332d91e7c..32e643a4a32 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -8,8 +8,10 @@ - + + + @@ -23,8 +25,9 @@ - + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index bd50918a48b..457476b426f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -39,6 +39,7 @@ + @@ -53,6 +54,7 @@ + From 40ddc0e3c4bb4c7f4ffd6651b42adbd54d634372 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Tue, 17 Feb 2026 11:44:04 +0100 Subject: [PATCH 4/9] [IMP] estate: action methods and offer management buttons * estate.property: - Add action_set_sold() method to mark property as sold - Add action_cancel() method to cancel property - Add validation: prevent selling canceled properties - Add validation: prevent canceling sold properties * estate.property.offer: - Add accept_offer() method: sets status to accepted, updates selling price, property state, and buyer - Add refuse_offer() method: sets status to refused, updates property state * Update views: - Add "Mark as Sold" and "Cancel" buttons to property form header - Add accept/refuse buttons (icons) to offer list view --- estate/models/estate_property.py | 19 ++++++++++++++++++- estate/models/estate_property_offer.py | 15 ++++++++++++++- estate/views/estate_property_offers_views.xml | 3 +++ estate/views/estate_property_views.xml | 4 ++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f33b93983c6..c90795bb704 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 def get_date_in_3_months() -> fields.Date: ''' @@ -51,6 +52,7 @@ class EstateProperty(models.Model): tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + @api.depends("living_area", "garden_area") def _compute_total_area(self): for record in self: @@ -73,4 +75,19 @@ def _onchange_garden(self): self.garden_orientation = 'north' else: self.garden_area = 0 - self.garden_orientation = False \ No newline at end of file + self.garden_orientation = False + + + def action_set_sold(self): + for record in self: + if record.state == "canceled": + raise UserError("Canceled properties cannot be sold.") + record.state = "sold" + return True + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("Sold properties cannot be canceled.") + record.state = "canceled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 769f32d30bf..922414d7acd 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -22,4 +22,17 @@ def _inverse_date_deadline(self): start = fields.Date.to_date(offer.create_date) or fields.Date.today() if offer.date_deadline and start: delta = offer.date_deadline - start - offer.validity = delta.days \ No newline at end of file + offer.validity = delta.days + + def accept_offer(self): + for offer in self: + offer.status = 'accepted' + offer.property_id.selling_price = offer.price + offer.property_id.state = 'offer_accepted' + offer.property_id.buyer_id = offer.partner_id + + def refuse_offer(self): + for offer in self: + offer.status = 'refused' + if offer.property_id.state != 'offer_accepted': + offer.property_id.state = 'offer_received' \ No newline at end of file diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index 32e643a4a32..488d2a71104 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -28,6 +28,9 @@ +