-
Notifications
You must be signed in to change notification settings - Fork 3.3k
19.0: Onboarding bepro #1365
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?
19.0: Onboarding bepro #1365
Changes from all commits
f5b4ede
5b8acd1
999d421
69c5e66
3cbf95a
00df62a
2beb69a
a7e7ef5
47cb955
7f6bc8e
3351716
5a2cd9f
3857fad
a1f01a6
ed07c6e
dab808c
64cd738
f978886
e0e121b
e0adc89
8dbc9bf
5067f4a
e21d2c5
f78dfb1
fb392c4
2ed2cb7
6ec1592
bbb358e
aa77f80
7d298fc
50b73e4
c6f70fa
fac26de
6d89bae
f21a3e7
3768c17
64d049a
bccf94a
0de649d
118285d
f7f80be
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,15 @@ | ||
| { | ||
| 'name': 'Real Estate', | ||
| 'depends': ['base'], | ||
| 'author': 'Odoo S.A.', | ||
| 'license': 'LGPL-3', | ||
| 'data': [ | ||
| 'security/ir.model.access.csv', | ||
| 'views/estate_property_views.xml', | ||
| 'views/estate_property_offer_views.xml', | ||
| 'views/estate_property_tag_views.xml', | ||
| 'views/estate_property_type_views.xml', | ||
| 'views/estate_menus.xml', | ||
| 'views/res_user_views.xml', | ||
| ], | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import property | ||
| from . import property_offer | ||
| from . import property_tag | ||
| from . import property_type | ||
| from . import res_users |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||||||||
| from odoo import _, api, exceptions, fields, models | ||||||||||||
| from odoo.tools.float_utils import float_compare, float_is_zero | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class Property(models.Model): | ||||||||||||
| _name = "estate.property" | ||||||||||||
| _description = "Properties of our managed estates" | ||||||||||||
| _order = "id desc" | ||||||||||||
|
|
||||||||||||
| name = fields.Char(string='Title', required=True) | ||||||||||||
| active = fields.Boolean(default=True) | ||||||||||||
| state = fields.Selection(required=True, default='new', copy=False, selection=[ | ||||||||||||
| ('new', 'New'), | ||||||||||||
| ('offer_received', 'Offer Received'), | ||||||||||||
| ('offer_accepted', 'Offer Accepted'), | ||||||||||||
| ('sold', 'Sold'), | ||||||||||||
| ('cancelled', 'Cancelled'), | ||||||||||||
| ]) | ||||||||||||
| 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) | ||||||||||||
| tag_ids = fields.Many2many("estate.property.tag") | ||||||||||||
| offer_ids = fields.One2many("estate.property.offer", "property_id") | ||||||||||||
| description = fields.Text(string='description') | ||||||||||||
| postcode = fields.Char(string='postcode') | ||||||||||||
| date_availability = fields.Date(string='Available from', default=lambda _: fields.Date.add(fields.Date.today(), months=3), copy=False) | ||||||||||||
|
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
We don't need the lambda here. We could just put the value as it is.
Author
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 tested the approach without lambda and fieldtype datetime. The form value is stuck at a datetime around startup of odoo server, meaning that the default value is generated once and reused as default. The purpose of the field is to have a date set 3 months into the future at point of record creation, implying the date must be calculated at that point in time, hence the lambda is required. Info @SaddemAmine |
||||||||||||
| 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 size') | ||||||||||||
| facades = fields.Integer(string='# facades') | ||||||||||||
| garage = fields.Boolean(string='Has garage') | ||||||||||||
| garden = fields.Boolean(string='Has garden') | ||||||||||||
| garden_area = fields.Integer(string='garden area size') | ||||||||||||
| garden_orientation = fields.Selection(string='garden orientation', | ||||||||||||
| selection=[ | ||||||||||||
| ('north', 'North'), | ||||||||||||
| ('south', 'South'), | ||||||||||||
| ('east', 'East'), | ||||||||||||
| ('west', 'West'), | ||||||||||||
| ]) | ||||||||||||
| total_area = fields.Float(string="Total area", compute="_compute_total_area") | ||||||||||||
| best_price = fields.Float(string="Best offer", compute="_compute_best_price") | ||||||||||||
|
|
||||||||||||
| _check_positive_expected_price = models.Constraint( | ||||||||||||
| 'CHECK (expected_price >= 0)', 'Expected price must be positive!') | ||||||||||||
| _check_positive_selling_price = models.Constraint( | ||||||||||||
| 'CHECK (selling_price >= 0)', 'Selling price must be positive!') | ||||||||||||
| _check_positive_living_area = models.Constraint( | ||||||||||||
| 'CHECK (living_area >= 0)', 'Living area must be positive!') | ||||||||||||
| _check_positive_amounts = models.Constraint( | ||||||||||||
| 'CHECK (facades >= 0)', 'Number of facades must be positive!') | ||||||||||||
| _check_positive_garden_area = models.Constraint( | ||||||||||||
| 'CHECK (garden_area >= 0)', 'Garden area 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: | ||||||||||||
| if record.offer_ids: | ||||||||||||
| record.best_price = max(record.offer_ids.mapped("price")) | ||||||||||||
| else: | ||||||||||||
| record.best_price = 0 | ||||||||||||
|
Comment on lines
+64
to
+67
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
Just a suggestion, feel free to ignore it if you want 👌
Author
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. ignored |
||||||||||||
|
|
||||||||||||
| @api.constrains('selling_price', 'expected_price') | ||||||||||||
| def _check_selling_price(self): | ||||||||||||
| for property in self: | ||||||||||||
| if float_is_zero(property.selling_price, 2): | ||||||||||||
| return | ||||||||||||
| if float_compare(property.selling_price, 0.9 * property.expected_price, 2) == -1: | ||||||||||||
| raise exceptions.ValidationError(_("The accepted price is less than 90% of the expected price!")) | ||||||||||||
|
|
||||||||||||
| @api.onchange("garden") | ||||||||||||
| def _onchange_garden(self): | ||||||||||||
| for record in self: | ||||||||||||
| record.garden_area = 10 if record.garden else 0 | ||||||||||||
| record.garden_orientation = 'north' if record.garden else False | ||||||||||||
|
|
||||||||||||
| @api.ondelete(at_uninstall=False) | ||||||||||||
| def _unlink_if_draft(self): | ||||||||||||
| for record in self: | ||||||||||||
| if record.state not in ['new', 'cancelled']: | ||||||||||||
| raise exceptions.ValidationError(_("Only properties in state New or Cancelled can be deleted!")) | ||||||||||||
|
|
||||||||||||
| def action_sold(self): | ||||||||||||
| for record in self: | ||||||||||||
| if record.state == 'cancelled': | ||||||||||||
| raise exceptions.UserError(_("Cancelled properties cannot be sold!")) | ||||||||||||
|
|
||||||||||||
| record.state = "sold" | ||||||||||||
| return True | ||||||||||||
|
|
||||||||||||
| def action_cancelled(self): | ||||||||||||
| for record in self: | ||||||||||||
| if record.state == "sold": | ||||||||||||
| raise exceptions.UserError(_("Sold properties cannot be cancelled!")) | ||||||||||||
|
|
||||||||||||
| record.state = "cancelled" | ||||||||||||
| return True | ||||||||||||
|
|
||||||||||||
| def confirm_offer(self): | ||||||||||||
| for property in self: | ||||||||||||
| accepted_offer = property.offer_ids.filtered(lambda r: r.status == 'accepted') | ||||||||||||
| accepted_offer.ensure_one() | ||||||||||||
| property.selling_price = accepted_offer.price | ||||||||||||
| property.buyer_id = accepted_offer.partner_id | ||||||||||||
| property.state = 'offer_accepted' | ||||||||||||
| # Refuse all other offers | ||||||||||||
| (property.offer_ids - accepted_offer).action_cancel() | ||||||||||||
|
|
||||||||||||
| def _set_offer_received(self): | ||||||||||||
| self.ensure_one() | ||||||||||||
| if self.state == 'new': | ||||||||||||
| self.state = 'offer_received' | ||||||||||||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,76 @@ | ||||
| import datetime as dt | ||||
| from math import floor | ||||
|
|
||||
| from odoo import _, api, fields, models | ||||
| from odoo.exceptions import UserError, ValidationError | ||||
|
|
||||
|
|
||||
| class PropertyOffer(models.Model): | ||||
| _name = "estate.property.offer" | ||||
| _description = "Bids for a property" | ||||
| _order = "price desc" | ||||
|
|
||||
| price = fields.Float(string="Price", required=True) | ||||
| status = fields.Selection(string="Status", copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) | ||||
| 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="Validity of offer", default=7) | ||||
| date_deadline = fields.Date(string="Offer expiry", compute="_compute_date_deadline", inverse="_inverse_date_deadline") | ||||
|
|
||||
| _check_positive_price = models.Constraint('CHECK(price >= 0)', 'Price has to be positive') | ||||
|
|
||||
| @api.depends("create_date", "validity") | ||||
| def _compute_date_deadline(self): | ||||
| for record in self: | ||||
| record.date_deadline = fields.Date.add(record.create_date or fields.Datetime.now(), days=record.validity) | ||||
|
|
||||
| @api.onchange('date_deadline') | ||||
| def _inverse_date_deadline(self): | ||||
| for record in self: | ||||
| if record.date_deadline: | ||||
| raw_difference = record.date_deadline - (record.create_date or fields.Datetime.now()).date() | ||||
| difference = raw_difference.total_seconds() | ||||
| if difference > 0: | ||||
| record.validity = floor(difference / dt.timedelta(days=1).total_seconds()) | ||||
|
|
||||
| @api.constrains('status') | ||||
| def _check_maximum_one_offer_accepted(self): | ||||
| accepted_offers = self.filtered(lambda r: r.status == "accepted") | ||||
| properties = accepted_offers.mapped('property_id') | ||||
| for property in properties: | ||||
| peer_offers = property.offer_ids | ||||
| accepted_peers = peer_offers.filtered(lambda r: r.status == 'accepted') | ||||
| if len(accepted_peers) > 1: | ||||
| raise ValidationError(_("A single offer can be accepted at a time!")) | ||||
|
|
||||
| @api.model_create_multi | ||||
| def create(self, vals_list): | ||||
| property_ids = self.env["estate.property"].browse(vals["property_id"] for vals in vals_list) | ||||
| # Precondition: price and property_id are required fields | ||||
| for vals in vals_list: | ||||
| property = property_ids.filtered(lambda p: p.id == vals["property_id"]) | ||||
| property.ensure_one() | ||||
|
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
Since the id is unique, there will only be at most one property matching
Author
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. kept for reason that filter is constructed with untrusted user input and property could contain empty recordset |
||||
| # Hook-approach (for composability) | ||||
| property._set_offer_received() | ||||
| if property.best_price and property.best_price > vals['price']: | ||||
| raise UserError(_("New offer price must be higher than those of pre-existing offers!")) | ||||
|
|
||||
| return super().create(vals_list) | ||||
|
|
||||
| def action_confirm(self): | ||||
| for record in self: | ||||
| if record.status == "refused": | ||||
| raise UserError(_("Offer is already refused!")) | ||||
|
YassinWalid marked this conversation as resolved.
|
||||
|
|
||||
| record.status = 'accepted' | ||||
| record.property_id.confirm_offer() | ||||
| return True | ||||
|
|
||||
| def action_cancel(self): | ||||
| for record in self: | ||||
| if record.status == "accepted": | ||||
| raise UserError(_("Offer is already accepted!")) | ||||
|
YassinWalid marked this conversation as resolved.
|
||||
|
|
||||
| record.status = "refused" | ||||
| return True | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class PropertyTag(models.Model): | ||
| _name = "estate.property.tag" | ||
| _description = "Tag assigned to property" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(string='Name', required=True) | ||
| color = fields.Integer(string='Color') | ||
|
|
||
| _name_idx = models.UniqueIndex('(name)', 'Another record already exists with the same name!') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
|
|
||
| class PropertyType(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "Type of property" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(string='Name', required=True) | ||
| sequence = fields.Integer('Sequence', default=1) | ||
| 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") | ||
|
|
||
| _name_idx = models.UniqueIndex('(name)', 'Another record already exists with the same name!') | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_offer_count(self): | ||
| for record in self: | ||
| record.offer_count = len(record.offer_ids) |
|
bepro-odoo marked this conversation as resolved.
|
| 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", "salesman_id", domain="[('state', 'in', ['new', 'offer_received'])]") |
|
bepro-odoo marked this conversation as resolved.
|
| 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_model,access.estate.property.model,estate.model_estate_property,base.group_user,1,1,1,1 | ||
| access_estate_property_type_model,access.estate.property.type.model,estate.model_estate_property_type,base.group_user,1,1,1,1 | ||
| access_estate_property_tag_model,access.estate.property.tag.model,estate.model_estate_property_tag,base.group_user,1,1,1,1 | ||
| access_estate_property_offer_model,access.estate.property.offer.model,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,14 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="estate_menu_root" name="Estate"> | ||
| <menuitem id="estate_menu_advertisements" name="Advertisements"> | ||
| <menuitem id="estate_property_menu" action="estate_property_action"/> | ||
| </menuitem> | ||
|
|
||
| <menuitem id="estate_menu_settings" name="Settings"> | ||
| <menuitem id="estate_property_type_menu" action="estate_property_type_action"/> | ||
| <menuitem id="estate_property_tag_menu" action="estate_property_tag_action"/> | ||
| </menuitem> | ||
| </menuitem> | ||
|
|
||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| <?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="view_mode">list,form</field> | ||
| <field name="domain">[('property_type_id', '=', active_id)]</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="Property Offers" editable="top" | ||
| decoration-danger="status in ['refused']" | ||
| decoration-success="status in ['accepted']"> | ||
| <field name="price" /> | ||
| <field name="partner_id" /> | ||
| <field name="validity" /> | ||
| <field name="date_deadline" /> | ||
| <button name="action_confirm" string="Confirm" type="object" icon="fa-check" invisible="status"/> | ||
| <button name="action_cancel" string="Cancel" 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 string="Estate Property"> | ||
| <sheet> | ||
| <group> | ||
| <group name="general"> | ||
| <field name="price" /> | ||
| <field name="partner_id" /> | ||
| <field name="validity" /> | ||
| <field name="date_deadline" /> | ||
| <field name="status" /> | ||
| </group> | ||
| </group> | ||
| </sheet> | ||
| </form> | ||
| </field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <?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="Property Tags" editable="top"> | ||
| <field name="name" /> | ||
| </list> | ||
| </field> | ||
| </record> | ||
| </odoo> |
Uh oh!
There was an error while loading. Please reload this page.