Skip to content
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

[TRAINING] Getting Started Odoo Tutorial #43

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
29d0fd9
[ADD] estate: create a empty module to start the tutorial
Clement-Cardot Mar 18, 2024
e3b445f
[ADD] estate: Chapter 4, add estate_property model
Clement-Cardot Mar 18, 2024
8006195
[ADD] estate: Chapter 5, add security file to restrict access to the …
Clement-Cardot Mar 18, 2024
9e5e90e
[ADD] estate: Chapter 6, add my first views and menus
Clement-Cardot Mar 18, 2024
34cf2e5
[REF] estate: refactor to match code review comments
Clement-Cardot Mar 19, 2024
fd9931c
[IMP] estate: Chapter 7-Adding Tree, Form and Search Views
Clement-Cardot Mar 19, 2024
e6e7e10
[IMP] estate: Chapter 9-Add Property Types, Tags, Buyer & Salesman
Clement-Cardot Mar 19, 2024
27b61e2
[IMP] estate: Chapter 9-Best Offer, Validity & Garden toogle
Clement-Cardot Mar 19, 2024
aee72ba
[IMP] estate: Chapter 10 Property and Offer Actions
Clement-Cardot Mar 20, 2024
5541c9c
[IMP] estate: Add contraints to amounts in property & offers
Clement-Cardot Mar 20, 2024
dd849b3
[FIX] estate: misc fixs & improvements
Clement-Cardot Mar 20, 2024
6999752
[REF] estate: refactor method to avoid over-anticipation
Clement-Cardot Mar 20, 2024
519a100
[IMP] estate: Chapter 12-Inline Views Widgets List Order & Stat Button
Clement-Cardot Mar 20, 2024
d83235a
[IMP] estate: Chapter 13-Python Model and View Inheritance
Clement-Cardot Mar 21, 2024
22f74cc
[ADD] estate_account: Chapter 14-link module estate and accounting
Clement-Cardot Mar 21, 2024
8974bf8
[IMP] estate: Chapter 15-Add a Kanban view to the properties
Clement-Cardot Mar 22, 2024
aebb728
[FIX] estate: fix two lint warnings
Clement-Cardot Mar 22, 2024
7f634d0
[IMP] awesome_owl: JS Framwork Chapter 1
Clement-Cardot Mar 25, 2024
729b9b9
[FIX] estate: misc fix related to code review
Clement-Cardot Mar 25, 2024
3741739
[FIX] estate: create offers method accept offers from multi properties
Clement-Cardot Mar 25, 2024
ef3d9de
[IMP] awesome_dashboard: Part 1 to 5 of the tutorial
Clement-Cardot Mar 25, 2024
c391011
[IMP] awesome_dashboard: Chapt2 Part 6-add pie chart
Clement-Cardot Mar 26, 2024
486a76b
[FIX] awesome_owl: Apply PR review
Clement-Cardot Mar 26, 2024
9e20b36
[IMP] awesome_dashboard: Chapt2-Part6&7 reactive and lazy
Clement-Cardot Mar 26, 2024
4eba9b9
[IMP] awesome_dashboard: Chapt2-9. make dashboard generic + some fixes
Clement-Cardot Mar 26, 2024
2957dac
[IMP] awesome_dashboard: Chapt2-End - Toogle dashboard item visibility
Clement-Cardot Mar 26, 2024
e34e72e
[FIX] awesome_dashboard: fix pie chart update
Clement-Cardot Mar 28, 2024
701d7db
[IMP] estate: Add some unittests
Clement-Cardot Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
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
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",
'version': '1.0',
'depends': ['base'],
'author': "Clément Cardot (cacl)",
'license': "LGPL-3",
'description': """
A new app to learn the Odoo framework
""",

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

'views/estate_property_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_offer_views.xml',
'views/res_users_views.xml',
'views/estate_menus.xml',
],

'application': True,
}
7 changes: 7 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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

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

DEFAULT_GARDEN_AREA = 10


class EstateProperty(models.Model):

def _get_default_date_availability(self):
return fields.Date.today() + relativedelta(months=3)

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

name = fields.Char(string="Title", required=True)
description = fields.Text()
active = fields.Boolean(default=True)
state = fields.Selection([
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('canceled', 'Canceled')
], default='new', required=True, copy=False)

tag_ids = fields.Many2many('estate.property.tag', string="Tags")
property_type_id = fields.Many2one('estate.property.type', string="Property Type")
salesman_id = fields.Many2one('res.users', string="Salesman", default=lambda self: self.env.user)
buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False)
offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers")

postcode = fields.Char(string="Postcode")
date_availability = fields.Date(string="Available From", default=_get_default_date_availability, copy=False)
expected_price = fields.Float(string="Expected Price", required=True)
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)

bedrooms = fields.Integer(string="Bedrooms", default=2)
living_area = fields.Integer(string="Living Area (sqm)")
facades = fields.Integer(string="Facades")
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Integer()
garden_orientation = fields.Selection([
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West')
])

total_area = fields.Integer(string="Total Area (sqm)", compute="_compute_total_area")
best_price = fields.Float(string="Best Offer", compute="_compute_best_price")

_sql_constraints = [
('check_expected_price_positive', 'CHECK(expected_price > 0)', 'The expected price must be strictly positive'),
('check_selling_price_positive', 'CHECK(selling_price >= 0)', 'The selling price must be positive')
]

