From 29c0511784e8ec46557591ddd5b7f9da3656750e Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Wed, 23 Apr 2025 06:17:04 +0200 Subject: [PATCH 01/18] [ADD] estate: added a new estate module We have added a module as an application with a model a some basic properties like (name, description, expected price ...etc) We have added security rules for read, write, create and delete actions on this model We have added menu items and UI interaction to access a form view of the records we create and we also added attributes to some properties of the model like (copy, default, readonly, required) --- estate/__init__.py | 1 + estate/__manifest__.py | 10 ++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 45 ++++++++++++++++++++++++++ estate/security/ir.model.access.csv | 2 ++ estate/views/estate_menus.xml | 7 ++++ estate/views/estate_property_views.xml | 15 +++++++++ 7 files changed, 81 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/security/ir.model.access.csv create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..b5c9829e1e4 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,10 @@ +{ + 'name': 'Estate', + 'depends': ['base'], + 'application': True, + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' + ] +} \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ 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 00000000000..599d75b64b7 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,45 @@ +from odoo import models, fields +from datetime import datetime +from dateutil.relativedelta import relativedelta + +def _default_date_availability(): + return fields.Date.today() + relativedelta(months=3) + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property Information" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + copy=False, + default=_default_date_availability() + ) + state = fields.Selection( + string='Status', + required=True, + copy=False, + default='new', + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + ) + 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='Garden Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + help="Orientation of the garden" + ) + active = fields.Boolean(default=True) \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ab63520e22b --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +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 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..f57bb515500 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..ab538048204 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,15 @@ + + + + + Properties + estate.property + list,form + +

+ Create a new property listing! +

+
+
+
+
\ No newline at end of file From ede556ed3b28fb287310d79aae06b9556731cb17 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Wed, 23 Apr 2025 06:31:20 +0200 Subject: [PATCH 02/18] [REF] estate: refactored some code Changed the individual definition of menuitems to nested menu items for better readability --- estate/views/estate_menus.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index f57bb515500..b5243efd24b 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,7 @@ - - - - - + + + + + From 84e5068665e50adfd9978d3e7cfd003f8f3a55c2 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Wed, 23 Apr 2025 12:43:15 +0200 Subject: [PATCH 03/18] [REF] estate: added flake8 linting Added linting with flake8 and fixed some linting warnings --- estate/__init__.py | 3 ++- estate/__manifest__.py | 5 ++--- estate/models/__init__.py | 3 ++- estate/models/estate_property.py | 5 +++-- estate/views/estate_property_views.xml | 17 +++++++++++++++++ 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..64a574e1d46 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1,2 @@ -from . import models \ No newline at end of file +from . import models +assert models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b5c9829e1e4..c9f635e0550 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,5 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', - 'views/estate_menus.xml' - ] -} \ No newline at end of file + 'views/estate_menus.xml'] +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..2412cac060a 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ -from . import estate_property \ No newline at end of file +from . import estate_property +assert estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 599d75b64b7..3a4cf7445fc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,10 +1,11 @@ from odoo import models, fields -from datetime import datetime from dateutil.relativedelta import relativedelta + def _default_date_availability(): return fields.Date.today() + relativedelta(months=3) + class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property Information" @@ -42,4 +43,4 @@ class EstateProperty(models.Model): selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], help="Orientation of the garden" ) - active = fields.Boolean(default=True) \ No newline at end of file + active = fields.Boolean(default=True) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ab538048204..0905a5216d1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -11,5 +11,22 @@

