Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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 estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
34 changes: 34 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
"name": "Real Estate Advertisement",
"author": "Odoo",
"category": "Sales/Real Estate",
"sequence": 15,
"summary": "Manage property listings and real estate advertisements",
"description": """
Real Estate Advertisement Management
====================================
This module allows you to manage real estate properties, including:
* Property listings with detailed information
* Property types and tags
* Property offers and negotiations
* Sales tracking
""",
"depends": ["base"],
"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",
],
"assets": {
"web.assets_backend": [
"estate/static/src/css/estate.css",
],
},
"application": True,
"license": "LGPL-3",
}
7 changes: 7 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import res_users
109 changes: 109 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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


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

_check_expected_price = models.Constraint('Check(expected_price > 0)', "The expected price must be strictly positive.")
_check_selling_price = models.Constraint('Check(selling_price >= 0)', "The selling price must be positive.")

name = fields.Char(required=True, string="Name")
description = fields.Text(string="Description")
postcode = fields.Char(string="Postcode")
date_availability = fields.Date(string="Available From")
expected_price = fields.Float(required=True, string="Expected Price")
selling_price = fields.Float(string="Selling Price")
bedrooms = fields.Integer(string="Bedrooms")
living_area = fields.Integer(string="Living Area (sqm)")
facades = fields.Integer(string="Facades")
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(string="Garden Area (sqm)")
garden_orientation = fields.Selection(
selection=[
('north', "North"),
('south', "South"),
('east', "East"),
('west', "West"),
],
string="Garden Orientation",
)
state = fields.Selection(
selection=[
('new', "New"),
('offer_received', "Offer Received"),
('offer_accepted', "Offer Accepted"),
('sold', "Sold"),
('cancelled', "Cancelled"),
],
default='new',
required=True,
string="Status",
)
buyer_id = fields.Many2one(comodel_name='res.partner', string="Buyer")
salesperson_id = fields.Many2one(comodel_name='res.users', string="Salesperson", default=lambda self: self.env.user)
property_type_id = fields.Many2one(comodel_name='estate.property.type', string="Property Type")
tag_ids = fields.Many2many(comodel_name='estate.property.tag', string="Tags")
offer_ids = fields.One2many(
comodel_name='estate.property.offer',
inverse_name='property_id',
string="Offers",
)
total_area = fields.Integer(compute='_compute_total_area', string="Total Area (sqm)")
best_price = fields.Float(compute='_compute_best_price', string="Best Offer Price")

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

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

@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for property in self:
if not float_is_zero(property.selling_price, precision_digits=2):
if float_compare(property.selling_price, property.expected_price * 0.9, precision_digits=2) < 0:
raise ValidationError(self.env._("The selling price cannot be lower than 90% of the expected price."))

def action_sold(self):
for property in self:
if property.state == 'cancelled':
raise UserError(self.env._("Cancelled property cannot be sold."))
property.state = 'sold'
return True

def action_cancel(self):
for property in self:
if property.state == 'sold':
raise UserError(self.env._("Sold property cannot be cancelled."))
property.state = 'cancelled'
return True

@api.ondelete(at_uninstall=True)
def delete(self):
for property in self:
if property.state not in ('new', 'cancelled'):
raise UserError(self.env._("Only new and cancelled properties can be deleted."))
return super().unlink()
69 changes: 69 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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


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

_check_price = models.Constraint('Check(price > 0)', "The offer price must be strictly positive.")

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

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
property_id = vals.get('property_id')
price = vals.get('price')

if property_id and price:
property = self.env['estate.property'].browse(property_id)
if property.offer_ids:
max_offer = max(property.offer_ids.mapped('price'))
if price < max_offer:
raise UserError(self.env._("The offer amount must be higher than %.2f") % max_offer)

offers = super().create(vals_list)
for offer in offers:
if offer.property_id.state == 'new':
offer.property_id.state = 'offer_received'
return offers

@api.depends('validity')
def _compute_date_deadline(self):
for offer in self:
offer.date_deadline = fields.Date.today() + timedelta(days=offer.validity)

def _inverse_date_deadline(self):
for offer in self:
if offer.date_deadline:
offer.validity = (offer.date_deadline - fields.Date.today()).days

