Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e1d7982
added init and manifest
snoek100 Aug 17, 2026
cb84f6e
added all the functionality until the basic views of server framework
snoek100 Aug 17, 2026
99af35b
end of day 1
snoek100 Aug 17, 2026
a969dc2
server framework 101 end of chapter 6
snoek100 Aug 18, 2026
031009b
finished server framework 101 chapter 9
snoek100 Aug 18, 2026
fa0cc1c
actually finished server framework 101 chapter 9
snoek100 Aug 18, 2026
b4428f5
finished server framework 101 chapter 10
snoek100 Aug 18, 2026
e63118d
finished server framework 101 chapter 11
snoek100 Aug 19, 2026
1ac00c6
finished server framework 101 chapter 12
snoek100 Aug 19, 2026
c3c4378
finished chapter server framework chapter 13
snoek100 Aug 20, 2026
0d07749
finished server framework 101 chapter 14
snoek100 Aug 20, 2026
721c3c7
fixed styling changes
snoek100 Aug 20, 2026
873b465
ammended styling fix
snoek100 Aug 20, 2026
65c6202
[ADD] estate: added test cases for estate class
snoek100 Aug 21, 2026
c1476ea
[REF] estate: separated estate views into seperat files
snoek100 Aug 21, 2026
20f44e2
[FIX] estate: added a field for the property type form so its name ca…
snoek100 Aug 21, 2026
d5e25b3
[ADD] added buttons and counters
snoek100 Aug 21, 2026
ef40a4d
[ADD] finished chapter one of web framework
snoek100 Aug 24, 2026
e734266
[ADD] started the git challenge
snoek100 Aug 24, 2026
cf99a0c
git challenge completed
snoek100 Aug 24, 2026
83db6b4
[ADD] web framework chapter 2 build a dashboard chapter 4 completed
snoek100 Aug 25, 2026
502b3c8
[ADD] [Tutorial] Restrict access to data started
snoek100 Aug 26, 2026
6d5c63e
[ADD] web framework tutorial chapter 2, part 5, added caching
snoek100 Aug 26, 2026
356cd8e
[ADD] web framework tutorial chapter 2, part 7 added chart to dashboard
snoek100 Aug 26, 2026
fdf84cf
[ADD] discover the web framework chapter 2 part 9 made the dashboard …
snoek100 Aug 27, 2026
a22a4ec
[ADD] web framework chapter 2 finished making the dashoard customizable
snoek100 Aug 27, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ dmypy.json

# Pyre type checker
.pyre/

.vscode
2 changes: 2 additions & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models

15 changes: 15 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
'name': 'estate-kehey',
'depends': [
'base',
],
'data': [
'data/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_property_settings_views.xml',
'views/users_extra_views.xml',
'data/estate_menus.xml'
],
'application':True
}

13 changes: 13 additions & 0 deletions estate/data/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo>
<menuitem id="test_menu_root" name="Real Estate">
<menuitem id="test_first_level_menu" name="Advertisements">
<menuitem id="test_model_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="settings" name="Settings">
<menuitem id="property_type_menu_action" action="estate_property_type_action"/>
<menuitem id="property_tag_menu_action" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>

6 changes: 6 additions & 0 deletions estate/data/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_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type_user,access_estate_property_user,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag_user,access_estate_property_user,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer_user,access_estate_property_user,model_estate_property_offer,base.group_user,1,1,1,1

6 changes: 6 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import inhereted_users

101 changes: 101 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from dateutil.relativedelta import relativedelta

from odoo import api, exceptions, fields, models


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

