Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b68a192
[FIX] website_airproof: fix template loading order issue
omarait-mlouk Sep 24, 2025
b7e18c9
[FIX] awesome_owl: Fix missing css variables
clbr-odoo Nov 21, 2025
781b590
[FIX] awesome_dashboard: Get rid of the deprecated warning
clbr-odoo Nov 21, 2025
bc75b91
[ADD] estate: initialize new application
ahmedamein100 Apr 22, 2026
703b451
[MOV] estate: move module to root
ahmedamein100 Apr 22, 2026
2b46f88
[ADD] estate: add property model and fields
ahmedamein100 Apr 22, 2026
2666fbd
[CLN] estate: fix formatting and typos
ahmedamein100 Apr 22, 2026
a1c6e1e
[ADD] estate: add access rights for property model
ahmedamein100 Apr 22, 2026
1fb914a
[CLN] estate: fix formatting and follow odoo guidelines
ahmedamein100 Apr 22, 2026
afbfbba
[CLN] estate: fix formatting and follow odoo guidelines
ahmedamein100 Apr 22, 2026
80d74ce
[CLN] estate: fix formatting and follow odoo guidelines
ahmedamein100 Apr 22, 2026
37bbff7
[ADD] estate: add basic UI and field business logic
ahmedamein100 Apr 22, 2026
39cd9c6
[FIX] estate: remove unlink permission for base users
vandroogenbd Apr 23, 2026
a930b33
[IMP] estate: add custom views and address PR feedback
ahmedamein100 Apr 23, 2026
b9b84e2
[ADD] estate: add relational models and fields
ahmedamein100 Apr 23, 2026
034623c
[FIX] estate: fix invalid search view definition
ahmedamein100 Apr 24, 2026
688ae48
[IMP] estate: add computed fields and onchange logic
ahmedamein100 Apr 24, 2026
f073812
[FIX] estate: fix python linting and styling issues
ahmedamein100 Apr 24, 2026
a8f5d11
[FIX] estate: fix python linting and styling issues
ahmedamein100 Apr 24, 2026
11467ce
[IMP] estate: add state management and offer actions
ahmedamein100 Apr 24, 2026
29377d5
[IMP] estate: add SQL and Python constraints for data integrity
ahmedamein100 Apr 24, 2026
9713b4c
[FIX] estate: clean up code styling
ahmedamein100 Apr 27, 2026
8e0f36c
[IMP] estate: add UI "sprinkles" and visual improvements
ahmedamein100 Apr 27, 2026
c6546c2
[CLN] estate: cleanup Python code and remove debug prints
ahmedamein100 Apr 27, 2026
4cec624
[CLN] estate: fix trailing whitespace in validation error
ahmedamein100 Apr 27, 2026
7df65a3
[CLN] estate: remove redundant invisible status field from offer list
ahmedamein100 Apr 27, 2026
158a5e3
[IMP] estate: add business logic and user inheritance
ahmedamein100 Apr 28, 2026
aafedb2
[IMP] estate: add basic test cases
vandroogenbd Apr 28, 2026
777efdb
[IMP] estate_account: link estate to account for invoicing
ahmedamein100 Apr 28, 2026
8a51cf8
[FIX] estate: enforce minimum price for south-facing gardens
ahmedamein100 Apr 28, 2026
ed456c8
[REF] estate: fix linting and PEP8 violations
ahmedamein100 Apr 28, 2026
e406286
[IMP] estate: implement advanced kanban view
ahmedamein100 Apr 29, 2026
27dac19
[FIX] estate: address review feedback on code standards
ahmedamein100 Apr 29, 2026
40e8c45
[REF] estate: fix whitespace and PEP8 linting violations
ahmedamein100 Apr 29, 2026
6d7c4d4
[FIX] estate: handle null create_date in offer deadline compute
ahmedamein100 Apr 29, 2026
02b6fb4
[REF] awesome_owl: implement Owl framework tutorial exercises
ahmedamein100 Apr 30, 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
56 changes: 56 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You should configure your global .gitignore to make sure such files don't get pushed to your PRs 😉

// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},