def action_accept(self):
for offer in self:
if offer.property_id.offer_ids.filtered(lambda o: o.status == 'accepted' and o.id != offer.id):
raise UserError(self.env._("Only one offer can be accepted per property."))
offer.status = 'accepted'
offer.property_id.buyer_id = offer.partner_id
offer.property_id.selling_price = offer.price
offer.property_id.state = 'offer_accepted'
return True

def action_refuse(self):
for offer in self:
offer.status = 'refused'
return True
14 changes: 14 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import fields, models


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

_check_unique_name = models.Constraint('UNIQUE(name)', "The tag name must be unique.")

name = fields.Char(required=True, string="Name")
color = fields.Integer(string="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 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import fields, models


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

_check_unique_name = models.Constraint('UNIQUE(name)', "The type name must be unique.")

name = fields.Char(required=True, string="Name")
sequence = fields.Integer(default=1, string="Sequence")
property_ids = fields.One2many(comodel_name='estate.property', inverse_name='property_type_id', string="Properties")
offer_count = fields.Integer(compute='_compute_offer_count', string="Offer Count")

def _compute_offer_count(self):

Choose a reason for hiding this comment

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

Is it normal that you don't have an api.depends here ?

Copy link
Author

Choose a reason for hiding this comment

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

that function is called when that field calculated 
offer_count = fields.Integer(compute='_compute_offer_count', string="Offer Count")
so, there is no need for any dependency

for property_type in self:
property_type.offer_count = sum(property_type.property_ids.mapped(lambda p: len(p.offer_ids)))
13 changes: 13 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import fields, models


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

property_ids = fields.One2many(
comodel_name='estate.property',
inverse_name='salesperson_id',
string="Estate Properties",
)
6 changes: 6 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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
access_res_users_estate_salesperson,access_res_users_estate_salesperson,model_res_users,base.group_user,1,1,1,1
18 changes: 18 additions & 0 deletions estate/static/src/css/estate.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* Estate Module Kanban View Styles */

.o_kanban_record_title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #212529;
}

.o_kanban_record_subtitle {
font-size: 13px;
color: #6c757d;
margin-bottom: 4px;
}

.o_kanban_tags_section {
margin-top: 8px;
}
40 changes: 40 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Main menu -->
<menuitem id="estate_menu_root" name="Real Estate"/>

<!-- Properties menu -->
<menuitem id="estate_property_menu"
name="Advertisements"
parent="estate_menu_root"/>

<menuitem id="estate_property_menu_action"
name="Properties"
parent="estate_property_menu"
action="estate_property_action"/>

<!-- Settings menu -->
<menuitem id="estate_settings_menu"
name="Settings"
parent="estate_menu_root"/>

<menuitem id="estate_property_type_menu_action"
name="Property Types"
parent="estate_settings_menu"
action="estate_property_type_action"/>

<menuitem id="estate_property_tag_menu_action"
name="Property Tags"
parent="estate_settings_menu"
action="estate_property_tag_action"/>

<menuitem id="estate_property_offer_menu_action"
name="Property Offers"
parent="estate_settings_menu"
action="estate_property_offer_action"/>

<menuitem id="estate_salesperson_menu_action"
name="Salespersons"
parent="estate_settings_menu"
action="estate_salesperson_action"/>
</odoo>
51 changes: 51 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="estate_property_offer_view_list" model="ir.ui.view">
<field name="name">estate.property.offer.view.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list editable="bottom" decoration-danger="status == 'refused'"
decoration-success="status == 'accepted'">
<field name="price" />
<field name="partner_id" />
<field name="property_id" />
<field name="property_id" />
<field name="validity" />
<field name="date_deadline" />
<field name="status" />
<button name="action_accept" type="object" icon="fa-check" title="Accept" />
<button name="action_refuse" type="object" icon="fa-times" title="Refuse" />


</list>
</field>
</record>

<!-- Form View -->
<record id="estate_property_offer_view_form" model="ir.ui.view">
<field name="name">estate.property.offer.view.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="property_id" />
<field name="validity" />
<field name="date_deadline" />
<field name="status" />
</group>
</sheet>
</form>
</field>
</record>

<!-- Action -->
<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>
</record>
</odoo>
Loading