8000 [ADD] estate: initial Real Estate module with models, views, and rela… by raaa-odoo · Pull Request #757 · odoo/tutorials · GitHub
[go: up one dir, main page]

Skip to content
< 8000 div class="d-flex flex-column flex-md-row flex-items-start flex-md-items-center">

[ADD] estate: initial Real Estate module with models, views, and rela… #757

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

Draft
wants to merge 15 commits into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
[IMP] estate: add website controller, offer wizard & tests
This commit includes three major enhancements to the estate module:

1. Website Controller:
   - Added a new website controller to render a list of available properties on
     the frontend (`/properties`).
   - Implemented optional filtering by min and max price using query parameters.

2. Add Offer Wizard:
   - Introduced a wizard to allow adding offers to multiple properties in bulk.
   - Activated via a new Add Offer button on the estate property list view.

3. Test Cases:
   - Added unit tests to ensure the correctness of the code and logic.

These improvements significantly enhance usability for both website visitors
and internal users, especially salespeople handling bulk property offers.
  • Loading branch information
raaa-odoo committed May 29, 2025
commit f2062aa5cdcff3d88bcd3dc6a66176312c9e7d7a
2 changes: 2 additions & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import models
from . import controllers
from . import wizard
4 changes: 3 additions & 1 deletion estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
{
'name': 'Real Estate',
'version': '1.0',
'depends': ['base', 'mail'],
'depends': ['base', 'mail', 'website'],
'author': 'Rajeev Aanjana',
'category': 'Real Estate/Brokerage',
'description': 'A module for managing real estate properties',
'application': True,
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'wizard/estate_property_offer_wizard.xml',
'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',
'views/estate_property_templates.xml',
# 'data/master_data.xml',
'data/estate_property_demo.xml',
'report/estate_property_templates.xml',
Expand Down
1 change: 1 addition & 0 deletions estate/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import main
39 changes: 39 additions & 0 deletions estate/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from odoo import http
from odoo.http import request

class EstateWebsiteController(http.Controller):

@http.route('/properties', type='http', auth='public', website=True)
def list_properties(self, min_price=0, max_price=0, **kwargs):
domain = []
try:
min_price = float(min_price)
except (ValueError, TypeError):
min_price = 0
try:
max_price = float(max_price)
except (ValueError, TypeError):
max_price = 0

if min_price:
domain.append(('selling_price', '>=', min_price))
if max_price:
domain.append(('selling_price', '<=', max_price))

properties = request.env['estate.property'].sudo().search(domain)

return request.render('estate.property_listing', {
'properties': properties,
'min_price': min_price,
'max_price': max_price,
})

@http.route('/properties/<int:property_id>', type='http', auth='public', website=True)
def property_detail(self, property_id, **kwargs):
property_rec = request.env['estate.property'].sudo().browse(property_id)
if not property_rec.exists():
return request.not_found()

return request.render('estate.property_detail', {
'property': property_rec
})
11 changes: 0 additions & 11 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,3 @@ def _check_state_before_delete(self):
for record in self:
if record.state not in ('new', 'canceled'):
raise UserError("You can only delete properties in 'New' or 'canceled' state.")

def action_sold(self):
# Check the user has write access to the properties
self.check_access_rights('write')
self.check_access_rule('write')

# Create invoice with sudo to bypass access rights
invoice = self.env['account.move'].sudo().create({
# ... invoice creation data ...
})
return super().action_sold()
4 changes: 2 additions & 2 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ class EstatePropertyType(models.Model):

@api.depends("property_ids.offer_ids")
def _compute_offer_count(self):
for prop_type in self:
prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
for record in self:
record.offer_count = len(record.offer_ids)
4 changes: 3 additions & 1 deletion estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ estate.access_estate_property_type_user,access_estate_property_type_user,estate.
estate.access_estate_property_type_manager,access_estate_property_type_manager,estate.model_estate_property_type,estate.estate_group_manager,1,1,1,0

estate.access_estate_property_tag_user,access_estate_property_tag_user,estate.model_estate_property_tag,estate.estate_group_user,1,0,0,0
estate.access_estate_property_tag_manager,access_estate_property_tag_manager,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,0
estate.access_estate_property_tag_manager,access_estate_property_tag_manager,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,0

estate.access_estate_property_offer_wizard_user,access_estate_property_offer_wizard_user,estate.model_estate_property_offer_wizard,estate.estate_group_user,1,1,1,0
1 change: 1 addition & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_estate_property
10000
99 changes: 99 additions & 0 deletions estate/tests/test_estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import tagged, Form
from odoo.tests.common import TransactionCase


@tagged("post_install", "-at_install")
class EstateTestCase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()

cls.properties = cls.env["estate.property"].create(
[
{
"name": "Sale Test Property",
"description": "Test Description",
"expected_price": 100000,
"living_area": 50,
},
{
"name": "Garden Test Property",
"description": "Test Description Garden",
"expected_price": 200000,
"living_area": 100,
},
]
)

cls.offers = cls.env["estate.property.offer"].create(
[
{
"partner_id": cls.env.ref("base.res_partner_2").id,
"offer_price": 110000,
"property_id": cls.properties[0].id,
},
{
"partner_id": cls.env.ref("base.res_partner_12").id,
"offer_price": 130000,
"property_id": cls.properties[0].id,
},
{
"partner_id": cls.env.ref("base.res_partner_2").id,
"offer_price": 150000,
"property_id": cls.properties[0].id,
},
]
)

def test_sell_property_without_accepted_offer(self):
"""
Test selling a property without an accepted offer.
Ensure that a UserError is raised when trying to sell a property without an accepted offer.
Ensure that other offers are not allowed to be created after the property is sold.
"""

with self.assertRaises(UserError):
self.properties[0].action_sold()

self.offers[1].action_accept()
self.properties[0].action_sold()

self.assertEqual(
self.properties[0].state, "sold", "Property was not marked as sold"
)

with self.assertRaises(UserError):
self.properties[0].offer_ids = [
Command.create(
{
"partner_id": self.env.ref("base.res_partner_2").id,
"price": 200000,
"property_id": self.properties[0].id,
}
)
]

def test_garden_toggle(self):
"""
Test toggling the garden field on the property.
Ensure that the garden area and orientation are resetting.
"""

with Form(self.properties[1]) as form:
form.garden = True
self.assertEqual(form.garden_area, 10, "Garden area should be reset to 10")
self.assertEqual(
form.garden_orientation,
"north",
"Garden orientation should be reset to north",
)

form.garden = False
self.assertEqual(form.garden_area, 0, "Garden area should be reset to 0")
self.assertEqual(
form.garden_orientation,
False,
"Garden orientation should be reset to False",
)
67 changes: 67 additions & 0 deletions estate/views/estate_property_templates.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<odoo>
<template id="property_listing" name="Property Listing">
<t t-call="website.layout">
<div class="container mt-4">
<!-- Search Form -->
<form method="get" class="mb-4">
<div class="row">
<div class="col-md-3">
<input type="text" name="name" class="form-control"
t-att-value="search_name" placeholder="Search by name..."/>
</div>
<div class="col-md-3">
<input type="number" name="min_price" class="form-control"
t-att-value="min_price" placeholder="Min Price"/>
</div>
<div class="col-md-3">
<input type="number" name="max_price" class="form-control"
t-att-value="max_price" placeholder="Max Price"/>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary">Filter</button>
</div>
</div>
</form>

<!-- Property Cards -->
<div class="row">
<t t-foreach="properties" t-as="property">
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title" t-esc="property.name"/>
<p class="card-text">
Price: <span t-esc="property.expected_price"/>
<br/>
Bedrooms: <span t-esc="property.bedrooms"/>
</p>
<a t-att-href="'/properties/%s' % property.id"
class="btn btn-primary">
View Details
</a>
</div>
</div>
</div>
</t>
</div>
</div>
</t>
</template>

<template id="property_detail" name="Property Detail">
<t t-call="website.layout">
<div class="container mt-5">
<div class="card shadow p-4">
<h1 class="mb-3"><t t-esc="property.name"/></h1>
<p><strong>Price:</strong> <t t-esc="property.selling_price"/></p>
<p><strong>Bedrooms:</strong> <t t-esc="property.bedrooms"/></p>
<p><strong>Living Area:</strong> <t t-esc="property.living_area"/> m²</p>
<p><strong>Garden:</strong> <t t-esc="property.garden and 'Yes' or 'No'"/></p>
<p><strong>Description:</strong> <t t-esc="property.description"/></p>
<a href="/properties" class="btn btn-outline-secondary mt-3">← Back to Listings</a>
</div>
</div>
</t>
</template>

</odoo>
11 changes: 10 additions & 1 deletion estate/views/estate_property_views.xml
10000
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@
</field>
</record>

<record id="open_url_action" model="ir.actions.act_url">
<field name="name">Open External Website</field>
<field name="url">/properties</field>
<field name="target">new</field>
</record>

<!-- List View -->
<record id="view_estate_property_list" model="ir.ui.view">
<field name="name">estate.property.list</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<list string="Properties"
decoration-success="state in ['offer_received', 'offer_accepted']" decoration-bf="state == 'offer_accepted'" decoration-muted="state == 'sold'" decoration-danger="state == 'canceled'">
<field name="name" string="Name" />
<header>
<button name="%(action_add_offer_wizard)d" string="Add Offer" type="action" class="btn-primary"/>
</header>
<field name="name" string="Name" />
<field name="postcode" string="Postcode" />
<field name="expected_price" string="Expected Price"/>
<field name="selling_price" string="Selling Price"/>
Expand Down
1 change: 1 addition & 0 deletions estate/wizard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import estate_property_offer_wizard
35 changes: 35 additions & 0 deletions estate/wizard/estate_property_offer_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from odoo import api, fields, models


class MakeOfferWizard(models.TransientModel):
_name = 'estate.property.offer.wizard'
_description = 'Property Offer Wizard'

offer_price = fields.Float('Offer Price', required=True)
status = fields.Selection(
[('accepted', 'Accepted'), ('refused', 'Refused')],
string='Status'
)
partner_id = fields.Many2one('res.partner', 'Buyer', required=True)
property_ids = fields.Many2many(
'estate.property',
string='Selected Properties',
)

@api.model
def default_get(self, fields):
res = super().default_get(fields)
property_ids = self.env.context.get("active_ids", [])
if property_ids:
res["property_ids"] = [(6, 0, property_ids)]
return res

def action_make_offer(self):
for property in self.property_ids:
self.env['estate.property.offer'].create({
"offer_price": self.offer_price,
'status': self.status,
'partner_id': self.partner_id.id,
'property_id': property.id,
})
return {'type': 'ir.actions.act_window_close'}
28 changes: 28 additions & 0 deletions estate/wizard/estate_property_offer_wizard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<odoo>
<record id="action_add_offer_wizard" model="ir.actions.act_window">
<field name="name">Add Offer</field>
<field name="res_model">estate.property.offer.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_estate_property"/>
<field name="binding_view_types">list</field>
</record>

<record id="view_estate_property_offer_wizard_form" model="ir.ui.view">
<field name="name">estate.property.offer.wizard.form</field>
<field name="model">estate.property.offer.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="offer_price"/>
<field name="partner_id"/>
<field name="property_ids" widget="many2many_tags"/>
</group>
<footer>
<button name="action_make_offer" string="Make Offer" type="object" class="btn-primary"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
0