Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eae996b
[ADD] estate: initialized Sevan Estate module
SebVde Apr 21, 2026
59ab049
[ADD] estate: created Property model
SebVde Apr 21, 2026
57edca3
[ADD] estate: add access rights to base.group-user
SebVde Apr 21, 2026
d662407
[CLN] estate: clean code following coach's comments
SebVde Apr 22, 2026
47a9eac
[ADD] estate: action and menus for the module's view
SebVde Apr 22, 2026
d260597
[ADD] estate: create list, form and search views
SebVde Apr 22, 2026
d09e0fa
[ADD] estate: end-of-the-day draft for Chapter 7
SebVde Apr 22, 2026
052c526
[FIX] estate: remove unlink permission for base users
vandroogenbd Apr 23, 2026
ad84cd3
[ADD] estate: add property type, salesman and buyer fields
SebVde Apr 23, 2026
a65fc53
[ADD] estate: add tags to property
SebVde Apr 23, 2026
58cc017
[ADD] estate: add property type, salesman and buyer fields
SebVde Apr 23, 2026
d22ebc0
[ADD] estate: add tags to property
SebVde Apr 23, 2026
cff0075
[ADD] estate: add offers to property
SebVde Apr 23, 2026
96e43bd
[ADD] estate: compute total area of property
SebVde Apr 23, 2026
39728f4
[ADD] estate: compute best offer, deadline and garden values
SebVde Apr 23, 2026
2cfe9bf
[ADD] estate: buttons linked to actions for property and offer
SebVde Apr 24, 2026
14d9c9a
[IMP] estate: constraints on prices
SebVde Apr 24, 2026
25a3e73
[ADD] estate: add attributes, options and widgets for the views
SebVde Apr 27, 2026
eafc4a3
[ADD] estate: add stat button in property type form
SebVde Apr 27, 2026
4209cd6
[FIX] estate: add missing license
SebVde Apr 27, 2026
7a8ea20
[IMP] estate: override CRUD methods, and inherit models and views
SebVde Apr 27, 2026
cd50a96
[ADD] estate account: add invoice creation for sold property
SebVde Apr 28, 2026
e703a19
[IMP] estate: add basic test cases
vandroogenbd Apr 28, 2026
cf25e2b
[IMP] estate: modify behavious of offer accept action
SebVde Apr 28, 2026
fae93ca
[IMP] estate: add kanban view for properties
SebVde Apr 28, 2026
478c594
[CLN] estate: cleaning following coding guidelines
SebVde Apr 28, 2026
d8542bd
[CLN] estate + estate_account
SebVde Apr 29, 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
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
21 changes: 21 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
'name': "Sevan Estate",
'license': 'LGPL-3',
'depends': ['base'],
'summary': "Just a simple estate app for Sevan.",
'application': True,
'installable': True,

'data': [
'security/ir.model.access.csv',

# Be careful with the order!
Comment thread
SebVde marked this conversation as resolved.
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/res_users_views.xml',
'views/estate_menus.xml',
],
'author': "Sevan Corp.",
}
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_users
118 changes: 118 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta

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


class Property(models.Model):
# Model definition
_name = "estate.property"
_description = "Estate Property"
_order = "id desc"
_check_expected_price = models.Constraint(
"CHECK(expected_price > 0)",
"The expected price should be strictly positive.",
)

# Fields
name = fields.Char("Title", required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
"Available From",
default=datetime.today() + relativedelta(months=3),
copy=False,
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(
readonly=True,
copy=False,
)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer("Living Area (sqm)")
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer("Garden Area (sqm)")
garden_orientation = fields.Selection(
string="Orientation",
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
)
total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area")
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
buyer_id = fields.Many2one("res.partner", copy=False)
salesman_id = fields.Many2one("res.users", default=lambda self: self.env.user)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id")
best_price = fields.Float(compute="_compute_best_price")
active = fields.Boolean(default=True)
state = fields.Selection(
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
required=True,
copy=False,
default="new",
store=True,
compute="_compute_state",
)
Comment thread
SebVde marked this conversation as resolved.

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

@api.depends("offer_ids")
def _compute_best_price(self):
for record in self:
record.best_price = max(record.offer_ids.mapped("price")) if record.offer_ids else 0.0
Comment thread
SebVde marked this conversation as resolved.

@api.depends("offer_ids")
def _compute_state(self):
for record in self:
if (record.state == "new" and len(record.offer_ids) > 0):
record.state = "offer_received"

@api.constrains("expected_price", "selling_price")
def _check_selling_price_to_expected_price_ratio(self):
for record in self:
if (not float_is_zero(record.selling_price, precision_digits=0) and
float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=0) == -1):
raise ValidationError("The selling price should be at least 90% of the expected price.")

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

@api.ondelete(at_uninstall=False)
def _unlink(self):
for record in self:
if record.state in ("new", "cancelled"):
raise UserError("New or cancelled properties cannot be deleted.")
return super().unlink()

def action_cancel_property(self):
if self.state == "sold":
raise UserError("Sold properties cannot be cancelled.")
return self.write({
"state": "cancelled",
})

