Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions .zed/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[
{
"label": "Odoo: Enterprise (Update estate)",
"command": "venv/bin/python",
"args": [
"odoo-bin",
"--addons-path=addons/,../enterprise/,../tutorials",
"-u", "estate",
"-d", "odoo"
],
"cwd": "/home/feraz/Documents/odoo",
"use_new_terminal": false,
"allow_concurrent_runs": false
},
{
"label": "Odoo: Community (Update estate)",
"command": "venv/bin/python",
"args": [
"odoo-bin",
"--addons-path=addons/,../tutorials",
"-u", "estate",
"-d", "odoo"
],
"cwd": "/home/feraz/Documents/odoo",
"use_new_terminal": false,
"allow_concurrent_runs": false
},
{
"label": "Odoo: Test (estate)",
"command": "venv/bin/python",
"args": [
"odoo-bin",
"--addons-path=addons/,../enterprise/,../tutorials",
"-d", "odoo",
"--test-tags", "/estate",
"--stop-after-init"
],
"cwd": "/home/feraz/Documents/odoo",
"use_new_terminal": false,
"allow_concurrent_runs": false
}
]
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
41 changes: 41 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
'name': "Estate",

'summary': """
Tutorial module for estate management
""",

'description': """
Tutorial module for estate management
""",

'author': "Odoo",
'website': "https://www.odoo.com",

# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Tutorials',
'version': '0.1',

# any module necessary for this one to work correctly
'depends': ['base'],
'application': True,
'installable': True,
'data': [
'security/ir.model.access.csv',

'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_property_menu_views.xml',
'views/res_user_views.xml',

'report/estate_property_templates.xml',
'report/estate_property_reports.xml',
],
'assets': {
},
'license': 'AGPL-3'
}
2 changes: 2 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# import filename_python_file_within_folder_or_subfolder
from . import estate_property_type, estate_property_tag, estate_property_offer, estate_property, res_users
120 changes: 120 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from dateutil.relativedelta import relativedelta

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


class EstateProperty(models.Model):

_name = "estate.property"
_description = "Estate property"
_order = "id desc"

name = fields.Char("Property Name", required=True)
description = fields.Text("Description")
postcode = fields.Char("Postcode")
date_availability = fields.Date("Availability Date", copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3))
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer("Number of Bedrooms", default=2)
living_area = fields.Integer("Living Area m²")
facades = fields.Integer("Number of Facades")
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer("Garden Area m²")
garden_orientation = fields.Selection(selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')])
state = fields.Selection(
selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'), ('cancelled', 'Cancelled'),
],
default='new',
required=True,
copy=False)
active = fields.Boolean(default=True)
type_id = fields.Many2one(string="Type", comodel_name="estate.property.type")
buyer_id = fields.Many2one("res.partner", copy=False)
seller_id = fields.Many2one(string="Salesperson", comodel_name="res.users", default=lambda self: self.env.user)
tag_ids = fields.Many2many(string="Tags", comodel_name="estate.property.tag")
offer_ids = fields.One2many(string="Offers", comodel_name="estate.property.offer", inverse_name="property_id")
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float("Best offer price", compute="_compute_best_price")

_expected_price_strictly_positive = models.Constraint(
'CHECK(expected_price > 0)',
'Expected price must be strictly positive'
)
_selling_price_strictly_positive = models.Constraint(
'CHECK(selling_price > 0)',
'Selling price must be strictly positive'
)

def _no_accepted_offer(self):
self.ensure_one()
return all(offer.state != "accepted" for offer in self.offer_ids)

@api.constrains("selling_price")
def _check_selling_price(self):
for property in self:
# No accepted offer and price is zero
if float_is_zero(property.selling_price, 2) and property._no_accepted_offer():
return

if float_compare(property.selling_price, property.expected_price * 0.9, 2) == -1:
raise ValidationError(
"Selling price must be at least 90% of expected price. Update expected price to accept offer."
)

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

@api.depends("offer_ids")
def _compute_best_price(self):
for property in self:
if not property.offer_ids:
property.best_price = 0
continue
best_offer_price = max(property.offer_ids.mapped('price'))
property.best_price = best_offer_price

@api.onchange("garden")
def _onchange_garden(self):
for property in self:
if property.garden:
property.garden_area = 10
property.garden_orientation = 'north'
else:
property.garden_area = None
property.garden_orientation = None

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

def action_sell_property(self):
for property in self:
if property.state == "cancelled":
raise UserError("Cancelled properties cannot be sold")