+ + + + estate.property.list + estate.property + + + + + + + + + + + + \ No newline at end of file From ef955eae3b49ce440c1003c8bac1aedc87a3f0a2 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Wed, 23 Apr 2025 13:41:54 +0200 Subject: [PATCH 04/18] [REF] estate: fixed review warnings fixed review warnings --- estate/security/ir.model.access.csv | 2 +- estate/views/estate_property_views.xml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index ab63520e22b..98f4671fb0d 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ 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 \ No newline at end of file +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 0905a5216d1..db248a923c3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -12,7 +12,6 @@ - estate.property.list estate.property @@ -29,4 +28,4 @@ - \ No newline at end of file + From 7f831ed533f8d42a942cd6a30f055f1ee55dc441 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Wed, 23 Apr 2025 16:27:49 +0200 Subject: [PATCH 05/18] [IMP] estate: added form view and search view In this commit we added form view with a well formated view of the created properties We also added a search view for searching by name, by selling price, etc .., added a filter by availability and a group by postcode --- estate/views/estate_property_views.xml | 59 ++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index db248a923c3..6face76494b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -24,8 +24,67 @@ + + + + estate.property.view.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.view.search + estate.property + + + + + + + + + + + + + + + From 4c14c9b0e351e216df1b373804a6a5132406dee6 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Thu, 24 Apr 2025 10:54:32 +0200 Subject: [PATCH 06/18] [IMP] estate: chapter7 models relations - Added Estate property type model and linked it with properties as a many2one field - Added buyer and salesperson fields to a property as many2one fields - Added tags to properties as many2many fields - Added offers model and linked it with properties as one2many field --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 5 +++ estate/models/estate_property.py | 5 +++ estate/models/estate_property_offer.py | 17 ++++++++ estate/models/estate_property_tag.py | 7 ++++ estate/models/estate_property_type.py | 7 ++++ estate/security/ir.model.access.csv | 3 ++ estate/views/estate_menus.xml | 7 +++- estate/views/estate_property_offers_views.xml | 32 +++++++++++++++ estate/views/estate_property_tags_views.xml | 39 +++++++++++++++++++ estate/views/estate_property_type_views.xml | 39 +++++++++++++++++++ estate/views/estate_property_views.xml | 16 ++++++++ 12 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py 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_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c9f635e0550..2e9e1b0d341 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,5 +5,8 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_offers_views.xml', 'views/estate_menus.xml'] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2412cac060a..7fcb08a10e1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,7 @@ from . import estate_property +from . import estate_property_type, estate_property_tag, estate_property_offer + assert estate_property +assert estate_property_type +assert estate_property_tag +assert estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3a4cf7445fc..00c9bc8ce0a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -12,6 +12,8 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() + property_tags_ids = fields.Many2many("estate.property.tag") + property_type_id = fields.Many2one("estate.property.type", string="Property Type") postcode = fields.Char() date_availability = fields.Date( copy=False, @@ -43,4 +45,7 @@ class EstateProperty(models.Model): selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], help="Orientation of the garden" ) + salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) + buyer_id = fields.Many2one('res.partner', string='Buyer') + offers_ids = fields.One2many('estate.property.offer', 'property_id') active = fields.Boolean(default=True) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..9b7145f034f --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,17 @@ +from odoo import models, fields + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + + 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) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..e31a37405b7 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + + name = fields.Char(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..a8a71c71476 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 98f4671fb0d..0c0b62b7fee 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index b5243efd24b..9f857a0e95f 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,12 @@ - + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..5f65b89b0b7 --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,32 @@ + + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.view.form + estate.property.offer + +
+ + + + + + + +
+
+
+
+
diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml new file mode 100644 index 00000000000..4d5315d53ab --- /dev/null +++ b/estate/views/estate_property_tags_views.xml @@ -0,0 +1,39 @@ + + + + + Property Tags + estate.property.tag + list,form + +

+ Create a new property tag +

+
+
+ + + estate.property.tag.list + estate.property.tag + + + + + + + + + estate.property.tag.view.form + estate.property.tag + +
+ +

+ +

+
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..3812d3524a7 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,39 @@ + + + + + Property Types + estate.property.type + list,form + +

+ Create a new property type +

+
+
+ + + estate.property.type.list + estate.property.type + + + + + + + + + estate.property.type.view.form + estate.property.type + +
+ +

+ +