def action_sold_property(self):
if self.state == "cancelled":
raise UserError("Cancelled properties cannot be sold.")
return self.write({
"state": "sold",
})
78 changes: 78 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta

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


class PropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer"
_order = "price desc"
_check_price = models.Constraint(
'CHECK(price > 0.00)',
"The offer's amount should be strictly positive.",
)

price = fields.Float()
status = fields.Selection(
selection=[
("accepted", "Accepted"),
("refused", "Refused"),
],
copy=False,
)
partner_id = fields.Many2one(
"res.partner",
string="Partner",
required=True,
)
property_id = fields.Many2one(
"estate.property",
string="Property",
required=True,
)
validity = fields.Integer("Validity (days)", default=7)
date_deadline = fields.Date("Deadline", compute="_compute_deadline", inverse="_inverse_deadline")
property_type_id = fields.Many2one("estate.property.type", store=True, related="property_id.property_type_id")

@api.depends("validity")
def _compute_deadline(self):
for record in self:
date = datetime.today() if not record.create_date else record.create_date.date()
record.date_deadline = date + relativedelta(days=record.validity)

def _inverse_deadline(self):
for record in self:
date = datetime.today() if not record.create_date else record.create_date.date()
record.validity = (record.date_deadline - date).days

@api.model
def create(self, vals_list):
for vals_dict in vals_list:
property_id = self.env["estate.property"].browse(vals_dict["property_id"])
offers = property_id.offer_ids
if (len(offers) > 0 and vals_dict["price"] < min(offers.mapped("price"))):
raise UserError("Offer with a lower value than an existing offer cannot be created.")
return super().create(vals_list)

def action_accept_offer(self):
for record in self:
if "accepted" in record.mapped("property_id.offer_ids.status"):
raise UserError("Only one offer can be accepted")
if (record.property_id.garden_orientation == "south" and
float_compare(record.property_id.expected_price, record.price, 2) == 1):
raise ValidationError("For properties with South oriented gardens, only offers having a price higher than the expected value of the property can be accepted")
return self.status.write({
"status": "accepted",
}) and self.property_id.write({
"state": "offer_accepted",
"selling_price": record.price,
"buyer_id": record.partner_id,
})

def action_refuse_offer(self):
return self.write({
"status": "refused",
})
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class PropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag"
_order = "name"

name = fields.Char(required=True)
color = fields.Integer()

_check_tag_name_unique = models.Constraint(
'UNIQUE(name)',
'This tag already exists.',
)
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 api, fields, models


class PropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate Property Type"
_order = "sequence, name"

name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "property_type_id")
sequence = fields.Integer("Sequence", default=1)
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer(compute="_compute_offer_count")

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


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

property_ids = fields.One2many(
"estate.property",
"salesman_id",
domain=[("state", "not in", ["sold", "cancelled"])]
)
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,0
estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,0
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,0
estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,0
1 change: 1 addition & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_estate_property
44 changes: 44 additions & 0 deletions estate/tests/test_estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from odoo import Command
from odoo.exceptions import ValidationError
from odoo.tests import TransactionCase


class TestEstateProperty(TransactionCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.estate = cls.env['estate.property'].create({
'name': 'Super test estate',
'expected_price': 100000.0,
'state': 'new',
})
cls.test_partner = cls.env['res.partner'].create({
'name': 'Maman ours',
})

def test_estate_best_price(self):
'''
Ensure best price is correctly updated when an offer is received.
'''
self.assertEqual(self.estate.best_price, 0.0)
self.estate.offer_ids = [Command.create({
'price': 125000.0,
'partner_id': self.test_partner.id,
})]
self.assertEqual(self.estate.best_price, 125000.0)

def test_accept_offer_south_facing_garden(self):
'''
Ensure offers for estates with south-facing gardens can only be accepted if above expected
price.
'''
self.estate.garden = True
self.estate.garden_orientation = 'south'
self.estate.expected_price = 500000
self.estate.offer_ids = [Command.create({
'price': 475000.0,
'partner_id': self.test_partner.id,
})]
with self.assertRaises(ValidationError):
self.estate.offer_ids.action_accept_offer()
12 changes: 12 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<odoo>
<menuitem id="estate_menu_root" name="Sevan Estate">
<menuitem id="estate_first_level_menu" name="Advertisements">
<menuitem id="estate_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings_menu" name="Settings">
<menuitem id="estate_property_type_menu_action" action="estate_property_type_action"/>
<menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
46 changes: 46 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_view_list" model="ir.ui.view">
<field name="name">estate.property.offer.view.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list editable="bottom" string="offers" decoration-success="status == 'accepted'"
decoration-danger="status == 'refused'">
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept_offer" string="Accept" type="object"
icon="fa-check" invisible="status"/>
<button name="action_refuse_offer" string="Refuse" type="object"
icon="fa-times" invisible="status"/>
</list>
</field>
</record>

<record id="estate_property_offer_view_form" model="ir.ui.view">
<field name="name">estate.property.offer.view.form</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
</group>
</group>
</sheet>
</form>
</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>
<field name="view_mode">list,form</field>
<field name="domain">[('property_type_id', '=', active_id)]</field>
</record>
</odoo>
Loading