Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ce16fab
[ADD] estate: copy estate from 19.0-tutorial-trcaz branch
trcazier Sep 15, 2025
d31e6b3
[IMP] estate: add CSV data file for property type
trcazier Oct 1, 2025
d1be6e1
[IMP] estate: add default property data
trcazier Oct 1, 2025
e065871
[IMP] estate: add offer data & change property data ids
trcazier Oct 1, 2025
6b6d7c3
[IMP] estate: renamed property type data id for clarity
trcazier Oct 1, 2025
0c9993b
[FIX] estate: modify data to comply with estate model constraints
trcazier Oct 1, 2025
b043d1a
[FIX] estate: add xml file missing in manifest
trcazier Oct 1, 2025
c3bed90
[IMP] estate: improve 90% offer error message
trcazier Oct 1, 2025
a127540
[IMP] estate: add property type to property data
trcazier Oct 1, 2025
002dfba
[FIX] estate: fix runbot warning for string
trcazier Oct 1, 2025
44f4e3e
[IMP] estate: modify date to be evaluated based on module installatio…
trcazier Oct 1, 2025
c0f4786
[IMP] estate: accept 1 data offer and refuse 2 + fix same id
trcazier Oct 1, 2025
193323b
[IMP] estate: add new property with offers created inside the field
trcazier Oct 1, 2025
244c35d
[FIX] estate: modify _ format parameters for runbot style
trcazier Oct 1, 2025
a07d26c
[ADD] x_estate: add new importable module x_estate
trcazier Oct 2, 2025
1e82b73
[IMP] x_estate: add property_type model, views, menu, and add many2on…
trcazier Oct 2, 2025
bdb9f65
[IMP] x_estate: add property tags model, view, menu and property fiel…
trcazier Oct 2, 2025
6def407
[CLN] x_estate: add empty line at the end of files
trcazier Oct 2, 2025
0b05411
[IMP] x_estate: add offer model, view and add it to property model an…
trcazier Oct 2, 2025
a5a4058
[FIX] x_estate: change offer buyer relation from user to partner
trcazier Oct 3, 2025
8cb6ed6
[IMP] x_estate: add selling price field to property model
trcazier Oct 3, 2025
f368391
[IMP] x_estate: add offer action to accept offer in list view
trcazier Oct 3, 2025
94f4a3c
[IMP] x_estate: add total area computed field to property
trcazier Oct 3, 2025
9bc6f35
[IMP] x_estate: add json route to get properties as a public user
trcazier Oct 3, 2025
3e89bd5
[CLN] x_estate: reduce the number of lines
trcazier Oct 3, 2025
bad506f
[CLN] x_estate: reduce the number of lines
trcazier Oct 3, 2025
47d5056
[CLN] x_estate: add empty line at the end of file
trcazier Oct 3, 2025
4348d79
[IMP] x_estate: remove optional=disabled from date availability
trcazier Oct 3, 2025
d9cc923
[IMP] estate: make test data relative and remove optional
trcazier Oct 6, 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 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
25 changes: 25 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': 'Estate',
'depends': [
'base',
],
'version': '19.0.0.0',
'author': "Odoo S.A.",
'license': "LGPL-3",
'installable': True,
'application': True,
'data': [
'views/res_users_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_views.xml',
'views/estate_menus.xml',
'security/ir.model.access.csv',
'data/estate.property.type.csv',
'data/estate_property_data.xml',
'data/estate_property_offer_data.xml',
],
}
5 changes: 5 additions & 0 deletions estate/data/estate.property.type.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"id","name","sequence","property_ids","offer_ids","offer_count"
property_type_1,"Residential",,,,
property_type_2,"Commercial",,,,
property_type_3,"Industrial",,,,
property_type_4,"Land",,,,
60 changes: 60 additions & 0 deletions estate/data/estate_property_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- the root elements of the data file -->
<odoo>
<record id="property_1" model="estate.property">
<field name="name">Big Villa</field>
<field name="property_type_id" ref="property_type_1"/>
<field name="state">new</field>
<field name="description">A nice and big villa</field>
<field name="postcode">12345</field>
<field name="date_availability" eval="datetime.today().date() - relativedelta(days=1)"/>
<field name="expected_price">1600000</field>
<field name="bedrooms">6</field>
<field name="living_area">100</field>
<field name="facades">4</field>
<field name="garage">True</field>
<field name="garden">True</field>
<field name="garden_area">100000</field>
<field name="garden_orientation">south</field>
</record>