name = fields.Char(required=True)
salesman_id = fields.Many2one("res.partner", string="Salesman")
buyer_id = fields.Many2one("res.users", default=lambda self: self.env.user, string="Buyer")
type_id = fields.Many2one("estate.property.type")
tags_id = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
active = fields.Boolean(default=True)
state = fields.Selection(
required=True,
copy=False,
default="new",
selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")],
)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Datetime(copy=False, default=fields.Datetime.today() + (relativedelta(months=3)))
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
string='type',
selection=[('north', 'North'), ('south', 'South'), ('East', 'east'), ('West', 'west')],
)
total_area = fields.Integer(string="Total Area", compute="_compute_total_surface")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
total_area = fields.Integer(string="Total Area", compute="_compute_total_surface")
total_area = fields.Integer(string="Total Area", compute="_compute_total_area")

We normally tend to name the compute method after the exact name of field

best_offer = fields.Float(string="Best Offer", compute="_compute_best_offer")

# ==========constraints===================
_check_positive_expected_price = models.Constraint("CHECK (expected_price > 0)", "expected price should be bigger than 0")
_check_positive_selling_price = models.Constraint("CHECK (selling_price > 0)", "expected price should be bigger than 0")

@api.constrains("selling_price", "expected_price")
def _check_enough_selling_price(self):
for record in self:
offer_made = "accepted" in record.offer_ids.mapped("status")
price_good_enough = record.selling_price > 0.9 * record.expected_price
if not price_good_enough and offer_made:
to_low_user_error = "selling price is too low for the expected price"
raise exceptions.ValidationError(to_low_user_error)

# ==========computed fields===============
@api.depends('garden_area', 'living_area')
def _compute_total_surface(self):
for record in self:
record.total_area = record.garden_area + record.living_area

@api.depends('offer_ids')
def _compute_best_offer(self):
for record in self:
if not record.offer_ids:
record.best_offer = 0
else:
record.best_offer = max(record.offer_ids.mapped("price"))

# ============onchage fields==============
@api.onchange("garden")
def _onchange_garden(self):
if self.garden:
self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = False
self.garden_orientation = False

# ==========button functions==============
def action_property_sold(self):
for record in self:
if record.state == "cancelled":
no_sell_cancelled_error = "Can't sell a cancelled property"
raise exceptions.UserError(no_sell_cancelled_error)
record.state = "sold"
return True

def action_property_cancelled(self):
for record in self:
if record.state == "sold":
no_sell_a_sold_property = "Can't cancel a sold property"
raise exceptions.UserError(no_sell_a_sold_property)
record.state = "cancelled"
return True

