Skip to content

[ADD] estate module #788

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 14 commits into
base: 18.0
Choose a base branch
from
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
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/

# IDE
.idea/
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': 'estate',
'license': 'AGPL-3',
'depends': [
'base'
],
'data': [
'ir.model.access.csv',
'views/estate_custom_user_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_views.xml',
'views/estate_menus.xml',
],
'application': True
}
5 changes: 5 additions & 0 deletions estate/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_property_model,access_property_model,model_estate_property,base.group_user,1,1,1,1
access_property_type_model,access_property_type_model,model_estate_property_type,base.group_user,1,1,1,1
access_property_tag_model,access_property_tag_model,model_estate_property_tag,base.group_user,1,1,1,1
access_property_offer_model,access_property_offer_model,model_estate_property_offer,base.group_user,1,1,1,1
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 estate_custom_users
8 changes: 8 additions & 0 deletions estate/models/estate_custom_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import models, fields


class EstateCustomUsers(models.Model):
_inherit = 'res.users'

property_ids = fields.One2many("estate.property", inverse_name='salesperson_id', string="Available Properties",
domain=[('state', 'in', ['new', 'offer_received'])])
113 changes: 113 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from datetime import datetime, timedelta
from odoo import api, fields, models, exceptions, tools


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Property's properties"
_order = "id desc"
_sql_constraints = [
('check_expected_price', 'CHECK (expected_price > 0)', "The expected price must be greater than 0"),
('check_selling_price', 'CHECK (selling_price >= 0)', "The selling price must be greater or equal to 0"),
]

name = fields.Char('Property name', required=True, default='Unknown')
description = fields.Text('Property Description')
postcode = fields.Char('Postcode')
date_availability = fields.Date('Availability', copy=False,
default=lambda self: datetime.now() + timedelta(days=90))
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 (sqm)')
facades = fields.Integer('Facades')
garage = fields.Boolean('Garage')
garden = fields.Boolean('Garden')
garden_area = fields.Integer('Garden Area')
garden_orientation = fields.Selection([('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')],
default='north')
active = fields.Boolean('Active', default=True)
state = fields.Selection([
('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer accepted'), ('sold', 'Sold'),
('cancelled', 'Cancelled')
], default='new', required=True, copy=False)

total_area = fields.Float('Total Area', compute='_compute_total_area')
best_price = fields.Float('Best Offer', compute='_compute_best_price')
is_state_set = fields.Boolean('Is State Set', compute='_compute_is_state_set')

property_type_id = fields.Many2one('estate.property.type', string="Property Type")
buyer_id = fields.Many2one("res.partner", string="Buyer")
salesperson_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")

@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")
def _compute_best_price(self):
for record in self:
best_offer = record.get_best_offer()
if best_offer is None:
record.best_price = 0
continue
record.best_price = best_offer.price

def get_best_offer(self):
if len(self.offer_ids) == 0:
return None
return self.offer_ids.sorted(lambda offer: offer.price)[-1]

@api.onchange("garden")
def _onchange_garden(self):
for record in self:
if record.garden:
record.garden_orientation = 'North'
record.garden_area = 10
else:
record.garden_orientation = None
record.garden_area = 0

def action_sold(self):
valid_records = self.filtered(lambda r: r.state != 'cancelled')
if not valid_records:
raise exceptions.UserError(self.env._("Can't sell a cancelled offer"))
for record in valid_records:
best_offer = record.get_best_offer()
if best_offer is None:
raise exceptions.UserError(self.env._("The offers for property %s are empty", record.name))
record.mark_as_sold(best_offer)

def mark_as_sold(self, offer):
offer.status = 'accepted'
self.state = 'sold'
self.buyer_id = offer.partner_id
self.selling_price = offer.price

def action_cancelled(self):
for record in self:
if record.state == 'sold':
raise exceptions.UserError(self.env._("Can't cancel a sold offer"))
record.state = 'cancelled'

@api.constrains('selling_price', 'expected_price')
def _check_prices(self):
for record in self:
if not tools.float_is_zero(record.selling_price, 3) and record.selling_price < record.expected_price * 0.9:
raise exceptions.ValidationError(
self.env._("The selling price cannot be lower than 90%% of the expected price (min %d€)",
record.expected_price * 0.9))

@api.depends('state')
def _compute_is_state_set(self):
for record in self:
record.is_state_set = record.state == 'cancelled' or record.state == 'sold'

