-
Notifications
You must be signed in to change notification settings - Fork 2.6k
trcaz - Industry onboarding #986
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
base: 19.0
Are you sure you want to change the base?
Changes from all commits
ce16fab
d31e6b3
d1be6e1
e065871
6b6d7c3
0c9993b
b043d1a
c3bed90
a127540
002dfba
44f4e3e
c0f4786
193323b
244c35d
a07d26c
1e82b73
bdb9f65
6def407
0b05411
a5a4058
8cb6ed6
f368391
94f4a3c
9bc6f35
3e89bd5
bad506f
47d5056
4348d79
d9cc923
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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', | ||
], | ||
} |
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",,,, |
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> | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. default value, not necessary
Suggested change
|
||||
<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> |
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> |
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, | ||
) |
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'")) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We try to have less technical messages for end-users, something like
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also note that |
||||||
|
||||||
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.")) | ||||||
Comment on lines
+85
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it too constraining the end user? You probably have specific cases where you want to bypass the "normal" flow and such conditions could be a burden for some users There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that was a requirement in the tutorial |
||||||
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 |
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 | ||||||||||
Comment on lines
+3
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
from odoo.tools.float_utils import float_compare | ||||||||||
|
||||||||||
|
||||||||||
class PropertyOffer(models.Model): | ||||||||||
_name = 'estate.property.offer' | ||||||||||
_description = "Test description for estate.property.offer model" | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👀 |
||||||||||
_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') | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if the owner changes its expected_price? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand |
||||||||||
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")) | ||||||||||
Comment on lines
+59
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a duplicate of lines 39-43, no? |
||||||||||
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 |
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Try to add random values such that every new tag has a different color (see example in codebase) |
||
|
||
_check_name = models.Constraint( | ||
'UNIQUE (name)', | ||
"Property tag name must be unique", | ||
) |
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) |
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') |
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 |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I bet you don't have to list all fields, only the one you define (and the required ones, obviously)