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..c3be819db08
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,26 @@
+{
+ 'name': 'Real Estate',
+ 'version': '1.0',
+ 'depends': ['base'],
+ 'author': 'viwar-odoo',
+ 'category': 'real estate',
+ 'description': "real estate App.",
+ "data": [
+ "security/security_groups.xml",
+ "security/ir.model.access.csv",
+ "views/estate_maintainance_form.xml",
+ "views/estate_property_visit_calender.xml",
+ "views/estate_property_visit_kanban_view.xml",
+ "views/estate_property_offer_view.xml",
+ "views/estate_property_tag_view.xml",
+ "views/estate_property_type_view.xml",
+ "views/estate_property_visit_action.xml",
+ "views/estate_property_view.xml",
+ "views/estate_menus.xml",
+ # "data/estate_property_demo.xml"
+ ],
+ 'application': True,
+ 'installable': True,
+ 'license': 'LGPL-3',
+ 'website': 'https://odoo.com',
+}
diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml
new file mode 100644
index 00000000000..3e60cc1424a
--- /dev/null
+++ b/estate/data/estate_property_demo.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ 1
+ tag1
+
+
+
+ 1
+ type1
+
+
+
+ 1
+ property
+
+ 1
+
+
+
+ 1
+ 100000
+ 1
+
+
+
+
+
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..64e711efa88
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,6 @@
+from . import estate_maintainance_request
+from . import estate_property
+from . import estate_property_offer
+from . import estate_property_tag
+from . import estate_property_type
+from . import estate_property_visit
diff --git a/estate/models/estate_maintainance_request.py b/estate/models/estate_maintainance_request.py
new file mode 100644
index 00000000000..4b8f11ec6b2
--- /dev/null
+++ b/estate/models/estate_maintainance_request.py
@@ -0,0 +1,37 @@
+from odoo import api, models, fields
+
+
+class EstateMaintainanceRequest(models.Model):
+ _name = 'estate.maintainance.request'
+ _description = 'table for the technician maintainance request'
+
+ property_id = fields.Many2one('estate.property', required=True, readonly=True)
+ buyer_id = fields.Many2one('res.users', required=True, default='self.env.uid')
+ technician_id = fields.Many2one('res.partner', required=False)
+ state = fields.Selection(
+ string='state',
+ default='new',
+ selection=[
+ ('new', "New"),
+ ('assigned', "Assigned"),
+ ('inprogress', "Inprogress"),
+ ('done', "Done"),
+ ('cancelled', "Cancelled"),
+ ],
+ )
+ estimate_cost = fields.Float(required=False)
+ actual_cost = fields.Float(compute='_compute_actual_cost', store=True)
+ current_stage = fields.Char(compute='_compute_state_after_assigned', store=True)
+
+ @api.depends('state', 'estimate_cost')
+ def _compute_actual_cost(self):
+ if self.state == 'done':
+ self.actual_cost = self.estimate_cost * 1.18
+
+ @api.depends('technician_id', 'state')
+ def _compute_state_after_assigned(self):
+ if self.state == 'new' and self.technician_id:
+ self.state = 'assigned'
+ self.current_stage = 'assigned'
+ else:
+ self.current_stage = self.state
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..0a1324011ca
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,151 @@
+from odoo import api, fields, models
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.float_utils import float_compare
+
+
+class EstateProperty(models.Model):
+ _name = 'estate.property'
+ _description = 'Test Model for real estate'
+ _check_positive_expected_price = models.Constraint(
+ 'CHECK (expected_price >= 0)', 'expected_price should be positive'
+ )
+ _check_positive_selling_price = models.Constraint(
+ 'CHECK (selling_price >= 0)', 'selling_price should be positive'
+ )
+ _order = 'id desc'
+
+ name = fields.Char(default='Unknown')
+ last_seen = fields.Datetime('Last Seen', default=fields.Datetime.now)
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date(
+ copy=False, default=fields.Date.add(fields.Date.today(), months=3)
+ )
+ expected_price = fields.Float()
+ selling_price = fields.Float(copy=False, readonly=True, default=0)
+ bedrooms = fields.Integer(default=2)
+ living_area = fields.Integer()
+ facades = fields.Integer()
+ garage = fields.Boolean()
+ garden = fields.Boolean()
+ state = fields.Selection(
+ string='status',
+ selection=[
+ ('new', "New"),
+ ('offer received', "Offer Received"),
+ ('offer accepted', "Offer Accepted"),
+ ('sold', "Sold"),
+ ('cancelled', "Cancelled"),
+ ],
+ default='new',
+ compute='_compute_statusbar',
+ store=True,
+ )
+ active = fields.Boolean(default=True)
+ garden_area = fields.Integer()
+ garden_orientation = fields.Selection(
+ string='garden orientation direction',
+ selection=[
+ ('north', "North"),
+ ('south', "South"),
+ ('east', "East"),
+ ('west', "West"),
+ ],
+ )
+ property_type_id = fields.Many2one('estate.property.type')
+ salesperson_id = fields.Many2one(
+ 'res.users',
+ string='Salesperson',
+ index=True,
+ default=lambda self: self.env.user,
+ )
+ buyer_id = fields.Many2one('res.partner', default='None', copy=False)
+ tag_ids = fields.Many2many('estate.property.tag')
+ offer_ids = fields.One2many('estate.property.offer', 'property_id', string='offers')
+ total_area = fields.Float(compute='_compute_total_area')
+ max_offer_price = fields.Float(
+ default=None, compute='_compute_max_offer_price', store=True
+ )
+ estate_maintainance_id = fields.One2many(
+ 'estate.maintainance.request', 'property_id'
+ )
+ visit_ids = fields.One2many('estate.property.visit', 'property_id', string='visits')
+
+ @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.onchange('garden')
+ def _onchange_garden(self):
+ if self.garden:
+ self.garden_area = 10
+ self.garden_orientation = 'north'
+ else:
+ self.garden_area = None
+ self.garden_orientation = None
+
+ def button_cancel(self):
+ if self.state == 'cancelled':
+ raise UserError('The property is already cancelled')
+ elif self.state == 'sold':
+ raise UserError('The property is already sold, you cannot cancel it')
+ else:
+ self.state = 'cancelled'
+
+ def button_sold(self):
+ if self.state == 'sold':
+ raise UserError('The property is already sold')
+ elif self.state == 'cancelled':
+ raise UserError('The property is already cancelled, and cannot be sold')
+ else:
+ self.state = 'sold'
+
+ @api.depends('offer_ids.price')
+ def _compute_max_offer_price(self):
+ if self.offer_ids:
+ new_max_offer_price = max(self.offer_ids.mapped('price'))
+ if self.max_offer_price != new_max_offer_price:
+ self.max_offer_price = new_max_offer_price
+
+ def accept_best_offer(self):
+ for offers in self.offer_ids:
+ if self.max_offer_price == offers.price:
+ offers.action_accept_offer()
+ ##################################
+ # old code
+ # offers.status = 'accepted'
+ # self.state = 'offer accepted'
+ # self.buyer_id = offers.partner_id
+ # self.selling_price = offers.price
+ ##################################
+ else:
+ offers.status = 'refused'
+
+ @api.depends('offer_ids')
+ def _compute_statusbar(self):
+ for record in self:
+ if record.offer_ids and record.state == 'new':
+ record.state = 'offer received'
+
+ @api.constrains('selling_price', 'expected_price')
+ def check_percentage(self):
+ for record in self:
+ if record.selling_price and record.expected_price:
+ if (
+ float_compare(
+ record.selling_price,
+ record.expected_price * 0.9,
+ precision_digits=1,
+ )
+ < 0
+ ):
+ raise ValidationError(
+ 'the selling perice cant be less than 90% of the expected price'
+ )
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_if_user_inactive(self):
+ for record in self:
+ if record.state not in ['cancelled', 'new']:
+ raise UserError('cannot delete - only delete from the state `new` and `cancelled`')
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..8a198117945
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,87 @@
+from odoo import fields, models, api
+from odoo.exceptions import UserError
+
+
+class EstatePropertyOffer(models.Model):
+ _name = 'estate.property.offer'
+ _description = 'estate property offer'
+ _check_positive_offer_price = models.Constraint(
+ 'CHECK (price >= 0)', 'price should be positive'
+ )
+ _order = 'price desc'
+
+ price = fields.Float(copy=False)
+ status = fields.Selection(
+ copy=False,
+ string='status',
+ selection=[('accepted', "Accepted"), ('refused', "Refused")],
+ )
+ partner_id = fields.Many2one(
+ 'res.partner', required=True, default=lambda self: self.env.user.partner_id.id
+ )
+ property_id = fields.Many2one('estate.property', required=True)
+ validity = fields.Integer(default=7, copy=False)
+ deadline = fields.Date(
+ compute='_compute_deadline', store=True, inverse='_inverse_deadline'
+ )
+
+ property_type_id = fields.Many2one(
+ 'estate.property.type', related='property_id.property_type_id', store=True
+ )
+
+ @api.depends('validity', 'deadline')
+ def _compute_deadline(self):
+ for record in self:
+ record.deadline = fields.Date.add(fields.Date.today(), days=record.validity)
+
+ def _inverse_deadline(self):
+ for record in self:
+ today_date = fields.Date.today()
+ record.validity = (record.deadline - today_date).days
+
+ def action_accept_offer(self):
+ for offer in self:
+ for offer.property_id in offer.property_id:
+ if (
+ offer.property_id.state == 'offer accepted'
+ or offer.property_id.state == 'sold'
+ ):
+ raise UserError(
+ 'An offer has already been accepted for this property.'
+ )
+ else:
+ offer.status = 'accepted'
+ offer.property_id.state = 'offer accepted'
+ offer.property_id.buyer_id = offer.partner_id
+ offer.property_id.selling_price = offer.price
+ for offers in self.property_id.offer_ids:
+ if offers != self:
+ offers.status = 'refused'
+
+ def action_reject_offer(self):
+ for offer in self:
+ if offer.status == 'accepted':
+ if offer.property_id.state == 'sold':
+ raise UserError(
+ 'the property is sold. CANT REJECT THE OFFER - THANK YOU'
+ )
+ else:
+ offer.status = 'refused'
+ offer.property_id.state = 'offer received'
+ offer.property_id.buyer_id = False
+ offer.property_id.selling_price = 0
+ elif offer.status == 'refused':
+ raise UserError('This offer has already been refused.')
+ else:
+ offer.status = 'refused'
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ property_id = self.env['estate.property'].browse(vals.get('property_id'))
+ if property_id.offer_ids:
+ max_offer_price = max(property_id.offer_ids.mapped('price'))
+ if vals.get('price', 0) <= max_offer_price:
+ raise UserError("The offer price must be higher than the current highest offer (%s)." % max_offer_price)
+ property_id.state = "offer received"
+ return super().create(vals_list)
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..3c9d5fe31cb
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,11 @@
+from odoo import fields, models
+
+
+class EstatePropertytTag(models.Model):
+ _name = 'estate.property.tag'
+ _description = 'Estate_property_tag'
+ _unique_tag = models.UniqueIndex('(name)', 'The name of the tag must be unique')
+ _order = 'name asc'
+
+ name = fields.Char(required=True)
+ color = fields.Integer()
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..faa784b7ed1
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,21 @@
+from odoo import fields, models, api
+
+
+class EstatePropertyType(models.Model):
+ _name = 'estate.property.type'
+ _description = 'Estate_property_type'
+ _unique_type = models.UniqueIndex('(name)', 'property type should be unique')
+ _order = 'sequence'
+
+ name = fields.Char(required=True)
+ sequence = fields.Integer(
+ default=1, help='Used to order the property types. Lower is better.'
+ )
+ offer_ids = fields.One2many('estate.property.offer', 'property_type_id')
+ offer_count = fields.Integer(default=0, compute='_compute_total_count', store=True)
+ property_ids = fields.One2many('estate.property', 'property_type_id')
+
+ @api.depends('offer_ids')
+ def _compute_total_count(self):
+ for rec in self:
+ rec.offer_count = len(rec.offer_ids.mapped('id'))
diff --git a/estate/models/estate_property_visit.py b/estate/models/estate_property_visit.py
new file mode 100644
index 00000000000..07ac4046892
--- /dev/null
+++ b/estate/models/estate_property_visit.py
@@ -0,0 +1,35 @@
+from odoo import api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class EstatePropertyVisit(models.Model):
+ _name = 'estate.property.visit'
+ _description = 'estate property visit'
+ _order = 'date desc'
+ _sql_const = models.UniqueIndex(
+ '(date, property_id)',
+ 'This property is already booked for a visit on this date!',
+ )
+
+ property_id = fields.Many2one('estate.property', required=True)
+ visitor_id = fields.Many2one('res.partner', required=True)
+ date = fields.Date(required=True)
+ comment = fields.Text()
+ state = fields.Selection(
+ string='status',
+ selection=[
+ ('new', "New"),
+ ('scheduled', "Scheduled"),
+ ('done', "Done"),
+ ('cancelled', "Cancelled"),
+ ],
+ default='new',
+ )
+
+ @api.constrains('date')
+ def _compute_date_clash(self):
+ for rec in self:
+ if rec.date < fields.Date.today():
+ raise ValidationError('The visit date cannot be in the past.')
+ if rec.date:
+ rec.state = 'scheduled'
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..5894250aa78
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,25 @@
+id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
+access_estate_property_admin,estate_property_admin,model_estate_property,estate.property_admin,1,1,1,1
+access_estate_maintainance_request,access_estate_maintainance_request,model_estate_maintainance_request,estate.property_admin,1,1,1,1
+access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,estate.property_admin,1,1,1,1
+access_estate_property_visit,access_estate_property_visit,model_estate_property_visit,estate.property_admin,1,1,1,1
+access_estate_property_type,access_estate_property_type,model_estate_property_type,estate.property_admin,1,1,1,1
+access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,estate.property_admin,1,1,1,1
+access_estate_property_agent,estate_property_agent,model_estate_property,estate.property_agent,1,1,1,0
+access_estate_property_offer_agent,estate_property_offer_agent,model_estate_property_offer,estate.property_agent,1,1,1,0
+access_estate_maintainance_request_agent,access_estate_maintainance_request_agent,model_estate_maintainance_request,estate.property_agent,1,0,0,0
+access_estate_property_visit_agent,access_estate_property_visit_agent,model_estate_property_visit,estate.property_agent,1,0,0,0
+access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate.property_agent,1,1,1,0
+access_estate_property_tag_agent,access_estate_property_tag_agent,model_estate_property_tag,estate.property_agent,1,1,1,0
+access_estate_property_manager,estate_property_manager,model_estate_property,estate.property_manager,1,1,1,1
+access_estate_property_offer_manager,estate_property_offer_manager,model_estate_property_offer,estate.property_manager,1,1,1,1
+access_estate_maintainance_request_manager,access_estate_maintainance_request_manager,model_estate_maintainance_request,estate.property_manager,1,0,0,0
+access_estate_property_visit_manager,access_estate_property_visit_manager,model_estate_property_visit,estate.property_manager,1,1,1,1
+access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate.property_manager,1,1,1,1
+access_estate_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate.property_manager,1,1,1,1
+access_estate_property_maintainance_staff,estate_property_maintainance_staff,model_estate_property,estate.property_maintainance_staff,1,1,0,0
+access_estate_property_offer_maintainance_staff,estate_property_offer_maintainance_staff,model_estate_property_offer,estate.property_maintainance_staff,1,0,0,0
+access_estate_maintainance_request_maintainance_staff,access_estate_maintainance_request_maintainance_staff,model_estate_maintainance_request,estate.property_maintainance_staff,1,1,1,1
+access_estate_property_visit_maintainance_staff,access_estate_property_visit_maintainance_staff,model_estate_property_visit,estate.property_maintainance_staff,1,0,0,0
+access_estate_property_type_maintainance_staff,access_estate_property_type_maintainance_staff,model_estate_property_type,estate.property_maintainance_staff,1,0,0,0
+access_estate_property_tag_maintainance_staff,access_estate_property_tag_maintainance_staff,model_estate_property_tag,estate.property_maintainance_staff,1,0,0,0
diff --git a/estate/security/security_groups.xml b/estate/security/security_groups.xml
new file mode 100644
index 00000000000..27c9063452e
--- /dev/null
+++ b/estate/security/security_groups.xml
@@ -0,0 +1,26 @@
+
+
+
+ Real Estate
+
+
+ Admin
+
+
+
+
+ Agent
+
+
+
+
+ Manager
+
+
+
+
+ Maintainance Staff
+
+
+
+
diff --git a/estate/views/estate_maintainance_form.xml b/estate/views/estate_maintainance_form.xml
new file mode 100644
index 00000000000..d4a04b0f56d
--- /dev/null
+++ b/estate/views/estate_maintainance_form.xml
@@ -0,0 +1,42 @@
+
+
+
+ estate_maintainance_action
+ estate.maintainance.request
+ list,form
+
+
+ estate.maintainance.type.list.view
+ estate.maintainance.request
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.maintainance.type.form.view
+ estate.maintainance.request
+
+
+
+
+
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..b1d01b028b9
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml
new file mode 100644
index 00000000000..683c13da3ac
--- /dev/null
+++ b/estate/views/estate_property_offer_view.xml
@@ -0,0 +1,43 @@
+
+
+
+ estate_property_offer
+ estate.property.offer
+ list,form
+
+
+ offers
+ estate.property.offer
+ list,form
+ [('property_type_id', '=', active_id)]
+
+
+ estate.property.offer.list.view
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form.view
+ estate.property.offer
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_tag_view.xml b/estate/views/estate_property_tag_view.xml
new file mode 100644
index 00000000000..1fa40197226
--- /dev/null
+++ b/estate/views/estate_property_tag_view.xml
@@ -0,0 +1,30 @@
+
+
+
+ estate_property_tag
+ estate.property.tag
+ list,form
+
+
+ estate.property.tag.list.view
+ estate.property.tag
+
+
+
+
+
+
+
+ estate.property.tag.form.view
+ estate.property.tag
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_type_view.xml b/estate/views/estate_property_type_view.xml
new file mode 100644
index 00000000000..420c9f7b4d6
--- /dev/null
+++ b/estate/views/estate_property_type_view.xml
@@ -0,0 +1,51 @@
+
+
+
+ estate_property_type_action
+ estate.property.type
+ list,form
+
+
+ estate.property.type.tree.view
+ estate.property.type
+
+
+
+
+
+
+
+
+
+ estate.property.type.form.view
+ estate.property.type
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_view.xml b/estate/views/estate_property_view.xml
new file mode 100644
index 00000000000..7e101a701af
--- /dev/null
+++ b/estate/views/estate_property_view.xml
@@ -0,0 +1,168 @@
+
+
+
+ Real-estate Property"s
+ estate.property
+ {'search_default_available': 1}
+ kanban,list,form
+
+
+ estate.property.list.view
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form.view
+ estate.property
+
+
+
+
+
+ estate.property.search.view
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_visit_action.xml b/estate/views/estate_property_visit_action.xml
new file mode 100644
index 00000000000..aa9cad9c57f
--- /dev/null
+++ b/estate/views/estate_property_visit_action.xml
@@ -0,0 +1,39 @@
+
+
+
+ estate_property_visit
+ estate.property.visit
+ list,form
+
+
+ estate.property.visit.list.view
+ estate.property.visit
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.visit.form.view
+ estate.property.visit
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_visit_calender.xml b/estate/views/estate_property_visit_calender.xml
new file mode 100644
index 00000000000..a05fd994c50
--- /dev/null
+++ b/estate/views/estate_property_visit_calender.xml
@@ -0,0 +1,16 @@
+
+
+
+ Real-estate Property"s
+ estate.property.visit
+ calendar,form,list
+
+
+ property
+ estate.property.visit
+
+
+
+
+
+
diff --git a/estate/views/estate_property_visit_kanban_view.xml b/estate/views/estate_property_visit_kanban_view.xml
new file mode 100644
index 00000000000..d0c7d844213
--- /dev/null
+++ b/estate/views/estate_property_visit_kanban_view.xml
@@ -0,0 +1,39 @@
+
+
+
+ Real-estate Property"s
+ estate.property
+ kanban,form,list
+
+
+ estate.property.view.kanban
+ estate.property
+
+
+
+
+
+
+
+ max offer:
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file