@api.ondelete(at_uninstall=False)
def _on_delete(self):
for record in self:
if not (record.state in ('cancelled', 'new')):
raise exceptions.UserError(self.env._("Can only delete a 'New' or 'Cancelled' property (this property is '%s').", record.state))
63 changes: 63 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from datetime import timedelta

from odoo import api, fields, models, exceptions


class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'An offer'
_order = 'price desc'
_sql_constraints = [
('check_price', 'CHECK (price > 0)', 'The price must be greater than 0'),
]

price = fields.Float("Price", required=True)
status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], copy=False)
offer_date = fields.Date(string="Creation Date", default=lambda self: fields.Date.today())
validity = fields.Integer("Validity (days)")

date_deadline = fields.Date("Deadline", readonly=False, compute="_compute_date_deadline",
inverse='_compute_date_deadline_inverse')

partner_id = fields.Many2one('res.partner', string="Partner", required=True)
property_id = fields.Many2one('estate.property', string="Property", required=True)
property_type_id = fields.Many2one('estate.property.type', string="Property Type",
related='property_id.property_type_id')

@api.depends("validity")
def _compute_date_deadline(self):
for offer in self:
if offer.offer_date is not None:
offer.date_deadline = fields.Date.to_date(offer.offer_date) + timedelta(days=offer.validity)

def _compute_date_deadline_inverse(self):
for offer in self:
offer.validity = (fields.Date.to_date(offer.date_deadline) - fields.Date.to_date(offer.offer_date)).days

def action_accepted(self):
for offer in self:
for prop_offer in offer.property_id.offer_ids:
if prop_offer.status == 'accepted':
raise exceptions.UserError(self.env._(
"Can't accept an offer when another offer has already been accepted for this property."))
offer.property_id.mark_as_sold(self)
offer.status = 'accepted'

def action_refused(self):
for offer in self:
offer.status = 'refused'

@api.model_create_multi
def create(self, val_list):
for val in val_list:
property = self.env['estate.property'].browse(val['property_id'])
property.state = "offer_received"

best_offer = property.get_best_offer()
if best_offer is None:
continue
if best_offer.price > val['price']:
raise exceptions.UserError(self.env._(
f"Can't create an offer with a lower price than the best offer for this property (current best "
f"price is {best_offer.price}€)."))
return super().create(val_list)
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 fields, models


class EstatePropertyTag(models.Model):
_name = 'estate.property.tag'
_description = 'A tag'
_order = "name asc"
_sql_constraints = [
('check_unicity', 'UNIQUE (name)', 'A tag with the same name already exists'),
]

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


class EstatePropertyType(models.Model):
_name = 'estate.property.type'
_description = 'Type of a property'
_order = "sequence, name asc"
_sql_constraints = [
('check_unicity', 'UNIQUE (name)', 'A property type with the same name already exists'),
]

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

property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties')
offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers')
offer_count = fields.Integer('Offer Count', compute='_compute_offer_count')

@api.depends('offer_ids')
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
14 changes: 14 additions & 0 deletions estate/views/estate_custom_user_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<odoo>
<record id="estate_custom_users_view_form" model="ir.ui.view">
<field name="name">estate.res.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='preferences']" position="after">
<page string="Available Properties">
<field name="property_ids"/>
</page>
</xpath>
</field>
</record>
</odoo>
11 changes: 11 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<odoo>
<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_ads_property_menu" name="Advertisements">
<menuitem id="estate_menu_action" action="estate_property_action" name="Properties"/>
</menuitem>
<menuitem id="estate_settings_property_menu" name="Settings">
<menuitem id="estate_type_menu_action" action="estate_property_type_action" name="Property Types"/>
<menuitem id="estate_tag_menu_action" action="estate_property_tag_action" name="Property Tags"/>
</menuitem>
</menuitem>
</odoo>
43 changes: 43 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<odoo>
<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">list,form</field>
</record>

<record id="estate_property_offer_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 editable="top" decoration-success="status == 'accepted'" decoration-danger="status == 'refused'">
<field name="price"/>
<field name="partner_id"/>
<field name="property_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accepted" title="" type="object" icon="fa-check" invisible="status == 'accepted' or status == 'refused'"/>
<button name="action_refused" title="" type="object" icon="fa-close" invisible="status == 'accepted' or status == 'refused'"/>
</list>
</field>
</record>

<record id="estate_property_offer_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>
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accepted" string="Accepted" type="object" icon="fa-check"/>
<button name="action_refused" string="Refused" type="object" icon="fa-close"/>
<field name="status"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
18 changes: 18 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<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_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 editable="top">
<field name="name"/>
<field name="color"/>
</list>
</field>
</record>
</odoo>
Loading