diff --git a/real_estate/__init__.py b/real_estate/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/real_estate/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py
new file mode 100644
index 00000000000..33bc396d702
--- /dev/null
+++ b/real_estate/__manifest__.py
@@ -0,0 +1,27 @@
+{
+ 'name': 'Real estate',
+ 'version': '0.1.0',
+ 'summary': 'Manage real estate properties',
+ 'sequence': '1',
+ 'description': """
+Buying & selling Properties
+===========================
+This Module provide functionalities from where you manage the real estate properties from finding buyer to get best price.
+ """,
+ 'category': 'sales',
+ 'website': 'https://www.ishw.tech',
+ 'depends': [
+ 'base'
+ ],
+ '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_menus.xml',
+ ],
+ 'installable': True,
+ 'application': True,
+ 'author': 'Ishwar',
+ 'license': 'LGPL-3',
+}
diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py
new file mode 100644
index 00000000000..ab17290997b
--- /dev/null
+++ b/real_estate/models/__init__.py
@@ -0,0 +1,4 @@
+from . import real_estate_property
+from . import real_estate_property_type
+from . import real_estate_property_tag
+from . import real_estate_property_offer
diff --git a/real_estate/models/real_estate_property.py b/real_estate/models/real_estate_property.py
new file mode 100644
index 00000000000..be0eb485bc7
--- /dev/null
+++ b/real_estate/models/real_estate_property.py
@@ -0,0 +1,131 @@
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.float_utils import float_compare, float_is_zero
+
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "Real Estate Property"
+
+ name = fields.Char(required=True)
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date(
+ copy=False,
+ default=lambda self: fields.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(
+ selection=[
+ ('north', 'North'),
+ ('south', 'South'),
+ ('east', 'East'),
+ ('west', 'West'),
+ ],
+ help='Garden facing direction',
+ )
+ active = fields.Boolean(default=True)
+ state = fields.Selection(
+ string='States',
+ selection=[
+ ('new', 'New'),
+ ('offer_received', 'Offer Received'),
+ ('offer_accepted', 'Offer Accepted'),
+ ('sold', 'Sold'),
+ ('cancelled', 'Cancelled'),
+ ],
+ required=True,
+ copy=False,
+ default='new',
+ )
+ property_type_id = fields.Many2one("estate.property.type")
+ buyer_id = fields.Many2one("res.partner", copy=False)
+ salesperson_id = fields.Many2one(
+ "res.users", string="Sales Person", default=lambda self: self.env.user
+ )
+ tag_ids = fields.Many2many("estate.property.tag")
+ offer_ids = fields.One2many("estate.property.offer", "property_id")
+ total_area = fields.Float(compute="_compute_total_area")
+ best_price = fields.Float(compute="_compute_best_price")
+
+ @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_price(self):
+ for record in self:
+ if not record.mapped("offer_ids.price"):
+ record.best_price = 0
+ else:
+ record.best_price = max(record.mapped("offer_ids.price"))
+
+ @api.onchange('garden')
+ def _onchange_garden(self):
+ for record in self:
+ if record.garden:
+ record.garden_area = 10
+ record.garden_orientation = 'north'
+ else:
+ record.garden_area = 0
+ record.garden_orientation = False
+
+ def action_sold(self):
+ for record in self:
+ if record.state == 'cancelled':
+ raise UserError("Cancelled Properties Can Not be sold")
+ if not record.buyer_id:
+ raise UserError("Can not sold property who has no buyer")
+ record.state = 'sold'
+ return True
+
+ def action_cancelled(self):
+ for record in self:
+ if record.state == 'sold':
+ raise UserError("Sold Properties can not be cancel")
+ record.state = 'cancelled'
+ return True
+
+ def action_approve(self):
+ for record in self:
+ if record.state == 'sold':
+ raise UserError("Sold properties cannot accept other Offers.")
+ target_offer = record.offer_ids.filtered(lambda r: r.price == record.best_price)
+ if target_offer:
+ target_offer = target_offer[0]
+ target_offer.status = 'accepted'
+ (record.offer_ids - target_offer).status = 'refused'
+ record.selling_price = target_offer.price
+ record.buyer_id = target_offer.partner_id
+ record.state = 'offer_accepted'
+
+ @api.constrains('expected_price', 'selling_price')
+ def _check_price(self):
+ for record in self:
+ if record.expected_price <= 0:
+ raise ValidationError("Expected Price Must be Positive")
+ if record.selling_price < 0:
+ raise ValidationError("Selling price Must be Positive")
+
+ @api.constrains('selling_price', 'expected_price')
+ def _check_expected_selling_price(self):
+ for record in self:
+ if float_is_zero(record.selling_price, precision_digits=2):
+ continue
+
+ expected_selling_price = record.expected_price * 0.9
+ if float_compare(record.selling_price, expected_selling_price, precision_digits=2) < 0:
+ raise ValidationError("Selling price Must be 90% of the expected price")
diff --git a/real_estate/models/real_estate_property_offer.py b/real_estate/models/real_estate_property_offer.py
new file mode 100644
index 00000000000..35c9af4e435
--- /dev/null
+++ b/real_estate/models/real_estate_property_offer.py
@@ -0,0 +1,57 @@
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError, ValidationError
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Offers on Buy or Sell for properties"
+
+ price = fields.Float()
+ status = fields.Selection(
+ [
+ ("refused", "Refused"),
+ ("accepted", "Accepted"),
+ ],
+ copy=False,
+ )
+ partner_id = fields.Many2one("res.partner", required=True)
+ property_id = fields.Many2one("estate.property", required=True)
+ validity = fields.Integer(default=7)
+ date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_deadline")
+
+ @api.depends('validity', 'create_date')
+ def _compute_deadline(self):
+ for record in self:
+ start_date = record.create_date or fields.Date.today()
+ record.date_deadline = start_date + relativedelta(days=record.validity)
+
+ def _inverse_deadline(self):
+ for record in self:
+ start_date = record.create_date or fields.Date.today()
+ record.validity = (record.date_deadline - fields.Date.to_date(start_date)).days
+
+ def action_accept(self):
+ for record in self:
+ if record.property_id.buyer_id:
+ raise UserError("You can accept offer only once per property")
+ record.status = 'accepted'
+ record.property_id.buyer_id = record.partner_id
+ record.property_id.selling_price = record.price
+ (record.property_id.offer_ids - record).status = 'refused'
+ record.property_id.state = 'offer_accepted'
+ return True
+
+ def action_refuse(self):
+ for record in self:
+ record.status = 'refused'
+ return True
+
+ @api.constrains('price')
+ def _check_offer_price(self):
+ for record in self:
+ if record.price <= 0:
+ raise ValidationError("Offer Price Must be Positive")
+
+ _check_partner = models.Constraint('UNIQUE(partner_id, property_id)', "User have already made a offer on this property")
diff --git a/real_estate/models/real_estate_property_tag.py b/real_estate/models/real_estate_property_tag.py
new file mode 100644
index 00000000000..4214f5047b5
--- /dev/null
+++ b/real_estate/models/real_estate_property_tag.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+
+
+class EstatePropertyTag(models.Model):
+ _name = 'estate.property.tag'
+ _description = 'This are tags used to identify property'
+
+ name = fields.Char(required=True)
+
+ _check_tag_name = models.Constraint('UNIQUE(name)', "Tag name Must be unique")
diff --git a/real_estate/models/real_estate_property_type.py b/real_estate/models/real_estate_property_type.py
new file mode 100644
index 00000000000..e1b60cd10c4
--- /dev/null
+++ b/real_estate/models/real_estate_property_type.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "Real Estate Property Types"
+
+ name = fields.Char(required=True)
+
+ _check_type_name = models.Constraint('UNIQUE(name)', "Type must be Unique")
diff --git a/real_estate/security/ir.model.access.csv b/real_estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..89f97c50842
--- /dev/null
+++ b/real_estate/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+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_tag,access_estate_property_tag,model_estate_property_tag,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
diff --git a/real_estate/views/estate_menus.xml b/real_estate/views/estate_menus.xml
new file mode 100644
index 00000000000..1abb970cc41
--- /dev/null
+++ b/real_estate/views/estate_menus.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/real_estate/views/estate_property_tag_views.xml b/real_estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..632da6e51b3
--- /dev/null
+++ b/real_estate/views/estate_property_tag_views.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ estate.property.tag.view.list
+ estate.property.tag
+
+
+
+
+
+
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
+
diff --git a/real_estate/views/estate_property_type_views.xml b/real_estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..8339fae15e1
--- /dev/null
+++ b/real_estate/views/estate_property_type_views.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ estate.property.type.view.list
+ estate.property.type
+
+
+
+
+
+
+
+
+ estate.property.type.view.form
+ estate.property.type
+
+
+
+
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+
diff --git a/real_estate/views/estate_property_views.xml b/real_estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..301a5dc85aa
--- /dev/null
+++ b/real_estate/views/estate_property_views.xml
@@ -0,0 +1,138 @@
+
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ estate_property_search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ properties
+ estate.property
+ list,form
+
+
+