{
"name": "Odoo Server",
"type": "debugpy",
"request": "launch",
"python": "/home/odoo/odoo/venv/bin/python3",
"program": "/home/odoo/odoo/odoo-bin",
"args" : [
"--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials",
"-d", "rd_demo",
"--dev", "all",
"--without-demo", "all",
"-p", "8069",
"--limit-time-cpu", "0",
"--limit-time-real", "0",
// "-i", "",
],
"console": "integratedTerminal",
"variablePresentation": {}
},
{
"name": "Test Odoo",
// "preLaunchTask": "drop_test_db",
"type": "debugpy",
"request": "launch",
"program": "/home/odoo/odoo/odoo-bin",
"args" : [
"--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials",
"-d", "DB_NAMEtest",
"-p", "8071",
"--limit-time-cpu", "0",
"--limit-time-real", "0",
"--without-demo", "all",
"--stop-after-init",
//"-i", "",
"--test-tags", "",
//"--log-level", "error",
],
"console": "integratedTerminal",
"variablePresentation": {}
}
]
}
59 changes: 59 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"folders": [
{
"path": "/home/odoo/odoo"
},
{
"path": "/home/enterprise"
},
{
"path": "/home/tutorials"
}
],
"settings": {
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Odoo Server",
"type": "debugpy",
"request": "launch",
"program": "/home/odoo/odoo/odoo-bin",
"args" : [
"--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials",
"-d", "rd_demo",
"--dev", "all",
"--without-demo", "all",
"-p", "8069",
"--limit-time-cpu", "0",
"--limit-time-real", "0",
// "-i", "",
],
"console": "integratedTerminal",
"variablePresentation": {}
},
{
"name": "Test Odoo",
// "preLaunchTask": "drop_test_db",
"type": "debugpy",
"request": "launch",
"program": "/home/odoo/odoo/odoo-bin",
"args" : [
"--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials",
"-d", "DB_NAMEtest",
"-p", "8071",
"--limit-time-cpu", "0",
"--limit-time-real", "0",
"--without-demo", "all",
"--stop-after-init",
//"-i", "",
"--test-tags", "",
//"--log-level", "error",
],
"console": "integratedTerminal",
"variablePresentation": {}
}
]
}
}
}
2 changes: 1 addition & 1 deletion awesome_dashboard/controllers/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
logger = logging.getLogger(__name__)

