Skip to content

18.0 training rvar #848

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 8 commits into
base: 18.0
Choose a base branch
from
Draft
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

Choose a reason for hiding this comment

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

Missing licensing

20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Real Estate",
"category": "All",
"summary": "Demo app for estate",
"description": "This is the demo app ",
Comment on lines +4 to +5

Choose a reason for hiding this comment

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

Add a proper summary and description

"installable": True,
"depends": ["base"],
"application": True,
"auto_install": False,

Choose a reason for hiding this comment

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

If the value is false, then no need to define it anyways

"license": "AGPL-3",
"data": [
"security/ir.model.access.csv",
"views/estate_property_offer_views.xml",
"views/estate_property_views.xml",
"views/estate_settings_views.xml",
"views/estate_property_tags_views.xml",
"views/res_users_views.xml",
"views/estate_menus.xml",
],
}
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 inherited_model
141 changes: 141 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from dateutil.relativedelta import relativedelta
from odoo import fields, models, api, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero


class RecurringPlan(models.Model):
_name = "estate.property"
_description = "estate property revenue plans"
Comment on lines +7 to +9

Choose a reason for hiding this comment

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

Mismatch class name and model name

_order = "id desc" # For ordering in ascending opr descending order one more way to do so is from view like: <list default_order="date desc">

Choose a reason for hiding this comment

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

Comment is not required here


