Skip to content
Open
Show file tree
Hide file tree
Changes from 20 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
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'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/estate_menus.xml',
],
'author': "Sevan Corp.",
}
4 changes: 4 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
110 changes: 110 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from odoo.tools.float_utils import float_compare, float_is_zero

from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from datetime import datetime
from dateutil.relativedelta import relativedelta
Comment thread
SebVde marked this conversation as resolved.
Outdated


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

# Fields
name = fields.Char('Title', required=True)
Comment thread
SebVde marked this conversation as resolved.
Outdated
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 = fields.Many2one("res.partner", copy=False)
Comment thread
SebVde marked this conversation as resolved.
Outdated
salesman = 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_if_offer',
)

_check_expected_price = models.Constraint(
Comment thread
SebVde marked this conversation as resolved.
Outdated
'CHECK(expected_price > 0)',
'The expected price should be strictly positive.',
)
Comment thread
SebVde marked this conversation as resolved.

@api.constrains('expected_price', 'selling_price')
def _check_selling_price_above_90(self):
Comment thread
SebVde marked this conversation as resolved.
Outdated
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.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_if_offer(self):
Comment thread
SebVde marked this conversation as resolved.
Outdated
for record in self:
if (record.state == 'new' and len(record.offer_ids) > 0):
record.state = 'offer_received'

@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
Comment thread
SebVde marked this conversation as resolved.
Outdated

def action_cancel_property(self):
if self.state == 'sold':
raise UserError("You can't cancel a property that is already sold.")
Comment thread
SebVde marked this conversation as resolved.
Outdated
self.state = 'cancelled'
return True
Comment thread
SebVde marked this conversation as resolved.
Outdated

def action_sold_property(self):
if self.state == 'cancelled':
raise UserError("You can't sell a property that is cancelled.")
self.state = 'sold'
return True
Comment thread
SebVde marked this conversation as resolved.
Outdated
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 odoo import api, fields, models
from odoo.exceptions import UserError
from datetime import datetime
from dateutil.relativedelta import relativedelta


class PropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "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,
)
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")

_check_price = models.Constraint(
Comment thread
SebVde marked this conversation as resolved.
Outdated
'CHECK(price > 0.00)',
"The offer's amount should be strictly positive.",
)

@api.depends("validity")
def _compute_deadline(self):
for record in self:
if not record.create_date:
record.create_date = datetime.today()
record.date_deadline = record.create_date + relativedelta(days=record.validity)
Comment thread
SebVde marked this conversation as resolved.
Outdated

def _inverse_deadline(self):
for record in self:
if not record.create_date:
Comment thread
SebVde marked this conversation as resolved.
Outdated
record.create_date = datetime.today()
record.validity = (record.date_deadline - record.create_date.date()).days

def action_accept_offer(self):
Comment thread
SebVde marked this conversation as resolved.
Outdated
if any(record != self and record.status == 'accepted' for record in self.property_id.offer_ids):
Comment thread
SebVde marked this conversation as resolved.
Outdated
raise UserError("Only one offer can be accepted")
self.status = 'accepted'
self.property_id.buyer = self.partner_id
self.property_id.selling_price = self.price
self.property_id.state = 'offer_accepted'
return True
Comment thread
SebVde marked this conversation as resolved.
Outdated

def action_refuse_offer(self):
for record in self:
if record.status == 'accepted':
record.property_id.buyer = None
Comment thread
SebVde marked this conversation as resolved.
Outdated
record.property_id.selling_price = 0.00
record.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 fields, models, api


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)
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
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.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.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>
32 changes: 32 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list editable="top" string="Tags">
<field name="name"/>
</list>
</field>
</record>

<record id="estate_property_tag_view_form" model="ir.ui.view">
<field name="name">estate.property.tag.form</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Property Tags</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
50 changes: 50 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_type_view_list" model="ir.ui.view">
<field name="name">estate.property.type.list</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<list string="Properties">
<field name="sequence" widget="handle"/>
<field name="name"/>
</list>
</field>
</record>

<record id="estate_property_type_view_form" model="ir.ui.view">
<field name="name">estate.property.type.form</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<form>
<header>
<button class="oe_stat_button" type="action"
name="%(estate.estate_property_offer_action)d" icon="fa-money">
<field name="offer_count" widget="statinfo" string="Offers"/>
</button>
</header>
<sheet>
<h1>
<field name="name"/>
</h1>
<notebook>
<page string="Properties">
<field name="property_ids">
<list>
<field name="name"/>
<field name="expected_price"/>
<field name="state"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

<record id="estate_property_type_action" model="ir.actions.act_window">
<field name="name">Property Types</field>
<field name="res_model">estate.property.type</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
Loading