-
Notifications
You must be signed in to change notification settings - Fork 2.3k
[ADD] estate: Module for managing real esate #876
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
base: 18.0
Are you sure you want to change the base?
Changes from all commits
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,12 @@ | ||
[[source]] | ||
url = "https://pypi.org/simple" | ||
verify_ssl = true | ||
name = "pypi" | ||
|
||
[packages] | ||
black = "*" | ||
|
||
[dev-packages] | ||
|
||
[requires] | ||
python_version = "3.12" |
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,18 @@ | ||
{ | ||
"name": "Real Estate", | ||
"version": "1.0", | ||
"category": "Tutorials/RealEstate", | ||
"summary": "This is a test", | ||
"depends": ["base"], | ||
"installable": True, | ||
"application": True, | ||
"data": [ | ||
"views/estate_property_offer_views.xml", | ||
"views/estate_property_tag_views.xml", | ||
"views/estate_property_type_views.xml", | ||
"views/estate_property_views.xml", | ||
"views/res_users_views.xml", | ||
"views/estate_menus.xml", | ||
"security/ir.model.access.csv", | ||
], | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,145 @@ | ||||||||||||||||||||||
#!/usr/bin/env python3 | ||||||||||||||||||||||
|
||||||||||||||||||||||
from typing import final | ||||||||||||||||||||||
from odoo import models, fields, api, exceptions, tools | ||||||||||||||||||||||
|
||||||||||||||||||||||
@final | ||||||||||||||||||||||
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. Huh ? |
||||||||||||||||||||||
class EstateProperty(models.Model): | ||||||||||||||||||||||
_name = "estate.property" | ||||||||||||||||||||||
_description = "Test Model Description here" | ||||||||||||||||||||||
_order = "id desc" | ||||||||||||||||||||||
|
||||||||||||||||||||||
name = fields.Char(required=True) | ||||||||||||||||||||||
description = fields.Text() | ||||||||||||||||||||||
postcode = fields.Char() | ||||||||||||||||||||||
date_availability = fields.Date( | ||||||||||||||||||||||
"Available From", | ||||||||||||||||||||||
copy=False, | ||||||||||||||||||||||
default=lambda _: fields.Date.add(fields.Datetime.now(), months=3), | ||||||||||||||||||||||
) | ||||||||||||||||||||||
|
||||||||||||||||||||||
@api.ondelete(at_uninstall=False) | ||||||||||||||||||||||
def _unlink_except_wrong_state(self) -> None: | ||||||||||||||||||||||
record: EstateProperty | ||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||
if record.state in ["new", "cancelled"]: | ||||||||||||||||||||||
raise exceptions.UserError(f"Cannot delete a property of state {record.state}") | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
||||||||||||||||||||||
@api.model_create_multi | ||||||||||||||||||||||
def create(self: "EstateProperty", vals_list: list[api.ValuesType]): | ||||||||||||||||||||||
for val in vals_list: | ||||||||||||||||||||||
val["state"] = "received" | ||||||||||||||||||||||
return super().create(vals_list) | ||||||||||||||||||||||
|
||||||||||||||||||||||
expected_price = fields.Float(required=True) | ||||||||||||||||||||||
selling_price = fields.Float(readonly=True, copy=False) | ||||||||||||||||||||||
bedrooms = fields.Integer(default=2) | ||||||||||||||||||||||
living_area = fields.Integer("Living Area (sqm)") | ||||||||||||||||||||||
facades = fields.Integer() | ||||||||||||||||||||||
garage = fields.Boolean() | ||||||||||||||||||||||
garden = fields.Boolean() | ||||||||||||||||||||||
garden_area = fields.Integer("Garden Area (sqm)") | ||||||||||||||||||||||
res_users_id = fields.Many2one("res.users") | ||||||||||||||||||||||
|
||||||||||||||||||||||
garden_orientation = fields.Selection( | ||||||||||||||||||||||
[ | ||||||||||||||||||||||
("north", "North"), | ||||||||||||||||||||||
("south", "South"), | ||||||||||||||||||||||
("east", "East"), | ||||||||||||||||||||||
("west", "West"), | ||||||||||||||||||||||
Comment on lines
+46
to
+50
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
Try to use single quote ' for strings that will not be displayed in the UI, i.e. dict keys and double quote " for UI strings This comment applies to other places too |
||||||||||||||||||||||
] | ||||||||||||||||||||||
) | ||||||||||||||||||||||
active = fields.Boolean(default=True) | ||||||||||||||||||||||
state = fields.Selection( | ||||||||||||||||||||||
[ | ||||||||||||||||||||||
("new", "New"), | ||||||||||||||||||||||
("offer", "Offer"), | ||||||||||||||||||||||
("received", "Offer Received"), | ||||||||||||||||||||||
("offer_accepted", "Offer Accepted"), | ||||||||||||||||||||||
("sold", "Sold"), | ||||||||||||||||||||||
("cancelled", "Cancelled"), | ||||||||||||||||||||||
], | ||||||||||||||||||||||
copy=False, | ||||||||||||||||||||||
default="new", | ||||||||||||||||||||||
string="State", | ||||||||||||||||||||||
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. Not needed; string default to the variable name (minus eventual _id(s)) suffixes |
||||||||||||||||||||||
) | ||||||||||||||||||||||
property_type_id = fields.Many2one("estate.property.type", string="Property Type") | ||||||||||||||||||||||
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) | ||||||||||||||||||||||
salesperson_id = fields.Many2one( | ||||||||||||||||||||||
"res.users", string="Sales Person", default=lambda self: self.env.uid | ||||||||||||||||||||||
) | ||||||||||||||||||||||
tag_ids = fields.Many2many("estate.property.tag", string="Tags") | ||||||||||||||||||||||
estate_property_offer_ids = fields.One2many( | ||||||||||||||||||||||
"estate.property.offer", "estate_property_id" | ||||||||||||||||||||||
) | ||||||||||||||||||||||
|
||||||||||||||||||||||
total_area = fields.Integer(compute="_compute_total_area") | ||||||||||||||||||||||
|
||||||||||||||||||||||
@api.depends("living_area", "garden_area") | ||||||||||||||||||||||
def _compute_total_area(self) -> None: | ||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||
record.total_area = record.living_area + record.garden_area | ||||||||||||||||||||||
|
||||||||||||||||||||||
best_price = fields.Float(compute="_compute_best_price", string="Best Offer") | ||||||||||||||||||||||
|
||||||||||||||||||||||
@api.depends("estate_property_offer_ids.price", "estate_property_offer_ids.status") | ||||||||||||||||||||||
def _compute_best_price(self) -> None: | ||||||||||||||||||||||
record: EstatePropert | ||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||
offers = record.estate_property_offer_ids | ||||||||||||||||||||||
offers = offers.filtered(lambda o: o.status != "refused") | ||||||||||||||||||||||
record.best_price = max(offers.mapped("price") or [0]) | ||||||||||||||||||||||
|
||||||||||||||||||||||
@api.onchange("garden") | ||||||||||||||||||||||
def _onchange_garden(self) -> None: | ||||||||||||||||||||||
self.garden_area = 10 if self.garden else 0 | ||||||||||||||||||||||
self.garden_orientation = "north" if self.garden else "" | ||||||||||||||||||||||
|
||||||||||||||||||||||
def action_cancel_property(self) -> bool: | ||||||||||||||||||||||
record: EstateProperty | ||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||
if record.state == "sold": | ||||||||||||||||||||||
raise exceptions.UserError("You can't cancel a sold property") | ||||||||||||||||||||||
record.state = "cancelled" | ||||||||||||||||||||||
return True | ||||||||||||||||||||||
|
||||||||||||||||||||||
def action_sell_property(self) -> bool: | ||||||||||||||||||||||
record: EstateProperty | ||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||
if record.state == "cancelled": | ||||||||||||||||||||||
raise exceptions.UserError("You can't sell a cancelled property") | ||||||||||||||||||||||
record.state = "sold" | ||||||||||||||||||||||
return True | ||||||||||||||||||||||
|
||||||||||||||||||||||
_sql_constraints = [ | ||||||||||||||||||||||
( | ||||||||||||||||||||||
"positive_expected_price", | ||||||||||||||||||||||
"CHECK(expected_price > 0)", | ||||||||||||||||||||||
"Expected price should be stictly positive", | ||||||||||||||||||||||
), | ||||||||||||||||||||||
( | ||||||||||||||||||||||
"positive_selling_price", | ||||||||||||||||||||||
"CHECK(selling_price >= 0)", | ||||||||||||||||||||||
"Selling price should be positive", | ||||||||||||||||||||||
), | ||||||||||||||||||||||
] | ||||||||||||||||||||||
|
||||||||||||||||||||||
@api.constrains("selling_price", "expected_price", "state") | ||||||||||||||||||||||
def _check_selling_price(self) -> None: | ||||||||||||||||||||||
record: EstateProperty | ||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||
if record.state != "offer_accepted": | ||||||||||||||||||||||
continue | ||||||||||||||||||||||
is_lower = ( | ||||||||||||||||||||||
tools.float_compare( | ||||||||||||||||||||||
record.selling_price, | ||||||||||||||||||||||
record.expected_price * 0.9, | ||||||||||||||||||||||
precision_digits=2, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
== -1 | ||||||||||||||||||||||
) | ||||||||||||||||||||||
if is_lower: | ||||||||||||||||||||||
raise exceptions.ValidationError( | ||||||||||||||||||||||
"Selling price cannot be lower than 90% of the expected price." | ||||||||||||||||||||||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from odoo import models, fields, api, exceptions | ||
|
||
|
||
class EstatePropertyOffer(models.Model): | ||
_name = "estate.property.offer" | ||
_description = "" | ||
_order = "price desc" | ||
|
||
price = fields.Float() | ||
status = fields.Selection( | ||
[ | ||
("accepted", "Accepted"), | ||
("refused", "Refused"), | ||
], | ||
copy=False, | ||
) | ||
partner_id = fields.Many2one("res.partner", required=True) | ||
estate_property_id = fields.Many2one("estate.property", required=True) | ||
|
||
validity = fields.Integer(default=7) | ||
date_deadline = fields.Date( | ||
compute="_compute_date_deadline", inverse="_inverse_date_deadline" | ||
) | ||
|
||
@api.depends("create_date", "validity") | ||
def _compute_date_deadline(self) -> None: | ||
record: EstatePropertyOffer | ||
for record in self: | ||
create_date: fields.Datetime = record.create_date or fields.Datetime.now() | ||
record.date_deadline = fields.Date.add(create_date, days=record.validity) | ||
|
||
def _inverse_date_deadline(self) -> None: | ||
# FIXME wasted too much time on this. | ||
record: EstatePropertyOffer | ||
for record in self: | ||
pass | ||
|
||
@api.model_create_multi | ||
def create(self: 'EstatePropertyOffer', vals_list: list[api.ValuesType]): | ||
for val in vals_list: | ||
property_id: int = val['estate_property_id'] | ||
property = self.env['estate.property'].browse(property_id) | ||
prices = property.estate_property_offer_ids.mapped('price') | ||
min_price = min(prices or [0]) | ||
if val['price'] < min_price: | ||
raise exceptions.UserError(f'Price cannot be lower than ${min_price}') | ||
|
||
|
||
return super().create(vals_list) | ||
|
||
def action_accept_offer(self) -> bool: | ||
record: EstatePropertyOffer | ||
for record in self: | ||
all_offers = record.estate_property_id.estate_property_offer_ids | ||
if all_offers.filtered(lambda o: o.status == "accepted"): | ||
raise exceptions.UserError("You can't accept two offers") | ||
record.estate_property_id.buyer_id = record.partner_id | ||
record.estate_property_id.selling_price = record.price | ||
record.estate_property_id.state = "offer_accepted" | ||
record.status = "accepted" | ||
return True | ||
|
||
def action_refuse_offer(self) -> bool: | ||
record: EstatePropertyOffer | ||
for record in self: | ||
record.status = "refused" | ||
return True | ||
|
||
_sql_constraints: list[tuple[str, str, str]] = [ | ||
( | ||
"offer_price_strictly_positive", | ||
"CHECK(offer > 0)", | ||
"Offer price must be stricly positive.", | ||
), | ||
] | ||
|
||
property_type_id = fields.Many2one(related="estate_property_id.property_type_id", store=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from odoo import models, fields | ||
from typing import final | ||
|
||
|
||
@final | ||
class EstatePropertyTag(models.Model): | ||
_name = "estate.property.tag" | ||
_description = "Tag for a property" | ||
_order = "name asc" | ||
|
||
name = fields.Char(required=True) | ||
|
||
_sql_constraints: list[tuple[str, str, str]] = [ | ||
( | ||
"unique_name", | ||
"UNIQUE (name)", | ||
"Tag name should be unique", | ||
), | ||
] |
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.
Runbot is already shouting at you for this so I won't