Skip to content

Commit

Permalink
[ADD] estate: a new mdoule to manage estate properties
Browse files Browse the repository at this point in the history
Problem
---------
Users are missing a way to manage real estate properties in odoo.

Objective
---------
- Add a way to create new properties, track their attributes and the
state of the sale (new, offer received, ..., sold).
- Add a way to visualise and filter the properties base on their
attributes and status. And give the user a way to categorize the
properties.
- Add a way to manage offer made by parters on individual properties. Be
able to accept/refuse the offers.
- Create an invoice to the client once a sale is closed.

Solution
---------
Add a new module `estate`. This model is in charge of managing the
properties, tracking their sales state and offers.
Add a new module `estate_account` to create invoices once a sale is
closed.

Closes #162
  • Loading branch information
lipiraux committed Oct 24, 2024
1 parent b16e643 commit 3e3c474
Show file tree
Hide file tree
Showing 21 changed files with 618 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# vscode stuff
.vscode
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
22 changes: 22 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
'name': "Real Estate",
'version': '0.1',
'depends': ['base'],
'description': "A real estate app",
'category': 'Tutorials/RealEstate',
'application': True,
# data files always loaded at installation
'data': [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/res_users_views.xml",
"views/estate_menus.xml"
],
'license': "LGPL-3",
'demo': [
"data/demo.xml",
],
}
13 changes: 13 additions & 0 deletions estate/data/estate_property_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>
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
104 changes: 104 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from dateutil.relativedelta import relativedelta
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 = "Real Estate property"

_sql_constraints = [
('check_posiive_expected_price', 'CHECK(expected_price > 0)', "The expected price must be strictly positive."),
('check_positive_selling_price', 'CHECK(selling_price > 0)', "The selling price must be strictly positive."),
]
_order = "id desc"

name = fields.Char("Title", required=True)
description = fields.Text("Description")
postcode = fields.Char("Postcode")
date_availability = fields.Date(
"Available From",
copy=False,
default=lambda self: fields.Date.today() + relativedelta(months=3),
)
expected_price = fields.Float("Expected Price", required=True)
selling_price = fields.Float("Selling Price", readonly=True)
bedrooms = fields.Integer("Bedrooms", default=2)
living_area = fields.Integer("Living Area (sqm)")
facades = fields.Integer("Facades")
garage = fields.Boolean("Garage")
garden = fields.Boolean("Garden")
garden_area = fields.Integer("Garden Area (sqm)")
garden_orientation = fields.Selection(
string="Garden Orientation",
selection=[('N', "North"), ('S', "South"), ('E', "East"), ('W', "West")],
)
active = fields.Boolean("Active", default=True)
state = fields.Selection(
string="Status",
selection=[
('new', "New"),
('offer_received', "Offer Received"),
('offer_accepted', "Offer Accepted"),
('sold', "Sold"),
('canceled', "Canceled"),
],
required=True,
copy=False,
default='new',
)
property_type_id = fields.Many2one('estate.property.type', string="Property Type")
buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False)
salesman_id = fields.Many2one('res.users', string="Salesman", default=lambda self: self.env.user)
tag_ids = fields.Many2many('estate.property.tag', string="Tags")
offer_ids = fields.One2many('estate.property.offer', "property_id", string="Offers")
total_area = fields.Integer(string="Total Area (sqm)", compute='_compute_total_area')
best_price = fields.Integer(string="Best Offer", compute='_compute_best_price')

@api.depends('offer_ids.price')
def _compute_best_price(self):
for estate in self:
estate.best_price = max(estate.offer_ids.mapped('price'), default=False)

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

@api.onchange('garden')
def _onchange_garden(self):
if self.garden:
self.garden_area = 10
self.garden_orientation = 'N'
else:
self.garden_area = 0
self.garden_orientation = ''

def action_sold(self):
self.ensure_one()
if self.state == 'canceled':
raise UserError(_("Canceled properties cannot be sold."))
self.state = 'sold'
return True

def action_cancel(self):
self.ensure_one()
if self.state == 'sold':
raise UserError(_("Sold properties cannot be canceled."))
self.state = 'canceled'
return True

@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for estate in self:
if (
not float_is_zero(estate.selling_price, 3)
and float_compare(estate.selling_price, estate.expected_price * 0.9, 3) == -1
):
raise ValidationError(_("The selling price cannot be lower than 90% of the expected price."))

@api.ondelete(at_uninstall=False)
def _unlink_expecpt_new_and_sold_properties(self):
if any(estate.state in ('new', 'sold') for estate in self):
raise UserError(_("Can't delete new or sold properties."))
80 changes: 80 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from datetime import timedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError


class PropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Property offer"

_sql_constraints = [
(
"check_positive_offer_price",
"CHECK(price > 0)",
"The offer price must be strictly positive.",
),
]
_order = "price desc"

price = fields.Float(string="Price")
status = fields.Selection(
string="Status",
selection=[("accepted", "Accepted"), ("refused", "Refused")],
copy=False,
)
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
property_id = fields.Many2one("estate.property", string="Property", required=True)
validity = fields.Integer(string="Validiy (days)", default=7)
date_deadline = fields.Date(
string="Deadline Date",
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
)
property_type_id = fields.Many2one(
"estate.property.type",
string="Property Type",
related="property_id.property_type_id",
)

@api.depends("create_date", "validity")
def _compute_date_deadline(self):
for offer in self:
create_date = offer.create_date or fields.Datetime.now()
offer.date_deadline = create_date.date() + timedelta(days=offer.validity)

def _inverse_date_deadline(self):
for offer in self:
create_date = offer.create_date or fields.Datetime.now()
offer.validity = (offer.date_deadline - create_date.date()).days

def action_accept_offer(self):
for offer in self:
if offer.status == "accepted":
raise UserError(_("You already accepted the offer."))
if offer.property_id.buyer_id:
raise UserError(_("Only one offer can be accepted."))
offer.status = "accepted"
offer.property_id.selling_price = offer.price
offer.property_id.buyer_id = offer.partner_id
for property_offer in offer.property_id.offer_ids:
if property_offer.id != offer.id:
offer.status = "refused"
return True

def action_refuse_offer(self):
for offer in self:
if offer.status == "accepted":
raise UserError(
_("You cannot refuse an offer once it has been accepted.")
)
offer.status = "refused"

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
property_id = self.env["estate.property"].browse(vals["property_id"])
price = vals["price"]
if any(prev_offer.price > price for prev_offer in property_id.offer_ids):
raise UserError(_("An offer with an higher price already exists."))
property_id.state = "offer_received"
return super().create(vals_list)
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from random import randint
from odoo import models, fields


class PropertyTag(models.Model):
_name = 'estate.property.tag'
_description = "Property tag"

_sql_constraints = [
('check_unique_name', 'UNIQUE(name)', "This tag name already exists."),
]
_order = "name asc"

name = fields.Char("Name", required=True)
color = fields.Integer("Color", default=lambda _: randint(1, 10))
21 changes: 21 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from odoo import api, fields, models


class PropertyType(models.Model):
_name = 'estate.property.type'
_description = "Property type"

_sql_constraints = [
('check_unique_name', 'UNIQUE(name)', "This type name already exists."),
]
_order = "name asc"

name = fields.Char("Name", required=True)
sequence = fields.Integer("Sequence")
offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Linked Offers")
offer_count = fields.Integer("Nb. Linked Offer", compute='_compute_offer_count')

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


class ResUsers(models.Model):
# _name = 'res.users'
_inherit = 'res.users'

property_ids = fields.One2many("estate.property", "salesman_id", string="Properties", domain=['|', ('state', '=', 'new'), ('state', '=', '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
12 changes: 12 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<odoo>
<menuitem id="estate_property_menu" name="Real Estate">
<menuitem id="estate_property_menu_advertisments" name="Advertisements">
<menuitem id="estate_property_menu_properties" name="Properties" action="estate.estate_property_action"/>
</menuitem>
<menuitem id="estate_property_menu_settings" name="Settings">
<menuitem id="estate_property_type_menu" name="Property Types" action="estate.estate_property_type_action"/>
<menuitem id="estate_property_tag_menu" name="Property Tags" action="estate.estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
47 changes: 47 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Real Estate Property Offer</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">estate.property.offer.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list string="Offers" editable="bottom"
decoration-success="status == 'accepted'"
decoration-danger="status == 'refused'"
>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept_offer" type="object" string="Accept" icon="fa-check" invisible="status"/>
<button name="action_refuse_offer" type="object" string="Refuse" icon="fa-times" invisible="status"/>
<field name="status"/>
<field name="property_type_id"/>
</list>
</field>
</record>

<record id="estate_property_offer_view_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 string="Offer">
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="status"/>
<field name="validity"/>
<field name="date_deadline"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
34 changes: 34 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Real Estate Property Tag</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</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="Property Tag">
<sheet>
<group>
<field name="name"/>
<field name="color" required="True" widget="color_picker"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list string="Property Tags" editable="bottom">
<field name="name"/>
<field name="color" widget="color_picker"/>
</list>
</field>
</record>
</odoo>
Loading

0 comments on commit 3e3c474

Please sign in to comment.