From 5936cb3646961205a8469b904513a6d8d9c412bb Mon Sep 17 00:00:00 2001 From: dhruv Date: Wed, 13 Nov 2024 18:39:35 +0530 Subject: [PATCH 01/17] [ADD] estate: develop module as a part of technical training Started learning by creating required files such as __init__ and manifest. Gained basic knowledge of the folder structure of a module. Began by learning basic fields in a model. Added a CSV file for security and defined access rights for base.group_user. While learning the basics of Odoo, encountered and resolved many errors. Gained an understanding of sequentially loading data files. After that, started learning about views, beginning with menus. Added menus, nested menus, and actions to organize access to each model. Tested many times, creating several menus and nested menu structures. Next, started learning about fields, their types, and parameters. Developed basic views and gained knowledge about record tag name in view. Organized fields in a logical way using lists, forms, searches, filters and Understood domains. Established relationships between models using One2many, Many2one, and Many2many fields. Gained practical experience with computed fields and onchange methods, enabling dynamic data updates in forms. --- estate/__init__.py | 2 + estate/__manifest__.py | 23 +++ estate/models/__init__.py | 4 + estate/models/estate_property.py | 64 +++++++++ estate/models/estate_property_offers.py | 46 ++++++ estate/models/estate_property_tag.py | 7 + estate/models/estate_property_types.py | 7 + estate/security/ir.model.access.csv | 5 + estate/views/estate_property_offers_views.xml | 36 +++++ estate/views/estate_property_tags_views.xml | 40 ++++++ estate/views/estate_property_types_views.xml | 40 ++++++ estate/views/estate_property_views.xml | 131 ++++++++++++++++++ estate/views/menu_actions.xml | 22 +++ estate/views/menu_views.xml | 18 +++ 14 files changed, 445 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/models/estate_property_offers.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_types.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_property_offers_views.xml create mode 100644 estate/views/estate_property_tags_views.xml create mode 100644 estate/views/estate_property_types_views.xml create mode 100644 estate/views/estate_property_views.xml create mode 100644 estate/views/menu_actions.xml create mode 100644 estate/views/menu_views.xml diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 0000000000..f5ba686bc2 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 0000000000..0a94aea976 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,23 @@ +{ + "name": "Real Estate", + "version": "1.0", + "depends": ["base"], + "author": "djip-odoo", + "description": """ + part of technical training + """, + "data": [ + "views/menu_actions.xml", + "views/menu_views.xml", + "views/estate_property_views.xml", + "views/estate_property_types_views.xml", + "views/estate_property_tags_views.xml", + "views/estate_property_offers_views.xml", + + "security/ir.model.access.csv", + ], + "application": True, + "installable": True, + "auto_install": False, + "license": "AGPL-3", +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 0000000000..44dfc2f6ec --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,4 @@ +from . import estate_property_offers +from . import estate_property +from . import estate_property_types +from . import estate_property_tag \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 0000000000..6f51e7056f --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,64 @@ +from odoo import models, fields, api +from datetime import datetime +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char(required=True,string="Name") + postcode = fields.Char(string="pincode") + date_availability = fields.Date(string="Available from",default=lambda self: (datetime.now() + relativedelta(months=3)).date(), copy=False) + expected_price = fields.Float(required=True,string="Expected Price") + selling_price = fields.Float(readonly=True,copy=False) + + description = fields.Char() + bedrooms = fields.Integer(default=2) + living_area = fields.Integer(string="living area(sqm)") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + active = fields.Boolean(default=True) + garden_area = fields.Integer(string="garden area(sqm)") + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ],) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('affer_Accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ],required=True,default="new") + total_area= fields.Float(compute="_compute_total_area",string="total area(sqm)") + propery_type_id=fields.Many2one('estate.property.types',required=True) + user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user) + partner_id = fields.Many2one("res.partner", string="Buyer",copy=False) + tag_ids = fields.Many2many("estate.property.tags", string="Tags") + offer_ids = fields.One2many('estate.property.offers', 'property_id', string="Offers") + best_price = fields.Float(compute="_compute_best_price") + + @api.depends("total_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("best_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.0 + + @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 None + + diff --git a/estate/models/estate_property_offers.py b/estate/models/estate_property_offers.py new file mode 100644 index 0000000000..0677d26300 --- /dev/null +++ b/estate/models/estate_property_offers.py @@ -0,0 +1,46 @@ +from odoo import models, fields, api +from datetime import datetime, timedelta + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offers" + _description = "Real Estate Property offer" + + price = fields.Float(required=True) + status = fields.Selection( + [ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + copy=False, + ) + partner_id = fields.Many2one("res.partner", string="Buyer") + property_id = fields.Many2one("estate.property") + + date_deadline = fields.Date( + compute="_compute_date_by_validity", + inverse="_compute_validity_by_date" + ) + validity = fields.Integer( + default=7, + inverse="_compute_date_by_validity", + compute="_compute_validity_by_date", + ) + + @api.depends("date_deadline") + def _compute_validity_by_date(self): + for record in self: + if record.date_deadline: + record.validity = (record.date_deadline - datetime.today().date()).days + else: + record.validity = 0 + + @api.depends("validity") + def _compute_date_by_validity(self): + for record in self: + if record.validity is not None: + record.date_deadline = datetime.today().date() + timedelta( + days=record.validity + ) + else: + record.date_deadline = False diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 0000000000..6203e366ad --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class EstatePropertyTags(models.Model): + _name = "estate.property.tags" + _description = "Real Estate Property tags" + + name = fields.Char(required=True,string="Property tags") diff --git a/estate/models/estate_property_types.py b/estate/models/estate_property_types.py new file mode 100644 index 0000000000..9d2ca04e59 --- /dev/null +++ b/estate/models/estate_property_types.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class EstatePropertyType(models.Model): + _name = "estate.property.types" + _description = "Real Estate Property Type" + + name = fields.Char(required=True,string="Property Type") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 0000000000..1e106bff77 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -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_types,access_estate_property_types,estate.model_estate_property_types,base.group_user,1,1,1,1 +estate.access_estate_property_tags,access_estate_property_tags,estate.model_estate_property_tags,base.group_user,1,1,1,1 +estate.access_estate_property_offers,access_estate_property_offers,estate.model_estate_property_offers,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 0000000000..e1fd552cff --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,36 @@ + + + + + estate.property.offers.tree + estate.property.offers + + + + + + + + + + + + + + estate.property.offers.tree + estate.property.offers + +
+ + + + + + + + +
+
+
+ +
\ No newline at end of file diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml new file mode 100644 index 0000000000..09025344e0 --- /dev/null +++ b/estate/views/estate_property_tags_views.xml @@ -0,0 +1,40 @@ + + + + + estate.property.tags.tree + estate.property.tags + + + + + + + + + + estate.property.tags.tree + estate.property.tags + +
+ +

+ +

+
+
+
+
+ + + + estate.property.tags.search + estate.property.tags + search + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_types_views.xml b/estate/views/estate_property_types_views.xml new file mode 100644 index 0000000000..3a23245e56 --- /dev/null +++ b/estate/views/estate_property_types_views.xml @@ -0,0 +1,40 @@ + + + + + estate.property.types.tree + estate.property.types + + + + + + + + + + estate.property.types.tree + estate.property.types + +
+ +

+ +

+
+
+
+
+ + + + estate.property.types.search + estate.property.types + search + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 0000000000..c6fcbe6bad --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,131 @@ + + + + estate.property.tree + estate.property + + + + + + + + + + + + + + + + + + + + estate.property.tree + estate.property + +
+ + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + estate.property.search + estate.property + search + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/menu_actions.xml b/estate/views/menu_actions.xml new file mode 100644 index 0000000000..e6946a63f2 --- /dev/null +++ b/estate/views/menu_actions.xml @@ -0,0 +1,22 @@ + + + + Real Estate Properties + estate.property + list,form + + + + + Property Types + estate.property.types + list,form + + + + + Property Tags + estate.property.tags + list,form + + \ No newline at end of file diff --git a/estate/views/menu_views.xml b/estate/views/menu_views.xml new file mode 100644 index 0000000000..f7a60e8786 --- /dev/null +++ b/estate/views/menu_views.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From 800fdd690241eb8558e1523fc764bb144075753d Mon Sep 17 00:00:00 2001 From: dhruv Date: Thu, 14 Nov 2024 18:50:12 +0530 Subject: [PATCH 02/17] [IMP] estate: revised relation mapping and learned actions and constraints Revised relation mapping concept of One2many and some others too. tried with diffrent example for relational maping. After that, started new chapeter actions , where faced many erros while appying actions on created new buttons in view. some attributes were may be removed but still in the docs is causing error. seen previously created modules, documentary even used chatgpt to come out the errors. and it get solved after many attempts. Apply buttons in view is also challenging. In next chapter related to constraints, i was getting error in adding sql constraints but resolved by getting advice from mentor. than i get understood. how to solvethat kind or error. changing database solve errors sometimes may be due to old data causing errors on new features or constraints added. Learned many things to why some errors occurs and how to solve it. --- estate/models/estate_property.py | 124 +++++++++++++----- estate/models/estate_property_offers.py | 23 +++- estate/models/estate_property_tag.py | 9 +- estate/models/estate_property_types.py | 11 +- estate/views/estate_property_offers_views.xml | 12 +- estate/views/estate_property_views.xml | 7 +- 6 files changed, 144 insertions(+), 42 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6f51e7056f..27e2540c2e 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,18 +1,24 @@ from odoo import models, fields, api from datetime import datetime from dateutil.relativedelta import relativedelta -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare + class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property" - - name = fields.Char(required=True,string="Name") + + name = fields.Char(required=True, string="Name") postcode = fields.Char(string="pincode") - date_availability = fields.Date(string="Available from",default=lambda self: (datetime.now() + relativedelta(months=3)).date(), copy=False) - expected_price = fields.Float(required=True,string="Expected Price") - selling_price = fields.Float(readonly=True,copy=False) - + date_availability = fields.Date( + string="Available from", + default=lambda self: (datetime.now() + relativedelta(months=3)).date(), + copy=False, + ) + expected_price = fields.Float(required=True, string="Expected Price") + selling_price = fields.Float(readonly=True, copy=False) + description = fields.Char() bedrooms = fields.Integer(default=2) living_area = fields.Integer(string="living area(sqm)") @@ -21,44 +27,100 @@ class EstateProperty(models.Model): garden = fields.Boolean() active = fields.Boolean(default=True) garden_area = fields.Integer(string="garden area(sqm)") - garden_orientation = fields.Selection([ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West'), - ],) - state = fields.Selection([ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('affer_Accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('cancelled', 'Cancelled'), - ],required=True,default="new") - total_area= fields.Float(compute="_compute_total_area",string="total area(sqm)") - propery_type_id=fields.Many2one('estate.property.types',required=True) - user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user) - partner_id = fields.Many2one("res.partner", string="Buyer",copy=False) + garden_orientation = fields.Selection( + [ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + ) + state = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + required=True, + default="new", + ) + total_area = fields.Float(compute="_compute_total_area", string="total area(sqm)") + propery_type_id = fields.Many2one("estate.property.types", required=True) + user_id = fields.Many2one( + "res.users", + string="Salesperson", + index=True, + default=lambda self: self.env.user, + ) + partner_id = fields.Many2one("res.partner", string="Buyer", copy=False) tag_ids = fields.Many2many("estate.property.tags", string="Tags") - offer_ids = fields.One2many('estate.property.offers', 'property_id', string="Offers") + offer_ids = fields.One2many( + "estate.property.offers", "property_id", string="Offers" + ) best_price = fields.Float(compute="_compute_best_price") - + @api.depends("total_area") def _compute_total_area(self): for record in self: record.total_area = record.living_area + record.garden_area - + + @api.depends("state") + def _update_state_on_offer_received(self): + for record in self: + if len(record.offer_ids) > 1: + record.state = "offer_received" + else: + record.state = "new" + @api.depends("best_price") def _compute_best_price(self): for record in self: if record.offer_ids: - record.best_price = max(record.offer_ids.mapped('price')) + record.best_price = max(record.offer_ids.mapped("price")) else: record.best_price = 0.0 - + @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 None - - + + def onclick_sold(self): + for record in self: + if record.state not in ["cancelled"]: + record.state = "sold" + else: + raise UserError("cancelled can not be sell") + + def onclick_cancel(self): + for record in self: + if record.state not in ["sold"]: + record.state = "cancelled" + else: + raise UserError("sold can not be cancel") + + _sql_constraints = [ + ( + "check_expected_price_cust", + "CHECK(expected_price > 0)", + "expected price cannot be less than 0", + ), + ] + + @api.constrains("selling_price") + def _check_selling_price(self): + for record in self: + if ( + float_compare( + record.selling_price, + record.expected_price * 90 / 100, + precision_digits=2, + ) + == -1 + ): + raise ValidationError( + "Selling price is too low! It must be at least 90% of the expected price." + ) diff --git a/estate/models/estate_property_offers.py b/estate/models/estate_property_offers.py index 0677d26300..d436a790ef 100644 --- a/estate/models/estate_property_offers.py +++ b/estate/models/estate_property_offers.py @@ -14,18 +14,24 @@ class EstatePropertyOffer(models.Model): ], copy=False, ) - partner_id = fields.Many2one("res.partner", string="Buyer") + partner_id = fields.Many2one("res.partner", string="Partner") property_id = fields.Many2one("estate.property") date_deadline = fields.Date( - compute="_compute_date_by_validity", - inverse="_compute_validity_by_date" + compute="_compute_date_by_validity", inverse="_compute_validity_by_date" ) validity = fields.Integer( default=7, inverse="_compute_date_by_validity", compute="_compute_validity_by_date", ) + property_state = fields.Selection( + related="property_id.state", string="Property State" + ) + + _sql_constraints = [ + ("check_price", "CHECK(price > 0)", "Price cannot be less than 0"), + ] @api.depends("date_deadline") def _compute_validity_by_date(self): @@ -44,3 +50,14 @@ def _compute_date_by_validity(self): ) else: record.date_deadline = False + + def action_confirm(self): + for record in self: + record.status = "accepted" + record.property_id.state = "offer_accepted" + record.property_id.selling_price = record.price + record.property_id.partner_id = record.partner_id + + def action_cancel(self): + for record in self: + record.status = "refused" diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 6203e366ad..cb0a770b2c 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,7 +1,12 @@ from odoo import models, fields + class EstatePropertyTags(models.Model): _name = "estate.property.tags" _description = "Real Estate Property tags" - - name = fields.Char(required=True,string="Property tags") + + name = fields.Char(required=True, string="Property tags") + + _sql_constraints = [ + ("check_tag_name", "UNIQUE(name)", "Tag must be unique"), + ] diff --git a/estate/models/estate_property_types.py b/estate/models/estate_property_types.py index 9d2ca04e59..4d44123112 100644 --- a/estate/models/estate_property_types.py +++ b/estate/models/estate_property_types.py @@ -1,7 +1,12 @@ -from odoo import models, fields +from odoo import models, fields + class EstatePropertyType(models.Model): _name = "estate.property.types" _description = "Real Estate Property Type" - - name = fields.Char(required=True,string="Property Type") + + name = fields.Char(required=True, string="Property Type") + + _sql_constraints = [ + ("check_property_type", "UNIQUE(name)", "Property type must be unique"), + ] diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index e1fd552cff..65a0f32567 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -10,7 +10,17 @@ - + + +

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 56df3a19e1..204dd3a7d1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,20 +4,24 @@ estate.property.tree estate.property - + - - + - - - - - - - + + + + + + + + + + @@ -42,12 +46,10 @@ - - - + - @@ -119,8 +121,7 @@ - + estate.property.search estate.property search @@ -130,10 +131,14 @@ - - + + + + + diff --git a/estate/views/menu_actions.xml b/estate/views/menu_actions.xml index 051eb5c217..75ec20d216 100644 --- a/estate/views/menu_actions.xml +++ b/estate/views/menu_actions.xml @@ -4,6 +4,7 @@ Real Estate Properties estate.property list,form + {'search_default_available': True} From b70fbf667d420479cdce587c3ca6ef6ff52926ea Mon Sep 17 00:00:00 2001 From: dhruv Date: Tue, 19 Nov 2024 18:48:54 +0530 Subject: [PATCH 05/17] [ADD] estate_account: module to generate ionvoices for sold properties - revised the concept of inheritance in odoo. figured out one deprecated warning in overridden create method in a module. found the new way to override that method to getting ride of the warning. Inherited res.users module and view to show estate properties in users form view. followed the exercise and taken reference from the documentation and existing codes. faced some minor errors and completed the task. Created new module estate account which generate invoice for sold property using inheritance. learned concept of module dependency, inheritance, generate invoice. Created stat button in property form to redirected to invoice list. it was literally challenging i got errors many times. but solved at the end. Generating invoice is very challenging than previous chapters. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 3 +- estate/models/estate_property.py | 18 +++---- estate/models/estate_property_offers.py | 30 ++++++----- estate/models/estate_property_tag.py | 6 +-- estate/models/estate_property_types.py | 4 +- estate/models/inherited_res_users.py | 6 +++ estate/views/inherited_res_users_view.xml | 14 +++++ estate_account/__init__.py | 2 + estate_account/__manifest__.py | 16 ++++++ estate_account/models/__init__.py | 2 + .../models/inherited_account_move.py | 7 +++ .../models/inherited_estate_property.py | 52 +++++++++++++++++++ estate_account/views/actions_smart_button.xml | 8 +++ .../views/estate_property_views.xml | 26 ++++++++++ 15 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 estate/models/inherited_res_users.py create mode 100644 estate/views/inherited_res_users_view.xml create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/inherited_account_move.py create mode 100644 estate_account/models/inherited_estate_property.py create mode 100644 estate_account/views/actions_smart_button.xml create mode 100644 estate_account/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 14aa9e085f..2f3f911cf5 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,6 +14,7 @@ "views/estate_property_types_views.xml", "views/estate_property_tags_views.xml", "views/estate_property_offers_views.xml", + "views/inherited_res_users_view.xml", "security/ir.model.access.csv", ], "application": True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 44dfc2f6ec..6e5fa9f559 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,4 +1,5 @@ from . import estate_property_offers from . import estate_property from . import estate_property_types -from . import estate_property_tag \ No newline at end of file +from . import estate_property_tag +from . import inherited_res_users \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index fba71161f6..1f5aed3b6a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -10,7 +10,7 @@ class EstateProperty(models.Model): _description = "Real Estate Property" name = fields.Char(required=True, string="Name") - postcode = fields.Char(string="pincode") + postcode = fields.Char(string="Pincode") date_availability = fields.Date( string="Available from", default=lambda self: (datetime.now() + relativedelta(months=3)).date(), @@ -21,12 +21,12 @@ class EstateProperty(models.Model): description = fields.Char() bedrooms = fields.Integer(default=2) - living_area = fields.Integer(string="living area(sqm)") + living_area = fields.Integer(string="Living Area (sqm)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() active = fields.Boolean(default=True) - garden_area = fields.Integer(string="garden area(sqm)") + garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( [ ("north", "North"), @@ -46,7 +46,7 @@ class EstateProperty(models.Model): required=True, default="new", ) - total_area = fields.Float(compute="_compute_total_area", string="total area(sqm)") + total_area = fields.Float(compute="_compute_total_area", string="Total Area (sqm)") property_type_id = fields.Many2one("estate.property.types", required=True) user_id = fields.Many2one( "res.users", @@ -91,20 +91,20 @@ def onclick_sold(self): if record.state not in ["cancelled"]: record.state = "sold" else: - raise UserError("cancelled can not be sell") + raise UserError("A cancelled property cannot be sold.") def onclick_cancel(self): for record in self: if record.state not in ["sold"]: record.state = "cancelled" else: - raise UserError("sold can not be cancel") + raise UserError("A sold property cannot be cancelled.") _sql_constraints = [ ( "check_expected_price_cust", "CHECK(expected_price > 0)", - "expected price cannot be less than 0", + "Expected price cannot be less than 0.", ), ] @@ -124,8 +124,8 @@ def _check_selling_price(self): ) @api.ondelete(at_uninstall=False) - def _prevent_deletion_of_record_while_state_is_not_new_cancelled(self): + def _prevent_deletion_of_record_while_state_is_not_new_or_cancelled(self): if any(record.state not in ["new", "cancelled"] for record in self): raise AccessError( - "You can only delete property if it is in new or cancelled state!" + "You can only delete a property if it is in the 'new' or 'cancelled' state." ) diff --git a/estate/models/estate_property_offers.py b/estate/models/estate_property_offers.py index c8fef1afa5..1389e60b01 100644 --- a/estate/models/estate_property_offers.py +++ b/estate/models/estate_property_offers.py @@ -5,10 +5,11 @@ class EstatePropertyOffer(models.Model): _name = "estate.property.offers" - _description = "Real Estate Property offer" + _description = "Real Estate Property Offer" _order = "price desc" + _sql_constraints = [ - ("check_price", "CHECK(price > 0)", "Price cannot be less than 0"), + ("check_price", "CHECK(price > 0)", "Price cannot be less than or equal to 0."), ] price = fields.Float(required=True) @@ -73,18 +74,19 @@ def action_cancel(self): for record in self: record.status = "refused" - @api.model - def create(self, vals): - if not isinstance(vals, dict): + @api.model_create_multi + def create(self, vals_list): + if not isinstance(vals_list, list): raise ValidationError("Unexpected input to create method.") - property_id = vals.get("property_id") - if property_id: - property = self.env["estate.property"].browse(property_id) - max_price = max(property.offer_ids.mapped("price"), default=0) - if vals["price"] <= max_price: - raise ValidationError( - f"The offer price should be greater than the current maximum offer price ({max_price})." - ) + for vals in vals_list: + property_id = vals.get("property_id") + if property_id: + property = self.env["estate.property"].browse(property_id) + max_price = max(property.offer_ids.mapped("price"), default=0) + if vals["price"] <= max_price: + raise ValidationError( + f"The offer price must be greater than the current maximum offer price of {max_price}." + ) - return super(EstatePropertyOffer, self).create(vals) + return super(EstatePropertyOffer, self).create(vals_list) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 351b62fdbe..ec07578720 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -3,11 +3,11 @@ class EstatePropertyTags(models.Model): _name = "estate.property.tags" - _description = "Real Estate Property tags" + _description = "Real Estate Property Tags" - name = fields.Char(required=True, string="Property tags") + name = fields.Char(required=True, string="Tag Name") color = fields.Integer(string="Color") _sql_constraints = [ - ("check_tag_name", "UNIQUE(name)", "Tag must be unique"), + ("check_tag_name", "UNIQUE(name)", "Tag name must be unique."), ] diff --git a/estate/models/estate_property_types.py b/estate/models/estate_property_types.py index 5887b40a3c..ba6ae08a41 100644 --- a/estate/models/estate_property_types.py +++ b/estate/models/estate_property_types.py @@ -9,7 +9,7 @@ class EstatePropertyType(models.Model): name = fields.Char(required=True, string="Property Type") _sql_constraints = [ - ("check_property_type", "UNIQUE(name)", "Property type must be unique"), + ("check_property_type", "UNIQUE(name)", "Property type must be unique."), ] offer_ids = fields.One2many("estate.property.offers", "property_type_id") @@ -17,7 +17,7 @@ class EstatePropertyType(models.Model): property_ids = fields.One2many("estate.property", "property_type_id") sequence = fields.Integer( - "Sequence", default=1, help="Used to order stages. Lower is better." + "Sequence", default=1, help="Used to order property types. Lower values are higher priority." ) offer_counts = fields.Integer(compute="_compute_offers_by_property_types") diff --git a/estate/models/inherited_res_users.py b/estate/models/inherited_res_users.py new file mode 100644 index 0000000000..ec1624b42c --- /dev/null +++ b/estate/models/inherited_res_users.py @@ -0,0 +1,6 @@ +from odoo import fields, models + +class InheritedModel(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "property_type_id") \ No newline at end of file diff --git a/estate/views/inherited_res_users_view.xml b/estate/views/inherited_res_users_view.xml new file mode 100644 index 0000000000..638570fbc6 --- /dev/null +++ b/estate/views/inherited_res_users_view.xml @@ -0,0 +1,14 @@ + + + res.users.form. + res.users + + + + + + + + + + \ No newline at end of file diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 0000000000..f5ba686bc2 --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models \ No newline at end of file diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 0000000000..d474b2ad0e --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Estate Account", + "version": "1.0", + "depends": ["base", "estate", "account"], + "author": "djip-odoo", + "description": """ + part of technical training + """, + "data": [ + "views/actions_smart_button.xml", + "views/estate_property_views.xml", + ], + "installable": True, + "auto_install": True, + "license": "LGPL-3", +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 0000000000..370faa5be2 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1,2 @@ +from . import inherited_estate_property +from . import inherited_account_move \ No newline at end of file diff --git a/estate_account/models/inherited_account_move.py b/estate_account/models/inherited_account_move.py new file mode 100644 index 0000000000..82a7083fa2 --- /dev/null +++ b/estate_account/models/inherited_account_move.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class InheritedAccountMove(models.Model): + _inherit = "account.move" + + property_id = fields.Many2one("estate.property", string="Property") diff --git a/estate_account/models/inherited_estate_property.py b/estate_account/models/inherited_estate_property.py new file mode 100644 index 0000000000..57ee514442 --- /dev/null +++ b/estate_account/models/inherited_estate_property.py @@ -0,0 +1,52 @@ +from odoo import models, Command, api, fields + + +class EstatePropertyInherited(models.Model): + _inherit = "estate.property" + + invoice_ids = fields.One2many("account.move", "property_id", string="Invoices") + + invoice_count = fields.Integer(compute="_calc_invoices", string="Invoice Count") + + @api.depends("invoice_ids") + def _calc_invoices(self): + for record in self: + record.invoice_count = len(record.invoice_ids) + + def onclick_sold(self): + super().onclick_sold() + invoice = self.env["account.move"].create( + { + "partner_id": self.partner_id.id, + "move_type": "out_invoice", + "property_id": self.id, + "invoice_line_ids": [ + Command.create( + { + "name": "Property selling price", + "quantity": 1, + "price_unit": self.selling_price, + } + ), + Command.create( + { + "name": "6% of the selling price", + "quantity": 1, + "price_unit": self.selling_price * 0.06, + } + ), + Command.create( + { + "name": "Administrative price", + "quantity": 1, + "price_unit": 100, + } + ), + ], + } + ) + return True + + def temp_action(self): + print("test temp_action") + pass diff --git a/estate_account/views/actions_smart_button.xml b/estate_account/views/actions_smart_button.xml new file mode 100644 index 0000000000..065c9a9796 --- /dev/null +++ b/estate_account/views/actions_smart_button.xml @@ -0,0 +1,8 @@ + + + Invoices + account.move + [('property_id', '=', active_id)] + list,form + + \ No newline at end of file diff --git a/estate_account/views/estate_property_views.xml b/estate_account/views/estate_property_views.xml new file mode 100644 index 0000000000..9be16d4e73 --- /dev/null +++ b/estate_account/views/estate_property_views.xml @@ -0,0 +1,26 @@ + + + estate.property.form + estate.property + + + +
+ +
+
+
+
+
\ No newline at end of file From 3ef4f7ef9d247474df1ed4eb8f381dcec3851007 Mon Sep 17 00:00:00 2001 From: dhruv Date: Wed, 20 Nov 2024 18:48:13 +0530 Subject: [PATCH 06/17] [IMP] estate: created kanban view, defined demo data for both modules - Started new chapter. read some documentation part and performed the exercise. created kanban view. applied some attributes of it to archive desired goal given in the exercise. - Coding guidelines of odoo. refactored some file, class or var name as per coding guideline. - Defined demo data for estate and estate_account modules in csv or xml format as per complexity. faced many errors while adding demo data. figured out problems using chatgpt, documentary and completed all the exercise and added more data too, to explain each functionality of the module. learned how to write demo data for inherited modules as well. cleared databases and started installing new database many times to come out of the errors. --- estate/__manifest__.py | 17 +- estate/demo/estate.property.tags.csv | 11 + estate/demo/estate.property.types.csv | 7 + estate/demo/estate_property_demo.xml | 210 ++++++++++++++++++ estate/demo/estate_property_offers_demo.xml | 122 ++++++++++ estate/models/__init__.py | 6 +- estate/models/estate_property.py | 23 +- ...rty_offers.py => estate_property_offer.py} | 0 estate/models/estate_property_tag.py | 2 +- ...perty_types.py => estate_property_type.py} | 0 .../{inherited_res_users.py => res_users.py} | 2 +- ...ctions.xml => actions_menu_and_button.xml} | 16 +- estate/views/actions_smart_button.xml | 8 - ...ws.xml => estate_property_offer_views.xml} | 8 +- ...iews.xml => estate_property_tag_views.xml} | 8 +- ...ews.xml => estate_property_type_views.xml} | 9 +- estate/views/estate_property_views.xml | 45 +++- estate/views/menu_views.xml | 45 +++- ...res_users_view.xml => res_users_views.xml} | 0 estate_account/__manifest__.py | 1 + .../demo/demo_invoice_data_property.xml | 43 ++++ estate_account/models/__init__.py | 4 +- ...erited_account_move.py => account_move.py} | 2 +- ..._estate_property.py => estate_property.py} | 6 +- estate_account/views/actions_smart_button.xml | 6 +- .../views/estate_property_views.xml | 7 +- 26 files changed, 531 insertions(+), 77 deletions(-) create mode 100644 estate/demo/estate.property.tags.csv create mode 100644 estate/demo/estate.property.types.csv create mode 100644 estate/demo/estate_property_demo.xml create mode 100644 estate/demo/estate_property_offers_demo.xml rename estate/models/{estate_property_offers.py => estate_property_offer.py} (100%) rename estate/models/{estate_property_types.py => estate_property_type.py} (100%) rename estate/models/{inherited_res_users.py => res_users.py} (78%) rename estate/views/{menu_actions.xml => actions_menu_and_button.xml} (55%) delete mode 100644 estate/views/actions_smart_button.xml rename estate/views/{estate_property_offers_views.xml => estate_property_offer_views.xml} (84%) rename estate/views/{estate_property_tags_views.xml => estate_property_tag_views.xml} (73%) rename estate/views/{estate_property_types_views.xml => estate_property_type_views.xml} (89%) rename estate/views/{inherited_res_users_view.xml => res_users_views.xml} (100%) create mode 100644 estate_account/demo/demo_invoice_data_property.xml rename estate_account/models/{inherited_account_move.py => account_move.py} (76%) rename estate_account/models/{inherited_estate_property.py => estate_property.py} (92%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 2f3f911cf5..a97a16409c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,16 +7,21 @@ part of technical training """, "data": [ - "views/menu_actions.xml", + "views/actions_menu_and_button.xml", "views/menu_views.xml", - "views/actions_smart_button.xml", "views/estate_property_views.xml", - "views/estate_property_types_views.xml", - "views/estate_property_tags_views.xml", - "views/estate_property_offers_views.xml", - "views/inherited_res_users_view.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offer_views.xml", + "views/res_users_views.xml", "security/ir.model.access.csv", ], + "demo": [ + "demo/estate.property.types.csv", + "demo/estate.property.tags.csv", + "demo/estate_property_demo.xml", + "demo/estate_property_offers_demo.xml" + ], "application": True, "installable": True, "auto_install": False, diff --git a/estate/demo/estate.property.tags.csv b/estate/demo/estate.property.tags.csv new file mode 100644 index 0000000000..b3bde24e0b --- /dev/null +++ b/estate/demo/estate.property.tags.csv @@ -0,0 +1,11 @@ +"id","name","color" +"tag_modern","Modern","1" +"tag_luxury","Luxury","2" +"tag_vintage","Vintage","3" +"tag_cozy","Cozy","4" +"tag_spacious","Spacious","5" +"tag_economical","Economical","6" +"tag_green","Green","7" +"tag_waterfront","Waterfront","8" +"tag_mountain_view","Mountain View","9" +"tag_city_center","City Center","10" \ No newline at end of file diff --git a/estate/demo/estate.property.types.csv b/estate/demo/estate.property.types.csv new file mode 100644 index 0000000000..ac64ec8205 --- /dev/null +++ b/estate/demo/estate.property.types.csv @@ -0,0 +1,7 @@ +"id","name","sequence" +"estate_property_type_residential","Residential","1" +"estate_property_type_commercial","Commercial","2" +"estate_property_type_land","Land","3" +"estate_property_type_industrial","Industrial","4" +"estate_property_type_villa","Villa","5" +"estate_property_type_apartment","Apartment","6" \ No newline at end of file diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml new file mode 100644 index 0000000000..f0595a87d6 --- /dev/null +++ b/estate/demo/estate_property_demo.xml @@ -0,0 +1,210 @@ + + + + Big Villa + offer_received + A nice and big villa + 12345 + 2020-02-02 + 1600000.00 + 0.00 + 6 + 100 + 4 + True + True + 100000 + south + + + + + Trailer home + cancelled + Home in a trailer park + 54321 + 1970-01-01 + 100000.00 + 91000.00 + 1 + 10 + 4 + False + False + 0 + + + + + + My home + offer_received + Home in a my park + 123456 + + 110000.00 + 0.00 + 15 + 10000 + 4 + False + False + 0 + + + + + + + + Modern Villa + offer_received + A sleek, modern villa with spacious living rooms. + 67890 + 2024-01-01 + 2500000.00 + 0.00 + 5 + 350 + 3 + True + True + 500 + north + + + + + + Commercial Office + new + A large commercial office space in the city center. + 11223 + 2024-06-01 + 1500000.00 + 0.00 + 0 + 1000 + 2 + False + False + 0 + + + + + + + Residential Apartment + offer_accepted + A modern residential apartment in a high-rise building. + 34567 + 2024-03-15 + 500000.00 + 510000.00 + 2 + 80 + 1 + True + False + 0 + + + + + + + + Land for Sale + sold + A plot of land ready for development. + 45678 + 2024-07-01 + 300000.00 + 2655000.00 + 0 + 0 + 4 + False + False + 0 + + + + + + + Industrial Warehouse + sold + An industrial warehouse with large storage capacity. + 98765 + 2024-04-10 + 1000000.00 + 10550000.00 + 0 + 2000 + 2 + False + False + 0 + + + + + + + Luxury Mountain View Villa + sold + A luxurious villa with a spectacular mountain view. + 54321 + 2024-08-01 + 5000000.00 + 48050000.00 + 7 + 800 + 5 + True + false + 2000 + west + + + + + + \ No newline at end of file diff --git a/estate/demo/estate_property_offers_demo.xml b/estate/demo/estate_property_offers_demo.xml new file mode 100644 index 0000000000..ea252e57ae --- /dev/null +++ b/estate/demo/estate_property_offers_demo.xml @@ -0,0 +1,122 @@ + + + + 1805000 + refused + + + 7 + + + 1905000 + + + 7 + + + 2005000 + + + 7 + + + 2105000 + + + + + + 2110000 + + + + + + 25000000 + + + 30 + + + 25500000 + + + 15 + + + 26000000 + + + 7 + + + 5205000 + + + 15 + + + 5305000 + refused + + + 10 + + + 5405000 + + + 5 + + + 2655000 + accepted + + + 40 + + + 2710000 + refused + + + 20 + + + 2805000 + refused + + + 10 + + + 10550000 + accepted + + + 30 + + + 11050000 + refused + + + 15 + + + 48050000 + accepted + + + 45 + + + 49050000 + refused + + + 30 + + + \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 6e5fa9f559..ddb32cae6b 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,5 +1,5 @@ -from . import estate_property_offers +from . import estate_property_offer from . import estate_property -from . import estate_property_types +from . import estate_property_type from . import estate_property_tag -from . import inherited_res_users \ No newline at end of file +from . import res_users \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 1f5aed3b6a..2bfbc973cb 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -111,17 +111,18 @@ def onclick_cancel(self): @api.constrains("selling_price") def _check_selling_price(self): for record in self: - if ( - float_compare( - record.selling_price, - record.expected_price * 90 / 100, - precision_digits=2, - ) - == -1 - ): - raise ValidationError( - "Selling price is too low! It must be at least 90% of the expected price." - ) + if record.state not in ["new","offer_received"]: + if ( + float_compare( + record.selling_price, + record.expected_price * 90 / 100, + precision_digits=2, + ) + == -1 + ): + raise ValidationError( + "Selling price is too low! It must be at least 90% of the expected price." + ) @api.ondelete(at_uninstall=False) def _prevent_deletion_of_record_while_state_is_not_new_or_cancelled(self): diff --git a/estate/models/estate_property_offers.py b/estate/models/estate_property_offer.py similarity index 100% rename from estate/models/estate_property_offers.py rename to estate/models/estate_property_offer.py diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index ec07578720..51f6b8da98 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,7 +1,7 @@ from odoo import models, fields -class EstatePropertyTags(models.Model): +class EstatePropertyTag(models.Model): _name = "estate.property.tags" _description = "Real Estate Property Tags" diff --git a/estate/models/estate_property_types.py b/estate/models/estate_property_type.py similarity index 100% rename from estate/models/estate_property_types.py rename to estate/models/estate_property_type.py diff --git a/estate/models/inherited_res_users.py b/estate/models/res_users.py similarity index 78% rename from estate/models/inherited_res_users.py rename to estate/models/res_users.py index ec1624b42c..6d7550d0ab 100644 --- a/estate/models/inherited_res_users.py +++ b/estate/models/res_users.py @@ -1,6 +1,6 @@ from odoo import fields, models -class InheritedModel(models.Model): +class ResUsers(models.Model): _inherit = "res.users" property_ids = fields.One2many("estate.property", "property_type_id") \ No newline at end of file diff --git a/estate/views/menu_actions.xml b/estate/views/actions_menu_and_button.xml similarity index 55% rename from estate/views/menu_actions.xml rename to estate/views/actions_menu_and_button.xml index 75ec20d216..f916463578 100644 --- a/estate/views/menu_actions.xml +++ b/estate/views/actions_menu_and_button.xml @@ -1,23 +1,31 @@ - + Real Estate Properties estate.property - list,form + kanban,list,form {'search_default_available': True} - + Property Types estate.property.types list,form - + Property Tags estate.property.tags list + + + + Offers + estate.property.offers + [('property_type_id', '=', active_id)] + list,form + \ No newline at end of file diff --git a/estate/views/actions_smart_button.xml b/estate/views/actions_smart_button.xml deleted file mode 100644 index 3562afb239..0000000000 --- a/estate/views/actions_smart_button.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Offers - estate.property.offers - [('property_type_id', '=', active_id)] - list,form - - \ No newline at end of file diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offer_views.xml similarity index 84% rename from estate/views/estate_property_offers_views.xml rename to estate/views/estate_property_offer_views.xml index 0867a2cfbd..85315e7795 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,7 +1,7 @@ - - estate.property.offers.tree + + estate.property.offer.list estate.property.offers - - estate.property.offers.tree + + estate.property.offer.form estate.property.offers diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tag_views.xml similarity index 73% rename from estate/views/estate_property_tags_views.xml rename to estate/views/estate_property_tag_views.xml index bd3d3dd1fc..f31b3f0bc7 100644 --- a/estate/views/estate_property_tags_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -1,8 +1,8 @@ - - estate.property.tags.tree + + estate.property.tag.list estate.property.tags @@ -13,8 +13,8 @@ - - estate.property.tags.search + + estate.property.tag.search estate.property.tags search diff --git a/estate/views/estate_property_types_views.xml b/estate/views/estate_property_type_views.xml similarity index 89% rename from estate/views/estate_property_types_views.xml rename to estate/views/estate_property_type_views.xml index 06b28a220b..cd688c32e1 100644 --- a/estate/views/estate_property_types_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,7 +1,6 @@ - - + estate.property.types.tree estate.property.types @@ -13,14 +12,14 @@ - + estate.property.types.tree estate.property.types
- + + + diff --git a/estate/views/actions_menu_and_button.xml b/estate/views/actions_menu_and_button.xml index f916463578..89859281c7 100644 --- a/estate/views/actions_menu_and_button.xml +++ b/estate/views/actions_menu_and_button.xml @@ -5,6 +5,14 @@ estate.property kanban,list,form {'search_default_available': True} + +

+ No records available. +

+

+ Please check your filters or create new records. +

+
@@ -28,4 +36,4 @@ [('property_type_id', '=', active_id)] list,form - \ No newline at end of file + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 85315e7795..70b3806d5e 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -8,8 +8,8 @@ decoration-danger="status=='refused'"> - +
+
+ +
@@ -60,18 +66,26 @@
- budget.budget.form budget.budget +
+ +