Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
26 changes: 26 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "Real Estate",
"version": "19.0.0.1.0",
"category": "Custom",
"summary": "My first application",
"installable": True,
"application": True,
"author": "belam",
"depends": [
"base",
],
"license": "LGPL-3",
"data": [
# Views per model
"views/estate_property_tag_views.xml",
"views/estate_property_salesman_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
# Menus
"views/estate_property_menus.xml",
# Security
"security/ir.model.access.csv",
# Dqtq
],
}
5 changes: 5 additions & 0 deletions estate/models/__init__.py
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_offer
from . import estate_property_tag
from . import estate_property_salesman
179 changes: 179 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Estate Property"
_order = "id desc"

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"),
],
default="new",
copy=False,
)
name = fields.Char(
required=True,
string="Title",
)
description = fields.Text(
string="Description",
)
postcode = fields.Char(
string="Postcode",
)
date_availability = fields.Date(
default=fields.Date.add(fields.Date.today(), months=3),
copy=False,
string="Available from",
)

expected_price = fields.Monetary(
string="Expected price", currency_field="currency_id",
)

selling_price = fields.Monetary(
string="Selling price",
readonly=True,
copy=False,
default_export_compatible=False,
currency_field="currency_id",
)
currency_id = fields.Many2one(
"res.currency", default=lambda self: self.env.company.currency_id,
)

bedrooms = fields.Integer(default=2, string="Bedrooms")
living_area = fields.Integer(string="Living Area", default=0)
facades = fields.Integer(string="Facades")
garage = fields.Boolean(string="Garage")
has_garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(string="Garden Area", default=0)
total_area = fields.Integer(compute="_compute_total_area", string="Total Area")
garden_orientation = fields.Selection(
string="Garden Orientation",
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
help="If you don't know where West is, wait for the sun to go to sleep. Its bedroom lies West.",
Comment thread
belam-odoo marked this conversation as resolved.
)

customer_id = fields.Many2one("res.partner", string="Customer", copy=False)

salesman_id = fields.Many2one(
"res.users",
string="Salesman",
default=lambda self: self.env.user,
required=True,
)
estate_property_type_id = fields.Many2one(
"estate.property.type",
string="Property Type",
)

tag_ids = fields.Many2many("estate.property.tag", string="Tags")

offer_ids = fields.One2many(
comodel_name="estate.property.offer",
inverse_name="estate_property_id",
)

best_price = fields.Monetary(
string="Best Offer",
compute="_compute_best_price",
)

#### CONSTRAINTS ####
_check_expected_price_positive = models.Constraint(
"CHECK(expected_price > 0)",
"Expected price must be positive",
)

_check_selling_price = models.Constraint(
"CHECK(selling_price >= 0)",
"the selling price must be positive.",
)

@api.constrains("selling_price", "expected_price")
def _check_selling_price(self):
for ep in self:
if float_is_zero(ep.selling_price, precision_digits=2):
continue

lowest_selling_price = ep.expected_price * 0.9
if float_compare(ep.selling_price, lowest_selling_price, precision_digits=2) == -1:
raise ValidationError(
_(
"The selling price must be at least 90%% of the expected price! "
"(Minimum expected: %s)", lowest_selling_price,
),
)

#### COMPUTED VALUES ####
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for ep in self:
ep.total_area = (
ep.living_area + ep.garden_area if ep.has_garden else ep.living_area
)

@api.depends("offer_ids.price")
def _compute_best_price(self):
for ep in self:
ep.best_price = max(ep.offer_ids.mapped("price")) if ep.offer_ids else None

@api.onchange("has_garden")
def _onchange_has_garden(self):
if self.has_garden:
self.garden_area = 10
self.garden_orientation = "north"
Comment thread
belam-odoo marked this conversation as resolved.
else:
self.garden_area = 0
self.garden_orientation = False

#### CRUD ####

#### ACTIONS ####
def action_set_accepted(self):
for ep in self:
# removed validation this looks a bit empty...
ep.state = "offer_accepted"

def action_set_sold(self):
for ep in self:
if ep.state == "cancelled":
raise UserError(_("Cancelled properties cannot be sold."))
if ep.state == "sold":
raise UserError(_("Sold properties cannot be sold (anymore)."))

accepted_offer = ep.offer_ids.filtered(lambda o: o.status == "accepted")
if not accepted_offer:
raise UserError(_("You must accept an offer before selling the property."))

ep.write({
"state": "sold",
"customer_id": accepted_offer[0].partner_id.id,
"selling_price": accepted_offer[0].price,
})
return True

def action_set_cancelled(self):
# todo: split action and validation (validation goes into python constraint)
for ep in self:
# todo later : add warning - dont know how to do this from the model
if ep.state == "sold":
raise UserError(_("A cancelled property cannot be sold"))
ep.state = "cancelled"
return True
114 changes: 114 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from dateutil.relativedelta import relativedelta

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer"
_order = "price desc"