+
+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6face76494b..7ef4c139ce8 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -18,6 +18,8 @@ + + @@ -40,6 +42,8 @@ + + @@ -62,6 +66,17 @@ + + + + + + + + + + + @@ -74,6 +89,7 @@ + From ab16cc98073104baea29a62d4b3c6f78deabfaf5 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Thu, 24 Apr 2025 13:24:35 +0200 Subject: [PATCH 07/18] [IMP] estate: chapter8 - computed fields and onchange - Added total area as computed field, as the sum of living area and garden area - Added best price as computed field calculated from the linked offers - Added 2 fields in the offer model: date_deadline and validity and linked them with inverse updates - Added an onchange callback to the garden field to set/reset the values of garden_area and garden_orientation --- estate/__manifest__.py | 1 + estate/models/estate_property.py | 29 ++++++++++++++++++- estate/models/estate_property_offer.py | 23 ++++++++++++++- estate/models/estate_property_tag.py | 1 + estate/models/estate_property_type.py | 1 + estate/views/estate_property_offers_views.xml | 4 +++ estate/views/estate_property_views.xml | 2 ++ 7 files changed, 59 insertions(+), 2 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 2e9e1b0d341..77b141ba6e0 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,6 +2,7 @@ 'name': 'Estate', 'depends': ['base'], 'application': True, + 'license': 'LGPL-3', 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 00c9bc8ce0a..be0dcb402b8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import api, models, fields from dateutil.relativedelta import relativedelta @@ -33,6 +33,7 @@ class EstateProperty(models.Model): ], ) expected_price = fields.Float(required=True) + best_price = fields.Float(string="Best Offer", compute="_compute_best_price") selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) living_area = fields.Integer() @@ -45,7 +46,33 @@ class EstateProperty(models.Model): selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], help="Orientation of the garden" ) + total_area = fields.Integer( + string="Total Area (sqm)", + compute="_compute_total_area", + ) salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) buyer_id = fields.Many2one('res.partner', string='Buyer') offers_ids = fields.One2many('estate.property.offer', 'property_id') active = fields.Boolean(default=True) + + @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('offers_ids.price') + def _compute_best_price(self): + for record in self: + if record.offers_ids: + record.best_price = max(record.offers_ids.mapped('price')) + else: + record.best_price = 0.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 = False diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9b7145f034f..0aad01261ff 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import models, fields +from odoo import api, models, fields +from dateutil.relativedelta import relativedelta + class EstatePropertyOffer(models.Model): _name = "estate.property.offer" @@ -13,5 +15,24 @@ class EstatePropertyOffer(models.Model): ('refused', 'Refused'), ], ) + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") partner_id = fields.Many2one("res.partner", required=True) property_id = fields.Many2one("estate.property", required=True) + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + base_date = record.create_date.date() + else: + base_date = fields.Date.today() + record.date_deadline = base_date + relativedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + if record.create_date: + base_date = record.create_date.date() + else: + base_date = fields.Date.today() + record.validity = (record.date_deadline - base_date).days diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index e31a37405b7..3f730299abb 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,5 +1,6 @@ from odoo import models, fields + class EstatePropertyTag(models.Model): _name = "estate.property.tag" _description = "Real Estate Property Tag" diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index a8a71c71476..ad121b33163 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,5 +1,6 @@ from odoo import models, fields + class EstatePropertyType(models.Model): _name = "estate.property.type" _description = "Real Estate Property Type" diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index 5f65b89b0b7..2bcda5f4e7e 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -8,6 +8,8 @@ + + @@ -22,6 +24,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7ef4c139ce8..bb9753935aa 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -49,6 +49,7 @@ + @@ -63,6 +64,7 @@ + From be5ca2f1239eb72501ab05dfba5a0b3d270f65cf Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Thu, 24 Apr 2025 16:33:15 +0200 Subject: [PATCH 08/18] [IMP] estate: chapter9 - added buttons - Added 2 buttons: cancel and set a property as sold - Added 2 buttons for the offers, accept and refuse - Once an offer is accepted, the selling price and the Buyer are Set in the property info --- estate/models/__init__.py | 4 +++- estate/models/estate_property.py | 17 ++++++++++++++++- estate/models/estate_property_offer.py | 18 ++++++++++++++++++ estate/views/estate_property_offers_views.xml | 2 ++ estate/views/estate_property_views.xml | 10 +++++++--- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 7fcb08a10e1..920403d20c7 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,5 +1,7 @@ from . import estate_property -from . import estate_property_type, estate_property_tag, estate_property_offer +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer assert estate_property assert estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index be0dcb402b8..76faabe18c5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, models, fields +from odoo.exceptions import UserError from dateutil.relativedelta import relativedelta @@ -12,7 +13,7 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() - property_tags_ids = fields.Many2many("estate.property.tag") + property_tag_ids = fields.Many2many("estate.property.tag") property_type_id = fields.Many2one("estate.property.type", string="Property Type") postcode = fields.Char() date_availability = fields.Date( @@ -55,6 +56,20 @@ class EstateProperty(models.Model): offers_ids = fields.One2many('estate.property.offer', 'property_id') active = fields.Boolean(default=True) + def action_set_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError("Canceled properties cannot be set as sold.") + record.state = 'sold' + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError("Sold properties cannot be canceled.") + record.state = 'cancelled' + return True + @api.depends('living_area', 'garden_area') def _compute_total_area(self): for record in self: diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 0aad01261ff..d1668fae4ff 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,8 @@ from odoo import api, models, fields from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError + class EstatePropertyOffer(models.Model): _name = "estate.property.offer" @@ -20,6 +22,22 @@ class EstatePropertyOffer(models.Model): partner_id = fields.Many2one("res.partner", required=True) property_id = fields.Many2one("estate.property", required=True) + def action_accept(self): + for record in self: + accepted_offers = record.property_id.offers_ids.filtered(lambda o: o.status == 'accepted') + if accepted_offers: + raise UserError("Another offer has already been accepted for this property.") + record.status = 'accepted' + record.property_id.state = 'offer_accepted' + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + return True + + def action_refuse(self): + for record in self: + record.status = 'refused' + return True + @api.depends('create_date', 'validity') def _compute_date_deadline(self): for record in self: diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index 2bcda5f4e7e..102ffae2685 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -11,6 +11,8 @@ + +

+ + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9cef72b67d2..6180a05afcb 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,6 +5,7 @@ Properties estate.property list,form + {'search_default_available': True, 'search_default_current': True}

Create a new property listing! @@ -16,7 +17,7 @@ estate.property.list estate.property - + @@ -25,8 +26,8 @@ - - + + @@ -37,8 +38,9 @@

-

@@ -46,8 +48,8 @@

