Skip to content

[ADD] estate: Added a new module named estate #733

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

Open
wants to merge 20 commits into
base: 18.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b027a64
[ADD] estate: Added a new module named estate
msho-odoo Apr 22, 2025
b33bf48
[FIX] estate: fixed the name of the module to Real Estate
msho-odoo Apr 22, 2025
329a381
[IMP] estate: created a model for estate_property in db
msho-odoo Apr 22, 2025
078c258
[FIX] estate: added a licence key in manifest
msho-odoo Apr 23, 2025
c9b1dec
[IMP] estate: give all right access to base group user for estate_ pr…
msho-odoo Apr 23, 2025
365d986
[IMP] estate: add root menu and action menu views for estate propery …
msho-odoo Apr 23, 2025
0c863ac
[IMP] estate: add new fields and fileds default values to property form
msho-odoo Apr 23, 2025
4bd1002
[FIX] estate: fix linting and EOF in xml and csv files
msho-odoo Apr 23, 2025
450100a
[FIX] estate: add whitespace around arithmetic operator in estate pro…
msho-odoo Apr 23, 2025
8ae30b8
[IMP] estate: add custom filters and group by fields to estate proper…
msho-odoo Apr 23, 2025
8eb5b37
[IMP] estate: add a new model estate_property_type
msho-odoo Apr 23, 2025
9b71df7
[IMP] estate: add a new model estate_property_tag
msho-odoo Apr 23, 2025
f09e8e4
[IMP] estate: add a new model estate_property_offer
msho-odoo Apr 23, 2025
1120d88
[IMP] estate: add compute fields for estate_property and estate_prope…
msho-odoo Apr 24, 2025
fc150fb
[IMP] estate: added accept and refuse offer buttons
msho-odoo Apr 24, 2025
538ac8c
[IMP] estate: add sql and python constraints to estate_property
msho-odoo Apr 24, 2025
4c98e9d
[IMP] estate: add default search for estate_property and stat button …
msho-odoo Apr 26, 2025
57d42f0
[IMP] estate: add inherit model for res_users and link offers to user…
msho-odoo Apr 28, 2025
8acd0f1
[IMP] estate: create invoices for sold properties
msho-odoo Apr 28, 2025
83aa04c
[IMP] estate: add kanban view to estate_property
msho-odoo Apr 28, 2025
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
17 changes: 17 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
'name': 'Real Estate',
'depends':
['base'],
'data': [
'security/ir.model.access.csv',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_views.xml',
'views/res_users_views.xml',
'views/estate_menu.xml',
],
'installable': True,
'application': True,
'license': 'AGPL-3',
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget the license key to avoid warnings in the log.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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_tag
from . import estate_property_offer
from . import res_users
102 changes: 102 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from dateutil.relativedelta import relativedelta

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guideline: external import first

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"
_sql_constraints = [
("check_expected_price", "CHECK(expected_price > 0)",
"The expected price must be strictly positive"),
("check_sell_price", "CHECK(selling_price > 0)",
"The selling price of a property must be positive")
]
_order = "id desc"

name = fields.Char('name', required=True)
description = fields.Text('description')
postcode = fields.Char('postcode')
availability_date = fields.Date('availabilty date', copy=False,
default=fields.Date.today() + relativedelta(months=3))
expected_price = fields.Float('expected price', required=True)
selling_price = fields.Float('selling price', readonly=True, copy=False)
bedrooms = fields.Integer('bedrooms', default=2)
living_area = fields.Integer('living area')
facades = fields.Integer('facades')
garage = fields.Boolean('garage')
garden = fields.Boolean('garden')
garden_area = fields.Integer('garden area')
garden_orientation = fields.Selection(
string='Garden Orientation',
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West')
],
)
active = fields.Boolean('active', default=True)
state = fields.Selection(
string='State',
selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Acccepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled')
],
default="new",
)
property_type_id = fields.Many2one("estate.property.type", string="property type")
user_id = fields.Many2one("res.users", string="Salesperson",
default=lambda self: self.env.user)
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
total_area = fields.Integer(compute='_compute_total_area')
best_offer = fields.Float(compute="_compute_best_offer")

@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
record.total_area = record.living_area + record.garden_area

@api.depends("offer_ids")
def _compute_best_offer(self):
for record in self:
record.best_offer = max(record.offer_ids.mapped("price"), default=0.0)

@api.onchange("garden")
def _onchange_garden(self):
if self.garden:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say has_garden is a clearer name, but that is debatable

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, just for the sake of the tutorial

self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = 0
self.garden_orientation = None

def action_sell(self):
if "cancelled" in self.mapped('state'):
raise UserError("You can't sell a cancelled property")
self.state = "sold"
return True

def action_cancel(self):
if "sold" in self.mapped('state'):
raise UserError("You can't cancel a sold property")
self.state = "cancelled"
return True

@api.constrains("selling_price", "expected_price")
def _check_selling_price(self):
for record in self:
if not float_is_zero(record.selling_price, 2) and \
float_compare(record.selling_price, 0.9 * record.expected_price, 2) < 0:
raise ValidationError("The selling price has to be at least 90% of the expected price")

