Skip to content

eadi - Technical Training #853

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

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
47f4912
Complete Chapter 2
EduardD22 Jul 15, 2025
0a42831
Chapter 3 Completed - created model for Estate Property with its fiel…
EduardD22 Jul 16, 2025
855c32e
Chapter 4 Completed - added access rights to the model by creating ir…
EduardD22 Jul 16, 2025
ef11110
Chapter 5 Completed - created views for the form, for the displayed l…
EduardD22 Jul 17, 2025
8774614
Chapter 7 Completed - Added 3 more models, configured the relationshi…
EduardD22 Jul 18, 2025
55cfeef
added computed field with a compute and inverse function to calculate…
EduardD22 Jul 21, 2025
fadc541
added computed fields for total area and best price and an onchange f…
EduardD22 Jul 21, 2025
c21e11c
added the new fields to the view
EduardD22 Jul 21, 2025
a8d4782
added accept and refuse functions
EduardD22 Jul 21, 2025
30d9dce
added sold and cancel functions
EduardD22 Jul 21, 2025
a819695
added button to set property as sold or canceled and offers as accept…
EduardD22 Jul 21, 2025
a196386
added sql constraints to check the offer's price
EduardD22 Jul 21, 2025
9c0f7d0
added sql constraint the check uniqueness of tag name
EduardD22 Jul 21, 2025
d2fb17f
added sql contraint to check uniqueness property type name
EduardD22 Jul 21, 2025
e61a9af
added sql constraints to check expected price, selling price and a py…
EduardD22 Jul 21, 2025
90651ff
added 2 more views
EduardD22 Jul 22, 2025
c10aa4f
Chapter 11 Completed - added more views, added stat button, modified …
EduardD22 Jul 22, 2025
b9e675f
Chapter 12 Completed - modified the create method by comparing the pr…
EduardD22 Jul 22, 2025
eb2c810
Chapter 13-14 Completed, creating new module for Estate Accounting, l…
EduardD22 Jul 23, 2025
0ac71d0
Chapter 15 Completed - Some refactoring
EduardD22 Jul 23, 2025
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 @@


from . import models
16 changes: 16 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
'name': "Estate",
'version': '1.0',
'author': "Eadi",
'depends': ['base'],
'description': "",
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_menus.xml',
'views/estate_property_type_views.xml',
'views/estate_property_offer_views.xml',
'views/res_users_views.xml'
],
'application': True
}
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 res_users
100 changes: 100 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@

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

class EstateProperty(models.Model):
_name = "estate.property"
_description = "Estate Property Model"
_order = "id desc"
_sql_constraints = [
("check_expected_price", "CHECK(expected_price > 0)", "The expected price must be strictly positive"),
("check_selling_price", "CHECK(selling_price >= 0)", "The selling price must be positive")
]