- - + + @@ -66,15 +68,14 @@ - - + + - - + @@ -99,9 +100,9 @@ - + - + From df29c68083d53e55a87e1a3b133b0e04837b6e8c Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Fri, 25 Apr 2025 14:42:14 +0200 Subject: [PATCH 11/18] [FIX] estate: fixed data import order Fixed views imports in the data field in the manifest file --- estate/__manifest__.py | 2 +- estate/views/estate_property_offers_views.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 77b141ba6e0..52beca08015 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,8 +6,8 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_offers_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tags_views.xml', - 'views/estate_property_offers_views.xml', 'views/estate_menus.xml'] } diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index 4d86ceae88d..f2a553741c7 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -4,6 +4,7 @@ Property Offers + [('property_type_id', '=', active_id)] estate.property.offer list,form From 0d101b04f972e9ad26f4f56050df3140bc0a9a06 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Fri, 25 Apr 2025 15:42:43 +0200 Subject: [PATCH 12/18] [IMP] estate: chapter12 - inheritance features - Overrided CRUD methods for property deletion and offer creation logic (don't allow deletion of ongoing properties, and don't create offers lower than the existing ones) - Add properties field to res.user model and add a view to see properties from the user list view --- estate/__manifest__.py | 1 + estate/models/__init__.py | 2 ++ estate/models/estate_property.py | 6 ++++++ estate/models/estate_property_offer.py | 21 +++++++++++++++++++++ estate/models/res_users.py | 11 +++++++++++ estate/views/res_users_views.xml | 16 ++++++++++++++++ 6 files changed, 57 insertions(+) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 52beca08015..b6287355929 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -9,5 +9,6 @@ 'views/estate_property_offers_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tags_views.xml', + 'views/res_users_views.xml', 'views/estate_menus.xml'] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 920403d20c7..7cf7209fcee 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,8 +2,10 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users assert estate_property assert estate_property_type assert estate_property_tag assert estate_property_offer +assert res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 8678989bf24..53f1296a51e 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -79,6 +79,12 @@ def action_cancel(self): record.state = 'cancelled' return True + @api.ondelete(at_uninstall=False) + def _check_unlink_state(self): + for record in self: + if record.state not in ('new', 'cancelled'): + raise UserError("You cannot delete a property that is not in 'New' or 'Cancelled' state.") + @api.constrains('expected_price', 'selling_price') def _check_selling_price(self): for record in self: diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 1171a7661ee..7e291645981 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -2,6 +2,7 @@ from dateutil.relativedelta import relativedelta from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare class EstatePropertyOffer(models.Model): @@ -28,6 +29,26 @@ class EstatePropertyOffer(models.Model): ('check_price', 'CHECK(price > 0)', 'The offer price must be strictly positive.') ] + @api.model_create_multi + def create(self, vals): + for val in vals: + property_id = val.get('property_id') + price = val.get('price') + + prop = self.env['estate.property'].browse(property_id) + + existing_offer_prices = prop.offers_ids.mapped('price') + if existing_offer_prices: + max_existing_offer = max(existing_offer_prices) + if float_compare(price, max_existing_offer, precision_digits=2) < 0: + raise UserError("Cannot create an offer with a price lower than an existing offer") + + if prop.state == 'new': + prop.write({'state': 'offer_received'}) + + new_offer = super(EstatePropertyOffer, self).create(vals) + return new_offer + def action_accept(self): for record in self: accepted_offers = record.property_id.offers_ids.filtered(lambda o: o.status == 'accepted') diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..600464c3d27 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + comodel_name='estate.property', + inverse_name='salesperson_id', + string="Real Estate Properties", + domain=[('state', 'in', ('new', 'offer_received'))] + ) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..e23526afecb --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,16 @@ + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + + \ No newline at end of file From 4878ba53b9354403809779c58e0b12192fc104d4 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Fri, 25 Apr 2025 23:12:15 +0200 Subject: [PATCH 13/18] [IMP] estate: chapter13 - invoice on sold property - Added a new link module estate_account responsible for creating an invoice when the sold button is created - Overriden the action_set_sold method to add the creation of the invoice in it --- estate/models/estate_property_offer.py | 2 +- estate/models/res_users.py | 1 + estate_account/__init__.py | 2 ++ estate_account/__manifest__.py | 12 +++++++ estate_account/models/__init__.py | 3 ++ estate_account/models/estate_property.py | 45 ++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) 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/estate_property.py diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 7e291645981..5d0a76bbbd3 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -46,7 +46,7 @@ def create(self, vals): if prop.state == 'new': prop.write({'state': 'offer_received'}) - new_offer = super(EstatePropertyOffer, self).create(vals) + new_offer = super().create(vals) return new_offer def action_accept(self): diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 600464c3d27..96cb1afc7c5 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -1,5 +1,6 @@ from odoo import fields, models + class ResUsers(models.Model): _inherit = 'res.users' diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..64a574e1d46 --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1,2 @@ +from . import models +assert models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..4b6b1c3455a --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,12 @@ +{ + 'name': "Real Estate Accounting Link", + 'depends': [ + 'estate', + 'account', + ], + 'data': [ + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..a126cc40c5b --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1,3 @@ +from . import estate_property + +assert estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..a5f255f8420 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,45 @@ +from odoo import models, Command +from odoo.exceptions import UserError + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_set_sold(self): + res = super().action_set_sold() + + if not self: + return res + + for record in self: + if not record.buyer_id: + raise UserError("Cannot create invoice: Buyer is not set for property") + + if record.selling_price <= 0: + raise UserError("Cannot create invoice: Selling price must be positive for property") + + commission = record.selling_price * 0.06 + admin_fees = 100.00 + + invoice_line_commands = [ + Command.create({ + 'name': '6% Commission', + 'quantity': 1, + 'price_unit': commission, + }), + Command.create({ + 'name': 'Administrative Fees', + 'quantity': 1, + 'price_unit': admin_fees, + }), + ] + + invoice_vals = { + 'partner_id': record.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': invoice_line_commands, + } + + self.env['account.move'].create(invoice_vals) + + return res From d27033691fff30172bdd07596294aa14fafb1f4c Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Mon, 28 Apr 2025 16:02:12 +0200 Subject: [PATCH 14/18] [IMP] estate: chapter14 - Add kanban view Added kanban view grouped by property type for properties --- estate/models/estate_property.py | 6 ++++- estate/views/estate_property_views.xml | 34 +++++++++++++++++++++++++- estate_account/__manifest__.py | 2 +- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 53f1296a51e..08fde83bd65 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -18,7 +18,7 @@ class EstateProperty(models.Model): description = fields.Text() sequence = fields.Integer('Sequence', default=10) property_tag_ids = fields.Many2many("estate.property.tag") - property_type_id = fields.Many2one("estate.property.type", string="Property Type") + property_type_id = fields.Many2one("estate.property.type", string="Property Type", group_expand="group_by_empty") postcode = fields.Char() date_availability = fields.Date( copy=False, @@ -65,6 +65,10 @@ class EstateProperty(models.Model): ('check_selling_price', 'CHECK(selling_price >= 0)', 'The selling price must be positive.'), ] + @api.model + def group_by_empty(self, types, domain): + return types.search([]) # this is equivalent to return self.env['estate.property.type'].search([]) + def action_set_sold(self): for record in self: if record.state == 'cancelled': diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6180a05afcb..5df06e41802 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,7 +4,7 @@ Properties estate.property - list,form + list,form,kanban {'search_default_available': True, 'search_default_current': True}