name = fields.Char(required=True)
description = fields.Text(string="Description")
postcode = fields.Char(string="Postcode")
date_availability = fields.Date(
string="Available From",
copy=False,
default=lambda self: fields.Date.today() + relativedelta(months=3),
)
expected_price = fields.Float(string="Expected Price", 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(string="Number of Facades")
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(string="Garden Area (sqm)")
garden_orientation = fields.Selection(
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
string="Garden Orientation",
help="Orientation of the garden relative to the property",
)
active = fields.Boolean(default=True)
state = fields.Selection(
selection=[
("new", "NEW"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
string="Status",
required=True,
default="new",
copy=False,
# readonly=True

Choose a reason for hiding this comment

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

If not required, please remove it.

)

total_area = fields.Integer(string="Total Area", compute="_compute_total_area")
best_price = fields.Float(string="Best Offer", compute="_compute_best_price")

property_type_id = fields.Many2one("estate.property.type", string="Property Type")
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
user_id = fields.Many2one(
"res.users", string="Salesman", copy=False, default=lambda self: self.env.user
)

tag_id = fields.Many2many("estate.property.tag", string="Tags", copy=False)

offer_id = fields.One2many("estate.property.offer", "property_id", string="Offer")

_sql_constraints = [
(
"check_expected_price",
"CHECK(expected_price > 0)",
"The Expected price of a property should be strictly positive.",
),
(
"check_selling_price",
"CHECK(selling_price IS NULL OR selling_price >= 0)",
"Selling price must be positive when set.",
),
]

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

@api.depends("offer_id.price")
def _compute_best_price(self):
for record in self:
# Get all prices from offer_ids
offer_prices = record.offer_id.mapped("price")
record.best_price = max(offer_prices) if offer_prices else 0.0

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

def action_property_sold(self):
for prop in self:
if prop.state == "cancelled":
raise UserError(_("Cancelled properties cannot be sold."))
prop.state = "sold"
return True

def action_property_cancel(self):
for prop in self:
if prop.state == "sold":
raise UserError(_("Sold properties cannot be cancelled."))
prop.state = "cancelled"
return True

@api.constrains("selling_price", "expected_price")
def _check_selling_price(self):
for record in self:
if float_is_zero(record.selling_price, precision_digits=2):
continue
min_allowed_price = record.expected_price * 0.9

if (
float_compare(
record.selling_price, min_allowed_price, precision_digits=2
)
< 0
):
Comment on lines +122 to +127

Choose a reason for hiding this comment

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

this could be inlined

raise ValidationError(
"Selling price cannot be lower than 90% of the expected price. "
f"(Minimum allowed: {min_allowed_price:.2f})"
)

@api.ondelete(at_uninstall=False)
def _unlink_except_state_not_new(self):
for rec in self:
if rec.state not in ["new", "cancelled"]:
raise UserError(
_(
"You cannot delete a property unless its state is 'New' or 'Cancelled'."
)
)
Comment on lines +137 to +141

Choose a reason for hiding this comment

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

this could be inlined

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


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

price = fields.Float(string="Price")
status = fields.Selection(
selection=[("accepted", "Accepted"), ("refused", "Refused")],
copy=False,
string="Status",
)

validity = fields.Integer(string="Validity", default=7)
date_deadline = fields.Date(
string="Deadline",
compute="_compute_deadline_date",
inverse="_inverse_deadline_date",
)

partner_id = fields.Many2one(
"res.partner", string="Partner", copy=False, required=True
)
property_id = fields.Many2one("estate.property", copy=False, required=True)
property_type_id = fields.Many2one(
"estate.property.type",
related="property_id.property_type_id",
store=True,
readonly=False,
)
_sql_constraints = [
(
"check_offer_price",
"CHECK(price > 0)",
"The Offer price must be strictly positive",
)
]

@api.depends("create_date", "validity")
def _compute_deadline_date(self):
for offer in self:
create_dt = offer.create_date or fields.Date.today()
offer.date_deadline = create_dt + relativedelta(days=offer.validity)

def _inverse_deadline_date(self):
for offer in self:
if offer.date_deadline:
create_dt = (offer.create_date or fields.Datetime.now()).date()
delta = (offer.date_deadline - create_dt).days
offer.validity = delta
else:
offer.validity = 0

def action_accept(self):
for offer in self:
# Allow only if property has no accepted offer
if offer.property_id.buyer_id:
raise UserError(
_("An offer has already been accepted for this property.")
)
# Set buyer and selling price
offer.property_id.buyer_id = offer.partner_id
offer.property_id.selling_price = offer.price
offer.status = "accepted"
offer.property_id.state = "offer_accepted"
# Setting remaining Offer as refused
other_offers = offer.property_id.offer_id - offer
# for other in other_offers: # -----> Normal for loop logic
# other.status = 'refused'
other_offers.write({"status": "refused"}) # -----> Odoo ORM Method
Comment on lines +60 to +74

Choose a reason for hiding this comment

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

Comment are unnecesary


return True

def action_refused(self):
for offer in self:
offer.status = "refused"
return True

@api.model_create_multi
def create(self, vals_list):
new_records = []
for vals in vals_list:
property_id = vals.get("property_id")

property_rec = self.env["estate.property"].browse(property_id)
# Business logic: set property state
if property_rec.state == "new":
property_rec.state = "offer_received"

# Business logic: validate price
if "price" in vals and vals["price"] < property_rec.best_price:
raise exceptions.ValidationError(
"Offer price must be higher than existing offers."
)

new_records.append(vals)

return super().create(new_records)

# vals_list---------> [{'partner_id': 23, 'price': 100, 'validity': 7, 'date_deadline': '2025-07-16', 'property_id': 19}]

Choose a reason for hiding this comment

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

Unnecessary

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


class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Tags of Estate property "
_order = "name asc"

name = fields.Char(required=True, string="Name")
color = fields.Integer(default=3)

_sql_constraints = [
(
"unique_property_tag_name",
"UNIQUE(name)",
"A property tag name must be unique.",
),
]
33 changes: 33 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from odoo import fields, models, api


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Types of Estate property "
_order = "sequence, name asc" # Order by sequence first

name = fields.Char(required=True)
sequence = fields.Integer(
"Sequence", default=10, help="Used to order property types."
)

property_ids = fields.One2many("estate.property", "property_type_id")
offer_ids = fields.One2many(
"estate.property.offer", "property_type_id", string="Offers"
)
offer_count = fields.Integer(
string="Offer Count", compute="_compute_offer_count", store=True
)

_sql_constraints = [
(
"unique_property_type_name",
"UNIQUE(name)",
"A property type name must be unique.",
),
]

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


class ResUsers(models.Model):
_inherit = "res.users"

property_ids = fields.One2many(
comodel_name="estate.property",
inverse_name="user_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
access_estate_model,estate.property,model_estate_property,base.group_user,1,1,1,1
access_estate_model_property,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_model_tag,estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_model_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1

Choose a reason for hiding this comment

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

End of file line is missing

29 changes: 29 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<odoo>
<menuitem id="test_menu_root"
name="Real Estate"> <!-- Root Level = which is displayed in the App switcher -->

<menuitem id="Advertisement_menu"
name="Advertisement"> <!-- First Level = Top Bar-->

<menuitem id="estate_property_menu_action"
action="estate_property_action"/> <!-- The action menus = Drop Down in topo bar -->

</menuitem>

<menuitem id="Settings_menu"
name="Settings"> <!-- First Level = Top Bar-->

<menuitem id="estate_property_menu_action_settings"
action="estate_property_action_settings"/>

<menuitem id="estate_property_menu_action_settings_tag"
action="estate_property_action_tag"/>

</menuitem>




</menuitem>

</odoo>
Loading