<record id="property_2" model="estate.property">
<field name="name">Trailer Home</field>
<field name="property_type_id" ref="property_type_1"/>
<field name="state">cancelled</field>
<field name="description">Home in a trailer park</field>
<field name="postcode">54321</field>
<field name="date_availability" eval="datetime.today().date() - relativedelta(days=2)"/>
<field name="expected_price">100000</field>
<field name="selling_price">120000</field>
<field name="bedrooms">1</field>
<field name="living_area">10</field>
<field name="facades">4</field>
<field name="garage">False</field>
</record>

<record id="property_3" model="estate.property">
<field name="name">Cozy Cottage</field>
<field name="property_type_id" ref="property_type_1"/>
<field name="state">new</field>
<field name="description">A cozy cottage in the forest</field>
<field name="postcode">98765</field>
<field name="date_availability" eval="datetime.today().date() - relativedelta(days=3)"/>
<field name="expected_price">50000</field>
<field name="bedrooms">1</field>
<field name="living_area">30</field>
<field name="facades">4</field>
<field name="garage">False</field>
<field name="offer_ids" eval="[
Command.create({
'price': 51000,
'partner_id': ref('base.res_partner_2'),
'validity': 14,
}),Command.create({
'price': 52000,
'partner_id': ref('base.res_partner_12'),
'validity': 14,
}),
]"/>
</record>
</odoo>
31 changes: 31 additions & 0 deletions estate/data/estate_property_offer_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- the root elements of the data file -->
<odoo>
<record id="property_offer_1" model="estate.property.offer">
<field name="price">1500000</field>
<field name="partner_id" ref="base.res_partner_12"/>
<field name="property_id" ref="property_1"/>
<field name="date_deadline" eval="(datetime.now() + timedelta(days=14)).date()"/>
</record>

<record id="property_offer_2" model="estate.property.offer">
<field name="price">1600000</field>
<field name="partner_id" ref="base.res_partner_12"/>
<field name="property_id" ref="property_1"/>
<field name="date_deadline" eval="(datetime.now() + timedelta(days=14)).date()"/>
</record>

<record id="property_offer_3" model="estate.property.offer">
<field name="price">1600001</field>
<field name="partner_id" ref="base.res_partner_2"/>
<field name="property_id" ref="property_1"/>
<field name="date_deadline" eval="(datetime.now() + timedelta(days=14)).date()"/>
</record>

<function model="estate.property.offer" name="action_offer_accept">
<value eval="[ref('property_offer_3')]"/>
</function>
<function model="estate.property.offer" name="action_offer_refuse">
<value eval="[ref('property_offer_1'), ref('property_offer_2')]"/>
</function>
</odoo>
9 changes: 9 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import (
estate_property,
estate_property_offer,
estate_property_tag,
estate_property_type,
res_users,
)
90 changes: 90 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from datetime import date, timedelta

from odoo import _, api, fields, models
from odoo.exceptions import UserError


class Property(models.Model):
_name = 'estate.property'
_description = "Test description for estate.property model"
_order = 'id DESC'