@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.price')
def _compute_best_price(self):
for record in self:
record.best_price = max(record.mapped('offer_ids.price'), default=0)

@api.onchange('garden')
def _onchange_garden(self):
if not self.garden:
self.garden_area = 0
self.garden_orientation = False
else:
self.garden_area = DEFAULT_GARDEN_AREA
self.garden_orientation = 'north'

def action_cancel(self):
self.ensure_one()

if self.state == 'sold':
raise exceptions.UserError("Sold properties cannot be canceled!")

return self.write({'state': 'canceled'})

def action_sold(self):
self.ensure_one()

if self.state == 'canceled':
raise exceptions.UserError("Canceled properties cannot be sold!")

return self.write({'state': 'sold'})

def accept_offer(self, price, partner_id):
self.ensure_one()
self.selling_price = price
self.buyer_id = partner_id
Clement-Cardot marked this conversation as resolved.
Show resolved Hide resolved

@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for record in self:
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) == -1:
raise exceptions.ValidationError("The selling price cannot be lower than 90% of the expected price.")

@api.ondelete(at_uninstall=False)
def _unlink_only_new_or_sold(self):
for record in self:
if record.state not in ['new', 'sold']:
raise exceptions.UserError("You cannot delete a property that is not new or sold.")
Clement-Cardot marked this conversation as resolved.
Show resolved Hide resolved
return super()
68 changes: 68 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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

class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Real Estate Property Offer"
_order = "price desc"

price = fields.Float()
status = fields.Selection([
('accepted', 'Accepted'),
('refused', 'Refused'),
], copy=False)

partner_id = fields.Many2one('res.partner', required=True)
property_id = fields.Many2one('estate.property', required=True)
property_type_id = fields.Many2one(related='property_id.property_type_id', store=True)

validity = fields.Integer(string="Offer Validity (days)", default=7)
date_deadline = fields.Date(string="Deadline", compute="_compute_deadline_date", inverse="_inverse_deadline_date")

_sql_constraints = [
('check_price_positive', 'CHECK(price > 0)', 'The price must be strictly positive.')
]

@api.depends('validity', 'create_date')
def _compute_deadline_date(self):
for record in self:
if record.create_date:
record.date_deadline = fields.Date.to_date(record.create_date) + relativedelta(days=record.validity)
else:
record.date_deadline = fields.Date.today() + relativedelta(days=record.validity)

def _inverse_deadline_date(self):
for record in self:
if record.create_date:
record.validity = (record.date_deadline - fields.Date.to_date(record.create_date)).days
else:
record.validity = (record.date_deadline - fields.Date.today()).days

def action_accept(self):
self.ensure_one()

if self.property_id.offer_ids.filtered(lambda r: r.status == 'accepted'):
raise exceptions.UserError("Another offer is already accepted on this property.")

self.property_id.accept_offer(self.price, self.partner_id)
return self.write({'status': 'accepted'})

def action_refuse(self):
self.ensure_one()
return self.write({'status': 'refused'})

@api.model_create_multi
def create(self, values):
best_price = self.env['estate.property'].browse(values[0]['property_id']).best_price
Clement-Cardot marked this conversation as resolved.
Show resolved Hide resolved
records = super().create(values)

for record in records:
if float_compare(record.price, best_price, 2) == -1:
raise exceptions.UserError("The price of your offer should not be lower than the best offer.")

record.property_id.state = 'offer_received'

return 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 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
Clement-Cardot marked this conversation as resolved.
Show resolved Hide resolved

from odoo import fields, models

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

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

_sql_constraints = [
('name_unique', 'unique (name)', "Tag name already exists!"),
]
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 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models

class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type"
_order = "name"
sequence = fields.Integer('Sequence', default=1, help="used to order property types")

name = fields.Char(required=True)
property_ids = fields.One2many('estate.property', 'property_type_id', string="Property Types")
offers_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers")
offer_count = fields.Integer(string="Nb. Offers", compute="_compute_offer_count")

_sql_constraints = [
('name_unique', 'unique (name)', "Type name already exists!"),
]

@api.depends('offers_ids')
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offers_ids)
8 changes: 8 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import fields, models

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

property_ids = fields.One2many('estate.property', 'salesman_id', domain=[('state', 'in', ('new', 'offer_received'))], string="Properties")
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
14 changes: 14 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<odoo>

<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_advertisement_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" name="Properties" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings_menu" name="Settings">
<menuitem id="estate_property_type_menu_action" name="Property Types" action="estate_property_type_action"/>
<menuitem id="estate_property_tag_menu_action" name="Property Tags" action="estate_property_tag_action"/>
</menuitem>
</menuitem>

</odoo>
43 changes: 43 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_action" model="ir.actions.act_window">
Clement-Cardot marked this conversation as resolved.
Show resolved Hide resolved
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('property_type_id', '=', active_id)]</field>
</record>

<record id="estate_property_offer_view_tree" model="ir.ui.view">
<field name="name">estate.property.offer.tree</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<tree editable="bottom" 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" type="object" icon="fa-check" invisible="status"/>
<button name="action_refuse" type="object" icon="fa-times" invisible="status"/>
<field name="status" column_invisible="True"/>
</tree>
</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>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<field name="status"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
21 changes: 21 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<odoo>

<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">tree,form</field>
</record>

<record id="estate_property_tag_view_tree" model="ir.ui.view">
<field name="name">estate.property.tag.tree</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="name"/>
<field name="color"/>
</tree>
</field>
</record>

</odoo>