name = fields.Char(required=True)
salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user)
buyer_id = fields.Many2one("res.partner", string="Buyer")
property_type_id = fields.Many2one("estate.property.type", string="Property Type", required=True)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers", help="All offers received for this property")
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(string="Available From", copy=False , default= lambda self: fields.Date.today() + timedelta(90))
expected_price = fields.Float(required=True)
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
bedrooms = fields.Integer(string="Bedrooms", default=2)
living_area = fields.Integer(string="Living Area(sqm)")
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(string="Garden Area(sqm)")
garden_orientation = fields.Selection(selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")])
active = fields.Boolean(string="Active", default=True)
state = fields.Selection(selection=[("new", "New"), ("offer received", "Offer Received"), ("offer accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")], required=True, default="new", copy=False, string="Status" )
total_area = fields.Integer(string="Total Area(sqm)", compute="_compute_total")
best_price = fields.Float(string="Best Price", compute="_compute_best_price")


@api.depends("living_area", "garden_area")
def _compute_total(self):
for record in self:
record.total_area = record.living_area + record.garden_area

@api.depends("offer_ids.price")
def _compute_best_price(self):
for record in self:
if record.offer_ids:
#mapped method takes the values of that specific column
record.best_price = max(record.offer_ids.mapped("price"))
else:
record.best_price = 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("expected_price", "selling_price")
def _check_selling_price(self):
for record in self:
# skipping all the records with selling price = 0, check only when an offer is accepted, so when selling price is updated
if float_is_zero(record.selling_price, precision_digits=2):
continue

min_selling_price = record.expected_price * 0.9
# float compare returns -1 if float 1 is less than float 2
if float_compare(record.selling_price, min_selling_price, precision_digits=2) == -1:
raise ValidationError("Selling price cannot be lower than 90% of the expected price")

@api.ondelete(at_uninstall=False)
def _prevent_deletion_if_not_new_or_cancelled(self):
# take all the records with state different than new or cancelled
# raise user error if these records are present
invalid_records = self.filtered(lambda r: r.state not in ["new", "cancelled"])

if invalid_records:
raise UserError("Cannot delete properties that are not ''new' or 'cancelled'")

def action_sold(self):
if "canceled" in self.mapped("state"):
raise UserError("Canceled properties can't be sold")
return self.write({"state": "sold"})

def action_cancel(self):
if "sold" in self.mapped("state"):
raise UserError("Sold properties can't be canceled")
return self.write({"state": "canceled"})








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

class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Offer for a property"
_order = "price desc"
_sql_constraints = [
("check_price", "CHECK(price > 0)", "The expected price must be strictly positive")
]

price = fields.Float(string="Price", required=True)
property_id = fields.Many2one("estate.property", string="Property", required=True, help="The property this offer is for")
partner_id = fields.Many2one("res.partner", string="Partner", required=True, help="The partner making the offer")
property_type_id = fields.Many2one("estate.property.type", string="Property Type", related="property_id.property_type_id", store=True)
status = fields.Selection([("accepted", "Accepted"), ("refused", "Refused")], string="Status", copy=False)
validity = fields.Integer(string="Validity(days)", default=7, help="Number of days this offer is valid")
date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline", help="Date when offer expires")

@api.depends('create_date', 'validity')
def _compute_date_deadline(self):
for record in self:
if record.create_date:
# if the record existis and it has a create_date
record.date_deadline = record.create_date.date() + timedelta(days=record.validity)
else:
# using today's date if record isn't created yet
record.date_deadline = fields.Date.today() + timedelta(days=record.validity)


def _inverse_date_deadline(self):
for record in self:
if record.date_deadline:
if record.create_date:
difference = record.date_deadline - record.create_date.date()
record.validity = difference.days
else:
# using today's date again
difference = record.date_deadline - fields.Date.today()
record.validity = difference.days

@api.model
def create(self, vals):
property_id = vals.get("property_id")
if not property_id:
raise ValidationError("Property is required for offer creation")

# Browsing in order to get the property record
property_record = self.env['estate.property'].browse(property_id)

# checking if there are any existing offer for the record
existing_offers = self.search([("property_id", "=", property_id)])

# getting max offer and comparing with the current price
if existing_offers:
max_existing_offer = max(existing_offers.mapped("price"))
new_price = vals.get("price", 0)

if float_compare(new_price, max_existing_offer, precision_digits=2) <= 0:
raise UserError(
f"Offer amount ({new_price:,.2f}) must be higher than "
f"existing offers (highest: {max_existing_offer:,.2f})"
)

# creating the offer
offer = super().create(vals)

# updating the state when an offer is added

if property_record.state == "new":
property_record.write({"state": "offer received"})

return offer

def action_accept(self):
# check if record with status accepted already exists
# if it exists raise error saying an offer was already accepted
# self.write status accepted
# self.write property with status, selling price and buyer id
if "accepted" in self.mapped("property_id.offer_ids.status"):
raise UserError("An offer was already accepted")
self.write(({"status":"accepted"}))
return self.mapped("property_id").write(
{
"state":"offer accepted",
"selling_price":self.price,
"buyer_id":self.partner_id
}
)

def action_refuse(self):
return self.write({"status":"refused"})









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

class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Tags for Properties"
_order = "name"
_sql_constraints = [("name_uniq", "UNIQUE(name)", "Tag name must be unique")]

name = fields.Char(required=True)
color = fields.Integer("Color Index")
18 changes: 18 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from odoo import fields, models, api

class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Type of a Property"
_order = "sequence, name"
_sql_constraints = [("name_uniq", "UNIQUE(name)", "Property type name must be unique")]

name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers")
sequence = fields.Integer(string="Sequence", default=10)
offer_count = fields.Integer(string="Number of Offers", compute="_compute_offer_count")

@api.depends("offer_ids")
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
8 changes: 8 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import fields, models


class ResUsers(models.Model):

_inherit = "res.users"

property_ids = fields.One2many("estate.property", "salesperson_id", string="Properties", domain=[("state", "in", ["new", "offer received"])])
5 changes: 5 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1
access_estate_property_type,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag,estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1
35 changes: 35 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0"?>
<odoo>

<menuitem id="estate_property_menu_root" name="Estate" sequence="25" />

<menuitem id="estate_property_menu_properties"
name="Properties"
parent="estate_property_menu_root"
sequence="1" />

<menuitem id="estate_property_menu_properties_all"
name="All Properties"
parent="estate_property_menu_properties"
action="estate_property_action"
sequence="1" />

<menuitem id="estate_property_menu_settings"
name="Settings"
parent="estate_property_menu_root"
sequence="20" />

<menuitem id="estate_property_type_menu"
name="Property Types"
parent="estate_property_menu_settings"
sequence="1"
action="estate_property_type_action"/>

<menuitem id="estate_property_tag_menu"
name="Property Tags"
parent="estate_property_menu_settings"
sequence="1"
action="estate_property_tag_action"/>


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

<record id="estate_property_offer_view_form" model="ir.ui.view">
<field name="name">estate.property.offer.form</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<form string="Property Offer">
<group>
<field name="price" />
<field name="partner_id" />
</group>
</form>
</field>
</record>

<record id="estate_property_offer_view_list" model="ir.ui.view">
<field name="name">estate.property.offer.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list editable="bottom" decoration-success="status == 'accepted'"
decoration-danger="status == 'refused'">
<field name="price" />
<field name="partner_id" />
<field name="status" optional="hide" />
<field name="validity" />
<field name="date_deadline" />
<button name="action_accept" type="object" title="Accept"
icon="fa-check" invisible="status != False" />
<button name="action_refuse" type="object" title="Refuse"
icon="fa-times" invisible="status != False" />
</list>
</field>
</record>

<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>
<!-- It filters the offers to only show those related to the current property type. active_id dynamically refers to the ID of the current record (the property type). -->
<field name="domain">[('property_type_id','=', active_id)]</field>
<field name="view_mode">list,form</field>
</record>

</odoo>
Loading