-
Notifications
You must be signed in to change notification settings - Fork 2.8k
leker - Technical training #1063
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: master
Are you sure you want to change the base?
Changes from all commits
1992796
f693dde
d0aab2d
604a7d8
e053b79
fb64396
83cb88b
dc21eba
681b28f
11a7ef1
304c0e3
79267f5
c330331
460268b
99f9309
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 @@ | ||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| 'name': 'Real Estate', | ||
| 'depends': ['base'], | ||
| 'data': [ | ||
| 'security/ir.model.access.csv', | ||
| 'views/base_res_users_views.xml', | ||
| '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', | ||
| ], | ||
| 'application': True, | ||
| 'author': 'Odoo S.A.', | ||
| 'license': 'LGPL-3', | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import estate_property | ||
| from . import estate_property_offer | ||
| from . import estate_property_tag | ||
| from . import estate_property_type | ||
| from . import base_res_users | ||
| 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', domain="['|', ('state', '=', 'new'), ('state', '=', 'received offer')]") |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,100 @@ | ||||||||
| from odoo import api, fields, models | ||||||||
| from odoo.exceptions import UserError, ValidationError | ||||||||
| from odoo.tools.float_utils import float_compare | ||||||||
|
|
||||||||
|
|
||||||||
| class Property(models.Model): | ||||||||
| _name = "estate.property" | ||||||||
| _description = "Real Estate property" | ||||||||
| _order = "id desc" | ||||||||
|
|
||||||||
| name = fields.Char('Title', required=True, default='Unknown') | ||||||||
| active = fields.Boolean('Active', default=True) | ||||||||
| state = fields.Selection([ | ||||||||
| ('new', 'New'), | ||||||||
| ('received offer', 'Received offer'), | ||||||||
| ('offer accepted', 'Offer Accepted'), | ||||||||
| ('sold', 'Sold'), | ||||||||
| ('cancelled', 'Cancelled'), | ||||||||
| ], string='Status', required=True, copy=False, default='new') | ||||||||
| description = fields.Text('Description') | ||||||||
| last_seen = fields.Datetime("Last Seen", default=lambda self: fields.Datetime.now()) | ||||||||
| postcode = fields.Char('Postcode') | ||||||||
| date_availability = fields.Date('Available from', copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3)) | ||||||||
| expected_price = fields.Float('Expected price', required=True) | ||||||||
| selling_price = fields.Float('Selling price', readonly=True, copy=False) | ||||||||
| bedrooms = fields.Integer('Bedrooms', default=2) | ||||||||
| living_area = fields.Integer('Living area (sqm)') | ||||||||
| facades = fields.Integer('Facades') | ||||||||
| garage = fields.Boolean('Garage') | ||||||||
| garden = fields.Boolean('Garden') | ||||||||
| garden_area = fields.Integer('Garden area (sqm)') | ||||||||
| garden_orientation = fields.Selection([ | ||||||||
| ('north', 'North'), | ||||||||
| ('south', 'South'), | ||||||||
| ('east', 'East'), | ||||||||
| ('west', 'West'), | ||||||||
| ], string='Garden Orientation') | ||||||||
| total_area = fields.Integer(compute="_compute_total_area") | ||||||||
| property_type_id = fields.Many2one('estate.property.type', string="Property Type") | ||||||||
| salesperson_id = fields.Many2one('res.users', string="Salesperson", default=lambda self: self.env.user) | ||||||||
| buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False) | ||||||||
| tag_ids = fields.Many2many('estate.property.tag', string="Tags") | ||||||||
| offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") | ||||||||
| best_price = fields.Float(compute="_compute_best_price") | ||||||||
|
|
||||||||
| # Functions | ||||||||
| @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: | ||||||||
| if record.offer_ids: | ||||||||
| record.best_price = max(record.offer_ids.mapped('price')) | ||||||||
| else: | ||||||||
| record.best_price = 0 | ||||||||
|
|
||||||||
| @api.onchange('garden') | ||||||||
| def _onchange_partner(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_if_property_new_or_cancelled(self): | ||||||||
| for record in self: | ||||||||
| if record.state not in ['new', 'cancelled']: | ||||||||
| raise UserError(f"Can\'t delete property '{record.name}' if state is not New or Cancelled") | ||||||||
|
|
||||||||
| def action_sell_property(self): | ||||||||
| for record in self: | ||||||||
| if record.state == 'cancelled': | ||||||||
| msg = "Cancelled properties cannot be sold." | ||||||||
| raise UserError(msg) | ||||||||
| record.state = 'sold' | ||||||||
| return True | ||||||||
|
|
||||||||
| def action_cancel_property(self): | ||||||||
| for record in self: | ||||||||
| if record.state == 'sold': | ||||||||
| msg = "Sold properties cannot be cancelled." | ||||||||
| raise UserError(msg) | ||||||||
| record.state = 'cancelled' | ||||||||
| return True | ||||||||
|
|
||||||||
| # Constraints | ||||||||
| _check_expected_price = models.Constraint('CHECK (expected_price > 0)', "A property expected price must be strictly positive") | ||||||||
| _check_selling_price = models.Constraint('CHECK (selling_price >= 0)', "A property selling price must be positive") | ||||||||
|
|
||||||||
| @api.constrains('selling_price', 'expected_price') | ||||||||
| def _check_selling_price_percentage(self): | ||||||||
| for record in self: | ||||||||
| if record.state == 'offer accepted' and float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=9) == -1: | ||||||||
| msg = "The selling price cannot be lower than 90% of the expected price" | ||||||||
| raise ValidationError(msg) | ||||||||
|
Comment on lines
+99
to
+100
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 believe error with raised here in the runbot when you have the string directly. this because the Runbot expect a translation object as error msg. we should discuss this together 🫡
Suggested change
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError | ||
|
|
||
|
|
||
| class PropertyOffer(models.Model): | ||
| _name = "estate.property.offer" | ||
| _description = "Real Estate Property Offer" | ||
| _order = "price desc" | ||
|
|
||
| price = fields.Float(name="Price") | ||
| status = fields.Selection([ | ||
| ('accepted', 'Accepted'), | ||
| ('refused', 'Refused'), | ||
| ], string='Status', 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(string="Validity (days)", default=7) | ||
| date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") | ||
| property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) | ||
|
|
||
| # Functions | ||
| @api.depends('create_date', 'validity') | ||
| def _compute_date_deadline(self): | ||
| for record in self: | ||
| if record.create_date: | ||
| record.date_deadline = fields.Date.add(record.create_date, days=record.validity) | ||
| else: | ||
| record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) | ||
|
|
||
| def _inverse_date_deadline(self): | ||
| for record in self: | ||
| record.validity = (record.date_deadline - record.create_date.date()).days | ||
|
|
||
| def action_accept(self): | ||
| for record in self: | ||
| if record.property_id.state == 'offer accepted' or record.property_id.state == 'sold': | ||
| msg = "An offer has already been accepted for this property!" | ||
| raise UserError(msg) | ||
| record.status = 'accepted' | ||
| record.property_id.state = 'offer accepted' | ||
| record.property_id.buyer_id = record.partner_id | ||
| record.property_id.selling_price = record.price | ||
| return True | ||
|
|
||
| def action_refuse(self): | ||
| for record in self: | ||
| record.status = 'refused' | ||
| return True | ||
|
|
||
| @api.model_create_multi | ||
| def create(self, vals): | ||
|
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. as create can be done in batch is should take |
||
| offer = super().create(vals) | ||
| property_record = offer.property_id | ||
| if property_record.state == 'new': | ||
| property_record.state = 'received offer' | ||
| else: | ||
| if offer.price < property_record.best_price: | ||
| msg = "Your offer is lower than an existing offer!" | ||
| raise UserError(msg) | ||
| return offer | ||
|
|
||
| # Constraints | ||
| _check_offer_price = models.Constraint('CHECK (price > 0)', "An offer price must be strictly positive") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class PropertyTag(models.Model): | ||
| _name = "estate.property.tag" | ||
| _description = "Real Estate Property Tag" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(name="Name", required=True) | ||
| color = fields.Integer() | ||
|
|
||
| # Constraints | ||
| _check_unique_name = models.Constraint("UNIQUE(name)", "A 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 = "Real Estate Property Type" | ||
| _order = "sequence, name" | ||
|
|
||
| name = fields.Char(name="Name", required=True) | ||
| sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.") | ||
| property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") | ||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id") | ||
| offer_count = fields.Integer(compute="_compute_offer_count", string="N. of offers") | ||
|
|
||
| # Functions | ||
|
|
||
| @api.depends('offer_ids') | ||
| def _compute_offer_count(self): | ||
| for record in self: | ||
| record.offer_count = len(record.offer_ids) | ||
|
|
||
| # Constraints | ||
| _check_unique_name = models.Constraint("UNIQUE(name)", "A property type name must be unique") |
| 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,1 | ||
| estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 | ||
| estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 | ||
| estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="view_users_form" model="ir.ui.view"> | ||
| <field name="name">res.users.form</field> | ||
| <field name="model">res.users</field> | ||
| <field name="inherit_id" ref="base.view_users_form"/> | ||
| <field name="arch" type="xml"> | ||
| <xpath expr="//notebook" position="inside"> | ||
| <page string="Real Estate Properties" name="estate_property"> | ||
| <field name="property_ids"/> | ||
| </page> | ||
| </xpath> | ||
| </field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="real_estate_menu_root" name="Real Estate"> | ||
| <menuitem id="advertisements_first_level_menu" name="Advertisements"> | ||
| <menuitem id="properties_model_menu_action" action="estate_property_action"/> | ||
| </menuitem> | ||
| <menuitem id="settings_first_level_menu" name="Settings"> | ||
| <menuitem id="property_types_model_menu_action" action="estate_property_type_action"/> | ||
| <menuitem id="property_tags_model_menu_action" action="estate_property_tag_action"/> | ||
| </menuitem> | ||
| </menuitem> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <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="domain">[('property_type_id','=', active_id)]</field> | ||
| <field name="view_mode">list,form</field> | ||
| </record> | ||
|
|
||
| <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 string="Offer" | ||
| editable="bottom" | ||
| decoration-danger="status=='refused'" | ||
| decoration-success="status=='accepted'"> | ||
| <field name="price"/> | ||
| <field name="partner_id"/> | ||
| <field name="validity"/> | ||
| <field name="date_deadline"/> | ||
| <button name="action_accept" type="object" icon="fa-check" title="Accept" invisible="status in ['accepted', 'refused']"/> | ||
| <button name="action_refuse" type="object" icon="fa-times" title="Refuse" invisible="status in ['accepted', 'refused']"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <?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">list,form</field> | ||
| </record> | ||
|
|
||
| <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 string="Tag" editable="bottom"> | ||
| <field name="name"/> | ||
| <field name="color"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <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> | ||
|
|
||
| <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="Property Type"> | ||
| <field name="name"/> | ||
| <field name="sequence" widget="handle"/> | ||
| <field name="offer_count"/> | ||
| </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> | ||
| <button class="oe_stat_button" type="action" name="%(estate.estate_property_offer_action)d" string="Offers" icon="fa-money"/> | ||
| <div class="oe_title"> | ||
| <h1> | ||
| <field name="name"/> | ||
| </h1> | ||
| </div> | ||
| <notebook> | ||
| <page string="Properties"> | ||
| <field name="property_ids"> | ||
| <list> | ||
| <field name="name"/> | ||
| <field name="expected_price"/> | ||
| <field name="state"/> | ||
| </list> | ||
| </field> | ||
| </page> | ||
| </notebook> | ||
| </form> | ||
| </field> | ||
| </record> | ||
| </odoo> |
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.
for the extension inherit using
_inheritwe advice to use same exact file name for file_name, ClassName to ease the process of searching across all the extensions