@api.ondelete(at_uninstall=False)
def _unlink_if_new_or_cancelled(self):
if any(x in self.mapped("state") for x in ["offer_received", "offer_accepted", "sold"]):
raise UserError("You can only delete a new or cancelled property")
65 changes: 65 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from dateutil.relativedelta import relativedelta
from odoo import api, models, fields
from odoo.exceptions import UserError


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate property offer"
_sql_constraints = [
("check_price", "CHECK(price > 0)", "The offer price must be strictly positive")
]
_order = "price desc"

price = fields.Float('price')
status = fields.Selection(
string='status',
selection=[
('accepted', 'Accepted'),
('refused', 'Refused')
],
)
partner_id = fields.Many2one('res.partner', required=True)
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")
property_type_id = fields.Many2one(related="property_id.property_type_id")

@api.depends("validity", "create_date")
def _compute_date_deadline(self):
for record in self:
start_date = record.create_date.date() if record.create_date else fields.Date.today()
record.date_deadline = start_date + \
relativedelta(days=record.validity)

def _inverse_date_deadline(self):
for record in self:
start_date = record.create_date.date() if record.create_date else fields.Date.today()
record.validity = (record.date_deadline - start_date).days

def action_accept(self):
for record in self:
if record.property_id.buyer_id:
raise UserError("You already accepted an offer")
if record.status == "refused":
raise UserError("You already refused this offer")
record.status = "accepted"
record.property_id.selling_price = record.price
record.property_id.buyer_id = record.partner_id
record.property_id.state = "offer_accepted"

def action_refuse(self):
for record in self:
if record.status == "accepted":
raise UserError("You already accepted this offer")
record.status = "refused"

@api.model_create_multi
def create(self, vals):
for val in vals:
property_record = self.env["estate.property"].browse(val["property_id"])
if val["price"] < property_record.best_offer:
raise UserError(f"Your offer can't be lower than {property_record.best_offer}")

property_record.state = "offer_received"
return super().create(vals)
13 changes: 13 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import models, fields


class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Estate property tag"
_sql_constraints = [
("check_unique_name", "UNIQUE(name)", "Property tag must be unqiue")
]
_order = "name"

name = fields.Char('name', required=True)
color = fields.Integer("color")
20 changes: 20 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from odoo import api, models, fields


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate property type"
_sql_constraints = [
("check_unique_name", "UNIQUE(name)", "Property type name must be unqie")
]
_order = "sequence, name"

name = fields.Char('name', required=True)
property_ids = fields.One2many("estate.property", "property_type_id")
sequence = fields.Integer("sequence")
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer(compute="_compute_offer_count")

@api.depends("offer_ids")
def _compute_offer_count(self):
self.offer_count = len(self.offer_ids)
7 changes: 7 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from odoo import fields, models


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

property_ids = fields.One2many("estate.property", "user_id", domain=[('state', 'in', ['new', 'offer_received'])])
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
access_estate_property, access_estate_property,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type, access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag, access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer, access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1
11 changes: 11 additions & 0 deletions estate/views/estate_menu.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<odoo>
<menuitem id="estate_root_menu" name="Real Estate">
<menuitem id="estate_advertisement_menu" name="Advertisements">
<menuitem id="estate_property_action_menu" action="estate_property_action"></menuitem>
</menuitem>
<menuitem id="estate_type_settings_menu" name="Settings">
<menuitem id="estate_property_type_action_menu" action="estate_property_type_action"></menuitem>
<menuitem id="estate_property_tag_action_menu" action="estate_property_tag_action"></menuitem>
</menuitem>
</menuitem>
</odoo>
46 changes: 46 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<odoo>
<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">list,form</field>
<field name="domain">[('property_type_id', '=', active_id)]</field>
</record>

<record id="estate_property_offer_view_list" model="ir.ui.view">
<field name="name">properties.view.offer.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list string="properties offer list" editable="bottom" decoration-success="status=='accepted'" decoration-danger="status=='refused'">
<field name="price"/>
<field name="validity"/>
<field name="date_deadline"/>
<field name="partner_id"/>
<field name="property_type_id"/>
<button name="action_accept" string = "accept" type="object" icon="fa-check" invisible="status in ['accepted', 'refused']"/>
<button name="action_refuse" string = "refuse" type="object" icon="fa-times" invisible="status in ['accepted', 'refused']"/>
</list>
</field>
</record>

<record id="estate_property_offer_view_form" model="ir.ui.view">
<field name="name">properties.view.offer.form</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<form string="properties offer from">
<sheet>
<group>
<group>
<field name="validity"/>
<field name="date_deadline"/>
</group>
<group>
<field name="price"/>
<field name="status"/>
<field name="partner_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
41 changes: 41 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<odoo>
<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Property Tags</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</field>
</record>

<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">properties.view.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list string="properties tag list" editable="bottom">
<field name="name"/>
</list>
</field>
</record>

<record id="estate_property_tag_view_form" model="ir.ui.view">
<field name="name">properties.view.tag.form</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<form string="properties tag from">
<sheet>
<h1>
<field name="name"/>
</h1>
</sheet>
</form>
</field>
</record>

<record id="estate_property_tag_search" model="ir.ui.view">
<field name="name">properties.tag.search</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<search string="property tag search">
<field name="name"/>
</search>
</field>
</record>
</odoo>
Loading