@@ -32,6 +32,38 @@ + + estate.property.kanban + estate.property + + + + + + + + + +

Expected Price: + +
+ +
+ Best Offer: +
+
+ Selling Price: +
+ +
+ +
+ + + +
+
+ estate.property.view.form estate.property diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 4b6b1c3455a..d0bb2361f67 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -9,4 +9,4 @@ 'installable': True, 'application': False, 'license': 'LGPL-3', -} \ No newline at end of file +} From 77d0bd8af5156c59c187d21424ee5a92aaf5f706 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Tue, 29 Apr 2025 17:28:40 +0200 Subject: [PATCH 15/18] [IMP] estate: chapter15 - corrected code after review - Removed un necessary assert (for the unused imports in the __init__ files) - Renamed looping variables' names for better readability (property instead of record, and offer instead of record) - Added auto_install property to True in the estate_account link module (used to auto install dependencies when we install estate_account) --- estate/__manifest__.py | 6 +++ estate/models/__init__.py | 6 --- estate/models/estate_property.py | 50 ++++++++++++------------ estate/models/estate_property_offer.py | 48 ++++++++++++----------- estate/views/estate_property_views.xml | 2 +- estate/views/res_users_views.xml | 2 +- estate_account/__manifest__.py | 7 ++++ estate_account/models/estate_property.py | 16 ++++---- 8 files changed, 73 insertions(+), 64 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b6287355929..1c52ad2b569 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,11 @@ { 'name': 'Estate', + 'summary': "Module to manage properties", + 'description': """ +This module is used to manage any type of properties and also to manage the selling pipeline for each property + """, + 'author': 'Odoo', + 'version': '1.0', 'depends': ['base'], 'application': True, 'license': 'LGPL-3', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 7cf7209fcee..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -3,9 +3,3 @@ from . import estate_property_tag from . import estate_property_offer from . import res_users - -assert estate_property -assert estate_property_type -assert estate_property_tag -assert estate_property_offer -assert res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 08fde83bd65..1bae9b5c633 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,11 +1,11 @@ -from odoo import api, models, fields -from odoo.exceptions import UserError, ValidationError from dateutil.relativedelta import relativedelta +from odoo import _, api, models, fields +from odoo.exceptions import UserError, ValidationError from odoo.tools.float_utils import float_compare, float_is_zero -def _default_date_availability(): +def _default_date_availability(_): return fields.Date.today() + relativedelta(months=3) @@ -22,7 +22,7 @@ class EstateProperty(models.Model): postcode = fields.Char() date_availability = fields.Date( copy=False, - default=_default_date_availability() + default=_default_date_availability ) state = fields.Selection( string='Status', @@ -70,46 +70,46 @@ def group_by_empty(self, types, domain): return types.search([]) # this is equivalent to return self.env['estate.property.type'].search([]) def action_set_sold(self): - for record in self: - if record.state == 'cancelled': - raise UserError("Canceled properties cannot be set as sold.") - record.state = 'sold' + for property in self: + if property.state == 'cancelled': + raise UserError(_("Canceled properties cannot be set as sold.")) + property.state = 'sold' return True def action_cancel(self): - for record in self: - if record.state == 'sold': - raise UserError("Sold properties cannot be canceled.") - record.state = 'cancelled' + for property in self: + if property.state == 'sold': + raise UserError(_("Sold properties cannot be canceled.")) + property.state = 'cancelled' return True @api.ondelete(at_uninstall=False) def _check_unlink_state(self): - for record in self: - if record.state not in ('new', 'cancelled'): - raise UserError("You cannot delete a property that is not in 'New' or 'Cancelled' state.") + for property in self: + if property.state not in ('new', 'cancelled'): + raise UserError(_("You cannot delete a property that is not in 'New' or 'Cancelled' state.")) @api.constrains('expected_price', 'selling_price') def _check_selling_price(self): - for record in self: - if not float_is_zero(record.expected_price, precision_digits=2): - accepted_offers = record.offers_ids.filtered(lambda o: o.status == 'accepted') + for property in self: + if not float_is_zero(property.expected_price, precision_digits=2): + accepted_offers = property.offers_ids.filtered(lambda o: o.status == 'accepted') if accepted_offers: - if float_compare(record.selling_price, record.expected_price * 0.90, precision_digits=2) == -1: + if float_compare(property.selling_price, property.expected_price * 0.90, precision_digits=2) == -1: raise ValidationError("Selling price cannot be lower than 90 percent of expected price") @api.depends('living_area', 'garden_area') def _compute_total_area(self): - for record in self: - record.total_area = record.living_area + record.garden_area + for property in self: + property.total_area = property.living_area + property.garden_area @api.depends('offers_ids.price') def _compute_best_price(self): - for record in self: - if record.offers_ids: - record.best_price = max(record.offers_ids.mapped('price')) + for property in self: + if property.offers_ids: + property.best_price = max(property.offers_ids.mapped('price')) else: - record.best_price = 0.0 + property.best_price = 0.0 @api.onchange('garden') def _onchange_garden(self): diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5d0a76bbbd3..12a4c6b5739 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import api, models, fields +from odoo import _, api, models, fields from dateutil.relativedelta import relativedelta from odoo.exceptions import UserError @@ -30,10 +30,12 @@ class EstatePropertyOffer(models.Model): ] @api.model_create_multi - def create(self, vals): - for val in vals: + def create(self, vals_list): + for val in vals_list: property_id = val.get('property_id') - price = val.get('price') + if not property_id: + continue + price = val.get('price', 0) prop = self.env['estate.property'].browse(property_id) @@ -41,43 +43,43 @@ def create(self, vals): if existing_offer_prices: max_existing_offer = max(existing_offer_prices) if float_compare(price, max_existing_offer, precision_digits=2) < 0: - raise UserError("Cannot create an offer with a price lower than an existing offer") + raise UserError(_("Cannot create an offer with a price lower than an existing offer")) if prop.state == 'new': prop.write({'state': 'offer_received'}) - new_offer = super().create(vals) + new_offer = super().create(vals_list) return new_offer def action_accept(self): - for record in self: - accepted_offers = record.property_id.offers_ids.filtered(lambda o: o.status == 'accepted') + for offer in self: + accepted_offers = offer.property_id.offers_ids.filtered(lambda o: o.status == 'accepted') if accepted_offers: - raise UserError("Another offer has already been accepted for this property.") - record.status = 'accepted' - record.property_id.state = 'offer_accepted' - record.property_id.selling_price = record.price - record.property_id.buyer_id = record.partner_id + raise UserError(_("Another offer has already been accepted for this property.")) + offer.status = 'accepted' + offer.property_id.state = 'offer_accepted' + offer.property_id.selling_price = offer.price + offer.property_id.buyer_id = offer.partner_id return True def action_refuse(self): - for record in self: - record.status = 'refused' + for offer in self: + offer.status = 'refused' return True @api.depends('create_date', 'validity') def _compute_date_deadline(self): - for record in self: - if record.create_date: - base_date = record.create_date.date() + for offer in self: + if offer.create_date: + base_date = offer.create_date.date() else: base_date = fields.Date.today() - record.date_deadline = base_date + relativedelta(days=record.validity) + offer.date_deadline = base_date + relativedelta(days=offer.validity) def _inverse_date_deadline(self): - for record in self: - if record.create_date: - base_date = record.create_date.date() + for offer in self: + if offer.create_date: + base_date = offer.create_date.date() else: base_date = fields.Date.today() - record.validity = (record.date_deadline - base_date).days + offer.validity = (offer.date_deadline - base_date).days diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5df06e41802..9b3740af78f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -136,7 +136,7 @@ - + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml index e23526afecb..52ae487ee2c 100644 --- a/estate/views/res_users_views.xml +++ b/estate/views/res_users_views.xml @@ -13,4 +13,4 @@
- \ No newline at end of file + diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index d0bb2361f67..296e966863d 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -1,5 +1,11 @@ { 'name': "Real Estate Accounting Link", + 'summary': "Link estate and account modules for creating invoice", + 'description': """ +This module is used as a bridge between estate and account modules in order to create an invoice automatically when marking a property as sold + """, + 'author': 'Odoo', + 'version': '1.0', 'depends': [ 'estate', 'account', @@ -7,6 +13,7 @@ 'data': [ ], 'installable': True, + 'auto_install': True, 'application': False, 'license': 'LGPL-3', } diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index a5f255f8420..bb940b08dc4 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import models, Command +from odoo import _, models, Command from odoo.exceptions import UserError @@ -11,14 +11,14 @@ def action_set_sold(self): if not self: return res - for record in self: - if not record.buyer_id: - raise UserError("Cannot create invoice: Buyer is not set for property") + for property in self: + if not property.buyer_id: + raise UserError(_("Cannot create invoice: Buyer is not set for property")) - if record.selling_price <= 0: - raise UserError("Cannot create invoice: Selling price must be positive for property") + if property.selling_price <= 0: + raise UserError(_("Cannot create invoice: Selling price must be positive for property")) - commission = record.selling_price * 0.06 + commission = property.selling_price * 0.06 admin_fees = 100.00 invoice_line_commands = [ @@ -35,7 +35,7 @@ def action_set_sold(self): ] invoice_vals = { - 'partner_id': record.buyer_id.id, + 'partner_id': property.buyer_id.id, 'move_type': 'out_invoice', 'invoice_line_ids': invoice_line_commands, } From 9c751ec019cd88a745a14c0119cdc1fddfc90a38 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Wed, 30 Apr 2025 10:58:34 +0200 Subject: [PATCH 16/18] [IMP] Web Framework Tuto - chapter 1 - Added Card and Counter components - Added markup html - Added props validation in Card --- awesome_owl/static/src/Card/card.js | 8 ++++++++ awesome_owl/static/src/Card/card.xml | 17 +++++++++++++++++ awesome_owl/static/src/Counter/counter.js | 13 +++++++++++++ awesome_owl/static/src/Counter/counter.xml | 13 +++++++++++++ awesome_owl/static/src/playground.js | 8 +++++++- awesome_owl/static/src/playground.xml | 5 +++-- 6 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 awesome_owl/static/src/Card/card.js create mode 100644 awesome_owl/static/src/Card/card.xml create mode 100644 awesome_owl/static/src/Counter/counter.js create mode 100644 awesome_owl/static/src/Counter/counter.xml diff --git a/awesome_owl/static/src/Card/card.js b/awesome_owl/static/src/Card/card.js new file mode 100644 index 00000000000..4a1457acb24 --- /dev/null +++ b/awesome_owl/static/src/Card/card.js @@ -0,0 +1,8 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static props = ['title', 'content']; + static template = "awesome_owl.card"; +} diff --git a/awesome_owl/static/src/Card/card.xml b/awesome_owl/static/src/Card/card.xml new file mode 100644 index 00000000000..885aa8afe44 --- /dev/null +++ b/awesome_owl/static/src/Card/card.xml @@ -0,0 +1,17 @@ + + + + +
+
+
+ +
+

