diff --git a/.gitignore b/.gitignore
index b6e47617de1..242b7777db4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ downloads/
eggs/
.eggs/
lib/
+.idea/
lib64/
parts/
sdist/
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..bc5f7e9a33c
--- /dev/null
+++ b/real_estate/__manifest__.py
@@ -0,0 +1,22 @@
+{
+ 'name': "real.estate",
+ 'summary': "summary of the real estate",
+ 'description': """Rugot first module""",
+ 'author': "Ruchita Gothi (Rugot)",
+ 'category': 'Real Estate',
+ 'version': '0.1',
+ 'depends': ['base', 'web'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/real_estate_property_offer_views.xml',
+ 'views/real_estate_property_views.xml',
+ 'views/real_estate_property_type_views.xml',
+ 'views/real_estate_tag_views.xml',
+ 'views/real_estate_property_maintenance_request_views.xml',
+ 'views/res_users_views.xml',
+ 'views/real_estate_menus.xml',
+ ],
+ 'application': True,
+ 'installable': True,
+ 'license': 'LGPL-3',
+}
diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py
new file mode 100644
index 00000000000..25960519ea3
--- /dev/null
+++ b/real_estate/models/__init__.py
@@ -0,0 +1,6 @@
+from . import real_estate_property
+from . import real_estate_tag
+from . import real_estate_property_offer
+from . import real_estate_property_type
+from . import real_estate_property_maintenance_request
+from . import res_users
diff --git a/real_estate/models/real_estate_property.py b/real_estate/models/real_estate_property.py
new file mode 100644
index 00000000000..aa322e0111c
--- /dev/null
+++ b/real_estate/models/real_estate_property.py
@@ -0,0 +1,150 @@
+from datetime import timedelta
+
+from odoo import models, fields, api
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools import float_is_zero, float_compare
+
+
+class real_estate(models.Model):
+ _name = 'real.estate'
+ _description = 'Real Estate Property'
+ _order = "id desc"
+
+ name = fields.Char(required=True)
+ property_type_id = fields.Many2one(
+ "real.estate.property.type", string="Property Type")
+ street_address = fields.Char()
+ description = fields.Text()
+ postcode = fields.Integer()
+ date_availability = fields.Datetime(default=lambda self: fields.Datetime.now() + timedelta(days=90))
+ expected_price = fields.Float()
+ bedrooms = fields.Integer(default=2)
+ living_area = fields.Integer()
+ bathrooms = 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')
+ ])
+ active = fields.Boolean(default=True)
+ tag_ids = fields.Many2many(
+ "real.estate.tag", string="Tags", ondelete='cascade')
+ offer_ids = fields.One2many(
+ "real.estate.property.offer", "property_id", string="Offers")
+ total_area = fields.Float(compute="_compute_total", store=True)
+ best_price = fields.Float(
+ string="Best Offer",
+ compute="_compute_best_price",
+ store=True)
+ # ist_time = fields.Char(
+ # string="Created On (IST)",
+ # compute="_compute_create_date_ist",
+ # store=True)
+ stage = fields.Selection([
+ ('new', 'New'),
+ ('offer_received', 'Offer Received'),
+ ('offer_accepted', 'Offer Accepted'),
+ ('sold', 'Sold'),
+ ('cancelled', 'Cancelled'),
+ ], default='new')
+ buyer_id = fields.Many2one(
+ 'res.partner',
+ string='Buyer',
+ copy=False)
+ selling_price = fields.Float()
+ maintenance_request_ids = fields.One2many(
+ "real.estate.property.maintenance.request", "property_id", string="Maintenance Requests")
+ total_maintenance_cost = fields.Float(compute="_compute_total_maintenance_cost", store=True, string="Total Cost")
+ salesperson_id = fields.Many2one(
+ 'res.users',
+ string='Salesperson'
+ )
+ _check_expected_price_positive = models.Constraint(
+ 'CHECK(expected_price > 0)',
+ 'The expected price must be strictly positive.',
+ )
+
+ @api.constrains('expected_price', 'selling_price')
+ def _check_selling_price(self):
+ for rec in self:
+ if float_is_zero(rec.selling_price, precision_rounding=0.01):
+ continue
+ if float_compare(
+ rec.selling_price,
+ rec.expected_price * 0.9,
+ precision_rounding=0.01) < 0:
+ raise ValidationError(
+ 'The selling price cannot be lower than 90% of the expected price.')
+
+ @api.depends('offer_ids.price')
+ def _compute_best_price(self):
+ for record in self:
+ prices = []
+ for offer in record.offer_ids:
+ prices.append(offer.price)
+ record.best_price = max(prices) if prices else 0.0
+
+ @api.depends('maintenance_request_ids.cost')
+ def _compute_total_maintenance_cost(self):
+ for record in self:
+ costs = record.maintenance_request_ids.mapped('cost')
+ record.total_maintenance_cost = sum(costs) if costs else 0.0
+
+ @api.depends('living_area', 'garden_area')
+ def _compute_total(self):
+ for record in self:
+ record.total_area = (record.living_area or 0) + (record.garden_area or 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
+
+ @api.ondelete(at_uninstall=False)
+ def _check_property_delete(self):
+ for record in self:
+ if record.stage not in ('new', 'cancelled'):
+ raise UserError(
+ "You can only delete properties in New or Cancelled state."
+ )
+
+ # @api.depends('create_date')
+ # def _compute_create_date_ist(self):
+ # for rec in self:
+ # if rec.create_date:
+ # ist_dt = fields.Datetime.context_timestamp(
+ # rec, rec.create_date
+ # )
+ # rec.ist_time = ist_dt.strftime("%Y-%m-%d %H:%M:%S")
+ # else:
+ # rec.ist_time = False
+
+ def action_cancel(self):
+ if self.stage == 'sold':
+ raise UserError("A sold property cannot be cancelled.")
+ self.stage = 'cancelled'
+
+ def action_sold(self):
+ if self.stage == 'cancelled':
+ raise UserError("A cancelled property cannot be sold.")
+ maintenace_request = self.maintenance_request_ids.filtered_domain([('status', '!=', 'done')])
+ if maintenace_request:
+ raise UserError("CProperty cannot be sold , there is any maintenance request not done")
+ self.stage = 'sold'
+
+ @api.constrains('expected_price')
+ def _check_expected_price(self):
+ for rec in self:
+ if rec.expected_price < 0:
+ raise ValidationError(
+ 'The selling price cannot be lower than 90% of the expected price.'
+ )
diff --git a/real_estate/models/real_estate_property_maintenance_request.py b/real_estate/models/real_estate_property_maintenance_request.py
new file mode 100644
index 00000000000..88263461747
--- /dev/null
+++ b/real_estate/models/real_estate_property_maintenance_request.py
@@ -0,0 +1,28 @@
+from odoo.exceptions import UserError
+
+from odoo import fields, models, api
+
+
+class real_estate_properties_maintenance_request(models.Model):
+ _name = 'real.estate.property.maintenance.request'
+ _description = 'Real Estate Property Maintenance Request'
+
+ name = fields.Char()
+ cost = fields.Integer()
+ status = fields.Selection([
+ ('new', 'New'),
+ ('approved', 'Approved'),
+ ('done', 'Done'),
+ ], string="Status", copy=False, default='new')
+ property_id = fields.Many2one('real.estate', string='Property', ondelete='restrict')
+
+ @api.onchange('status')
+ def _check_cost_on_accepted_status(self):
+ if self.status == 'approved' and self.cost <= 0:
+ raise UserError("Approved cost must be greater than 0")
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_if_maintenance_request_not_done(self):
+ maintenace_request = self.filtered_domain([('status', '!=', 'done')])
+ if maintenace_request:
+ raise UserError("Can't delete an active Maintenance Request Record!")
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..2db29d468f2
--- /dev/null
+++ b/real_estate/models/real_estate_property_offer.py
@@ -0,0 +1,149 @@
+from datetime import timedelta
+# from collections import defaultdict
+
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+from odoo.tools import float_compare
+
+
+class real_estate_property_offer(models.Model):
+ _name = 'real.estate.property.offer'
+ _description = 'Real Estate Property Offer'
+ _order = "price desc"
+
+ price = fields.Float(required=True)
+ property_id = fields.Many2one('real.estate', string='Property', ondelete='restrict')
+ property_type_id = fields.Many2one(
+ related="property_id.property_type_id",
+ store=True
+ )
+ status = fields.Selection([
+ ('accepted', 'Accepted'),
+ ('refused', 'Refused'),
+ ], string="Status", copy=False)
+ partner_id = fields.Many2one('res.partner', string="Buyer", required=True)
+ validity = fields.Integer(default=7)
+ date_deadline = fields.Date(
+ string="Deadline",
+ compute="_compute_date_deadline",
+ inverse="_inverse_date_deadline",
+ store=True
+ )
+ _check_offer_price_positive = models.Constraint(
+ 'CHECK(price > 0)',
+ 'The offer price must be strictly positive.',
+ )
+
+ # @api.model
+ # def create(self, vals):
+ # grouped = defaultdict(list)
+ # for val in vals:
+ # property_id = val.get('property_id')
+ # price = val.get('price')
+ # if property_id and price:
+ # grouped[property_id].append(price)
+ #
+ # for property_id, new_prices in grouped.items():
+ # property_rec = self.env['real.estate'].browse(property_id)
+ # existing_prices = property_rec.best_price
+ # new_max = max(new_prices)
+ # max_price = max(existing_prices, new_max)
+ # for price in new_prices:
+ # if price < max_price:
+ # raise UserError(
+ # "Only the highest offer is allowed"
+ # )
+ # offers = super().create(vals)
+ # for offer in offers:
+ # if offer.property_id and offer.property_id.stage == 'new':
+ # offer.property_id.stage = 'offer_received'
+ # return offers
+
+ @api.model
+ def create(self, vals):
+ for val in vals:
+ price = val.get('price')
+ property_id = val.get('property_id')
+ property = self.env['real.estate'].browse(property_id)
+ if property.stage == "new":
+ property.best_price = price
+ elif float_compare(price, property.best_price, precision_rounding=0.01) < 0:
+ raise UserError(
+ f"Price should be greater than {property.best_price}")
+ else:
+ property.best_price = price
+ if property and property.stage == 'new':
+ property.stage = 'offer_received'
+
+ return super().create(vals)
+
+ # @api.model
+ # def create(self, vals):
+ # for property_id, n
+ # for val in vals:
+ # property_id = val.get('property_id')
+ # price = val.get('price')
+ # if property_id and price:
+ # property_rec = self.env['real.estate'].browse(property_id)
+ # if property_rec.offer_ids:
+ # # max_offer = max(property_rec.offer_ids.mapped('price'))
+ # if price < property_rec.best_price:
+ # raise UserError(
+ # "The offer must be higher than existing offers."
+ # )
+ # offer = super().create(vals)
+ # if offer.property_id and offer.property_id.stage == 'new':
+ # offer.property_id.stage = 'offer_received'
+ # return offer
+
+ @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:
+ offer.date_deadline = fields.Date.today() + timedelta(days=offer.validity)
+
+ def _inverse_date_deadline(self):
+ for offer in self:
+ if offer.create_date and offer.date_deadline:
+ offer.validity = (offer.date_deadline - offer.create_date.date()).days
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_if_accepted_offer(self):
+ accepted_offer = self.filtered_domain([('status', '=', 'accepted')])
+ if accepted_offer:
+ raise UserError("Can't delete an active record!")
+
+ def action_accept(self):
+ # accepted_offer = self.search([
+ # ('property_id', '=', self.property_id.id),
+ # ('status', '=', 'accepted')
+ # ], limit=1)
+ # accepted_offer = self.property_id.offer_ids.filtered(
+ # lambda o: o.status == 'accepted'
+ # )
+ accepted_offer = self.property_id.offer_ids.filtered_domain([
+ ('status', '=', 'accepted')
+ ])
+ maintenace_request = self.property_id.maintenance_request_ids.filtered_domain([('status', '!=', 'done')])
+ if accepted_offer:
+ raise UserError(
+ "Only one offer can be accepted for a property.")
+ if maintenace_request:
+ raise UserError("Property cannot be sold , there is any maintenance request not done")
+ self.status = 'accepted'
+ self.property_id.write({
+ 'selling_price': self.price,
+ 'stage': 'offer_accepted',
+ 'buyer_id': self.partner_id.id,
+ })
+ refused_offer = self.property_id.offer_ids.filtered_domain([
+ ('id', '!=', 'self.id'),
+ ('status', '!=', 'accepted')
+ ])
+ for refuse in refused_offer:
+ refuse.status = 'refused'
+
+ def action_refuse(self):
+ self.status = 'refused'
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..bac5ceccd8f
--- /dev/null
+++ b/real_estate/models/real_estate_property_type.py
@@ -0,0 +1,31 @@
+from odoo import fields, models
+
+
+class RealEstateTag(models.Model):
+ _name = 'real.estate.property.type'
+ _description = 'Real Estate Property Type'
+ _order = "sequence, name desc"
+
+ name = fields.Char(required=True)
+ sequence = fields.Integer(string="Sequence", default=10)
+ property_ids = fields.One2many(
+ 'real.estate',
+ 'property_type_id',
+ string='Properties'
+ )
+ offer_ids = fields.One2many(
+ "real.estate.property.offer",
+ "property_type_id",
+ string="Offers"
+ )
+ _unique_type_name = models.Constraint(
+ 'UNIQUE(name)',
+ 'The property type name must be unique.',
+ )
+ offer_count = fields.Integer(
+ compute="_compute_offer_count"
+ )
+
+ def _compute_offer_count(self):
+ for record in self:
+ record.offer_count = len(record.offer_ids)
diff --git a/real_estate/models/real_estate_tag.py b/real_estate/models/real_estate_tag.py
new file mode 100644
index 00000000000..60f9a5c06b0
--- /dev/null
+++ b/real_estate/models/real_estate_tag.py
@@ -0,0 +1,19 @@
+from random import randint
+
+from odoo import fields, models
+
+
+class RealEstateTag(models.Model):
+ _name = 'real.estate.tag'
+ _description = 'Real Estate Tag'
+ _order = "name desc"
+
+ def _get_default_color(self):
+ return randint(1, 11)
+
+ name = fields.Char(required=True)
+ color = fields.Integer(default=_get_default_color)
+ _unique_tag_name = models.Constraint(
+ 'UNIQUE(name)',
+ 'The property tag name must be unique.',
+ )
diff --git a/real_estate/models/res_users.py b/real_estate/models/res_users.py
new file mode 100644
index 00000000000..572dd1ecac3
--- /dev/null
+++ b/real_estate/models/res_users.py
@@ -0,0 +1,11 @@
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ property_ids = fields.One2many(
+ 'real.estate',
+ 'salesperson_id',
+ string='Assigned Properties'
+ )
diff --git a/real_estate/security/ir.model.access.csv b/real_estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..970d60f19f0
--- /dev/null
+++ b/real_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
+real_estate,real_estate,model_real_estate,base.group_user,1,1,1,1
+real_estate_tag,real_estate_tag,model_real_estate_tag,base.group_user,1,1,1,1
+real_estate_property_offer,real_estate_property_offer,model_real_estate_property_offer,base.group_user,1,1,1,1
+real_estate_property_type,real_estate_property_type,model_real_estate_property_type,base.group_user,1,1,1,1
+real_estate_property_maintenance_request,real_estate_property_maintenance_request,model_real_estate_property_maintenance_request,base.group_user,1,1,1,1
+,,,,,,,
\ No newline at end of file
diff --git a/real_estate/views/real_estate_menus.xml b/real_estate/views/real_estate_menus.xml
new file mode 100644
index 00000000000..f35786b7999
--- /dev/null
+++ b/real_estate/views/real_estate_menus.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/real_estate/views/real_estate_property_maintenance_request_views.xml b/real_estate/views/real_estate_property_maintenance_request_views.xml
new file mode 100644
index 00000000000..8f82d5f7f77
--- /dev/null
+++ b/real_estate/views/real_estate_property_maintenance_request_views.xml
@@ -0,0 +1,43 @@
+
+
+
+ real.estate.property.maintenance.request.view.tree
+ real.estate.property.maintenance.request
+
+
+
+
+
+
+
+
+
+
+
+ real.estate.property.maintenance.request.view.form
+ real.estate.property.maintenance.request
+
+
+
+
+
+
+ Maintenance request
+ real.estate.property.maintenance.request
+ list,form
+
+
+
diff --git a/real_estate/views/real_estate_property_offer_views.xml b/real_estate/views/real_estate_property_offer_views.xml
new file mode 100644
index 00000000000..4a69e6bd2a2
--- /dev/null
+++ b/real_estate/views/real_estate_property_offer_views.xml
@@ -0,0 +1,65 @@
+
+
+
+ real.estate.property.offer.view.tree
+ real.estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ real.estate.property.offer.view.form
+ real.estate.property.offer
+
+
+
+
+
+
+ Property Offer
+ real.estate.property.offer
+ list,form
+
+
+
+ Property Offer
+ real.estate.property.offer
+ list
+ [('property_type_id', '=', active_id)]
+
+
+
diff --git a/real_estate/views/real_estate_property_type_views.xml b/real_estate/views/real_estate_property_type_views.xml
new file mode 100644
index 00000000000..fab81b7db26
--- /dev/null
+++ b/real_estate/views/real_estate_property_type_views.xml
@@ -0,0 +1,57 @@
+
+
+
+ real.estate.property.type.view.tree
+ real.estate.property.type
+
+
+
+
+
+
+
+
+
+ real.estate.property.type.view.form
+ real.estate.property.type
+
+
+
+
+
+ Property Type
+ real.estate.property.type
+ list,form
+
+
+
diff --git a/real_estate/views/real_estate_property_views.xml b/real_estate/views/real_estate_property_views.xml
new file mode 100644
index 00000000000..733a63704d3
--- /dev/null
+++ b/real_estate/views/real_estate_property_views.xml
@@ -0,0 +1,167 @@
+
+
+
+ real.estate.view.list
+ real.estate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ real.estate.view.form
+ real.estate
+
+
+
+
+
+
+ real.estate.view.search
+ real.estate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Real Estate
+ real.estate
+ list,form
+
+ {
+ 'search_default_active_record': 1,
+ }
+
+
+
+
diff --git a/real_estate/views/real_estate_tag_views.xml b/real_estate/views/real_estate_tag_views.xml
new file mode 100644
index 00000000000..aab44a3ce64
--- /dev/null
+++ b/real_estate/views/real_estate_tag_views.xml
@@ -0,0 +1,35 @@
+
+
+
+ real.estate.tag.view.tree
+ real.estate.tag
+
+
+
+
+
+
+
+
+
+ real.estate.tag.view.form
+ real.estate.tag
+
+
+
+
+
+
+ Property Tag
+ real.estate.tag
+ list,form
+
+
+
diff --git a/real_estate/views/res_users_views.xml b/real_estate/views/res_users_views.xml
new file mode 100644
index 00000000000..56364a561be
--- /dev/null
+++ b/real_estate/views/res_users_views.xml
@@ -0,0 +1,33 @@
+
+
+
+ res.users.view.form.inherit.real.estate
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/real_estate_account/__init__.py b/real_estate_account/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/real_estate_account/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/real_estate_account/__manifest__.py b/real_estate_account/__manifest__.py
new file mode 100644
index 00000000000..8721133bb59
--- /dev/null
+++ b/real_estate_account/__manifest__.py
@@ -0,0 +1,15 @@
+{
+ 'name': "real.estate.account",
+ 'summary': "summary of the real estate account",
+ 'description': """Link between Estate and Accounting""",
+ 'author': "Ruchita Gothi (Rugot)",
+ 'category': 'Real Estate',
+ 'version': '0.1',
+ 'depends': ['real_estate', 'account'],
+ 'data': [
+ # 'security/ir.model.access.csv',
+ ],
+ 'application': True,
+ 'installable': True,
+ 'license': 'LGPL-3',
+}
diff --git a/real_estate_account/models/__init__.py b/real_estate_account/models/__init__.py
new file mode 100644
index 00000000000..c97e681daec
--- /dev/null
+++ b/real_estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import real_estate_property
diff --git a/real_estate_account/models/real_estate_property.py b/real_estate_account/models/real_estate_property.py
new file mode 100644
index 00000000000..e37f7e244cb
--- /dev/null
+++ b/real_estate_account/models/real_estate_property.py
@@ -0,0 +1,26 @@
+from odoo import models
+
+
+class real_estate(models.Model):
+ _inherit = 'real.estate'
+
+ def action_sold(self):
+ res = super().action_sold()
+ for property in self:
+ self.env['account.move'].create({
+ 'partner_id': property.buyer_id.id,
+ 'move_type': 'out_invoice',
+ 'invoice_line_ids': [
+ ({
+ 'name': '6% of selling price',
+ 'quantity': 1,
+ 'price_unit': property.selling_price * 0.06,
+ }),
+ ({
+ 'name': 'Administrative fees',
+ 'quantity': 1,
+ 'price_unit': 100.0,
+ }),
+ ],
+ })
+ return res
diff --git a/real_estate_account/security/ir.model.access.csv b/real_estate_account/security/ir.model.access.csv
new file mode 100644
index 00000000000..9581ebac755
--- /dev/null
+++ b/real_estate_account/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
+,,,,,,,
\ No newline at end of file