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
44 changes: 44 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# EditorConfig is awesome: https://editorconfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8

# 4 space indentation
[*.py]
indent_style = space
indent_size = 4

# Tab indentation (no size specified)
[Makefile]
indent_style = tab

# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2

# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2


[*.{xml,xsd}]
max_line_length = off
end_of_line = lf
indent_style = space
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_size = 2
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
24 changes: 24 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Estate',
'version': '0.1',
'sequence': 99,
'summary': 'Estate Management',
'depends': [
'base',
'web',
],
'installable': True,
'application': True,
'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_tags_views.xml',
'views/res_users_views.xml',
'views/estate_menus.xml',
],
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}
9 changes: 9 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import (
estate_property,
estate_property_offer,
estate_property_tag,
estate_property_type,
res_users,
)
107 changes: 107 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare


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

DEFAULT_BEDROOM_COUNT = 2
DEFAULT_AVAILABILITY_DATE = fields.Date.today() + relativedelta(months=3)

name = fields.Char(string='Title', required=True, default='New Property')
description = fields.Text()
active = fields.Boolean(default=True)
date_availability = fields.Date(string='Available From', copy=False, default=DEFAULT_AVAILABILITY_DATE)

expected_price = fields.Float(required=True)

selling_price = fields.Float(readonly=True, copy=False)

state = fields.Selection(
selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('canceled', 'Canceled'),
],
default='new',
)

bedrooms = fields.Integer(default=DEFAULT_BEDROOM_COUNT)
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(string='Garden Area (sqm)')
garden_orientation = fields.Selection(
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
],
)
living_area = fields.Integer(string='Living Area (sqm)')
total_area = fields.Integer(string='Total Area (sqm)', compute='_compute_total_area')
postcode = fields.Char()

property_type_id = fields.Many2one('estate.property.type', string='Property Type')
salesperson_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user)
buyer_id = fields.Many2one('res.partner', string='Buyer')

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

offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
best_price = fields.Float(string='Best Offer', compute='_compute_best_price')

_check_selling_price = models.Constraint('check(selling_price > 0)', 'Selling Price must be greater than 0')
_check_expected_price = models.Constraint('check(expected_price > 0)', 'Expected Price must be greater than 0')

@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.price')
def _compute_best_price(self):
for record in self:
record.best_price = max(record.offer_ids.mapped('price'), default=0.0)

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

def action_sold(self):
CANNOT_SELL_CANCELED_PROPERTY = "You cannot sell a canceled property"
if self.state == 'canceled':
raise UserError(CANNOT_SELL_CANCELED_PROPERTY)
self.state = 'sold'

def action_cancel(self):
CANNOT_SELL_SOLD_PROPERTY = "You cannot cancel a sold property"
if self.state == 'sold':
raise UserError(CANNOT_SELL_SOLD_PROPERTY)
self.state = 'canceled'

@api.constrains('selling_price')
def _check_selling_price(self):
SELLING_PRICE_MUST_BE_AT_LEAST_90_PERCENT_OF_EXPECTED_PRICE = 'Selling Price must be at least 90% of Expected Price'
for record in self:
if float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) == -1:
raise UserError(SELLING_PRICE_MUST_BE_AT_LEAST_90_PERCENT_OF_EXPECTED_PRICE)

@api.ondelete(at_uninstall=False)
def _unlink_if_not_new_or_canceled(self):
YOU_CANNOT_DELETE_A_PROPERTY_THAT_IS_NOT_NEW_OR_CANCELED = 'You cannot delete a property that is not new or canceled'
if any(record.state not in ('new', 'canceled') for record in self):
raise UserError(YOU_CANNOT_DELETE_A_PROPERTY_THAT_IS_NOT_NEW_OR_CANCELED)
71 changes: 71 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from dateutil.relativedelta import relativedelta

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


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

price = fields.Float()
status = fields.Selection(
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)
property_type_id = fields.Many2one(
related='property_id.property_type_id',
store=True,
string='Property Type',
)
validity = fields.Integer(default=7)
date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline')

_check_price = models.Constraint('check(price > 0)', 'Price must be greater than 0')

@api.depends('create_date', 'validity')
def _compute_date_deadline(self):
for record in self:
create_date = fields.Date.today()
if record.create_date:
create_date = record.create_date.date()

record.date_deadline = create_date + relativedelta(days=record.validity)

def _inverse_date_deadline(self):
for record in self:
create_date = fields.Date.today()
if record.create_date:
create_date = record.create_date.date()

record.validity = (record.date_deadline - create_date).days

def action_accept(self):
CANNOT_ACCEPT_ACCEPTED_OFFER = "You cannot accept an offer for a property that already has an accepted offer"
if self.property_id.state == 'offer_accepted':
raise UserError(CANNOT_ACCEPT_ACCEPTED_OFFER)

self.status = 'accepted'
self.property_id.state = 'offer_accepted'
self.property_id.selling_price = self.price
self.property_id.buyer_id = self.partner_id

def action_refuse(self):
CANNOT_REFUSE_ACCEPTED_OFFER = "You cannot refuse an accepted offer"
if self.status == 'accepted':
raise UserError(CANNOT_REFUSE_ACCEPTED_OFFER)
self.status = 'refused'

@api.model
def create(self, vals_list):
for vals in vals_list:
property_id = self.env['estate.property'].browse(vals['property_id'])
property_id.state = 'offer_received'

return super().create(vals_list)
12 changes: 12 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

from odoo import fields, models


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

name = fields.Char(required=True)
color = fields.Integer()
_check_name = models.Constraint('unique(name)', 'Name must be set')
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 api, fields, models


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

name = fields.Char(required=True)
sequence = fields.Integer(default=10)
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(compute='_compute_offer_count')
_check_name = models.Constraint(
'unique(name)',
'A property type with this name already exists.',
)

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


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

property_ids = fields.One2many(
'estate.property',
'salesperson_id',
string='Real Estate Properties',
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
16 changes: 16 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" ?>
<odoo>
<data>
<menuitem id="estate_menu_root" name="Estate">
<menuitem id="estate_property_menu" name="Property">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings_menu" name="Settings">
<menuitem id="estate_property_type_menu_action" action="estate_property_type_action" name="Property Types"/>
<menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action" name="Property Tags"/>
</menuitem>


</menuitem>
</data>
</odoo>
49 changes: 49 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" ?>
<odoo>
<data>
<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>
<field name="domain">[('property_type_id', '=', active_id)]</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 string="Offers" editable="bottom" decoration-danger="status == 'refused'" decoration-success="status == 'accepted'">
<field name="price"/>
<field name="partner_id"/>
<field name="property_type_id" optional="hide"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept" type="object" icon="fa-check" title="Accept" invisible="status"/>
<button name="action_refuse" type="object" icon="fa-times" title="Refuse" invisible="status"/>
</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 string="Offer">
<header>
<button name="action_accept" type="object" class="oe_highlight" string="Accept" icon="fa-check" invisible="status"/>
<button name="action_refuse" type="object" class="oe_highlight" string="Refuse" icon="fa-times" invisible="status"/>
</header>
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<field name="status" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
</data>
</odoo>
Loading