-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ADD] estate: Added new real estate property management module #110
base: 17.0
Are you sure you want to change the base?
Changes from 8 commits
e21ae09
629dbab
f88c0f2
bf87bb0
00b16df
f8bc4ff
0ec7bd9
45e4a94
e6f6928
b81da5f
0ca9e6e
6beee95
958aef6
b310b1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
'name': 'Real Estate', | ||
'version': '1.0', | ||
'summary': 'Manage real estate properties', | ||
'description': 'Module to manage real estate properties', | ||
'author': 'Akya', | ||
'depends': ['base', 'mail'], | ||
'data': [ | ||
'security/ir.model.access.csv', | ||
'views/estate_property_views.xml', | ||
'views/estate_property_offer_view.xml', | ||
'views/estate_property_tag_view.xml', | ||
'views/estate_property_type_view.xml', | ||
'views/res_users_view.xml', | ||
'views/estate_menus.xml', | ||
], | ||
'installable': True, | ||
'application': True, | ||
'license': 'AGPL-3' | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import estate_property, estate_property_type, estate_property_tag, estate_property_offer, res_users | ||
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,107 @@ | ||||||||||
from odoo import models, fields, api | ||||||||||
from odoo.exceptions import UserError, ValidationError | ||||||||||
from odoo.tools.float_utils import float_is_zero, float_compare | ||||||||||
|
||||||||||
|
||||||||||
class EstateProperty(models.Model): | ||||||||||
_name = 'estate.property' | ||||||||||
_description = 'Real Estate Property' | ||||||||||
_order = 'id desc' | ||||||||||
_inherit = ['mail.thread', 'mail.activity.mixin'] | ||||||||||
|
||||||||||
title = fields.Char(required=True) | ||||||||||
description = fields.Text() | ||||||||||
postcode = fields.Char() | ||||||||||
availability_date = fields.Date(copy=False) | ||||||||||
expected_price = fields.Float(required=True, default=0.0) | ||||||||||
selling_price = fields.Float(string="Selling Price", readonly=True) | ||||||||||
bedrooms = fields.Integer(default=2) | ||||||||||
living_area = fields.Integer() | ||||||||||
facades = fields.Integer() | ||||||||||
garage = fields.Boolean() | ||||||||||
garden = fields.Boolean() | ||||||||||
garden_area = fields.Integer() | ||||||||||
best_price = fields.Float(compute="_compute_best_price") | ||||||||||
total_area = fields.Float(compute="_compute_total") | ||||||||||
garden_orientation = fields.Selection([ | ||||||||||
('north', 'North'), | ||||||||||
('south', 'South'), | ||||||||||
('east', 'East'), | ||||||||||
('west', 'West'), | ||||||||||
]) | ||||||||||
state = fields.Selection([ | ||||||||||
('new', 'New'), | ||||||||||
('offer_received', 'Offer_received'), | ||||||||||
('offer_accepted', 'Offer_accepted'), | ||||||||||
('sold', 'Sold'), | ||||||||||
('canceled', 'Canceled'), | ||||||||||
('refused', 'Refused') | ||||||||||
], string='Status', default='new', tracking=True) | ||||||||||
active = fields.Boolean(string='Active', default=True) | ||||||||||
property_type_id = fields.Many2one('estate.property.type', string="Property Type") | ||||||||||
buyer_id = fields.Many2one('res.partner', string="Buyer") | ||||||||||
seller_id = fields.Many2one('res.users', string="Salesperson", ondelete='set null') | ||||||||||
tag_ids = fields.Many2many('estate.property.tag') | ||||||||||
offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") | ||||||||||
|
||||||||||
@api.depends('living_area', 'garden_area') | ||||||||||
def _compute_total(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 record.offer_ids: | ||||||||||
record.best_price = max(record.offer_ids.mapped('price')) | ||||||||||
else: | ||||||||||
record.best_price = 0.0 | ||||||||||
|
||||||||||
@api.onchange('garden') | ||||||||||
def _onchange_garden(self): | ||||||||||
if self.garden: | ||||||||||
self.garden_area = 10.0 | ||||||||||
self.garden_orientation = 'north' | ||||||||||
else: | ||||||||||
self.garden_area = 0.0 | ||||||||||
self.garden_orientation = False | ||||||||||
|
||||||||||
def action_cancel(self): | ||||||||||
if self.state != "sold": | ||||||||||
self.state = "canceled" | ||||||||||
elif self.state == "sold": | ||||||||||
raise UserError("This property can't be canceled as it is sold already") | ||||||||||
return True | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think it is not necessary check for all the actions. |
||||||||||
|
||||||||||
def action_sold(self): | ||||||||||
if self.state != "canceled": | ||||||||||
if self.state == 'offer_accepted': | ||||||||||
self.state = "sold" | ||||||||||
else: | ||||||||||
raise UserError("This property can't be sold as there are no offers") | ||||||||||
elif self.state == "canceled": | ||||||||||
raise UserError("This property can't be sold as it is canceled already") | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Can't we use else directly instead of elif? |
||||||||||
return True | ||||||||||
|
||||||||||
_sql_constraints = [ | ||||||||||
('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must be strictly positive.'), | ||||||||||
('check_selling_price', 'CHECK(selling_price >= 0)', 'The selling price must be positive.') | ||||||||||
] | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally we define constraints just after field defination. |
||||||||||
|
||||||||||
@api.constrains('selling_price', 'expected_price') | ||||||||||
def _check_selling_price(self): | ||||||||||
for record in self: | ||||||||||
if not float_is_zero(record.selling_price, precision_rounding=0.01): | ||||||||||
if float_compare(record.selling_price, record.expected_price * 0.9, precision_rounding=0.01) == -1: | ||||||||||
raise ValidationError("The selling price cannot be lower than 90% of the expected price.") | ||||||||||
|
||||||||||
def action_offer_received(self): | ||||||||||
self.state = 'offer_received' | ||||||||||
|
||||||||||
@api.ondelete(at_uninstall=False) | ||||||||||
def _delete_property(self): | ||||||||||
for record in self: | ||||||||||
if record.state not in ['new', 'canceled']: | ||||||||||
raise UserError( | ||||||||||
"You cannot delete a property unless it is in the 'New' or 'Canceled' state." | ||||||||||
) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,63 @@ | ||||||||||||||||||
from odoo import models, fields, api | ||||||||||||||||||
from datetime import timedelta | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Generally we follow a convention in which we first we import all the external libraries and then the import of odoos in alphabetical order. |
||||||||||||||||||
from odoo.exceptions import UserError | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
class EstatePropertyOffer(models.Model): | ||||||||||||||||||
_name = 'estate.property.offer' | ||||||||||||||||||
_description = 'Property Offer' | ||||||||||||||||||
_order = 'price desc' | ||||||||||||||||||
|
||||||||||||||||||
price = fields.Float(string="Price") | ||||||||||||||||||
status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string="Status", copy=False) | ||||||||||||||||||
partner_id = fields.Many2one('res.partner', string="Partner", required=True) | ||||||||||||||||||
validity = fields.Integer(string="Validity (days)", default=7) | ||||||||||||||||||
date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") | ||||||||||||||||||
property_id = fields.Many2one('estate.property', 'Property', required=True, ondelete="cascade") | ||||||||||||||||||
property_type_id = fields.Many2one(related="property_id.property_type_id") | ||||||||||||||||||
|
||||||||||||||||||
@api.depends('create_date', 'validity') | ||||||||||||||||||
def _compute_date_deadline(self): | ||||||||||||||||||
for record in self: | ||||||||||||||||||
if record.create_date: | ||||||||||||||||||
record.date_deadline = record.create_date + timedelta(days=record.validity) | ||||||||||||||||||
else: | ||||||||||||||||||
record.date_deadline = fields.Date.today() + timedelta(days=record.validity) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
||||||||||||||||||
def _inverse_date_deadline(self): | ||||||||||||||||||
for record in self: | ||||||||||||||||||
if record.date_deadline and record.create_date: | ||||||||||||||||||
create_date_date = record.create_date.date() | ||||||||||||||||||
record.validity = (record.date_deadline - create_date_date).days | ||||||||||||||||||
else: | ||||||||||||||||||
record.validity = 0 | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Can you give it a try? Make sure to check all possible cases. |
||||||||||||||||||
|
||||||||||||||||||
def action_accept(self): | ||||||||||||||||||
if not self.property_id.buyer_id: | ||||||||||||||||||
self.status = 'accepted' | ||||||||||||||||||
self.property_id.selling_price = self.price | ||||||||||||||||||
self.property_id.buyer_id = self.partner_id | ||||||||||||||||||
self.property_id.state = "offer_accepted" | ||||||||||||||||||
else: | ||||||||||||||||||
raise UserError("Offer has been already Accepted") | ||||||||||||||||||
return True | ||||||||||||||||||
|
||||||||||||||||||
def action_refuse(self): | ||||||||||||||||||
if self.property_id.buyer_id == self.partner_id: | ||||||||||||||||||
self.property_id.buyer_id = '' | ||||||||||||||||||
self.property_id.selling_price = 0 | ||||||||||||||||||
self.status = 'refused' | ||||||||||||||||||
return True | ||||||||||||||||||
|
||||||||||||||||||
_sql_constraints = [ | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here for constraints |
||||||||||||||||||
('check_offer_price', 'CHECK(price > 0)', 'The offer price must be strictly positive.') | ||||||||||||||||||
] | ||||||||||||||||||
|
||||||||||||||||||
@api.model | ||||||||||||||||||
def create(self, vals): | ||||||||||||||||||
record = super().create(vals) | ||||||||||||||||||
if record.property_id.state == 'new': | ||||||||||||||||||
record.property_id.state = 'offer_received' | ||||||||||||||||||
if record.price < max(record.property_id.offer_ids.mapped('price')): | ||||||||||||||||||
raise UserError("Price should be greater than the existing offer") | ||||||||||||||||||
return record |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from odoo import models, fields | ||
|
||
|
||
class EstatePropertyTag(models.Model): | ||
_name = 'estate.property.tag' | ||
_description = 'Property Tag' | ||
_order = 'name' | ||
|
||
name = fields.Char(string="Name", required=True) | ||
color = fields.Integer() | ||
|
||
_sql_constraints = [ | ||
('unique_name', 'UNIQUE(name)', 'This property tag already exists.'), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from odoo import models, fields, api | ||
|
||
|
||
class EstateProperty(models.Model): | ||
_name = 'estate.property.type' | ||
_description = 'Estate Property Type' | ||
_order = 'sequence, name' | ||
|
||
name = fields.Char(string="Name", required=True) | ||
sequence = fields.Integer(string='Sequence', default=10) | ||
property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties") | ||
offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") | ||
offer_count = fields.Integer( | ||
string="Number of Offers", | ||
compute='_compute_offer_count' | ||
) | ||
|
||
_sql_constraints = [ | ||
('unique_name', 'UNIQUE(name)', 'This property type already exists.'), | ||
] | ||
|
||
@api.depends('offer_ids') | ||
def _compute_offer_count(self): | ||
for record in self: | ||
record.offer_count = len(record.offer_ids) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from odoo import models, fields | ||
|
||
|
||
class ResUsers(models.Model): | ||
_inherit = 'res.users' | ||
|
||
property_ids = fields.One2many( | ||
'estate.property', | ||
'seller_id', | ||
string='Properties', | ||
domain=[('state', '=', 'available')] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
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 | ||
estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 | ||
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 | ||
estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<odoo> | ||
<menuitem id="estate_menu_root" name="Real Estate"> | ||
<menuitem id="Advertisements" name="Advertisements"> | ||
<menuitem id="Properties" action="action_estate_property"/> | ||
</menuitem> | ||
<menuitem id="Settings" name="Settings"> | ||
<menuitem id="Properties_Types" name="Property Types" action="action_estate_property_type"/> | ||
<menuitem id="Properties_Tags" name="Property Tags" action="action_estate_property_tag"/> | ||
<menuitem id="Users_and_companies" name="Users and Companies" action="action_res_users"/> | ||
</menuitem> | ||
</menuitem> | ||
</odoo> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?xml version="1.0"?> | ||
<odoo> | ||
|
||
|
||
<record id="action_estate_property_offer" model="ir.actions.act_window"> | ||
<field name="name">Property Offers</field> | ||
<field name="res_model">estate.property.offer</field> | ||
<field name="view_mode">tree,form</field> | ||
<field name="domain">[('property_type_id', '=', active_id)]</field> | ||
</record> | ||
|
||
|
||
|
||
|
||
<record id="view_estate_property_offer_form" model="ir.ui.view"> | ||
<field name="name">estate.property.offer.form</field> | ||
<field name="model">estate.property.offer</field> | ||
<field name="arch" type="xml"> | ||
<form> | ||
<sheet> | ||
<group> | ||
<field name="price"/> | ||
<field name="partner_id"/> | ||
<field name="validity" /> | ||
<field name="date_deadline" /> | ||
|
||
</group> | ||
</sheet> | ||
</form> | ||
</field> | ||
</record> | ||
|
||
<record id="view_estate_property_offer_tree" model="ir.ui.view"> | ||
<field name="name">estate.property.offer.tree</field> | ||
<field name="model">estate.property.offer</field> | ||
<field name="arch" type="xml"> | ||
<tree string="Property Offers" editable="bottom" decoration-success="status in ['accepted']" decoration-danger="status in ['refused']"> | ||
<field name="partner_id"/> | ||
<field name="price"/> | ||
<field name="validity"/> | ||
<field name="status" column_invisible='true'/> | ||
<field name="date_deadline"/> | ||
<field name="property_type_id"/> | ||
<button name="action_accept" type="object" icon="fa-check" invisible="status in ['accepted', 'refused']"/> | ||
<button name="action_refuse" type="object" icon="fa-times" invisible="status in ['accepted', 'refused']"/> | ||
</tree> | ||
</field> | ||
</record> | ||
|
||
|
||
</odoo> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need a blank line at EOF |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<odoo> | ||
|
||
<record id="action_estate_property_tag" model="ir.actions.act_window"> | ||
<field name="name">Property Tags</field> | ||
<field name="res_model">estate.property.tag</field> | ||
<field name="view_mode">tree,form</field> | ||
</record> | ||
|
||
<record id="view_estate_property_tag_tree" model="ir.ui.view"> | ||
<field name="name">estate.property.tag.tree</field> | ||
<field name="model">estate.property.tag</field> | ||
<field name="arch" type="xml"> | ||
<tree string="Property Tags" editable="bottom"> | ||
<field name="name"/> | ||
<field name="color"/> | ||
</tree> | ||
</field> | ||
</record> | ||
|
||
<record id="estate_property_tag_view_form" model="ir.ui.view"> | ||
<field name="name">estate.property.tag.form</field> | ||
<field name="model">estate.property.tag</field> | ||
<field name="arch" type="xml"> | ||
<form string="Estate Property Tag"> | ||
<sheet> | ||
<group> | ||
<h1> <field name="name"/> </h1> | ||
</group> | ||
</sheet> | ||
</form> | ||
</field> | ||
</record> | ||
|
||
</odoo> | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, we import every model in new lines.