-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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: a new module to manage estate properties #163
base: 18.0
Are you sure you want to change the base?
Changes from 23 commits
cd26d0b
98dc91c
2430f51
8478e5f
be2d317
760525b
640bb1d
9324b1e
9ff6ca6
45fd826
dddd880
5539fb8
940160e
c49def8
3c8de6e
a896aae
1a7e21f
2f82a7a
63198aa
b0c767f
ae52a8a
7d94274
7de8a5f
a23543e
9fc9a00
69f7e7f
fb39311
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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,21 @@ | ||
{ | ||
'name': "Real Estate", | ||
'description': "Create real estate properties and keep track of status", | ||
'depends': [ | ||
'base', | ||
], | ||
'data': [ | ||
'security/ir.model.access.csv', | ||
'views/estate_menus.xml', | ||
'views/estate_property_views.xml', | ||
'views/estate_property_types_views.xml', | ||
'views/estate_property_tag_views.xml', | ||
'views/estate_property_offer_views.xml', | ||
'views/users.xml', | ||
], | ||
'license': "LGPL-3", | ||
# data files containing optionally loaded demonstration data | ||
'demo': [ | ||
"demo/demo.xml", | ||
], | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<odoo> | ||
<data> | ||
<record id="model_estate_property_action_cancel" model="ir.actions.server"> | ||
<field name="name">Mass cancel</field> | ||
<field name="model_id" ref="estate.model_estate_property"/> | ||
<field name="binding_model_id" ref="estate.model_estate_property"/> | ||
<field name="binding_view_types">list</field> | ||
<field name="state">code</field> | ||
<field name="code">action = records.action_cancel()</field> | ||
</record> | ||
</data> | ||
</odoo> |
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, users | ||
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,108 @@ | ||||||||||
from dateutil.relativedelta import relativedelta | ||||||||||
from odoo import api, fields, models, _ | ||||||||||
from odoo.exceptions import UserError | ||||||||||
from odoo.tools import float_compare | ||||||||||
|
||||||||||
|
||||||||||
class EstateProperty(models.Model): | ||||||||||
_name = 'estate.property' | ||||||||||
_description = 'Create real estate properties and keep track of status' | ||||||||||
_order = 'id desc' | ||||||||||
|
||||||||||
name = fields.Char(string="Name", required=True) | ||||||||||
description = fields.Text(string="Description") | ||||||||||
postcode = fields.Char(string="Postcode") | ||||||||||
date_availability = fields.Date(string="Date Availability", default=lambda self: fields.Datetime.today() + relativedelta(months=3), copy=False) | ||||||||||
expected_price = fields.Float(string="Expected Price", required=True) | ||||||||||
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) | ||||||||||
bedrooms = fields.Integer(string="Bedrooms", default=2) | ||||||||||
living_area = fields.Integer(string="Living Area") | ||||||||||
facades = fields.Integer(string="Facades") | ||||||||||
garage = fields.Boolean(string="Garage") | ||||||||||
garden = fields.Boolean(string="Garden") | ||||||||||
garden_area = fields.Integer(string="Garden Area") | ||||||||||
garden_orientation = fields.Selection( | ||||||||||
string="Garden Orientation", | ||||||||||
selection=[('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")] | ||||||||||
) | ||||||||||
active = fields.Boolean(string="Active", default=True) | ||||||||||
state = fields.Selection( | ||||||||||
string="State", | ||||||||||
default='new', | ||||||||||
selection=[ | ||||||||||
('new', "New"), | ||||||||||
('offer_received', "Offer Received"), | ||||||||||
('offer_accepted', "Offer Accepted"), | ||||||||||
('sold', "Sold"), | ||||||||||
('cancelled', "Cancelled") | ||||||||||
] | ||||||||||
) | ||||||||||
property_type_id = fields.Many2one('estate.property.types', string="Property Type") | ||||||||||
salesman_id = fields.Many2one('res.users', string="Salesman", default=lambda self: self.env.user) | ||||||||||
buyer_id = fields.Many2one('res.partner', string="Buyer") | ||||||||||
tag_ids = fields.Many2many('estate.property.tag', string="Property Tags") | ||||||||||
offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") | ||||||||||
total_area = fields.Float(compute="_compute_total_area", string="Total Area") | ||||||||||
best_price = fields.Float(compute="_compute_best_price", string="Best Price") | ||||||||||
Comment on lines
+40
to
+46
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. Those are missing the string option, useful for translation 👍 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. Isn't it the second parameter in all of these? The string=""? |
||||||||||
|
||||||||||
_sql_constraints = [ | ||||||||||
('check_expected_price', 'CHECK(expected_price > 0)', | ||||||||||
"The expected price should be greater than 0."), | ||||||||||
('check_selling_price', 'CHECK(buyer_id IS NULL OR selling_price > 0)', | ||||||||||
"The selling price should be greater than 0."), | ||||||||||
] | ||||||||||
|
||||||||||
@api.constrains('selling_price', 'expected_price') | ||||||||||
def _check_prices_difference(self): | ||||||||||
for record in self: | ||||||||||
if record.buyer_id and float_compare(record.selling_price, 0.9 * record.expected_price, 5) == -1: | ||||||||||
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. Use of a meaningful variable name to ease the understanding of the method
Suggested change
|
||||||||||
raise UserError(_("Selling price cannot be lower than 90% of the expected price!")) | ||||||||||
|
||||||||||
@api.depends('garden_area', 'living_area') | ||||||||||
def _compute_total_area(self): | ||||||||||
for record in self: | ||||||||||
record.total_area = record.garden_area + record.living_area | ||||||||||
|
||||||||||
@api.depends('offer_ids.price') | ||||||||||
def _compute_best_price(self): | ||||||||||
for record in self: | ||||||||||
prices = record.offer_ids.mapped('price') | ||||||||||
|
||||||||||
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. No need of empty line here |
||||||||||
record.best_price = max(prices) if len(prices) > 0 else 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. Lil python trick
Suggested change
|
||||||||||
|
||||||||||
@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 = '' | ||||||||||
|
||||||||||
def action_cancel(self): | ||||||||||
for record in self: | ||||||||||
if record.state == 'cancelled': | ||||||||||
raise UserError(_("Property already cancelled!")) | ||||||||||
elif record.state == 'sold': | ||||||||||
raise UserError(_("A sold property cannot be cancelled!")) | ||||||||||
else: | ||||||||||
self.state = 'cancelled' | ||||||||||
|
||||||||||
return True | ||||||||||
|
||||||||||
def action_sold(self): | ||||||||||
for record in self: | ||||||||||
if record.state == 'sold': | ||||||||||
raise UserError(_("Property already sold!")) | ||||||||||
elif record.state == 'cancelled': | ||||||||||
raise UserError(_("A cancelled property cannot be sold!")) | ||||||||||
else: | ||||||||||
self.state = 'sold' | ||||||||||
|
||||||||||
return True | ||||||||||
|
||||||||||
@api.ondelete(at_uninstall=False) | ||||||||||
def prevent_deletion(self): | ||||||||||
for record in self: | ||||||||||
if record.state != 'new' and record.state != 'cancelled': | ||||||||||
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. Easier to add and remove values in the future
Suggested change
|
||||||||||
raise UserError(_("You can only delete new or cancelled properties!")) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,85 @@ | ||||||
from datetime import timedelta | ||||||
from odoo import api, fields, models, _ | ||||||
from odoo.exceptions import UserError | ||||||
from odoo.tools import float_compare | ||||||
|
||||||
|
||||||
class EstatePropertyOffer(models.Model): | ||||||
_name = 'estate.property.offer' | ||||||
_description = 'Model representing the offers from partners to a specific property' | ||||||
_order = 'price desc' | ||||||
|
||||||
price = fields.Float(string="Price") | ||||||
status = fields.Selection( | ||||||
string="Status", | ||||||
copy=False, | ||||||
selection=[ | ||||||
('accepted', "Accepted"), | ||||||
('refused', "Refused") | ||||||
] | ||||||
) | ||||||
partner_id = fields.Many2one('res.partner', string="Partner", required=True) | ||||||
property_id = fields.Many2one('estate.property', string="Property", required=True) | ||||||
Comment on lines
+21
to
+22
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. String parameter |
||||||
validity = fields.Integer(string="Validity", default=7) | ||||||
date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', string="Date Deadline") | ||||||
create_date = fields.Date(default=lambda self: fields.Datetime.now()) | ||||||
property_type_id = fields.Many2one(related="property_id.property_type_id") | ||||||
|
||||||
_sql_constraints = [ | ||||||
('check_offer_price', 'CHECK(price > 0)', | ||||||
"The offer price should be greater than 0."), | ||||||
] | ||||||
|
||||||
@api.depends('validity', 'create_date') | ||||||
def _compute_date_deadline(self): | ||||||
for record in self: | ||||||
record.date_deadline = record.create_date + timedelta(days=record.validity) | ||||||
|
||||||
def _inverse_date_deadline(self): | ||||||
for record in self: | ||||||
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. No placing it everywhere but all you looped method need this change hehe
Suggested change
|
||||||
delta = record.date_deadline - record.create_date | ||||||
|
||||||
record.validity = delta.days | ||||||
|
||||||
def action_confirm(self): | ||||||
self.ensure_one() | ||||||
|
||||||
if self.status == 'accepted': | ||||||
raise UserError(_("Offer already accepted!")) | ||||||
elif self.status == 'refused': | ||||||
raise UserError(_("Can't accept a refused offer!")) | ||||||
elif self.property_id.buyer_id: | ||||||
raise UserError(_("Can't accept more than 1 offer!")) | ||||||
else: | ||||||
if float_compare(self.price, 0.9 * self.property_id.expected_price, 5) == -1: | ||||||
raise UserError(_("Selling price cannot be lower than 90% of the expected price!")) | ||||||
|
||||||
self.status = 'accepted' | ||||||
self.property_id.state = 'offer_accepted' | ||||||
self.property_id.selling_price = self.price | ||||||
self.property_id.buyer_id = self.partner_id | ||||||
|
||||||
return True | ||||||
|
||||||
def action_refuse(self): | ||||||
for record in self: | ||||||
if record.status == 'refused': | ||||||
raise UserError(_("Offer already refused!")) | ||||||
elif record.status == 'accepted': | ||||||
raise UserError(_("Can't refuse an accepted offer!")) | ||||||
else: | ||||||
record.status = 'refused' | ||||||
|
||||||
return True | ||||||
|
||||||
@api.model_create_multi | ||||||
def create(self, vals): | ||||||
for val in vals: | ||||||
current_property_id = self.env['estate.property'].browse(val['property_id']) | ||||||
|
||||||
if current_property_id.best_price > val['price']: | ||||||
raise UserError(_("Can't create an offer with a price lower than the best offer!")) | ||||||
|
||||||
current_property_id.state = 'offer_received' | ||||||
|
||||||
return super().create(vals) |
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.
Better indent