From 82eb32ada3b923e0ef5f46c49ad5cf83d995f5ed Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Wed, 2 Jul 2025 17:42:52 +0530 Subject: [PATCH 01/10] [ADD] estate module/app added (chapter-2 done) --- 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..1a520bb1891 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +{ + 'name' : 'Estate', + 'installable': True, + 'application': True, + 'auto_install': False +} \ No newline at end of file From db9d006200fc911b7c3f6c544471677ac5bd0ed4 Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Fri, 4 Jul 2025 10:08:58 +0530 Subject: [PATCH 02/10] [ADD] estate: added security and enhanced UI --- estate/__init__.py | 1 + estate/__manifest__.py | 12 +++- estate/models/__init__.py | 1 + estate/models/estate_property.py | 33 +++++++++ estate/security/ir.model.access.csv | 2 + estate/views/estate_menu.xml | 9 +++ estate/views/estate_property_views.xml | 99 ++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 2 deletions(-) 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_menu.xml create mode 100644 estate/views/estate_property_views.xml 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/__manifest__.py b/estate/__manifest__.py index 1a520bb1891..128f9d3515f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,14 @@ { - 'name' : 'Estate', + 'name': 'Estate', 'installable': True, 'application': True, - 'auto_install': False + 'auto_install': False, + + 'data' : [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menu.xml', + ], + + 'license': 'LGPL-3', } \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +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..af1fce46b83 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,33 @@ +from odoo import fields,models +from datetime import date +from dateutil.relativedelta import relativedelta + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property is defined" + + name= fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_avaiblity = fields.Date(copy=False,default=date.today()+ relativedelta(months=3)) + 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( + string = 'Direction', + selection=[('north','North'), ('south','South'), ('east','East'), ('west','West')], + help = "Orientation is used to locate garden's direction" + ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[('new','new'), ('Offer Received','Offer Received'), ('Offer Accepted','Offer Accepted'), ('sold','sold'),('Cancelled','Cancelled')], + required=True, + default='new', + copy=False, + ) \ 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..ab63520e22b --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml new file mode 100644 index 00000000000..85edf509fed --- /dev/null +++ b/estate/views/estate_menu.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..76664a30ab4 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,99 @@ + + + + Properties + estate.property + list,form + + + + estate.property + estate.property + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate_property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file From 7695fbff77b29747eecffd06743254cfd59aa198 Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Mon, 7 Jul 2025 10:43:38 +0530 Subject: [PATCH 03/10] [ADD] estate: SQL constraints , compute and inverse , till ch-10 completed --- estate/__manifest__.py | 2 + estate/models/__init__.py | 5 +- estate/models/estate_property.py | 71 ++++++++++++- estate/models/estate_property_offer.py | 79 ++++++++++++++ estate/models/estate_property_tag.py | 11 ++ estate/models/estate_property_type.py | 11 ++ estate/security/ir.model.access.csv | 5 +- estate/views/estate_menu.xml | 12 ++- estate/views/estate_property_tag_views.xml | 9 ++ estate/views/estate_property_type_views.xml | 23 ++++ estate/views/estate_property_views.xml | 111 +++++++++++++------- 11 files changed, 295 insertions(+), 44 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 128f9d3515f..e179930d1b7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,8 @@ 'data' : [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', 'views/estate_menu.xml', ], diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..09b2099fe84 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ -from . import estate_property \ No newline at end of file +from . import estate_property +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 af1fce46b83..bf4009b1b8b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ -from odoo import fields,models +from odoo import api,fields,models +from odoo.exceptions import UserError from datetime import date from dateutil.relativedelta import relativedelta @@ -30,4 +31,70 @@ class EstateProperty(models.Model): required=True, default='new', copy=False, - ) \ No newline at end of file + ) + salesman_id = fields.Many2one("res.users",index=True,string="salesman",default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner",string="buyer",index=True,default=lambda self:self.env.user.partner_id.id) + + property_type_id = fields.Many2one("estate.property.type",string="property type") + + property_tag_ids = fields.Many2many("estate.property.tag",string="tags") + + offer_ids = fields.One2many("estate.property.offer","property_id",string="offers") + + + + _sql_constraints = [ + ('selling_price_positive','CHECK(selling_price>=0)','The selling price should be positive.'), + ('expected_price_positive','CHECK(expected_price>=0)','The expected price should be positive.') + ] + + total_area = fields.Float(compute="_compute_total") + + @api.depends("garden_area","living_area") + def _compute_total(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + best_offer = fields.Float(compute="_find_best") + + @api.depends("offer_ids.price") + def _find_best(self): + for record in self: + 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 + + + status = fields.Selection( + selection=[ + ('new', 'New'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + default='new', + copy=False + ) + + def sold_button_action(self): + for record in self: + if record.status == 'cancelled': + raise UserError("Cancelled property cannot be marked as sold.") + record.status = 'sold' + + def cancel_button_action(self): + for record in self: + if record.status == 'sold': + raise UserError("Sold property cannot be cancelled.") + record.status = 'cancelled' \ 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..c9c36330d29 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,79 @@ +from odoo import api,fields,models +from datetime import timedelta +from odoo.exceptions import UserError,ValidationError +from odoo.tools.float_utils import float_compare + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "estate property OFFER created" + + price=fields.Float() + status = fields.Selection( + selection=[('accepted','Accepted'), ('refused','Refused')], + copy=False, + ) + + partner_id = fields.Many2one("res.partner",string="Partner",index=True,required=True,default=lambda self:self.env.user.partner_id.id) + property_id = fields.Many2one("estate.property",index=True,required=True) + + validity = fields.Integer(default = 7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True + ) + + _sql_constraints = [ + ('offer_price_positive','CHECK(price>=0)','The offer price should be positive.') + ] + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + create_date = record.create_date.date() + else: + create_date = fields.Date.today() + record.date_deadline = create_date + timedelta(days=record.validity) + + + def _inverse_date_deadline(self): + for record in self: + if record.create_date: + create_date = record.create_date.date() + else: + create_date = fields.Date.today() + if record.date_deadline: + record.validity = (record.date_deadline - create_date).days + + + + def accept_icon_action(self): + for record in self: + # Ensure only one accepted offer per property + if any( + offer.status == 'accepted' + for offer in record.property_id.offer_ids + ): + raise UserError("Only one offer can be accepted per property.") + + # ✅ Enforce 90% price check + min_price = record.property_id.expected_price * 0.9 + if float_compare(record.price, min_price, precision_digits=2) < 0: + raise ValidationError("Offer must be at least 90% of the expected price to be accepted.") + + record.status = 'accepted' + + # Refuse all other offers + other_offers = record.property_id.offer_ids - record + other_offers.write({'status': 'refused'}) + + # Set buyer and selling price on property + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + + def refuse_icon_action(self): + for record in self: + record.status = 'refused' diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..3fd28199509 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,11 @@ +from odoo import fields,models + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "estate property TAG created" + + name = fields.Char(required=True) + + _sql_constraints = [ + ('tag_name_unique','unique(name)','Tag name should be unique') + ] \ 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..a0ecc298620 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,11 @@ +from odoo import fields,models + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property type is defined" + + name = fields.Char(required=True) + + _sql_constraints = [ + ('property_type_unique','unique(name)','property type should be unique') + ] \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index ab63520e22b..0c0b62b7fee 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +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 +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 diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml index 85edf509fed..07a77c484e5 100644 --- a/estate/views/estate_menu.xml +++ b/estate/views/estate_menu.xml @@ -1,9 +1,15 @@ - - + + + + + + + + + - \ 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..d23fbda27e5 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,9 @@ + + + + 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..dbfb6c061f0 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,23 @@ + + + + property type + estate.property.type + list,form + + + + estate.property.type.search + estate.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 76664a30ab4..89b3ed4b15d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,6 +6,7 @@ list,form + estate.property estate.property @@ -29,11 +30,24 @@ estate.property
+
+

+ + + + + + + + + @@ -46,54 +60,77 @@ + + + - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + +

+ +

+ + + + + + + + + + + + + + +
+ +
@@ -12,12 +68,25 @@ - - + + + + + estate.property.offer.tree + estate.property.offer + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 89b3ed4b15d..cf7e9da8d51 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,6 +4,7 @@ Properties estate.property list,form + {'search_default_state': 1} @@ -11,14 +12,19 @@ estate.property estate.property - + + + - + @@ -31,22 +37,23 @@
-

- + - + @@ -73,8 +80,8 @@ - - + + @@ -83,15 +90,18 @@ - - + + -