+ +

+
+
+
+ +
diff --git a/awesome_owl/static/src/Counter/counter.js b/awesome_owl/static/src/Counter/counter.js new file mode 100644 index 00000000000..b1bceb21b68 --- /dev/null +++ b/awesome_owl/static/src/Counter/counter.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + counter = useState({ value: 0 }); + + increment() { + this.counter.value++; + } +} diff --git a/awesome_owl/static/src/Counter/counter.xml b/awesome_owl/static/src/Counter/counter.xml new file mode 100644 index 00000000000..b038c9cf079 --- /dev/null +++ b/awesome_owl/static/src/Counter/counter.xml @@ -0,0 +1,13 @@ + + + + +
+

+ +

+ +
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..d41d97aa9ac 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,13 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, markup } from "@odoo/owl"; +import { Counter } from './Counter/counter'; +import { Card } from './Card/card'; export class Playground extends Component { static template = "awesome_owl.playground"; + + html = markup("
some content
") + + static components = { Card }; } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..6d09e569678 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,8 +2,9 @@ -
- hello world +
+ +
From b4e70b6c88a87b2947707a3d241a4a967e7f79f4 Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Fri, 2 May 2025 13:17:48 +0200 Subject: [PATCH 17/18] [IMP] Web Framework Tuto - chapter 1 - Added todo list (with adding and deleting functionnality) - Added dynamic attributes for setting a todo item to Done - Added Focusing the input - Modified the Card component to accept a default slot as content instead of a simple text - Added toggle hide or show the content of the card --- awesome_owl/static/src/Card/card.js | 18 +++++++++-- awesome_owl/static/src/Card/card.xml | 5 +-- awesome_owl/static/src/Counter/counter.js | 8 ++++- awesome_owl/static/src/ToDo/todo_item.js | 18 +++++++++++ awesome_owl/static/src/ToDo/todo_item.xml | 17 ++++++++++ awesome_owl/static/src/ToDo/todo_list.js | 38 +++++++++++++++++++++++ awesome_owl/static/src/ToDo/todo_list.xml | 15 +++++++++ awesome_owl/static/src/playground.js | 10 ++++-- awesome_owl/static/src/playground.xml | 7 +++-- awesome_owl/static/src/utils.js | 10 ++++++ 10 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 awesome_owl/static/src/ToDo/todo_item.js create mode 100644 awesome_owl/static/src/ToDo/todo_item.xml create mode 100644 awesome_owl/static/src/ToDo/todo_list.js create mode 100644 awesome_owl/static/src/ToDo/todo_list.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/Card/card.js b/awesome_owl/static/src/Card/card.js index 4a1457acb24..a5e68959380 100644 --- a/awesome_owl/static/src/Card/card.js +++ b/awesome_owl/static/src/Card/card.js @@ -1,8 +1,22 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; export class Card extends Component { - static props = ['title', 'content']; + static props = { + title: String, + slots: { + type: Object, + } + }; + + setup() { + this.state = useState({ isOpen: true }); + } + + toggleIsOpen() { + this.state.isOpen = !this.state.isOpen; + } + static template = "awesome_owl.card"; } diff --git a/awesome_owl/static/src/Card/card.xml b/awesome_owl/static/src/Card/card.xml index 885aa8afe44..be29308e399 100644 --- a/awesome_owl/static/src/Card/card.xml +++ b/awesome_owl/static/src/Card/card.xml @@ -6,9 +6,10 @@
+
-

- +

+

diff --git a/awesome_owl/static/src/Counter/counter.js b/awesome_owl/static/src/Counter/counter.js index b1bceb21b68..b9355c6a26c 100644 --- a/awesome_owl/static/src/Counter/counter.js +++ b/awesome_owl/static/src/Counter/counter.js @@ -3,11 +3,17 @@ import { Component, useState } from "@odoo/owl"; export class Counter extends Component { + static props = { onChange: { type: Function, optional: true } }; static template = "awesome_owl.counter"; - counter = useState({ value: 0 }); + setup() { + this.counter = useState({ value: 0 }); + } increment() { this.counter.value++; + if (this.props.onChange) { + this.props.onChange(); + } } } diff --git a/awesome_owl/static/src/ToDo/todo_item.js b/awesome_owl/static/src/ToDo/todo_item.js new file mode 100644 index 00000000000..f65f9f0f427 --- /dev/null +++ b/awesome_owl/static/src/ToDo/todo_item.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static props = { + id: Number, description: String, isCompleted: Boolean, toggle: Function, removeTodo: Function + }; + static template = "awesome_owl.todo_item"; + + toggle(_) { + this.props.toggle(this.props.id); + } + + removeTodo(_) { + this.props.removeTodo(this.props.id); + } +} diff --git a/awesome_owl/static/src/ToDo/todo_item.xml b/awesome_owl/static/src/ToDo/todo_item.xml new file mode 100644 index 00000000000..c8fdb9006d8 --- /dev/null +++ b/awesome_owl/static/src/ToDo/todo_item.xml @@ -0,0 +1,17 @@ + + + + +
+ + +. + + + +
+
+ +
diff --git a/awesome_owl/static/src/ToDo/todo_list.js b/awesome_owl/static/src/ToDo/todo_list.js new file mode 100644 index 00000000000..060d409abc2 --- /dev/null +++ b/awesome_owl/static/src/ToDo/todo_list.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +import { Component, useState, setState } from "@odoo/owl"; +import { TodoItem } from "./todo_item" +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + + static components = { TodoItem }; + + setup() { + this.todos = useState(new Array()); + useAutofocus("input"); + } + + onInputEnter(event) { + if (event.keyCode === 13 && event.target.value != "") { + this.todos.push({ "id": this.todos.length + 1, "description": event.target.value, "isCompleted": false }); + event.target.value = ""; + } + } + + toggle(id) { + for (let todo of this.todos) { + if (todo.id === id) { + todo.isCompleted = !todo.isCompleted; + } + } + } + + removeTodo(id) { + const index = this.todos.findIndex((elem) => elem.id === id); + if (index >= 0) { + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/ToDo/todo_list.xml b/awesome_owl/static/src/ToDo/todo_list.xml new file mode 100644 index 00000000000..c7922fd3f14 --- /dev/null +++ b/awesome_owl/static/src/ToDo/todo_list.xml @@ -0,0 +1,15 @@ + + + + +
+ + +

+ +

+
+
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index d41d97aa9ac..4d5095cf761 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,13 +1,19 @@ /** @odoo-module **/ -import { Component, markup } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; import { Counter } from './Counter/counter'; import { Card } from './Card/card'; +import { TodoList } from "./ToDo/todo_list"; + export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { TodoList, Card, Counter }; html = markup("
some content
") + counters_sum = useState({ value: 0 }); - static components = { Card }; + incrementSum() { + this.counters_sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 6d09e569678..4bd8207b1fe 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -3,9 +3,12 @@
- - +
+ + + +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..c6b7cce0d5d --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,10 @@ +import { useRef, useEffect } from "@odoo/owl"; + + +export function useAutofocus(name) { + let ref = useRef(name); + useEffect( + (el) => el && el.focus(), + () => [ref.el] + ); +} \ No newline at end of file From 2fdc85df310c8afbf7ed37620acfbf689136661e Mon Sep 17 00:00:00 2001 From: abeg-odoo Date: Mon, 5 May 2025 11:33:16 +0200 Subject: [PATCH 18/18] [IMP] Web Framework Tuto - chapter 2 - Added dashboard layout - Added a statistics service for getting statistics using rpc call - Cached the statistics using memoize --- awesome_dashboard/controllers/controllers.py | 1 - awesome_dashboard/static/src/dashboard.js | 39 ++++++++++++++++++- awesome_dashboard/static/src/dashboard.scss | 5 +++ awesome_dashboard/static/src/dashboard.xml | 17 +++++++- .../static/src/dashboard_item.js | 22 +++++++++++ .../static/src/dashboard_item.xml | 10 +++++ .../static/src/statistics_service.js | 20 ++++++++++ 7 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 56d4a051287..eafba6ffbd0 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -33,4 +33,3 @@ def get_statistics(self): }, 'total_amount': random.randint(100, 1000) } - diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 637fa4bb972..30c0f94e84c 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,10 +1,47 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, onWillStart, useState } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item"; +import { rpc } from "@web/core/network/rpc"; + class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; + static components = { + Layout, + DashboardItem + }; + + async setup() { + this.action = useService("action"); + console.log("test"); + this.statisticsService = useService("statistics"); + + onWillStart(async () => { + this.statistics = await this.statisticsService.getStatisticsMemoize(); + }); + console.log(this.statistics); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "All leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + }); + } + } registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss new file mode 100644 index 00000000000..42a500ef0a2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.scss @@ -0,0 +1,5 @@ +$bg-gray: rgb(150, 150, 150); + +.o_dashboard { + background-color: $bg-gray; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 1a2ac9a2fed..9d42cd0809c 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -2,7 +2,22 @@ - hello dashboard + + + + + + the average number of t-shirts by order + + + + + the average time (in hours) elapsed between the + moment an order is created, and the moment is it sent + + + + diff --git a/awesome_dashboard/static/src/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item.js new file mode 100644 index 00000000000..6a47be984a6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + slots: { + type: Object, + }, + size: { + type: Number, + optional: true, + } + } + static defaultProps = { + size: 1, + } + +} diff --git a/awesome_dashboard/static/src/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item.xml new file mode 100644 index 00000000000..d4fa836a0e0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ +
diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..0b8b6d5602c --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,20 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; + + +const getStatisticsMemoize = memoize(async () => { + return rpc("/awesome_dashboard/statistics"); +}); + +const getStatistics = (async () => { + return rpc("/awesome_dashboard/statistics"); +}); + +const statisticsService = { + start() { + return { getStatistics, getStatisticsMemoize }; + }, +}; + +registry.category("services").add("statistics", statisticsService);