-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[ADD] Estate: init #1028
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?
[ADD] Estate: init #1028
Changes from all commits
c408da5
a946449
6deb7a6
e429205
ffb822c
c71e880
e01993f
de4a315
de78e31
adb3316
e66dd36
55064b2
218b8f0
ff73e1b
871dc8d
6407935
0b791ab
b5fc120
ac39bed
ee8a1be
0faad12
be0cfdf
75b4558
d76827d
88cd366
70eb50e
bcbe15d
73aacdc
5f60173
cd5b754
c8cb661
6f106c1
3607166
09fdf45
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 |
|---|---|---|
|
|
@@ -127,3 +127,6 @@ dmypy.json | |
|
|
||
| # Pyre type checker | ||
| .pyre/ | ||
|
|
||
|
|
||
| .vscode/ | ||
| 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,19 @@ | ||
| { | ||
| 'name': 'Estate', | ||
| 'version': '1.0', | ||
| 'depends': ['base'], | ||
| 'author': 'Odoo S.A.', | ||
| 'application': True, | ||
| 'installable': True, | ||
| 'category': '', | ||
| 'description': '', | ||
| 'license': 'LGPL-3', | ||
| 'data': [ | ||
| 'security/ir.model.access.csv', | ||
| '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' | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from . import estate_property | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,108 @@ | ||||||
| from odoo import api, fields, models | ||||||
| from odoo.exceptions import UserError, ValidationError | ||||||
| from odoo.tools.float_utils import float_compare | ||||||
|
|
||||||
|
|
||||||
| class EstateProperty(models.Model): | ||||||
| _name = "estate.property" | ||||||
| _description = "Estate Property" | ||||||
| _order = "id desc" | ||||||
| _check_expected_price = models.Constraint( | ||||||
| 'CHECK(expected_price > 0)', | ||||||
| 'The expected price should be stricly positive' | ||||||
| ) | ||||||
| _check_selling_price = models.Constraint( | ||||||
| 'CHECK(selling_price >= 0)', | ||||||
| 'The selling price should be positive' | ||||||
| ) | ||||||
|
|
||||||
| name = fields.Char(required=True) | ||||||
| description = fields.Text() | ||||||
| postcode = fields.Char() | ||||||
| date_availability = fields.Date(copy=False, default=fields.Date.add(fields.Date.today(), months=3), string="Available From") | ||||||
| expected_price = fields.Float(required=True) | ||||||
| selling_price = fields.Float(readonly=True, copy=False) | ||||||
| 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'), | ||||||
| ('west', 'West'), | ||||||
| ('south', 'South'), | ||||||
| ('east', 'East') | ||||||
| ], | ||||||
| help="Choose the appropriate orientation of the garden" | ||||||
| ) | ||||||
| active = fields.Boolean(default=True) | ||||||
| state = fields.Selection( | ||||||
| string="Estate status", | ||||||
| selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], | ||||||
| help='This field explain the estate status.', | ||||||
| required=True, | ||||||
| copy=False, | ||||||
| default='new' | ||||||
| ) | ||||||
| property_type_id = fields.Many2one("estate.property.type", string="Type") | ||||||
| seller_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user, domain="[('type', '=', 'internal')]") | ||||||
| buyer_id = fields.Many2one("res.partner", string="Buyer", domain="[('type', '=', 'portal')]") | ||||||
| tag_ids = fields.Many2many("estate.property.tag") | ||||||
| offer_ids = fields.One2many("estate.property.offer", "property_id") | ||||||
| total_area = fields.Float(compute="_compute_total_area", string="Total Area (sqm)") | ||||||
| best_price = fields.Float(compute="_compute_best_offer", string="Best Offer") | ||||||
|
|
||||||
| @api.depends("garden_area", "living_area") | ||||||
| def _compute_total_area(self): | ||||||
| for record in self: | ||||||
| record.total_area = record.garden_area + record.living_area | ||||||
|
|
||||||
| @api.depends("offer_ids") | ||||||
| def _compute_best_offer(self): | ||||||
| for record in self: | ||||||
| record.best_price = max(record.offer_ids.mapped("price")) if len(record.offer_ids) > 0 else 0.0 | ||||||
|
Contributor
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
|
||||||
|
|
||||||
| @api.onchange("garden") | ||||||
| def _onchange_garden(self): | ||||||
| for record in self: | ||||||
| if not record.garden: | ||||||
| record.garden_area = 0 | ||||||
| record.garden_orientation = '' | ||||||
| else: | ||||||
| record.garden_area = 10 | ||||||
| record.garden_orientation = 'north' | ||||||
|
|
||||||
| def estate_property_action_sold(self): | ||||||
| self.__estate_property_action_sold_cancel('sold', "A cancelled property cannot be sold!", "This property is already sold!") | ||||||
|
Contributor
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
|
||||||
|
|
||||||
| def estate_property_action_cancel(self): | ||||||
| self.__estate_property_action_sold_cancel('cancelled', "A sold property cannot be cancelled!", "This property is already cancelled!") | ||||||
|
|
||||||
| def __estate_property_action_sold_cancel(self, target, error_message, error_same_target_message): | ||||||
| for record in self: | ||||||
| # exclude target from next condition | ||||||
| if record.state == target: | ||||||
| raise UserError(error_same_target_message) | ||||||
| # easiest way to exclude the other state | ||||||
| elif record.state in ('sold', 'cancelled'): | ||||||
| raise UserError(error_message) | ||||||
| # the property can be sold/cancelled | ||||||
| else: | ||||||
| record.state = target | ||||||
|
|
||||||
| @api.constrains('selling_price') | ||||||
| def _check_price_constraint(self): | ||||||
| for record in self: | ||||||
| if record.selling_price and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=4) < 0: | ||||||
| raise ValidationError("The price cannot be les than 90% of the expected price") | ||||||
|
|
||||||
| @api.onchange("offer_ids") | ||||||
| def _onchange_offer_ids(self): | ||||||
| for record in self: | ||||||
| if len(record.offer_ids) > 0: | ||||||
|
Contributor
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
|
||||||
| record.state = "offer_received" | ||||||
| else: | ||||||
| record.state = "new" | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
|
|
||
| class EstatePropertyOffer(models.Model): | ||
| _name = "estate.property.offer" | ||
| _description = "Estate Property Offer" | ||
| _order = "price desc" | ||
| _check_offer_price = models.Constraint( | ||
| 'CHECK(price > 0)', | ||
| 'The offer price should be stricly positive' | ||
| ) | ||
|
|
||
| price = fields.Float() | ||
| 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(default=7) | ||
| date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline") | ||
|
|
||
| @api.depends("validity") | ||
| def _compute_date_deadline(self): | ||
| for record in self: | ||
| record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) | ||
|
|
||
| @api.onchange("date_deadline") | ||
| def _inverse_date_deadline(self): | ||
| for record in self: | ||
| record.validity = (record.date_deadline - fields.Date.today()).days | ||
|
|
||
| def action_accept_offer(self): | ||
| for record in self: | ||
| if record.status == 'accepted': | ||
| return False | ||
|
Comment on lines
+40
to
+41
Contributor
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. It's weird to have a method that returns False in the middle of the loop. |
||
| record.property_id.selling_price = record.price | ||
| record.property_id.buyer_id = record.partner_id | ||
| for offer in record.property_id.offer_ids: | ||
| offer.status = "refused" | ||
| record.status = 'accepted' | ||
| record.property_id.state = 'offer_accepted' | ||
| return True | ||
|
|
||
| def action_refuse_offer(self): | ||
| for record in self: | ||
| record.status = "refused" | ||
| return True | ||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def _unlink_if_deleted(self): | ||
| for record in self: | ||
| record.property_id.selling_price = 0.0 | ||
|
Comment on lines
+57
to
+58
Contributor
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. You can actually "batch write" it self.property_id.selling_price = 0.0 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyTag(models.Model): | ||
| _name = "estate.property.tag" | ||
| _description = "Estate Property Tag" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(required=True) | ||
| color = fields.Integer() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
|
|
||
| class EstatePropertyType(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "Estate Property Types" | ||
| _order = "sequence, name" | ||
|
|
||
| name = fields.Char(required=True) | ||
| property_ids = fields.One2many("estate.property", "property_type_id") | ||
| sequence = fields.Integer('Sequence', default=1, help="Used to order estate property types.") | ||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id") | ||
| offer_count = fields.Integer(compute="_compute_offer_count") | ||
|
|
||
| @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,6 @@ | ||
| id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink | ||
| access_estate_model,estate_property_users,model_estate_property,base.group_user,1,1,1,1 | ||
| access_estate_model_type,estate_property_type_users,model_estate_property_type,base.group_user,1,1,1,1 | ||
| portal_access_estate_model,estate_property_portal,model_estate_property,base.group_portal,1,0,0,0 | ||
| access_estate_model_tag,estate_property_tag_users,model_estate_property_tag,base.group_user,1,1,1,1 | ||
| access_estate_model_offer,estate_property_offer_users,model_estate_property_offer,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="estate_menu_root" name="Estate"> | ||
| <menuitem id="estate_first_level_menu" name="Advertisements"> | ||
| <menuitem id="estate_property_menu_action" action="estate_property_action"/> | ||
| </menuitem> | ||
|
|
||
| <menuitem id="estate_property_type_settings_menu" 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,24 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <odoo> | ||
| <record id="estate_property_offer_list_view" 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="Offers" editable="bottom" decoration-success="status == 'accepted'" decoration-danger="status == 'refused'"> | ||
| <field name="price"/> | ||
| <field name="partner_id"/> | ||
| <field name="validity" string="Validity (days)"/> | ||
| <field name="date_deadline" string="Deadline"/> | ||
| <button type="object" name="action_accept_offer" title="accept" icon="fa-check" invisible="status in ('accepted', 'refused')"/> | ||
| <button type="object" name="action_refuse_offer" title="refuse" icon="fa-times" invisible="status in ('accepted', 'refused')"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_offer_action" model="ir.actions.act_window"> | ||
| <field name="name">Offer</field> | ||
| <field name="res_model">estate.property.offer</field> | ||
| <field name="view_mode">list</field> | ||
| <field name="domain">[('property_type_id', '=', active_id)]</field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <odoo> | ||
| <record id="estate_property_tag_form_view" model="ir.ui.view"> | ||
| <field name="name">estate.property.tag.form</field> | ||
| <field name="model">estate.property.tag</field> | ||
| <field name="arch" type="xml"> | ||
| <form string="New Tag"> | ||
| <sheet> | ||
| <group> | ||
| <h1><field name="name" placeholder="New property tag"/></h1> | ||
| </group> | ||
| </sheet> | ||
| </form> | ||
| </field> | ||
| </record> | ||
|
|
||
| <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</field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="estate_property_type_form_view" 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 string="New Type"> | ||
| <sheet> | ||
| <group class="border-bottom"> | ||
| <group> | ||
| <h1><field name="name" nolabel="1" placeholder="New property type"/></h1> | ||
| </group> | ||
| <group> | ||
| <div class="d-flex flex-row-reverse"> | ||
| <button class="oe_stat_button border" name="%(estate.estate_property_offer_action)d" type="action" string="Stat button" icon="fa-money"> | ||
| <field name="offer_count"/> | ||
| </button> | ||
| </div> | ||
| </group> | ||
| </group> | ||
| <notebook> | ||
| <page string="Properties"> | ||
| <field name="property_ids"> | ||
| <list> | ||
| <field name="name"/> | ||
| <field name="expected_price"/> | ||
| <field name="state" string="Status"/> | ||
| </list> | ||
| </field> | ||
| </page> | ||
| </notebook> | ||
| </sheet> | ||
| </form> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_type_list_view" 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> | ||
| <field name="sequence" widget="handle"/> | ||
| <field name="name" string="Type"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <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> | ||
| </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.
Constraints should be just below field definition. Check the coding guidelines for the order of the methods and fields because it's not the only one misplaced :)