-
Notifications
You must be signed in to change notification settings - Fork 3.1k
[ADD] estate: doing the tutorial #1281
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
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,2 @@ | ||
| from . import models | ||
| from odoo import api, SUPERUSER_ID |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| 'name':"estate", | ||
| 'author':"Odoo S.A.", | ||
| 'licence':"none", | ||
|
|
||
| 'summary': """ | ||
| Houses! | ||
| """, | ||
|
|
||
| 'description': """ | ||
| Lots of Houses!!" | ||
| """, | ||
|
|
||
| 'application': True, | ||
| 'installable': True, | ||
| 'data':[ | ||
| 'security/ir.model.access.csv', | ||
| 'views/estate_property_views.xml', | ||
| 'views/estate_property_types_views.xml', | ||
| 'views/estate_property_tags_views.xml', | ||
| 'views/estate_property_offers_views.xml', | ||
| 'views/estate_menus.xml', | ||
| 'views/estate_users.xml', | ||
| ], | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import est_property | ||
| from . import est_property_offers | ||
| from . import est_property_tags | ||
| from . import est_property_types | ||
| from . import inherited_model |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| from datetime import timedelta | ||
| from odoo import models, fields, api | ||
| from odoo.exceptions import UserError, ValidationError | ||
| from odoo.tools import float_compare, float_is_zero | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = "est.property" | ||
| _description = "Real Estate" | ||
| _order = "id desc" | ||
|
|
||
| _check_expected_price = models.Constraint( | ||
| 'CHECK (expected_price >= 0)', | ||
| 'The Expected Price should be positive!', | ||
| ) | ||
|
|
||
| _check_selling_price = models.Constraint( | ||
| 'CHECK (selling_price >= 0)', | ||
| 'The Selling Price should be positive!', | ||
| ) | ||
| def _today_plus_90days(self): | ||
| return fields.Date.today(self) + timedelta(days=90) | ||
|
|
||
|
|
||
| name = fields.Char('Property Name', required=True) | ||
| description = fields.Text(required=True) | ||
| post_code = fields.Char(required=True) | ||
| date_availability = fields.Date(default=(_today_plus_90days)) #add 90 days which is about 3 months | ||
| expected_price = fields.Float(required=True) | ||
| selling_price = fields.Float() | ||
| bedrooms = fields.Integer(default=2) | ||
| living_area = fields.Integer("Living Area (sqm)") | ||
| facades = fields.Integer() | ||
| garage = fields.Boolean() | ||
| garden = fields.Boolean() | ||
| garden_area = fields.Integer() | ||
| garden_orientation = fields.Selection( | ||
| selection=[("north","North"),("south","South"),("east","East"),("west","West")]) | ||
| active = fields.Boolean("Active",default=True) | ||
| state = fields.Selection( | ||
| selection = [("new","New"),("offer_received","Offer Received"),("offer_accepted","Offer Accepted"),("sold","Sold"),("cancelled","Cancelled")], | ||
| default = "new" | ||
| ) | ||
| partner_id = fields.Many2one("res.partner", string="Buyer") | ||
| user_id = fields.Many2one("res.users", string="Salesman") | ||
| property_type_id = fields.Many2one("est.property.type", string="Property Type") | ||
|
|
||
| tag_ids = fields.Many2many("est.property.tag",string="Tags") | ||
|
|
||
| offers_ids = fields.One2many("est.property.offer","property_id",string="Offers") | ||
|
|
||
| total_area = fields.Integer(compute="_compute_total", readonly=True) | ||
| max_offer = fields.Integer(compute="_compute_best_offer", readonly=True) | ||
|
|
||
| @api.constrains("selling_price") | ||
| def _check_selling_price(self): | ||
| for property in self: | ||
| ## if different than 0 and selling_price < 90% of expected_price | ||
| if (not float_is_zero(self.selling_price,precision_digits=3) and (1 == float_compare(self.expected_price * .9, self.selling_price, precision_digits=3))): | ||
| raise ValidationError(r"Selling price can't be less than 90% expected price") | ||
|
|
||
|
|
||
| @api.depends("living_area","garden_area") | ||
| def _compute_total(self): | ||
| for property in self: | ||
| property.total_area = property.living_area + property.garden_area | ||
|
|
||
| @api.depends("offers_ids") | ||
| def _compute_best_offer(self): | ||
|
|
||
| for property in self: | ||
|
|
||
| property.max_offer = max(property.offers_ids.mapped("price"),default=0) | ||
|
|
||
|
|
||
|
|
||
| @api.onchange("garden") | ||
| def _onchange_garden(self): | ||
| if self.garden: | ||
| self.garden_area = 10 | ||
| self.garden_orientation = "north" | ||
| else: | ||
| self.garden_area = 0 | ||
| self.garden_orientation = None | ||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def _ondelete(self): | ||
| for property in self: | ||
| if not(property.state == "new" or property.state == "cancelled"): | ||
| raise UserError("You can only delete properties that are 'New' or 'Cancelled'.") | ||
|
|
||
|
|
||
| # ------------- Actions ------------------------------- | ||
|
|
||
| def sell_action(self): | ||
| for property in self: | ||
| if self.state == "cancelled": | ||
| #Raise error | ||
| raise UserError("Can't change to Sold if current state is Cancelled!") | ||
| else: | ||
| self.state = "sold" | ||
|
|
||
| def cancel_action(self): | ||
| for property in self: | ||
| if self.state =="sold": | ||
| #Raise error | ||
| raise UserError("Can't change to Cancel if current state is Sold!") | ||
| else: | ||
| self.state = "cancelled" | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| from datetime import timedelta | ||
| from odoo import models, fields, api | ||
| from odoo.exceptions import UserError | ||
|
|
||
|
|
||
| class EstateOffer(models.Model): | ||
| _name = "est.property.offer" | ||
| _description = "Property Offers" | ||
| _order = "price desc" | ||
|
|
||
| _check_price = models.Constraint( | ||
| 'CHECK (price >= 0)', | ||
| 'The Price should be positive!', | ||
| ) | ||
|
|
||
|
|
||
| name = fields.Char("name") | ||
| price = fields.Integer("price",required=True) | ||
| status = fields.Selection( | ||
| string="status", | ||
| default="waiting", | ||
| selection = [ | ||
| ("accepted","Accepted"), | ||
| ("refused","Refused"), | ||
| ("waiting","Waiting"), | ||
| ], | ||
| ) | ||
| partner_id = fields.Many2one("res.partner") | ||
| property_id = fields.Many2one("est.property",required=True) | ||
|
|
||
| property_type_id = fields.Many2one("est.property.type", related="property_id.property_type_id", stored=True, compute="_compute_property_type_id") | ||
|
|
||
|
|
||
| validity_date = fields.Date(compute="_compute_validity_date",inverse="_inverse_validity_duration") | ||
| validity_duration = fields.Integer(default=7) | ||
|
|
||
| @api.depends("validity_duration") | ||
| def _compute_validity_date(self): | ||
| for offer in self: | ||
| offer.validity_date = fields.Date.today() + timedelta(days=offer.validity_duration) | ||
|
|
||
| @api.depends("property_id") | ||
| def _compute_property_type_id(self): | ||
| for offer in self: | ||
| if offer.property_id.property_type_id: | ||
| offer.property_type_id = offer.property_id.property_type_id | ||
| else: | ||
| offer.property_type_id = False | ||
|
|
||
| def _inverse_validity_duration(self): | ||
| for offer in self: | ||
| offer.validity_duration = (offer.validity_date - fields.Date.today()).days | ||
|
|
||
| def action_confirm(self): | ||
|
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.
Currently, through the UI, you cannot trigger the error, but from the command line or a server action, users could get the traceback. side note: you can run odoo in terminal mode by adding There are two ways to solve the issue:
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. Functionally speaking, could you confirm an offer if the property was cancelled ? |
||
| for offer in self: | ||
| if offer.property_id.state == "sold": | ||
| raise UserError(self.env._("Property was already sold!")) | ||
| elif offer.property_id.state == "cancelled": | ||
| raise UserError(self.env._("Property was already cancelled!")) | ||
| elif offer.status == "refused": | ||
| raise UserError(self.env._("Cannot accept a proposal that was already refused!")) | ||
| else: | ||
| offer.status = "accepted" | ||
| offer.property_id.selling_price = offer.price | ||
| offer.property_id.partner_id = offer.partner_id | ||
| offer.property_id.state = "sold" | ||
|
|
||
| def action_reject(self): | ||
| self.status = "refused" | ||
|
|
||
| @api.model | ||
| def create(self,vals_list): | ||
| for vals in vals_list: | ||
| property = self.env['est.property'].browse(vals.get('property_id')) | ||
|
|
||
| if property.state == 'new': | ||
| property.state = 'offer_received' | ||
|
|
||
| if property.max_offer > vals.get('price',0): | ||
| raise UserError(self.env._("Cannot create an offer with a lower value than a previous one!")) | ||
|
|
||
| return super().create(vals_list) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| from odoo import models, fields | ||
|
|
||
| class EstateTag(models.Model): | ||
| _name = "est.property.tag" | ||
| _description = "Property tags" | ||
| _order = "name asc" | ||
|
|
||
| _check_name = models.Constraint( | ||
| 'unique(name)', | ||
| 'There is already a property tag with that name!', | ||
| ) | ||
|
|
||
| name = fields.Char("name", required=True) | ||
| color = fields.Integer("color") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| from odoo import models, fields, api | ||
|
|
||
| class EstateType(models.Model): | ||
| _name = "est.property.type" | ||
| _description = "Property types" | ||
| _order = "sequence,name" | ||
|
|
||
| _check_name = models.Constraint( | ||
| 'unique(name)', | ||
| 'There is already a property type with that name!', | ||
| ) | ||
|
|
||
| name = fields.Char("name", required=True) | ||
| sequence = fields.Integer(default=1) | ||
|
|
||
|
|
||
| offer_ids = fields.One2many("est.property.offer","property_type_id") | ||
| offer_count = fields.Integer(compute="_compute_count") | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_count(self): | ||
| for type in self: | ||
| type.offer_count = len(type.offer_ids) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| from odoo import models, fields | ||
|
|
||
|
|
||
| class User(models.Model): | ||
| _inherit = "res.users" | ||
|
|
||
| property_ids = fields.One2many("est.property","user_id",string="Properties") |
| 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_model,access_estate_model,model_est_property,base.group_user,1,1,1,1 | ||
| access_type,access_type,model_est_property_type,base.group_user,1,1,1,1 | ||
| access_tag,access_tag,model_est_property_tag,base.group_user,1,1,1,1 | ||
| access_offer,access_offer,model_est_property_offer,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
|
|
||
| <menuitem id="est_test_model_menu" name="Real Estate"> | ||
| <menuitem id="est_testing_stuff" name='Advertisements'> | ||
| <menuitem id="est_test_model_menu_action" action="est_test_model_action"/> | ||
| </menuitem> | ||
| <menuitem id="est_Settings" name='Settings'> | ||
| <menuitem id="est_property_type_menu_action" action="est_property_type_model_action"/> | ||
| <menuitem id="est_property_tag_menu_action" action="est_property_tag_model_action"/> | ||
| <menuitem id="est_property_offer_menu_action" action="est_property_offer_model_action"/> | ||
|
|
||
| </menuitem> | ||
| </menuitem> | ||
|
|
||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
|
|
||
| <record id="est_test_view_tree" model="ir.ui.view"> | ||
| <field name="name">est_property_offer_view_list</field> | ||
| <field name="model">est.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Tests"> | ||
|
|
||
|
|
||
| <field name="name"/> | ||
| <field name="price" /> | ||
| <field name="status"/> | ||
|
|
||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="est_property_offer_model_action" model="ir.actions.act_window"> | ||
| <field name="name">Property offers</field> | ||
| <field name="res_model">est.property.offer</field> | ||
| <field name="view_mode">list,form</field> | ||
| </record> | ||
|
|
||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
|
|
||
| <record id="est_property_tag_model_action" model="ir.actions.act_window"> | ||
| <field name="name">Property tags</field> | ||
| <field name="res_model">est.property.tag</field> | ||
| <field name="view_mode">list,form</field> | ||
| </record> | ||
|
|
||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
|
|
||
|
|
||
| <record id="est_property_type_view_list" model="ir.ui.view"> | ||
| <field name="name">est.property.type.list</field> | ||
| <field name="model">est.property.type</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Tests"> | ||
|
|
||
| <field name="sequence" widget="handle"/> | ||
|
|
||
| <field name="name"/> | ||
|
|
||
|
|
||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="est_property_type_from_offer_event" model="ir.actions.act_window"> | ||
| <field name="res_model">est.property.offer</field> | ||
| <field name="name">Property Types</field> | ||
| <field name="view_mode">list</field> | ||
| <field name="domain">[('property_type', '=', active_id)]</field> | ||
| </record> | ||
|
|
||
|
|
||
| <record id="est_property_type_view_form" model="ir.ui.view"> | ||
| <field name="name">est.property.type.form</field> | ||
| <field name="model">est.property.type</field> | ||
| <field name="arch" type="xml"> | ||
| <form string="Something"> | ||
| <header> | ||
|
|
||
| <button type="action" | ||
| name="%(est_property_type_from_offer_event)d" | ||
| string="View Offer" | ||
| class="oe_highlight"/> | ||
|
|
||
| </header> | ||
| <group> | ||
| <field name="name"/> | ||
|
|
||
| <field name="offer_ids"/> | ||
| </group> | ||
| </form> | ||
| </field> | ||
| </record> | ||
|
|
||
|
|
||
| <record id="est_property_type_model_action" model="ir.actions.act_window"> | ||
| <field name="name">Property types</field> | ||
| <field name="res_model">est.property.type</field> | ||
| <field name="view_mode">list,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.
The string attribute defaults to the name of the field - suffix such as
_id.s. Thus, when you want the string to be the same as the field name, you don't need to add it