class AwesomeDashboard(http.Controller):
@http.route('/awesome_dashboard/statistics', type='json', auth='user')
@http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user')
def get_statistics(self):
"""
Returns a dict of statistics about the orders:
Expand Down
2 changes: 2 additions & 0 deletions awesome_owl/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
'assets': {
'awesome_owl.assets_playground': [
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap'),
('include', 'web._assets_core'),
'web/static/src/libs/fontawesome/css/font-awesome.css',
Expand Down
1 change: 1 addition & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'name': "Estate",
'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.

Version should match the Odoo version, this way Odoo will not allow the module to be installed in other versions.

Suggested change
'version': '1.0',
'version': '19.0.0.1.0',

'depends': [
'base',
],
'category': 'Tutorials',
'application': True,
'data': [
'security/ir.model.access.csv',
'views/res_users_views.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/estate_menus.xml',
],
'author': "Odoo",
'license': 'AGPL-3'
}
5 changes: 5 additions & 0 deletions 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_user
131 changes: 131 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from odoo import api, fields, models, exceptions
from odoo.exceptions import ValidationError, UserError
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Please respect alphabetical order for imports

from odoo.tools import float_compare, float_is_zero


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

name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(copy=False, default=lambda self: fields.Date.add(fields.Date.today(), 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'),
]
)
Comment on lines +23 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Indentation

Suggested change
garden_orientation = fields.Selection(
string='Type',
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
]
)
garden_orientation = fields.Selection(
string='Type',
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
],
)

active = fields.Boolean(default=True)
state = fields.Selection(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Indentation here as well

selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled'),
],
required=True,
copy=False,
default='new',
)
type_id = fields.Many2one(
"estate.property.type",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Try to stay consistent with ' and "

string="Tag",
)
salesperson_id = fields.Many2one(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Indentation

"res.users",
string="Salesperson",
default=lambda self: self.env.user,
)
buyer_id = fields.Many2one(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Indentation

"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.Integer(compute="_compute_total_area")
best_price = fields.Float(compute="_compute_best_price")
_check_expected_price = models.Constraint(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Indentation is off, also constraints should be after computes (see doc)

'CHECK(expected_price > 0)',
'The Expected price of a property should be > 0',
)
_check_selling_price = models.Constraint(
'CHECK(selling_price > 0)',
'The selling price of a property should be > 0',
)
_check_name_unique = models.Constraint(
'UNIQUE(name)',
'The prop name must be unique!',
)

@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
record.total_area = (record.living_area or 0) + (record.garden_area or 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.

Good use of short-circuiting 👍


@api.depends("offer_ids")
def _compute_best_price(self):
for record in self:
highest_price = 0
for offer in record.offer_ids:
highest_price = max(highest_price, offer.price)
record.best_price = highest_price
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Max can take a default value, or you could use ternary assignation.

Default value:

Suggested change
highest_price = 0
for offer in record.offer_ids:
highest_price = max(highest_price, offer.price)
record.best_price = highest_price
record.best_price = max(record.offer_ids.mapped('price'), default=0.0)

Ternary assignation

Suggested change
highest_price = 0
for offer in record.offer_ids:
highest_price = max(highest_price, offer.price)
record.best_price = highest_price
record.best_price = max(record.offer_ids.mapped('price')) if record.offer_ids 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 = ''
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tricky (?) question time!

At first glance, this method could also be an inverse. Can you tell why it makes more sense for it to be an onchange?

Answer

inverse are triggered whenever the field is written. onchange are called by form views.

It doesn't make much sense to set arbitrary default values when programmatically updating records, so onchange is the correct approach here.


def action_sold(self):
for record in self:
if record.state == 'cancelled':
raise exceptions.UserError("cancelled prop can't be sold")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Message could use a lifting, it is meant for user display

Suggested change
raise exceptions.UserError("cancelled prop can't be sold")
raise exceptions.UserError("Cancelled properties cannot be sold.")

record.state = 'sold'

def action_cancel(self):
for record in self:
if record.state == 'sold':
raise exceptions.UserError("Sold prop can't be cancelled")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here

record.state = 'cancelled'

@api.constrains('selling_price')
def _check_price(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.

This name is not very explicit in what it is meant to check. Also, you should define all of the fields required for the constraint.

Suggested change
@api.constrains('selling_price')
def _check_price(self):
@api.constrains('selling_price', 'expected_price')
def _check_expected_to_selling_price_ratio(self):

for record in self:
if float_is_zero(record.selling_price, precision_digits=2):
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is already checked by your constraint on selling_price so it is redundant to add it here.

limit = record.expected_price * 0.9
if float_compare(record.selling_price, limit, precision_digits=2) == -1:
raise ValidationError("The selling price cannot be lower than 90% of the expected price!")

@api.ondelete(at_uninstall=False)
def _check_property_state_on_delete(self):
for record in self:
if record.state not in ['new', 'canceled']:
raise UserError(
"You cannot delete a property that is not 'New' or 'Cancelled'!"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No need to line break here 😉

85 changes: 85 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from odoo import api, fields, models, exceptions
from odoo.exceptions import ValidationError


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

price = fields.Float()
status = fields.Selection(
selection=[
('accepted', 'Accepted'),
('refused', 'Refused'),
],
copy=False,
)
partner_id = fields.Many2one(
"res.partner",
string="Partner",
required=True
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
required=True
required=True,

)
property_id = fields.Many2one(
"estate.property",
string="Property",
required=True,
)
property_type_id = fields.Many2one(
related="property_id.type_id",
string="Property Type",
store=True
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
store=True
store=True,

)
validity = fields.Integer(default=7)
date_deadline = fields.Date(
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
)
_check_price = models.Constraint(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ordering

'CHECK(price > 0)',
'The offer price should be > 0',
)

@api.depends("validity", "create_date")
def _compute_date_deadline(self):
for record in self:
record.date_deadline = fields.Date.add(
(fields.Date.to_date(record.create_date) or fields.Date.today()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You can just do create_date.date()

Suggested change
(fields.Date.to_date(record.create_date) or fields.Date.today()),
record.create_date.date() or fields.Date.today(),

days=record.validity
)

def _inverse_date_deadline(self):
for record in self:
record.validity = (
record.date_deadline - (fields.Date.to_date(record.create_date) or fields.Date.today())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here

).days

def action_accept_offer(self):
for record in self:
if record.property_id.state == 'sold':
raise exceptions.UserError("Prob is already sold")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Message

if record.property_id.garden and record.property_id.garden_orientation == 'south':
if record.price < record.property_id.expected_price:
raise ValidationError("South facing house should have offer with >= tothe prop expected price")
record.property_id.state = 'offer_accepted'
record.property_id.selling_price = record.price
record.property_id.buyer_id = record.partner_id
record.status = 'accepted'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Try to use write in such cases, to commit all of the changes at once.

Suggested change
record.property_id.state = 'offer_accepted'
record.property_id.selling_price = record.price
record.property_id.buyer_id = record.partner_id
record.status = 'accepted'
record.property_id.write({
"state": "offer_accepted",
"selling_price": record.price,
"buyer": record.partner_id,
})
record.write({
"status": "accepted",
})


def action_refuse_offer(self):
for record in self:
record.status = 'refused'

@api.model
def create(self, vals_list):
for vals in vals_list:
prop = self.env['estate.property'].browse(vals.get('property_id'))

if prop.best_price > 0 and vals.get('price') < prop.best_price:
raise exceptions.UserError(
f"The offer must be at least {prop.best_price}."
)
if prop.state == 'new':
prop.state = 'offer_received'
Comment on lines +83 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This would make more sense as a compute function of estate.property rather than being hardcoded here.


return super().create(vals_list)
Loading