partner_id = fields.Many2one("res.partner", string="Customer", required=True)
estate_property_id = fields.Many2one(
comodel_name="estate.property",
string="Estate Property",
required=True,
)
price = fields.Monetary(string="Price", required=True, currency_field="currency_id")
currency_id = fields.Many2one(
"res.currency", default=lambda self: self.env.company.currency_id,
)
# STATUS AND DATE #
status = fields.Selection(
selection=[("accepted", "Accepted"), ("refused", "Refused")],
default=None,
string="Offer Status",
)

validity = fields.Integer(string="Validity (days)", default=7)
date_deadline = fields.Date(
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
string="Deadline",
)

# USEFUL FOR DISPLAY #
property_name = fields.Char("Property Name", related="estate_property_id.name")
property_type_name = fields.Char(
"Type Name", related="estate_property_id.estate_property_type_id.name",
)
property_price = fields.Monetary("Expected price", related="estate_property_id.expected_price")
property_postcode = fields.Char("Postcode", related="estate_property_id.postcode")
property_type_id = fields.Many2one("estate.property.type", related="estate_property_id.estate_property_type_id", string="Property Type", store=True)

### CONSTRAINTS AND VALIDATION ###
_check_expected_price_positive = models.Constraint(
"CHECK(price > 0)",
"Selling price must be positive",
)

@api.constrains("status", "estate_property_id")
def _check_unique_accepted_offer(self):
for offer in self:
accepted_offers = offer.estate_property_id.offer_ids.filtered(
lambda o: o.status == "accepted",
)
if len(accepted_offers) > 1:
raise ValidationError(
_("You cannot have multiple accepted offers for the same property."),
)

### COMPUTATED VALUES ###
@api.depends("validity")
def _compute_date_deadline(self):
for record in self:
base_date = record.create_date or fields.Date.today()
days_to_add = record.validity or 0
record.date_deadline = base_date + relativedelta(days=days_to_add)

def _inverse_date_deadline(self):
for o in self:
o.validity = (o.date_deadline - o.create_date.date()).days

#### CRUD ####
@api.model
def create(self, vals_list):
for vals in vals_list:
related_property = self.env["estate.property"].browse(vals.get("estate_property_id"))
if related_property.state in ("offer_accepted", "sold", "cancelled"):
raise UserError(_("Offers cannot be submitted for this property."))
offered_price = vals.get("price", 0)
if related_property.offer_ids:
max_offer = max(related_property.offer_ids.mapped("price"))
if offered_price < max_offer:
raise UserError(_("The offer must be higher than %.2f", max_offer))
related_property.state = "offer_received"
return super().create(vals_list)

@api.ondelete(at_uninstall=False)
def _unlink_by_user(self):
for o in self:
if o.status not in ("new", "cancelled"):
raise UserError(_("An accepted offer cannot be deleted."))

### ACTIONS ###
def action_set_refused(self):
for o in self:
if o.status:
raise UserError(_("This offer has already been %s", o.status))
o.status = "refused"
return True

def action_set_accepted(self):
for record in self:
if record.estate_property_id.offer_ids.filtered(
lambda o: o.status == "accepted",
):
raise UserError(
_("An offer has already been accepted for this property."),
)

record.status = "accepted"
record.estate_property_id.action_set_accepted()
return True
12 changes: 12 additions & 0 deletions estate/models/estate_property_salesman.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from odoo import fields, models


class EstatePropertySalesman(models.Model):
_inherit = "res.users"

property_ids = fields.One2many(
"estate.property",
"salesman_id",
string="Assigned properties",
domain=[("state", "in", ["new", "offer_received"])],
)
20 changes: 20 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from random import randint

from odoo import fields, models


class Tag(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag"
_order = "name"

def _get_default_color(self):
return randint(0, 11)

name = fields.Char(string="Tag Name", required=True)
color = fields.Integer(default=_get_default_color)

_name_uniq = models.Constraint(
"unique (name)",
"Tag name must be unique",
)
25 changes: 25 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from odoo import api, fields, models


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate Property Type"
_order = "name"

name = fields.Char(required=True, string="Property Type Name")
sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.")

property_ids = fields.One2many("estate.property", "estate_property_type_id", string="Properties")
offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers")

offer_count = fields.Integer(compute="_compute_offer_count", string="Offer Count")

_name_uniq = models.Constraint(
"unique (name)",
"Property Type name must be unique",
)

@api.depends("offer_ids")
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
5 changes: 5 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
estate.access_estate_property,access_estate_property,estate.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_offer,access_estate_property_offer,estate.model_estate_property_offer,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
1 change: 1 addition & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_estate_property
Loading