diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..c9aa6d3d91e --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Real Estate", + "category": "Tutorials/RealEstate", + "application": True, + "installable": True, + "data": [ + "views/estate_property_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_type_views.xml", + "views/estate_menus.xml", + "views/inherit_res_users_view.xml", + "security/ir.model.access.csv", + ], + "license": "AGPL-3", +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..ef260eecf25 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import inherited_res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..5788516d9b2 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,175 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError, ValidationError +from dateutil.relativedelta import relativedelta +from datetime import date +from odoo.tools.float_utils import float_compare + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Model" + + name = fields.Char(string="Name") + description = fields.Text(string="Description") + postcode = fields.Text(string="Postcode") + date_availability = fields.Date( + string="Date Availability", + copy=False, + default=lambda self: date.today() + relativedelta(months=3), + ) + expected_price = fields.Float(string="Expected Price") + selling_price = fields.Integer(string="Selling Price") + bedrooms = fields.Integer(string="Bedrooms") + living_area = fields.Integer(string="Living Area") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area") + + garden_orientation = fields.Selection( + string="Type", + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + ) + + active = fields.Boolean(default=True, string="Active") + + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received "), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + string="State", + default="new", + copy=False, + ) + + # property will have Many to one relation with property type since many properties can belong to one property type + + property_type_id = fields.Many2one("estate.property.type", "Property Type") + + user_id = fields.Many2one( + "res.users", + string="Salesperson", + copy=False, + default=lambda self: self.env.user, + ) + + partner_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") + + total_area = fields.Integer( + compute="_compute_total_property_area", string="Total Area" + ) + + best_price = fields.Integer(compute="_compute_best_price", string="Best Price") + + status = fields.Char(default="new", string="Status") + + _order = "id desc" + + _sql_constraints = [ + ( + "check_expected_price", + "CHECK(expected_price > 0)", + "Expected price must be strictly positive", + ), + ( + "check_selling_price", + "CHECK(selling_price >= 0)", + "Selling price should be positive", + ), + ] + + @api.depends("garden_area", "living_area") + def _compute_total_property_area(self): + for area in self: + area.total_area = area.garden_area + area.living_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + offers_list = record.mapped("offer_ids.price") + if offers_list: + record.best_price = max(offers_list) + else: + record.best_price = 0 + + # on change of garden status , update gardern area and its orientation + + @api.onchange("garden") + def _onchange_garden_status(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = "north" + return + self.garden_area = 0 + self.garden_orientation = "" + + # acts when property is sold + # In case property is cancelled it cannot be sold + def action_sell_property(self): + # dictionary for the property status + property_sell_status_dict = {"new": True, "sold": True, "cancelled": False} + + for record in self: + if property_sell_status_dict[record.status]: + record.status = "sold" + else: + raise UserError("Cancelled property cannot be sold.") + + # action in case of cancel property button + # If property is sold than Cannot be cancelled + + def action_cancel_property_selling(self): + property_cancel_status_dict = { + "new": True, + "cancelled": True, + "sold": False, + } + for record in self: + if property_cancel_status_dict[record.status]: + record.status = "cancelled" + else: + raise UserError("Sold property cannot be cancelled.") + + # constrains for the selling price + + @api.constrains("selling_price", "expected_price") + def _check_selling_price(self): + for data in self: + # if call will come after selling price change than it will allow updated price to work + if data.selling_price <= 0: + return + + price_float_ratio = data.selling_price / data.expected_price + ratio_diffrence = float_compare(price_float_ratio, 0.9, precision_digits=2) + if ratio_diffrence == -1: + data.selling_price = 0 + raise ValidationError( + "The selling price cannot be lower than 90% of the expected price" + ) + + # delete opration for the process + + @api.ondelete(at_uninstall=False) + def _unlink_if_state_new_or_cancelled(self): + for data in self: + if not bool(data.state == "new" or data.state == "cancelled"): + raise UserError( + "Can't delete property which is not in new or cancelled state!" + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..52c8e7b9403 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,113 @@ +# offers model that will offer us the offers + + +from odoo import fields, models, api +from datetime import datetime +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Offer model for the properties of real estate" + price = fields.Float() + status = fields.Selection( + 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") + + property_type_id = fields.Many2one( + "estate.property.type", + related="property_id.property_type_id", + store=True, + readonly=False, + ) + + validity = fields.Integer(default=7) + + date_deadline = fields.Date( + compute="_compute_offer_deadline", inverse="_deadline_update" + ) + + # constrains of sql + + _sql_constraints = [ + ("check_price", "CHECK(price > 0)", "Offered price must be strictly positive") + ] + + # order in which data is fetched + + _order = "price desc" + + # deadline will be computed based upon the validity date + @api.depends("validity") + def _compute_offer_deadline(self): + for offer in self: + if not offer.create_date: + offer.date_deadline = datetime.now() + relativedelta( + days=(offer.validity or 0) + ) + else: + offer.date_deadline = offer.create_date + relativedelta( + days=(offer.validity or 0) + ) + + # deadline date can also be changed and once this is saved validity will be updated + def _deadline_update(self): + for offer in self: + offer.validity = ( + offer.date_deadline - (offer.create_date or datetime.now()).date() + ).days + + # action for the accepting the offer + def action_offer_confirm(self): + for record in self: + # since saling price is only updated when offer is accepted therefore it validates if offer + # is already accepted than warning + + if record.property_id.selling_price > 0: + raise UserError("Offer price already accepted for the property") + + record.status = "accepted" + record.property_id.selling_price = self.price + record.property_id.partner_id = record.partner_id + record.property_id.state = "offer_accepted" + + # action for the refusal of the status + def action_offer_refuse(self): + for record in self: + if record.status == "accepted": + record.property_id.selling_price = 0 + record.property_id.partner_id = False + record.status = "refused" + + # now in case of offer creation CRUD + # self will be a proxy object , + # property_id feilds is available in vals + @api.model_create_multi + def create(self, vals): + # will check the offer value and does property has other offers which are max thw\an this one + for value in vals: + property_details = self.env["estate.property"].browse( + value.get("property_id") + ) + for property_data in property_details: + offers_list = property_data.mapped("offer_ids.price") + max_offer = max(offers_list, default=0) + comparison_result = float_compare( + value.get("price"), max_offer, precision_digits=2 + ) + + if comparison_result == -1: + raise UserError("Offer with a lower amount than an existing offer") + + if property_data.state == "new": + property_data.state = "offer_received" + + return super().create(vals) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..81b2b19aa22 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Tag model for the estate properties" + name = fields.Char(required=True) + color = fields.Integer(default=1) + _sql_constraints = [("check_uniquness", " UNIQUE(name)", "Tag name must be unique")] + + # order on which data will be fetched + _order = "name desc" diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..67de059518c --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,33 @@ +from odoo import fields, models, api + + +# Property type model for properties +# prpoerties can be of type house, penthouse, etc. +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Type of properties of estate model" + name = fields.Char(required=True) + + property_ids = fields.One2many("estate.property", "property_type_id") + + offer_ids = fields.One2many( + "estate.property.offer", "property_type_id", "Offer For Property Type" + ) + + offer_count = fields.Integer(compute="_compute_offer_count") + sequence = fields.Integer("Sequence", default=1) + + # sql constrains ::: + + _sql_constraints = [ + ("check_uniquness", " UNIQUE(name)", "Type of property name must be unique") + ] + + # order on which data will be fetched + + _order = "sequence, name desc" + + @api.depends("offer_ids") + def _compute_offer_count(self): + for data in self: + data.offer_count = len(data.offer_ids) diff --git a/estate/models/inherited_res_users.py b/estate/models/inherited_res_users.py new file mode 100644 index 00000000000..3c62e3a51b7 --- /dev/null +++ b/estate/models/inherited_res_users.py @@ -0,0 +1,13 @@ +# This model inherits from res.users model + +from odoo import models, fields + + +class InheritedResUsers(models.Model): + _inherit = "res.users" + property_ids = fields.One2many( + "estate.property", + "user_id", + "Estate Property", + domain=[("state", "in", ["new", "offer_received"])], + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..8ba48a75b9e --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property, ir.model.access,model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,ir.model.access,model_estate_property_type,base.group_user,1,1,1,1 + +estate.access_estate_property_tag,ir.model.access,model_estate_property_tag,base.group_user,1,1,1,1 + +estate.access_estate_property_offer,ir.model.access,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..265ec21a3e6 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..709eab0492e --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,23 @@ + + + + + Offers + estate.property.offer + list,form + + + + estate.property.list + estate.property.offer + + + + + + + + + + + \ 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..4678cf09687 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,11 @@ + + + + + + Property Tag + 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..ac949a6b992 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,66 @@ + + + + + + + + + Property Type + estate.property.type + list,form + + + + + estate.property.type.list + estate.property.type + + + + + + + + + + + estate.property.type.form + 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 new file mode 100644 index 00000000000..a33f5a67750 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,160 @@ + + + Properties + estate.property + list,form + {'search_default_available': 1} + + + + Property Type + estate.property.type + list,form + + + + + Property Tag + estate.property.tag + list,form + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +