-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
[ADD] estate: add real estate module #221
base: 18.0
Are you sure you want to change the base?
Changes from 18 commits
b1dd482
fa90e2e
97fb4f9
2574d72
e984273
92c5207
62402fd
9542868
743b5a0
6da58e7
1d89221
cabbc56
90904ed
c37c5cb
4907827
13455c5
28a21db
1261bf9
17d0010
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"python.analysis.typeCheckingMode": "standard", | ||
"python.languageServer": "None" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from . import models | ||
from . import wizard | ||
from . import controllers |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "Estate", | ||
"summary": """ | ||
Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game" | ||
""", | ||
"description": """ | ||
Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game" | ||
""", | ||
"author": "Odoo", | ||
"website": "https://www.odoo.com", | ||
"category": "Real Estate/Brokerage", | ||
"version": "0.1", | ||
"application": True, | ||
"installable": True, | ||
"depends": ["base_setup", "website"], | ||
"data": [ | ||
"security/estate_security.xml", | ||
"security/ir.model.access.csv", | ||
"report/estate_property_templates.xml", | ||
"report/estate_property_report.xml", | ||
"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_menu_views.xml", | ||
"views/estate_website_template.xml", | ||
"data/estate.property.type.csv", | ||
"wizard/estate_property_wizard_view.xml", | ||
], | ||
"demo": ["demo/estate_property_demo.xml", "demo/estate_property_offers.xml"], | ||
"license": "AGPL-3", | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import estate_website |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
from odoo import http | ||
from odoo.http import request | ||
|
||
|
||
class EstateWebsite(http.Controller): | ||
@http.route( | ||
["/properties", "/properties/page/<int:page>"], | ||
type="http", | ||
auth="user", | ||
website=True, | ||
) | ||
def list_properties(self, page=1, **kwargs): | ||
step = 6 | ||
offset = (page - 1) * step | ||
|
||
#! Fetch only the properties for the current page | ||
properties = ( | ||
request.env["estate.property"] | ||
.sudo() | ||
.search( | ||
[ | ||
"&", | ||
("status", "in", ["new", "offer_received", "offer_accepted"]), | ||
("active", "=", True), | ||
], | ||
limit=step, | ||
offset=offset, | ||
) | ||
) | ||
|
||
total_properties = request.env["estate.property"].sudo().search_count( | ||
[ | ||
"&", | ||
("status", "in", ["new", "offer_received", "offer_accepted"]), | ||
("active", "=", True), | ||
] | ||
) | ||
|
||
pager = request.website.pager( | ||
url="/properties", total=total_properties, step=step, page=page | ||
) | ||
|
||
# Render the template with paginated properties and the pager | ||
return request.render( | ||
"estate.listing_page", | ||
{"properties": properties, "pager": pager}, | ||
) | ||
|
||
@http.route( | ||
"/property/<model('estate.property'):property>", | ||
type="http", | ||
auth="user", | ||
website=True, | ||
) | ||
def property_detail(self, property, **kwargs): | ||
return request.render("estate.property_detail_page", {"property": property}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
id,name | ||
property_1,Residential | ||
property_2,Commercial | ||
property_3,Industrial | ||
property_4,Land |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<odoo> | ||
<!-- Record 1: Big Villa --> | ||
<record id="estate_property_big_villa" model="estate.property"> | ||
<field name="name">Big Villa</field> | ||
<field name="status">new</field> | ||
<field name="description">A nice and big villa</field> | ||
<field name="postcode">12345</field> | ||
<field name="date_availability">2020-02-02</field> | ||
<field name="expected_price">160000.00</field> | ||
<field name="bedrooms">6</field> | ||
<field name="living_area">100</field> | ||
<field name="facades">4</field> | ||
<field name="garage">True</field> | ||
<field name="garden">True</field> | ||
<field name="garden_area">100000</field> | ||
<field name="garden_orientation">south</field> | ||
<field name="property_type_id" ref="estate.property_1"/> | ||
<field name="image" type="base64" file="estate/static/demo_images/1.jpg"></field> | ||
</record> | ||
|
||
<!-- Record 2: Trailer Home --> | ||
<record id="estate_property_trailer_home" model="estate.property"> | ||
<field name="name">Trailer home</field> | ||
<field name="status">canceled</field> | ||
<field name="description">Home in a trailer park</field> | ||
<field name="postcode">54321</field> | ||
<field name="date_availability">1970-01-01</field> | ||
<field name="expected_price">100000.00</field> | ||
<field name="selling_price">120000.00</field> | ||
<field name="bedrooms">1</field> | ||
<field name="living_area">10</field> | ||
<field name="facades">4</field> | ||
<field name="garage">False</field> | ||
<field name="property_type_id" ref="estate.property_1"/> | ||
<field name="image" type="base64" file="estate/static/demo_images/2.jpg"></field> | ||
</record> | ||
|
||
<!-- record 3: Duplex with offers made directly here --> | ||
<record id="estate_property_duplex" model="estate.property"> | ||
<field name="name">Duplex</field> | ||
<field name="status">new</field> | ||
<field name="description">A nice duplex</field> | ||
<field name="postcode">12345</field> | ||
<field name="date_availability">2020-02-02</field> | ||
<field name="expected_price">160000.00</field> | ||
<field name="selling_price">160000.00</field> | ||
<field name="bedrooms">6</field> | ||
<field name="living_area">100</field> | ||
<field name="facades">4</field> | ||
<field name="garage">True</field> | ||
<field name="property_type_id" ref="estate.property_1"/> | ||
<field name="image" type="base64" file="estate/static/demo_images/3.jpg"></field> | ||
<field name="offer_ids" eval="[ | ||
Command.create({'partner_id':ref('base.res_partner_12'),'price': 150000.00}), | ||
Command.create({'partner_id':ref('base.res_partner_18'),'price':155000.00}), | ||
Command.create({'partner_id':ref('base.res_partner_10'),'price':160000.00})]"/> | ||
</record> | ||
|
||
<record id="menu_properties" model="website.menu"> | ||
<field name="name">Properties</field> | ||
<field name="url">/properties</field> | ||
<field name="parent_id" ref="website.main_menu"/> | ||
<field name="sequence" type="int">60</field> | ||
</record> | ||
</odoo> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<odoo> | ||
|
||
<record id="big_villa_offer_2" model="estate.property.offer"> | ||
<field name="partner_id" ref="base.res_partner_12"/> | ||
<field name="property_id" ref="estate.estate_property_big_villa" /> | ||
<field name="price">150000.00</field> | ||
<field name="validity">14</field> | ||
</record> | ||
|
||
<record id="big_villa_offer_3" model="estate.property.offer"> | ||
<field name="partner_id" ref="base.res_partner_2"/> | ||
<field name="property_id" ref="estate.estate_property_big_villa" /> | ||
<field name="price">150001.00</field> | ||
<field name="validity">14</field> | ||
</record> | ||
|
||
<record id="big_villa_offer_1" model="estate.property.offer"> | ||
<field name="partner_id" ref="base.res_partner_12"/> | ||
<field name="property_id" ref="estate.estate_property_big_villa" /> | ||
<field name="price">160000.00</field> | ||
<field name="validity">14</field> | ||
<field name="date_deadline" eval="(datetime.today() + relativedelta(months=3)).date()" /> | ||
</record> | ||
|
||
<function model="estate.property.offer" name="action_refuse" eval="[[ref('big_villa_offer_2'), ref('big_villa_offer_3')]]"> | ||
</function> | ||
|
||
<function model="estate.property.offer" name="action_accept" eval="[ref('big_villa_offer_1')]"> | ||
</function> | ||
|
||
|
||
</odoo> |
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_tags | ||
from . import estate_property_offer | ||
from . import inherited_users |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,144 @@ | ||||||||||||||||||||
from odoo import models, fields, api | ||||||||||||||||||||
from odoo.exceptions import UserError | ||||||||||||||||||||
from datetime import datetime | ||||||||||||||||||||
from dateutil.relativedelta import relativedelta | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Generally we first make external imports and then we import from odoo in alphabetical order. |
||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
class EstateProperty(models.Model): | ||||||||||||||||||||
_name = "estate.property" | ||||||||||||||||||||
_description = "Estate Property" | ||||||||||||||||||||
_order = "id desc" | ||||||||||||||||||||
|
||||||||||||||||||||
name = fields.Char(required=True) | ||||||||||||||||||||
description = fields.Text("description") | ||||||||||||||||||||
postcode = fields.Char() | ||||||||||||||||||||
date_availability = fields.Date( | ||||||||||||||||||||
copy=False, default=(datetime.today() + relativedelta(months=3)).date() | ||||||||||||||||||||
) | ||||||||||||||||||||
expected_price = fields.Float(required=True) | ||||||||||||||||||||
selling_price = fields.Float(readonly=True, copy=False) | ||||||||||||||||||||
bedrooms = fields.Integer() | ||||||||||||||||||||
|
||||||||||||||||||||
living_area = fields.Integer() | ||||||||||||||||||||
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"), | ||||||||||||||||||||
], | ||||||||||||||||||||
) | ||||||||||||||||||||
sequence = fields.Integer("Sequence", default=1) | ||||||||||||||||||||
active = fields.Boolean(default=True) | ||||||||||||||||||||
property_type_id = fields.Many2one("estate.property.type", ondelete="cascade") | ||||||||||||||||||||
salesman_id = fields.Many2one( | ||||||||||||||||||||
"res.users", ondelete="restrict", default=lambda self: self.env.user | ||||||||||||||||||||
) | ||||||||||||||||||||
buyer_id = fields.Many2one("res.partner", ondelete="restrict", copy=False) | ||||||||||||||||||||
tag_ids = fields.Many2many("estate.property.tags") | ||||||||||||||||||||
offer_ids = fields.One2many( | ||||||||||||||||||||
comodel_name="estate.property.offer", inverse_name="property_id" | ||||||||||||||||||||
) | ||||||||||||||||||||
total_area = fields.Integer(compute="_compute_total_area") | ||||||||||||||||||||
best_offer = fields.Float(compute="_compute_best_offer") | ||||||||||||||||||||
status = fields.Selection( | ||||||||||||||||||||
selection=[ | ||||||||||||||||||||
("new", "New"), | ||||||||||||||||||||
("offer_received", "Offer received"), | ||||||||||||||||||||
("offer_accepted", "Offer accepted"), | ||||||||||||||||||||
("sold", "Sold"), | ||||||||||||||||||||
("canceled", "Canceled"), | ||||||||||||||||||||
], | ||||||||||||||||||||
default="new", | ||||||||||||||||||||
copy=False, | ||||||||||||||||||||
) | ||||||||||||||||||||
company_id = fields.Many2one( | ||||||||||||||||||||
"res.company", default=lambda self: self.env.company, required=True | ||||||||||||||||||||
) | ||||||||||||||||||||
image = fields.Image("Image") | ||||||||||||||||||||
_sql_constraints = [ | ||||||||||||||||||||
( | ||||||||||||||||||||
"check_expected_price", | ||||||||||||||||||||
"CHECK(expected_price > 0)", | ||||||||||||||||||||
"Expected price must be strictly positive", | ||||||||||||||||||||
), | ||||||||||||||||||||
( | ||||||||||||||||||||
"check_selling_price", | ||||||||||||||||||||
"CHECK(selling_price >= 0)", | ||||||||||||||||||||
"Selling price must be non-negative", | ||||||||||||||||||||
), | ||||||||||||||||||||
] | ||||||||||||||||||||
|
||||||||||||||||||||
@api.ondelete(at_uninstall=False) | ||||||||||||||||||||
def _unlink_if_state_new_canceled(self): | ||||||||||||||||||||
for record in self: | ||||||||||||||||||||
if ( | ||||||||||||||||||||
record.status == "offer_received" | ||||||||||||||||||||
or record.status == "offer_accepted" | ||||||||||||||||||||
or record.status == "sold" | ||||||||||||||||||||
): | ||||||||||||||||||||
raise UserError("Only new and canceled properties can be deleted") | ||||||||||||||||||||
|
||||||||||||||||||||
@api.depends("offer_ids.price") | ||||||||||||||||||||
def _compute_best_offer(self): | ||||||||||||||||||||
for record in self: | ||||||||||||||||||||
if not record.offer_ids: | ||||||||||||||||||||
record.best_offer = 0 | ||||||||||||||||||||
continue | ||||||||||||||||||||
record.best_offer = max(record.offer_ids.mapped("price")) | ||||||||||||||||||||
|
||||||||||||||||||||
@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.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 | ||||||||||||||||||||
|
||||||||||||||||||||
def action_sold(self): | ||||||||||||||||||||
for record in self: | ||||||||||||||||||||
if ( | ||||||||||||||||||||
record.status == "new" | ||||||||||||||||||||
or record.status == "offer_accepted" | ||||||||||||||||||||
or record.status == "offer_received" | ||||||||||||||||||||
): | ||||||||||||||||||||
record.status = "sold" | ||||||||||||||||||||
elif record.status == "canceled": | ||||||||||||||||||||
raise UserError("Canceled property can't be sold") | ||||||||||||||||||||
else: | ||||||||||||||||||||
raise UserError("Property already sold") | ||||||||||||||||||||
|
||||||||||||||||||||
def action_cancel(self): | ||||||||||||||||||||
for record in self: | ||||||||||||||||||||
if ( | ||||||||||||||||||||
record.status == "new" | ||||||||||||||||||||
or record.status == "offer_accepted" | ||||||||||||||||||||
or record.status == "offer_received" | ||||||||||||||||||||
): | ||||||||||||||||||||
record.status = "canceled" | ||||||||||||||||||||
elif record.status == "sold": | ||||||||||||||||||||
raise UserError("Sold property can't be canceled") | ||||||||||||||||||||
else: | ||||||||||||||||||||
raise UserError("Property already canceled") | ||||||||||||||||||||
|
||||||||||||||||||||
def action_make_offer(self): | ||||||||||||||||||||
return { | ||||||||||||||||||||
"name": "Make offer", | ||||||||||||||||||||
"type": "ir.actions.act_window", | ||||||||||||||||||||
"target": "new", | ||||||||||||||||||||
"view_mode": "form", | ||||||||||||||||||||
"res_model": "estate.property.make.offer", | ||||||||||||||||||||
"context": { | ||||||||||||||||||||
"default_property_ids": self.ids, | ||||||||||||||||||||
}, | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it will not be needed we can get the selected property by |
||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can use auth="public" here