@api.ondelete(at_uninstall=False)
def ondelete(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It is kinda a nitpick but we usually name those method as _unlink_**** for example:

Suggested change
def ondelete(self):
def _unlink_except_new_cancelled(self):

for property in self:
if property.state in ("new", "cancelled"):
no_delete_new_or_cancelled_record = "cannot delete new or cancelled record"
raise exceptions.UserError(no_delete_new_or_cancelled_record)
66 changes: 66 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from dateutil.relativedelta import relativedelta

from odoo import api, exceptions, fields, models


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "estate offer model"
_order = "price desc"

name = fields.Char(required=True)
price = fields.Float()
status = fields.Selection(
string='status',
copy=False,
selection=[('accepted', 'Accepted'), ('refused', 'Refused')],
)
partner_id = fields.Many2one("res.users", required=True)
property_id = fields.Many2one("estate.property", required=True, ondelete="cascade")
date_deadline = fields.Datetime(string="Deadline", compute="compute_deadline", inverse="_inverse_deadline")
validity = fields.Integer(string="validity", default=7)
property_type_id = fields.Many2one("estate.property.type", related="property_id.type_id", string="Property Type", store=True)

# =========contraints============
_check_positive_offer_price = models.Constraint("CHECK (price > 0)", "expected price should be bigger than 0")

@api.depends('validity', "create_date")
def compute_deadline(self):
for record in self:
if record.create_date:
record.date_deadline = record.create_date + relativedelta(days=record.validity)
else:
record.date_deadline = fields.Datetime.today() + relativedelta(days=record.validity)
Comment on lines +29 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
for record in self:
if record.create_date:
record.date_deadline = record.create_date + relativedelta(days=record.validity)
else:
record.date_deadline = fields.Datetime.today() + relativedelta(days=record.validity)
for record in self:
check_date = record.create_date if record.create_date else fields.Datetime.today()
record.date_deadline = check_date + relativedelta(days=record.validity)

To avoid duplicating some of the code we can do this, wdyt?


def _inverse_deadline(self):
for record in self:
record.validity = (record.date_deadline - record.create_date).days

# ===========button actions===========
def action_accept(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nitpick 😇 not a needed line

for record in self:
if "accepted" in record.property_id.offer_ids.mapped("status"):
already_accepted_exception = "already accepted an offer!"
raise exceptions.UserError(already_accepted_exception)
record.property_id.buyer_id = record.partner_id
record.property_id.state = "offer_accepted"
record.status = "accepted"
record.property_id.selling_price = record.price

def action_refuse(self):
for record in self:
for property in record.property_id:
record.status = "refused"

@api.model
def create(self, vals):
for to_create in vals:
property = self.env["estate.property"].browse(to_create["property_id"])
new_bid = to_create["price"]
for offer in property.offer_ids:
if offer.price > new_bid:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

since you introduced an order for the offers here https://github.com/odoo/tutorials/pull/1383/changes#diff-8000c23a2907f34f0cc387be9dc4178411fda81b674724854bd4d048f9e11b16R9 the recordset will be ordered upon price by default so normally offer_ids[0] will have the offer that has the highest price

cant_bit_lower_exception = "can't bid lower than the highest bid"
raise exceptions.UserError(cant_bit_lower_exception)
property.state = "offer_received"
return super().create(vals)
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 tag model"
_order = "name"

name = fields.Char(required=True)


_check_unique_tag = models.Constraint("UNIQUE(name)", "tags should be unique")

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 @@
from odoo import api, fields, models

class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "estate type model"
_order = "sequence, name"

name = fields.Char()
property_ids = fields.One2many("estate.property", "type_id")
sequence = fields.Integer(default=0)
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer(compute="_compute_offer_count")

_check_unique_type = models.Constraint("UNIQUE(name)", "types should be unique")

@api.depends("offer_ids")
def _compute_offer_count(self):
for property_type_record in self:
property_type_record.offer_count = len(property_type_record.offer_ids)

10 changes: 10 additions & 0 deletions estate/models/inhereted_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from odoo import fields, models

class InheritedModel(models.Model):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When inherit a model we name the class the same as the parent class

_inherit = "res.users"

property_ids = fields.One2many(
"estate.property",
"salesman_id",
domain=[("state", "in", ["new", "offer_received"])])

62 changes: 62 additions & 0 deletions estate/views/estate_property_settings_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?xml version="1.0"?>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

it is a better practice to have a XML view file for each model.
So here separate the estate_property and the estate_property_type and estate_property_tag and estate_property_offer views each in a separate file

<odoo>

<record id="estate_property_type_view_list" model="ir.ui.view">
<field name="name">estate.property.type.list</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<list string="Tests">
<field name="sequence" string="sequence" widget="handle"/>
<field name="name"/>
</list>
</field>
</record>

<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list string="Tests" editable="bottom">
<field name="name"/>
</list>
</field>
</record>

<record id="estate_property_type_view_form" model="ir.ui.view">
<field name="name">estate.property.type.form</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<form string="Properties">
<header>
<button class="oe_stat_button" type="action" name="%(estate.estate_property_offers_action)d" icon="fa-money" string="Offers"/>
</header>
<sheet>
<notebook>
<page string="Properties">
<field name="property_ids">
<list>
<field name="name" string="Title"/>
<field name="expected_price" string="Expected Price"/>
<field name="state" string="Status"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

<record id="estate_property_type_action" model="ir.actions.act_window">
<field name="name">Property Types</field>
<field name="res_model">estate.property.type</field>
<field name="view_mode">list,form</field>
</record>

<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Property Tags</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

Loading