diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..d4c3532319d --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,3 @@ +from . import wizards +from . import controllers +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..007b1432dd6 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,31 @@ +{ + 'name': "Real State", + 'depends': ['base', 'website'], + 'author': "Sahil Mangukiya", + 'category': 'Real Estate/Brokerage', + 'description': "This is my First tutorial module.", + 'data': [ + 'security/estate_security.xml', + 'security/ir.model.access.csv', + 'data/estate.property.type.csv', + 'reports/estate_property_website_template.xml', + 'reports/estate_property_detail_template.xml', + 'reports/print_offer_table_subtemplate.xml', + 'reports/estate_property_offer_report_template.xml', + 'reports/estate_salesman_property_offer_report_template.xml', + 'reports/estate_property_reports.xml', + 'views/res_users_views.xml', + 'wizards/add_offer.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_views.xml', + 'views/estate_menu.xml' + ], + 'demo': [ + 'demo/estate_property_demo.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False +} diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py new file mode 100644 index 00000000000..c5ba8c48107 --- /dev/null +++ b/estate/controllers/__init__.py @@ -0,0 +1 @@ +from . import property_website diff --git a/estate/controllers/property_website.py b/estate/controllers/property_website.py new file mode 100644 index 00000000000..12d35aa865d --- /dev/null +++ b/estate/controllers/property_website.py @@ -0,0 +1,39 @@ +from odoo import http +from odoo.http import request + +from odoo.addons.portal.controllers.portal import pager as portal_pager + + +class propertyWebsiteController(http.Controller): + + @http.route(['/properties', '/properties/page/'], website=True, auth='public', type='http') + def property_website(self, page=1, **kwargs): + property_limit_per_page = 6 + property_count = request.env["estate.property"].sudo().search_count( + domain=[('state','in',('new', 'offer received', 'offer accepted')), ('active', '=', True)] + ) + pager = portal_pager( + url="/properties", + total=property_count, + page=page, + step=property_limit_per_page + ) + property = request.env["estate.property"].sudo().search( + domain=[('state','in',('new', 'offer received', 'offer accepted')), ('active', '=', True)], + offset=pager['offset'], + limit=property_limit_per_page + ) + return request.render('estate.estate_property_website_template', + { + 'properties': property, + 'pager': pager + }) + + + @http.route('/property/', website=True, auth='public', type='http') + def property_detail_view(self, property_id): + property = request.env['estate.property'].sudo().browse(property_id) + return request.render('estate.estate_property_detail_template', + { + 'property' : property, + }) diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv new file mode 100644 index 00000000000..2dba79266ca --- /dev/null +++ b/estate/data/estate.property.type.csv @@ -0,0 +1,5 @@ +id,name +1,"Residential" +2,"Commercial" +3,"Industrial" +4,"Land" diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml new file mode 100644 index 00000000000..bad0fc8989c --- /dev/null +++ b/estate/demo/estate_property_demo.xml @@ -0,0 +1,121 @@ + + + + Big villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 6 + 100 + 4 + True + True + 100000 + south + + + + + Trailer home + cancelled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + False + + + + + Sahil's Land + new + This is my land, Don't play cricket!! + 12345 + 2020-02-02 + 100 + 30 + 100 + 4 + True + True + 100000 + south + + + + + + + 1600000 + + 14 + + + + + 1600001 + + 14 + + + + + 1600002 + + 14 + + + + + 100001 + + 7 + + + + + 100002 + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /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 res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..541c3b5e08e --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,98 @@ +from datetime import timedelta + +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError + + +class estateProperty(models.Model): + _name = "estate.property" + _description = "This is property Table." + _order = "selling_price desc, name" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + data_availability = fields.Date(default=lambda self: fields.Date.today() + timedelta(days=90), copy=False) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(change_default=True) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + help="Garden Orientation is avialable in four faces of direction." + ) + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + help="Offer available in five type.", + required=True, + copy=False, + default='new' + ) + number = fields.Integer(related='property_type_id.number') + totalArea = fields.Integer(compute='_compute_total_area', store="true") + best_offer = fields.Float(compute='_compute_best_offer') + property_image = fields.Binary() + + property_type_id = fields.Many2one(comodel_name='estate.property.type', string='Property Type', domain="[('number', '>=', 0)]", ondelete='cascade') + seller_id = fields.Many2one('res.users', 'Salesman', default=lambda self: self.env.user) + buyer_id = fields.Many2one('res.partner', 'Buyer') + tag_ids = fields.Many2many(comodel_name='estate.property.tag', string='Tag', relation='property_join_tag', column1='property_id', column2='tag_id') + offer_ids = fields.One2many('estate.property.offer', 'property_id') + company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) + + + _sql_constraints = [ + ('check_selling_price', 'CHECK(selling_price >= 0)', 'A property selling price must be positive.'), + ('check_expected_price', 'CHECK(expected_price > 0)', 'A property expected price must be strictly positive.') + ] + + @api.depends('garden_area', 'living_area') + def _compute_total_area(self): + for record in self: + record.totalArea = record.garden_area + record.living_area + + @api.depends('offer_ids.price') + def _compute_best_offer(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 + + def action_sold(self): + if(self.state == 'cancelled'): + raise UserError("You can not mark a cancelled property as sold.") + self.state = 'sold' + + def action_cancelled(self): + if(self.state == 'sold'): + raise UserError("You can not mark sold property as cancelled") + self.state = 'cancelled' + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if record.selling_price < (0.9 * record.expected_price) and (record.selling_price > 0): + raise ValidationError("Selling price cannot be lower than 90 percentage of the expected price.") + + @api.ondelete(at_uninstall=False) + def _prevent_delete(self): + for record in self: + if record.state not in ("new", "cancelled"): + raise UserError("You can not delete a property which are in offer received, offer accepted or sold state.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..bc4c718767b --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,78 @@ +from datetime import timedelta, datetime + +from odoo import models, fields, api +from odoo.exceptions import UserError + + +class estatePropertyOffer(models.Model): + + _name = "estate.property.offer" + _description = "This is offer table" + _order = "id desc" + + price = fields.Float(required=True) + status = fields.Selection(string='Status', selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + validity = fields.Integer(default=7, string="Validity") + date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string="Date Deadline") + sum = fields.Integer(compute='_compute_sum', string="Sum") + sum2 = fields.Integer(compute='_compute_sum2', string="Sum2") + + partner_id = fields.Many2one('res.partner', string='Buyer', required=True) + property_id = fields.Many2one('estate.property', string='Property') + property_type_id = fields.Many2one(comodel_name="estate.property.type", related="property_id.property_type_id", store=True) + + _sql_constraints = [ + ('check_offer_price','CHECK(price > 0)','A property offer price must be strictly positive.') + ] + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = record.create_date + timedelta(record.validity) + else: + record.date_deadline = datetime.today() + timedelta(record.validity) + + def _inverse_deadline(self): + for record in self: + if record.date_deadline and record.create_date: + record.validity = (record.date_deadline - record.create_date.date()).days + else: + record.validity = (record.date_deadline - datetime.today()).days + + @api.depends('validity') + def _compute_sum(self): + for record in self: + record.sum = 7 + record.validity + + @api.depends('sum') + def _compute_sum2(self): + for record in self: + record.sum2 = 7 + record.sum + + def action_accept(self): + for record in self: + if record.property_id.state in ['sold', 'offer accepted']: + raise UserError("Already one offer is accepted.") + if record.status == 'accepted': + continue + record.status = 'accepted' + record.property_id.state = 'offer accepted' + record.property_id.buyer_id = self.partner_id + record.property_id.selling_price = self.price + + def action_refuse(self): + if(self.status == 'accepted'): + self.status = 'refused' + self.property_id.buyer_id = False + self.property_id.selling_price = False + else: + self.status = 'refused' + + @api.model + def create(self, offer): + property_id = self.env["estate.property"].browse(offer["property_id"]) + if offer["price"] < max(property_id.offer_ids.mapped("price") + [0]): + raise UserError("Offer price must be higher than existing offer.") + property_id.state = "offer received" + return super().create(offer) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..5988f5c2fab --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,16 @@ +from odoo import models, fields + + +class estatePropertyTag(models.Model): + + _name = "estate.property.tag" + _description = "This is property Tag model" + _order = "name" + + name = fields.Char(required=True, string="Tag") + active = fields.Boolean(required=True) + color = fields.Integer(default=12) + + _sql_constraints = [ + ('check_unique_tag_name','UNIQUE(name)','This tag is already exists.') + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..31b225bf68e --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,25 @@ +from odoo import models, fields, api + + +class estatePropertyType(models.Model): + _name = "estate.property.type" + _description = "This is property Type table." + _order = "sequence, name desc" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + number = fields.Integer(default=10) + sequence = fields.Integer('Sequence', default=5) + offer_count = fields.Integer(compute="_compute_offer_count") + + property_ids = fields.One2many('estate.property', 'property_type_id', string='Property') + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + + _sql_constraints = [ + ('check_unique_type_name', 'UNIQUE(name)', 'This type is already exists.') + ] + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..933b9dcfb69 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class resUser(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many(comodel_name="estate.property", inverse_name="seller_id", domain=[('state', 'in', ['new', 'offer received'])]) diff --git a/estate/reports/estate_property_detail_template.xml b/estate/reports/estate_property_detail_template.xml new file mode 100644 index 00000000000..fb15c543367 --- /dev/null +++ b/estate/reports/estate_property_detail_template.xml @@ -0,0 +1,64 @@ + + + + diff --git a/estate/reports/estate_property_offer_report_template.xml b/estate/reports/estate_property_offer_report_template.xml new file mode 100644 index 00000000000..ac0387c659b --- /dev/null +++ b/estate/reports/estate_property_offer_report_template.xml @@ -0,0 +1,38 @@ + + + + diff --git a/estate/reports/estate_property_reports.xml b/estate/reports/estate_property_reports.xml new file mode 100644 index 00000000000..cc759dfcf17 --- /dev/null +++ b/estate/reports/estate_property_reports.xml @@ -0,0 +1,23 @@ + + + + Property Offer Report + estate.property + qweb-pdf + estate.estate_property_offer_report_template + estate.estate_property_offer_report_template + '%s Offers Report' % object.name + + report + + + Property Offer Report + res.users + qweb-pdf + estate.estate_salesman_property_offer_report_template + estate.estate_salesman_property_offer_report_template + '%s Properties Offer Report' % object.name + + report + + diff --git a/estate/reports/estate_property_website_template.xml b/estate/reports/estate_property_website_template.xml new file mode 100644 index 00000000000..7a8c9c978c8 --- /dev/null +++ b/estate/reports/estate_property_website_template.xml @@ -0,0 +1,60 @@ + + + + Properties + /properties + + + + + diff --git a/estate/reports/estate_salesman_property_offer_report_template.xml b/estate/reports/estate_salesman_property_offer_report_template.xml new file mode 100644 index 00000000000..3d05c2ed7a1 --- /dev/null +++ b/estate/reports/estate_salesman_property_offer_report_template.xml @@ -0,0 +1,45 @@ + + + + diff --git a/estate/reports/print_offer_table_subtemplate.xml b/estate/reports/print_offer_table_subtemplate.xml new file mode 100644 index 00000000000..78cd5ad87e2 --- /dev/null +++ b/estate/reports/print_offer_table_subtemplate.xml @@ -0,0 +1,42 @@ + + + + diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..836ae6958db --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,34 @@ + + + + + Agent + + + + + Manager + + + + + + + A description of the rule's for agent + + + + ['|', ('seller_id', '=', user.id), ('seller_id', '=', False), ('company_id', 'in', company_ids)] + + + + + A description of the rule's for manager + + + + [(1, '=', 1)] + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ef5f36a60b7 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,13 @@ +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,estate.estate_group_manager,1,1,1,0 +access_estate_property_type,access_estate_property_type,model_estate_property_type,estate.estate_group_manager,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,estate.estate_group_manager,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,estate.estate_group_manager,1,1,1,1 +access_add_offer,access_add_offer,model_add_offer,estate.estate_group_manager,1,1,1,1 + +access_estate_property,access_estate_property,model_estate_property,estate.estate_group_user,1,1,1,0 +access_estate_property_type,access_estate_property_type,model_estate_property_type,estate.estate_group_user,1,0,0,0 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,estate.estate_group_user,1,0,0,0 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,estate.estate_group_user,1,1,1,1 +access_add_offer,access_add_offer,model_add_offer,estate.estate_group_user,1,1,1,1 diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml new file mode 100644 index 00000000000..fef0e406a7a --- /dev/null +++ b/estate/views/estate_menu.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..73089209f87 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,24 @@ + + + + Offers + estate.property.offer + tree,form + [('property_type_id', '=', active_id)] + + + + estate_property_offer_view_tree + estate.property.offer + + + + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..5a2cd68dcf6 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,63 @@ + + + + Tag + estate.property.tag + tree,form,kanban + + + estate_property_tag_view_tree + estate.property.tag + + + + + + + + + + estate_property_tag_view_form + estate.property.tag + +
+ +

+ +

+ + + + +
+
+
+
+ + estate_property_tag_view_search + estate.property.tag + + + + + + + + estate_property_tag_view_kanban + estate.property.tag + + + + +
+

+ +

+ +
+
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..5133186d849 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,57 @@ + + + + Types + estate.property.type + tree,form + + + + estate_property_type_view_tree + estate.property.type + + + + + + + + + + + + estate_property_type_view_form + estate.property.type + +
+ +
+ +
+

+ +

+ + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..2c4b0fc5095 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,148 @@ + + + + Properties + estate.property + list,form,kanban + {'search_default_state' : True} + + + + estate_property_view_tree + estate.property + + +
+
+ + + + + + +
+
+
+ + + estate_property_view_form + estate.property + +
+
+
+ + +

+ Name: + +

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