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 addons/estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
19 changes: 19 additions & 0 deletions addons/estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
'name': 'Estate',
'author': 'Codoeh',
'license': 'LGPL-3',
'version': '1.0',
'category': 'Tutorials',
'depends': [
'base',
],
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_offer_views.xml',
'views/res_users_views.xml',
'views/estate_menus.xml',
],
}
5 changes: 5 additions & 0 deletions addons/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
142 changes: 142 additions & 0 deletions addons/estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from dateutil.relativedelta import relativedelta
from odoo import api, exceptions, fields, models, tools


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

name = fields.Char(required=True, string="Title")
active = fields.Boolean(default=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
copy=False,
default=lambda self: fields.Date.today() + relativedelta(months=3),
string="Available From",
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer(string="Living Area (sqm)")
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
]
)
state = fields.Selection(
selection=[
('new', 'NEW'),
('offer_received', 'OFFER RECEIVED'),
('offer_accepted', 'OFFER ACCEPTED'),
('sold', 'SOLD'),
('cancelled', 'CANCELLED'),
],
required=True,
default='new',
copy=False,
)

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,
index=True,
)
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.Float(compute='_compute_total_area')
best_price = fields.Float(
compute='_compute_best_price', string="Best Offer"
)

_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)', 'Expected price must be greater than 0.'
)
_check_selling_price = models.Constraint(
'CHECK(selling_price >= 0)', 'Selling price must be at least 0.'
)

@api.constrains('selling_price', 'expected_price')
def _check_selling_price_logic(self):
for record in self:
if not tools.float_is_zero(
record.selling_price, precision_digits=2
):
if (
tools.float_compare(
record.selling_price,
record.expected_price * 0.9,
precision_digits=2,
)
== -1
):
raise exceptions.ValidationError(
'Price must be greater than 90% of expected price.'
)

@api.depends('living_area', 'garden_area', 'garden')
def _compute_total_area(self):
for record in self:
record.total_area = record.living_area + (
record.garden_area if record.garden else 0
)

@api.depends('offer_ids.price')
def _compute_best_price(self):
for record in self:
prices = record.offer_ids.mapped('price')
record.best_price = max(prices) if prices else 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 = None

@api.ondelete(at_uninstall=False)
def _unlink_if_state_in_new_cancelled(self):
for record in self:
if record.state not in ['new', 'cancelled']:
raise exceptions.UserError(
"Cannot unlink property while "
"state is different than new or cancelled."
)

def action_set_state_cancelled(self):
for record in self:
if record.state == 'sold':
raise exceptions.UserError(
"Sold properties cannot be canceled."
)
else:
record.state = 'cancelled'
return True

def action_set_state_sold(self):
for record in self:
if record.state == 'cancelled':
raise exceptions.UserError(
"Cancelled properties cannot be sold."
)
else:
record.state = 'sold'
return True
95 changes: 95 additions & 0 deletions addons/estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from dateutil.relativedelta import relativedelta
from odoo import api, exceptions, fields, models


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

price = fields.Float(string='Price')
status = fields.Selection(
[
('accepted', 'Accepted'),
('refused', 'Refused'),
],
string='Status',
copy=False,
)
partner_id = fields.Many2one(
'res.partner', string='Partner', required=True
)
property_id = fields.Many2one(
'estate.property', string='Property', required=True, ondelete='cascade'
)

property_type_id = fields.Many2one(
'estate.property.type',
related='property_id.property_type_id',
string='Property Type',
store=True,
)

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

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

@api.depends('create_date', 'validity')
def _compute_date_deadline(self):
for record in self:
start_date = record.create_date or fields.Date.today()
start_date = fields.Date.to_date(start_date)
record.date_deadline = start_date + relativedelta(
days=record.validity
)

def _inverse_date_deadline(self):
for record in self:
start_date = record.create_date or fields.Date.today()
start_date = fields.Date.to_date(start_date)
if record.date_deadline and start_date:
diff = record.date_deadline - start_date
record.validity = diff.days

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
property_record = self.env['estate.property'].browse(
vals['property_id']
)
if vals.get('price') <= property_record.best_price:
raise exceptions.UserError(
'New offer must be higher than existing offers.'
)

offers = super().create(vals_list)

for record in offers:
if record.property_id.state == 'new':
record.property_id.state = 'offer_received'

return offers

def action_set_offer_status_accepted(self):
for record in self:
if record.property_id.state == 'offer_accepted':
raise exceptions.UserError('Only one offer can be accepted.')
else:
record.status = 'accepted'
record.property_id.buyer_id = record.partner_id
record.property_id.selling_price = record.price
record.property_id.state = 'offer_accepted'

return True

def action_set_offer_status_refused(self):
for record in self:
record.status = 'refused'
return True
12 changes: 12 additions & 0 deletions addons/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 = 'Property Tag'
_order = 'name'

name = fields.Char(string='Name', required=True)
color = fields.Integer()

_check_name = models.Constraint('UNIQUE(name)', 'Name must be unique.')
28 changes: 28 additions & 0 deletions addons/estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from odoo import api, fields, models


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

name = fields.Char(required=True)
sequence = fields.Integer('Sequence', default=1, help='Sequence number')

_check_name = models.Constraint('UNIQUE(name)', 'Name must be unique.')

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(
string='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)
12 changes: 12 additions & 0 deletions addons/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='Estate Properties',
domain="[('state', 'in', ['new', 'offer_received', 'offer_accepted'])]"
)
5 changes: 5 additions & 0 deletions addons/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 addons/estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_first_level_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action" />
</menuitem>
<menuitem id="estate_property_type_first_level_menu" name="Settings">
<menuitem id="estate_property_type_menu_action" action="estate_property_type_action" />
<menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action" />
</menuitem>
</menuitem>
</odoo>
43 changes: 43 additions & 0 deletions addons/estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<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="New Offer">
<header>
<button name="action_set_offer_status_accepted" type="object" string="Accept" icon="fa-check" />
<button name="action_set_offer_status_refused" type="object" string="Refuse" icon="fa-times" />
</header>
<sheet>
<group>
<field name="price" />
<field name="partner_id" />
<field name="validity" />
<field name="date_deadline" />
<field name="status" />
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_offer_view_tree" 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="Property Offers" editable="top" decoration-danger="status=='refused'"
decoration-success="status=='accepted'">
<field name="price" />
<field name="partner_id" class="text-right" />
<field name="validity" />
<field name="date_deadline" />
<button name="action_set_offer_status_accepted" type="object" string="Accept" icon="fa-check"
invisible="status" />
<button name="action_set_offer_status_refused" type="object" string="Refuse" icon="fa-times"
invisible="status" />
<field name="status" column_invisible="True" />
</list>
</field>
</record>
</odoo>
Loading