if not property.offer_ids:
raise UserError("Cannot sell property with no offer")

if property._no_accepted_offer():
raise UserError("Cannot sell property with no accepted offer")

property.state = "sold"
return True

@api.ondelete(at_uninstall=False)
def _unlink_only_new_cancelled(self):
for property in self:
if property.state not in ('new', 'cancelled'):
raise UserError("You can only delete new or cancelled properties")
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 datetime import timedelta

from odoo import models, fields, api, exceptions


class EstatePropertyOffer(models.Model):

_name = "estate.property.offer"
_description = "Estate property offer"
_order = "price desc"

price = fields.Float("Offer Price")
state = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')])
partner_id = fields.Many2one(string="Buyer", comodel_name="res.partner", required=True)
property_id = fields.Many2one(comodel_name="estate.property", required=True)
property_type_id = fields.Many2one(related="property_id.type_id", store=True)

validity = fields.Integer(default=7)
date_deadline = fields.Date("Offer Deadline", compute="_compute_deadline", inverse="_inverse_deadline")

_price_strictly_positive = models.Constraint(
'CHECK(price > 0)',
'Offer price must be strictly positive'
)

@api.depends("validity")
def _compute_deadline(self):
for offer in self:
compare_date = offer.create_date.date() if offer.create_date else fields.Date.today()
offer.date_deadline = compare_date + timedelta(days=offer.validity)

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

def action_accept_offer(self):
for offer_to_accept in self:
# Ensure there is no existing accepted offer
if not offer_to_accept.property_id._no_accepted_offer():
raise exceptions.UserError("Property already has an accepted offer.")

offer_to_accept.state = "accepted"

offer_to_accept.property_id.write({
'buyer_id': offer_to_accept.partner_id.id,
'state': 'offer_accepted',
'selling_price': offer_to_accept.price,
})

return True

def action_refuse_offer(self):
for offer in self:
# We could forbid refusing accepted offers
offer.state = "refused"
return True

@api.model
def create(self, vals_list):
for vals in vals_list:
property = self.env['estate.property'].browse(vals['property_id'])
if property.state == 'new':
property.state = 'offer_received'
elif property.state == 'sold':
raise exceptions.UserError("Property already has been sold, new offers can not be created for it.")
return super().create(vals_list)
16 changes: 16 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from odoo import models, fields


class EstatePropertyTag(models.Model):

_name = "estate.property.tag"
_description = "Estate property tag"
_order = "name"

name = fields.Char("Property Tag Name", required=True)
color = fields.Integer()

_name_uniq = models.Constraint(
'unique(name)',
'A property tag with the same name already exists.',
)
25 changes: 25 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from odoo import models, fields, api


class EstatePropertyType(models.Model):

_name = "estate.property.type"
_description = "Estate property type"
_order = "sequence,name"

name = fields.Char("Property Type Name", required=True)
sequence = fields.Integer(default=1, help="Used to order stages. Lower is ranked higher.")
property_ids = fields.One2many(comodel_name="estate.property", inverse_name="type_id")
offer_ids = fields.One2many(related="property_ids.offer_ids", inverse_name="property_type_id")

offer_count = fields.Integer(compute="_compute_offer_count")

_name_uniq = models.Constraint(
'unique(name)',
'A property type with the same name already exists.',
)

@api.depends("property_ids.offer_ids")
def _compute_offer_count(self):
for type in self:
type.offer_count = self.env['estate.property.offer'].search_count([('property_id', 'in', type.property_ids.ids)])
23 changes: 23 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from odoo import models, fields, api


class Users(models.Model):

_inherit = 'res.users'

property_ids = fields.One2many(
comodel_name="estate.property",
inverse_name="seller_id"
)

available_property_ids = fields.One2many(
comodel_name="estate.property",
compute="_compute_available_properties"
)

@api.depends("property_ids")
def _compute_available_properties(self):
for user in self:
user.available_property_ids = user.property_ids.filtered(
lambda property: property.state in ["new", "offer_received"]
)
15 changes: 15 additions & 0 deletions estate/report/estate_property_reports.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="report_estate_property" model="ir.actions.report">
<field name="name">Estate Property</field>
<field name="model">estate.property</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">estate.report_property_offers</field>
<field name="report_file">estate.report_property_offers</field>
<field name="print_report_name">'Estate Property - %s' % (object.name or 'Offer').replace('/','')</field>
<field name="binding_model_id" ref="model_estate_property"/>
<field name="binding_type">report</field>
</record>

</odoo>
Loading