name = fields.Char(required=True)
expected_price = fields.Float(required=True)
property_type_id = fields.Many2one('estate.property.type', string="Property Type")
state = fields.Selection(
selection=[('new', "New"), ('offer_received', "Offer Received"), ('offer_accepted', "Offer accepted"), ('sold', "Sold"), ('cancelled', "Cancelled")],
default='new',
)
description = fields.Text()
postcode = fields.Char()
selling_price = fields.Float(copy=False, readonly=True)
date_availability = fields.Date(copy=False, default=lambda self: date.today() + timedelta(days=90))
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
string='Orientation',
selection=[('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")])
active = fields.Boolean(default=True)
buyer_id = fields.Many2one('res.partner')
salesperson_id = fields.Many2one('res.users', copy=False, default=lambda self: self.env.user)
tag_ids = fields.Many2many('estate.property.tag')
offer_ids = fields.One2many('estate.property.offer', 'property_id')
total_area = fields.Float(compute='_compute_total_area')
best_price = fields.Float(compute='_compute_best_price')

_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
"The expected price must be strictly positive",
)
_check_selling_price = models.Constraint(
'CHECK (selling_price >= 0)',
"The selling price must be positive",
)

@api.depends('garden_area', 'living_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(o.price for o in record.offer_ids) if record.offer_ids else 0

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

@api.ondelete(at_uninstall=False)
def _unlink_check_status(self):
for record in self:
if record.state not in ['new', 'cancelled']:
raise UserError(_("A property can only be deleted if its state is 'New' or 'Cancelled'"))

def action_property_cancel(self):
for record in self:
if record.state == 'sold':
raise UserError(_("A sold property cannot be cancelled."))
record.state = 'cancelled'
return True

def action_property_sold(self):
for record in self:
if record.state == 'cancelled':
raise UserError(_("A cancelled property cannot be sold."))
if not record.offer_ids:
raise UserError(_("A property with no offers cannot be sold."))
if not record.buyer_id or not record.best_price > 0 or not any(o for o in record.offer_ids if o.status == "accepted"):
raise UserError(_("A property with no accepted offer cannot be sold"))
record.state = 'sold'
return True
67 changes: 67 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from datetime import datetime, timedelta

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


class PropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = "Test description for estate.property.offer model"
_order = 'price DESC'

price = fields.Float()
status = fields.Selection(
string="Offer Status",
copy=False,
selection=[('accepted', "Accepted"), ('refused', "Refused")])
partner_id = fields.Many2one('res.partner', required=True)
property_id = fields.Many2one('estate.property', required=True)
validity = fields.Integer(default=7)
date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline')
property_type_id = fields.Many2one(related='property_id.property_type_id', store=True)

_check_price = models.Constraint(
'CHECK (price > 0)',
"The price must be strictly positive",
)

@api.depends('validity')
def _compute_date_deadline(self):
for record in self:
# record.create_date is "falsy" so if checking with `record.create_date if hasattr(record.create_date) else datetime.today()` then it's true because it hasattr but it's None so it's converted to false
record.date_deadline = ((record.create_date or datetime.today()) + timedelta(days=record.validity)).date()

def _inverse_date_deadline(self):
for record in self:
record.validity = (record.date_deadline - record.create_date.date()).days

@api.constrains('price')
def _check_selling_price_90_percent(self):
for record in self:
if float_compare(record.price, 0.9 * record.property_id.expected_price, precision_digits=2) == -1:
raise exceptions.UserError(_("The selling price (%s) cannot be lower than 90%% of the expected price (%s)", record.price, record.property_id.expected_price))

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
property = self.env['estate.property'].browse(vals['property_id'])
if property.state == 'sold':
raise UserError(_("Cannot create an offer for a sold property"))
property.state = 'offer_received'
return super().create(vals_list)

@api.depends('property_id', 'property_id.offer_ids')
def action_offer_accept(self):
for record in self:
if any(o.status == 'accepted' for o in record.property_id.offer_ids):
raise exceptions.UserError(_("Cannot accept more than one offer"))
if float_compare(record.price, 0.9 * record.property_id.expected_price, precision_digits=2) == -1:
raise exceptions.UserError(_("The selling price cannot be lower than 90% of the expected price"))
record.status = 'accepted'
record.property_id.buyer_id = record.partner_id
record.property_id.state = 'offer_accepted'
record.property_id.selling_price = record.price

def action_offer_refuse(self):
self.status = 'refused' # assigns the same value to all the records
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 = "Test description for estate.property.tag model"
_order = 'name'

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

_check_name = models.Constraint(
'UNIQUE (name)',
"Property tag name must be unique",
)
23 changes: 23 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from odoo import api, fields, models


class PropertyType(models.Model):
_name = 'estate.property.type'
_description = "Test description for estate.property.type model"
_order = 'name'

name = fields.Char(required=True)
sequence = fields.Integer('Sequence', default=1, help="Used to order types.")
property_ids = fields.One2many('estate.property', 'property_type_id')
offer_ids = fields.One2many('estate.property.offer', 'property_type_id')
offer_count = fields.Integer(compute='_compute_offer_count', default=0)

_check_name = models.Constraint(
'UNIQUE (name)',
"Property type name must be unique",
)

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


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

property_ids = fields.One2many('estate.property', 'salesperson_id')
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_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1
5 changes: 5 additions & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import common
from . import test_estate_property
from . import test_estate_property_offer
Loading