From d98b8561dcd93a70f8ce2547b9b2c3915142fc75 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Mon, 16 Dec 2024 14:29:28 +0100 Subject: [PATCH 01/25] [ADD] estate: start new module for training --- estate/__init__.py | 1 + estate/__manifest__.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 0000000000..7c68785e9d --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 0000000000..756368f868 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Real Estate', + 'depends': ['base'], + 'application': True, + 'installable': True, +} \ No newline at end of file From 6123ef7a49eb57c3b6c3ecbc0b748e1c7d15ffdb Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Mon, 16 Dec 2024 15:53:38 +0100 Subject: [PATCH 02/25] [IMP] add fields for real estate table --- estate/__init__.py | 4 +++- estate/models/__init__.py | 3 +++ estate/models/estate_property.py | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index 7c68785e9d..5305644df1 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1,3 @@ -# -*- coding: utf-8 -*- \ No newline at end of file +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 0000000000..f2db223b7d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +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 0000000000..871f02b4ce --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")] + ) + + + From 7d89dad87a87262ed7a87f1f30f0c20c126974cb Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Mon, 16 Dec 2024 16:10:14 +0100 Subject: [PATCH 03/25] [IMP] estate: add security access rules --- estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 756368f868..6b4ca6bf5a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,4 +5,7 @@ 'depends': ['base'], 'application': True, 'installable': True, + 'data': [ + 'security/ir.model.access.csv' + ] } \ 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 0000000000..fe21e56c6d --- /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 From 42bf41b67ed5a1b06f1414d625f4e827256f3b30 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Mon, 16 Dec 2024 17:18:04 +0100 Subject: [PATCH 04/25] [IMP] estate: add views and menu --- estate/__manifest__.py | 4 +++- estate/models/estate_property.py | 16 ++++++++++------ estate/views/estate_menus.xml | 10 ++++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6b4ca6bf5a..741992e88d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,6 +6,8 @@ 'application': True, 'installable': True, 'data': [ - 'security/ir.model.access.csv' + '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/estate_property.py b/estate/models/estate_property.py index 871f02b4ce..99fb6d75bf 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,10 +9,10 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date(copy=False, default=lambda x: fields.Datetime.add(fields.Datetime.today(), months=3)) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -21,6 +21,10 @@ class EstateProperty(models.Model): garden_orientation = fields.Selection( selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")] ) - - - + active = fields.Boolean(default=True) + state = fields.Selection( + required=True, + copy=False, + default="new", + selection=[("new", "New"), ("offer_received", "Offer received"), ("offer_accepted", "Offer accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")] + ) \ 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 0000000000..14b7a6cbb8 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ 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..affda3b8ab --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + \ No newline at end of file From 973bf386084665318c64072436000b131539479e Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Tue, 17 Dec 2024 15:53:20 +0100 Subject: [PATCH 05/25] [IMP] estate: add list, form and search views --- estate/__init__.py | 4 +- estate/__manifest__.py | 22 ++++----- estate/models/__init__.py | 4 +- estate/models/estate_property.py | 25 +++++++--- estate/views/estate_menus.xml | 4 +- estate/views/estate_property_views.xml | 68 ++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 27 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 5305644df1..0650744f6b 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - -from . import models \ No newline at end of file +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 741992e88d..161abe3961 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- - { - 'name': 'Real Estate', - 'depends': ['base'], - 'application': True, - 'installable': True, - 'data': [ - 'security/ir.model.access.csv', - 'views/estate_property_views.xml', - 'views/estate_menus.xml' - ] -} \ No newline at end of file + "name": "Real Estate", + "depends": ["base"], + "application": True, + "installable": True, + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_menus.xml", + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f2db223b7d..5e1963c9d2 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 99fb6d75bf..b0b79038e6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- - from odoo import fields, models + class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property" @@ -9,7 +8,10 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(copy=False, default=lambda x: fields.Datetime.add(fields.Datetime.today(), months=3)) + date_availability = fields.Date( + copy=False, + default=lambda x: fields.Datetime.add(fields.Datetime.today(), months=3), + ) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) @@ -19,12 +21,23 @@ class EstateProperty(models.Model): garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( - selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")] + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ] ) active = fields.Boolean(default=True) state = fields.Selection( required=True, copy=False, default="new", - selection=[("new", "New"), ("offer_received", "Offer received"), ("offer_accepted", "Offer accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")] - ) \ No newline at end of file + selection=[ + ("new", "New"), + ("offer_received", "Offer received"), + ("offer_accepted", "Offer accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 14b7a6cbb8..bdde9118d9 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -2,9 +2,7 @@ - - - + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index affda3b8ab..346ec67452 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,72 @@ estate.property list,form + + estate.property.list + estate.property + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + estate.property.search + estate.property + + + + + + + + + + + + + \ No newline at end of file From 720e52aaeafb9e86f38739af0785ce6addd8bc40 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Wed, 18 Dec 2024 12:42:27 +0100 Subject: [PATCH 06/25] [IMP] estate: relations between models --- estate/__manifest__.py | 4 +++ estate/models/__init__.py | 2 +- estate/models/estate_property.py | 5 ++++ estate/models/estate_property_offer.py | 11 ++++++++ estate/models/estate_property_tag.py | 8 ++++++ estate/models/estate_property_type.py | 9 ++++++ estate/security/ir.model.access.csv | 5 +++- estate/views/estate_menus.xml | 6 +++- estate/views/estate_property_offer_views.xml | 29 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 8 ++++++ estate/views/estate_property_type_views.xml | 8 ++++++ estate/views/estate_property_views.xml | 23 +++++++++++++--- 12 files changed, 111 insertions(+), 7 deletions(-) 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_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 161abe3961..28ed1b2405 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,9 +3,13 @@ "depends": ["base"], "application": True, "installable": True, + "license": "LGPL-3", "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offer_views.xml", "views/estate_menus.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2..93a6bd86ab 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property +from . import estate_property, estate_property_type, estate_property_tag, estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b0b79038e6..b397f43cb9 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -41,3 +41,8 @@ class EstateProperty(models.Model): ("cancelled", "Cancelled"), ], ) + property_type_id = fields.Many2one(comodel_name="estate.property.type") + buyer_id = fields.Many2one(comodel_name="res.partner", copy=False) + salesman_id = fields.Many2one(comodel_name="res.users", default=lambda self: self.env.user) + tag_ids = fields.Many2many(comodel_name="estate.property.tag") + offer_ids = fields.One2many(comodel_name="estate.property.offer", inverse_name="property_id") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 0000000000..93e8273f9d --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + + price = fields.Float() + status = fields.Selection(copy=False, selection=[("accepted", "Accepted"), ("refused", "Refused")]) + partner_id = fields.Many2one(comodel_name="res.partner", required=True) + property_id = fields.Many2one(comodel_name="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 0000000000..198c1037c4 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +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 0000000000..c854044b3b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +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 fe21e56c6d..78458c1cb6 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 \ No newline at end of file +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 bdde9118d9..54a9f04911 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,9 @@ + + + + - \ No newline at end of file + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 0000000000..d922b61b46 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,29 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 0000000000..4dcd3674e3 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 0000000000..e50051fee3 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 346ec67452..7c97d8d53b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -12,6 +12,7 @@ + @@ -29,8 +30,11 @@

+ +

+ @@ -52,6 +56,15 @@ + + + + + + + + + @@ -61,16 +74,18 @@ estate.property.search estate.property - + + - - + + + - \ No newline at end of file + From eb9b85a25b8b5f1a66165d0c657e46ba5bfa25e3 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Wed, 18 Dec 2024 14:09:02 +0100 Subject: [PATCH 07/25] [IMP] estate: add compute and onchange fields --- estate/models/estate_property.py | 33 ++++++++++++++++++-- estate/models/estate_property_offer.py | 23 ++++++++++++-- estate/views/estate_property_offer_views.xml | 6 +++- estate/views/estate_property_views.xml | 5 ++- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b397f43cb9..b30704b0f8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -43,6 +43,33 @@ class EstateProperty(models.Model): ) property_type_id = fields.Many2one(comodel_name="estate.property.type") buyer_id = fields.Many2one(comodel_name="res.partner", copy=False) - salesman_id = fields.Many2one(comodel_name="res.users", default=lambda self: self.env.user) + salesman_id = fields.Many2one( + comodel_name="res.users", default=lambda self: self.env.user + ) tag_ids = fields.Many2many(comodel_name="estate.property.tag") - offer_ids = fields.One2many(comodel_name="estate.property.offer", inverse_name="property_id") + offer_ids = fields.One2many( + comodel_name="estate.property.offer", inverse_name="property_id" + ) + total_area = fields.Integer(compute="_compute_total_area") + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + best_price = fields.Float(compute="_compute_best_price") + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + prices = record.offer_ids.mapped("price") + record.best_price = max(prices) if len(prices) else 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 = "" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 93e8273f9d..ea3d3af8d6 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ -from odoo import fields, models +from odoo import api, fields, models +from odoo.tools.date_utils import date class EstatePropertyOffer(models.Model): @@ -6,6 +7,24 @@ class EstatePropertyOffer(models.Model): _description = "Real Estate Property Offer" price = fields.Float() - status = fields.Selection(copy=False, selection=[("accepted", "Accepted"), ("refused", "Refused")]) + status = fields.Selection( + copy=False, selection=[("accepted", "Accepted"), ("refused", "Refused")] + ) partner_id = fields.Many2one(comodel_name="res.partner", required=True) property_id = fields.Many2one(comodel_name="estate.property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", inverse="_inverse_date_deadline" + ) + + @api.depends("create_date", "validity") + def _compute_date_deadline(self): + for record in self: + create_date = record.create_date if record.create_date else date.today() + record.date_deadline = fields.Datetime.add( + create_date, days=record.validity + ) + + def _inverse_date_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index d922b61b46..4f13338da2 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,6 +7,8 @@ + +
@@ -19,8 +21,10 @@ - + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7c97d8d53b..4744da0947 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -31,7 +31,8 @@ -

+
+
@@ -40,6 +41,7 @@ + @@ -54,6 +56,7 @@ + From 66a5c8dff2abba760faf355603374a4ffa43f40f Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Wed, 18 Dec 2024 15:19:18 +0100 Subject: [PATCH 08/25] [IMP] estate: add sold/cancel and accept/refuse buttons --- estate/models/estate_property.py | 15 ++++++++++++++ estate/models/estate_property_offer.py | 21 ++++++++++++++++++++ estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 5 +++++ 4 files changed, 43 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b30704b0f8..45e6a5696f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -73,3 +74,17 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = "" + + def action_property_sold(self): + self.ensure_one() + if self.state == "cancelled": + raise UserError("Cancelled properties cannot be sold.") + self.state = "sold" + return True + + def action_property_cancelled(self): + self.ensure_one() + if self.state == "sold": + raise UserError("Sold properties cannot be cancelled.") + self.state = "cancelled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ea3d3af8d6..319e6889c3 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ from odoo import api, fields, models from odoo.tools.date_utils import date +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -28,3 +29,23 @@ def _compute_date_deadline(self): def _inverse_date_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days + + def action_accept_offer(self): + self.ensure_one() + other_records = self.search([("id", "!=", str(self.id))]) + for rec in other_records: + if rec.status == "accepted": + raise UserError("Cannot have more than one accepted offer.") + self.status = "accepted" + self.property_id.buyer_id = self.partner_id + self.property_id.selling_price = self.price + return True + + def action_refuse_offer(self): + self.ensure_one() + old_status = self.status + self.status = "refused" + if old_status == "accepted": + self.property_id.buyer_id = "" + self.property_id.selling_price = 0 + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 4f13338da2..5e3533059d 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + + +
+

+ +

+
+ + + + + + + + + + + + + +
+ diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3e84d7b849..e3b711c148 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,20 +4,22 @@ Properties estate.property list,form + {'search_default_available_filter': True} estate.property.list estate.property - + + - + @@ -27,22 +29,23 @@
-

- +

- - + + @@ -59,13 +62,13 @@ - - + + - + @@ -88,7 +91,7 @@ - + From 63f631ca4fe1d5140ddec37d4ff21ea3b567cf5a Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Fri, 20 Dec 2024 09:29:35 +0100 Subject: [PATCH 13/25] [IMP] estate: add inheritance --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 8 ++++++++ estate/models/estate_property_offer.py | 15 ++++++++++++++- estate/models/res_users.py | 11 +++++++++++ estate/views/res_users_views.xml | 15 +++++++++++++++ 6 files changed, 50 insertions(+), 1 deletion(-) 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 28ed1b2405..91a6153745 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,6 +10,7 @@ "views/estate_property_type_views.xml", "views/estate_property_tag_views.xml", "views/estate_property_offer_views.xml", + "views/res_users_views.xml", "views/estate_menus.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c..9a2189b638 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9f7c57aa1a..a322dac9f5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -98,6 +98,14 @@ def _onchange_garden(self): self.garden_area = 0 self.garden_orientation = "" + @api.ondelete(at_uninstall=False) + def _unlink_except_new_cancelled(self): + if any( + record.state in ("offer_received", "offer_accepted", "sold") + for record in self + ): + raise UserError("You can only delete new and cancelled properties.") + def action_property_sold(self): self.ensure_one() if self.state == "cancelled": diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 430580924d..2cfe9329cc 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,6 @@ from odoo import api, fields, models from odoo.tools.date_utils import date -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError class EstatePropertyOffer(models.Model): @@ -39,6 +39,19 @@ def _compute_date_deadline(self): create_date, days=record.validity ) + @api.model_create_multi + def create(self, vals_list): + all_offers = self.search([]) + for vals in vals_list: + self.env["estate.property"].browse( + vals["property_id"] + ).state = "offer_received" + if any(record.price > vals["price"] for record in all_offers): + raise ValidationError( + "Cannot create an offer with a lower amount than an existing offer." + ) + return super().create(vals_list) + def _inverse_date_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 0000000000..8a239c66b2 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class Users(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + comodel_name="estate.property", + inverse_name="salesman_id", + domain=["|", ("state", "=", "new"), ("state", "=", "offer_received")], + ) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 0000000000..177ea76ffa --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + From 9c873c095a4c4e82cec5c5969ee4260ab1f3182d Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Fri, 20 Dec 2024 09:35:45 +0100 Subject: [PATCH 14/25] [IMP] estate: refactor code --- estate/models/estate_property_offer.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 2cfe9329cc..e6ab2511b2 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -58,12 +58,15 @@ def _inverse_date_deadline(self): def action_accept_offer(self): self.ensure_one() - other_records = self.search( - [("id", "!=", str(self.id)), ("property_id", "=", self.property_id.id)] + accepted_offers = self.search( + [ + ("id", "!=", str(self.id)), + ("property_id", "=", self.property_id.id), + ("status", "=", "accepted"), + ] ) - for rec in other_records: - if rec.status == "accepted": - raise UserError("Cannot have more than one accepted offer.") + if accepted_offers: + raise UserError("Cannot have more than one accepted offer.") self.status = "accepted" self.property_id.buyer_id = self.partner_id self.property_id.selling_price = self.price @@ -72,10 +75,9 @@ def action_accept_offer(self): def action_refuse_offer(self): self.ensure_one() - old_status = self.status - self.status = "refused" - if old_status == "accepted": + if self.status == "accepted": self.property_id.buyer_id = "" self.property_id.selling_price = 0 self.property_id.state = "offer_received" + self.status = "refused" return True From d6db1941143abcdaec79701bbee9a6107f28dacc Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Fri, 20 Dec 2024 10:40:14 +0100 Subject: [PATCH 15/25] [IMP] estate: link sold properties to invoices --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 9 ++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 31 +++++++++++++++++++++ estate_account/security/ir.model.access.csv | 2 ++ 5 files changed, 44 insertions(+) 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 create mode 100644 estate_account/security/ir.model.access.csv diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 0000000000..33855c1e1a --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,9 @@ +{ + "name": "Real Estate Accounting", + "depends": ["estate", "account"], + "installable": True, + "license": "LGPL-3", + "data": [ + "security/ir.model.access.csv", + ], +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 0000000000..5e1963c9d2 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 0000000000..a2280a6b5f --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,31 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_property_sold(self): + self.ensure_one() + self.env["account.move"].create( + { + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create( + { + "name": self.name, + "quantity": 1, + "price_unit": self.selling_price * 0.06, + } + ), + Command.create( + { + "name": "Administrative fees", + "quantity": 1, + "price_unit": 100.0, + } + ), + ], + } + ) + return super().action_property_sold() diff --git a/estate_account/security/ir.model.access.csv b/estate_account/security/ir.model.access.csv new file mode 100644 index 0000000000..85de405deb --- /dev/null +++ b/estate_account/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 From c745b802cceea1356887202930caa611bae3fbf6 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Fri, 20 Dec 2024 11:19:33 +0100 Subject: [PATCH 16/25] [IMP] estate: add kanban view --- estate/views/estate_property_views.xml | 42 +++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e3b711c148..53ed696b09 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Properties estate.property - list,form + list,form,kanban {'search_default_available_filter': True} @@ -81,6 +81,46 @@ + + estate.property.kanban + estate.property + + + + + +
+
+
+ + + + + +
+
+
+ Expected Price: + +
+
+ Best Offer: + +
+
+ Selling Price: + +
+
+
+ +
+
+
+
+
+
+
estate.property.search estate.property From b22d5d919ee161cdcb7ab566307bdf5cb931fcce Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Fri, 20 Dec 2024 11:48:36 +0100 Subject: [PATCH 17/25] [FIX] estate: fix kanban warning --- estate/views/estate_property_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 53ed696b09..b0b47a43a1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -88,7 +88,7 @@ - +
From 977f427360ba0a34d61f30fcb2132f9ad91c9d55 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Fri, 20 Dec 2024 17:14:03 +0100 Subject: [PATCH 18/25] [IMP] estate: refactor code --- estate/models/estate_property.py | 6 +++--- estate/models/estate_property_offer.py | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a322dac9f5..03960d2dda 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -100,9 +100,9 @@ def _onchange_garden(self): @api.ondelete(at_uninstall=False) def _unlink_except_new_cancelled(self): - if any( - record.state in ("offer_received", "offer_accepted", "sold") - for record in self + if self.filtered( + lambda property: property.state + in ("offer_received", "offer_accepted", "sold") ): raise UserError("You can only delete new and cancelled properties.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e6ab2511b2..beed26e6b3 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -41,15 +41,24 @@ def _compute_date_deadline(self): @api.model_create_multi def create(self, vals_list): - all_offers = self.search([]) + offers_per_property = {} for vals in vals_list: - self.env["estate.property"].browse( - vals["property_id"] - ).state = "offer_received" - if any(record.price > vals["price"] for record in all_offers): - raise ValidationError( - "Cannot create an offer with a lower amount than an existing offer." + if vals["property_id"] not in offers_per_property: + offers_per_property[vals["property_id"]] = self.search( + [ + ("property_id", "=", vals["property_id"]), + ] ) + + property_offers = offers_per_property[vals["property_id"]] + property_model = self.env["estate.property"].browse(vals["property_id"]) + if property_model.exists(): + property_model.state = "offer_received" + + if any(record.price > vals["price"] for record in property_offers): + raise ValidationError( + "Cannot create an offer with a lower amount than an existing offer." + ) return super().create(vals_list) def _inverse_date_deadline(self): From dbcf657007a3695ad69629e7993f007dc93de4b9 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Mon, 23 Dec 2024 13:05:14 +0100 Subject: [PATCH 19/25] [ADD] awesome owl: chapter 1 --- awesome_owl/static/src/card/card.js | 19 ++++++++ awesome_owl/static/src/card/card.xml | 14 ++++++ awesome_owl/static/src/counter/counter.js | 23 +++++++++ awesome_owl/static/src/counter/counter.xml | 11 +++++ awesome_owl/static/src/playground.js | 18 ++++++- awesome_owl/static/src/playground.xml | 25 ++++++++-- awesome_owl/static/src/todolist/todoitem.js | 22 +++++++++ awesome_owl/static/src/todolist/todoitem.xml | 11 +++++ awesome_owl/static/src/todolist/todolist.js | 50 ++++++++++++++++++++ awesome_owl/static/src/todolist/todolist.xml | 11 +++++ awesome_owl/static/src/utils.js | 9 ++++ awesome_owl/views/templates.xml | 5 +- 12 files changed, 210 insertions(+), 8 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 create mode 100644 awesome_owl/static/src/todolist/todoitem.js create mode 100644 awesome_owl/static/src/todolist/todoitem.xml create mode 100644 awesome_owl/static/src/todolist/todolist.js create mode 100644 awesome_owl/static/src/todolist/todolist.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 new file mode 100644 index 0000000000..8fbe92f850 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: { type: String }, + slots: { type: Object } + }; + + setup() { + this.state = useState({ hidden: false }); + } + + toggle() { + this.state.hidden = !this.state.hidden; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 0000000000..82d1155b31 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,14 @@ + + + +
+
+
+ + +
+ +
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 0000000000..0f04cdd9d9 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.Counter"; + static props = { + onChange: { type: Function, optional: true } + }; + + setup() { + this.state = useState({ + value: 0 + }); + }; + + increment() { + this.state.value++; + if (this.props.onChange) { + this.props.onChange(); + } + }; +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 0000000000..a293096f9a --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + +
+

Counter: + +

+ +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07b..68ee5df9ee 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,23 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter" +import { Card } from "./card/card" +import { TodoList } from "./todolist/todolist"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + html = "This is bold"; + html_markup = markup("This is bold"); + + setup() { + this.state = useState({ + sum: 0 + }); + }; + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f..76615ccc11 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,27 @@ - -
- hello world +
+
+

Counters

+
+ + +
+

The sum is + +

+
+
+

Cards

+ + + +
+
+

Todo list

+ +
- diff --git a/awesome_owl/static/src/todolist/todoitem.js b/awesome_owl/static/src/todolist/todoitem.js new file mode 100644 index 0000000000..25c6afd6fd --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + id: { type: Number }, + description: { type: String }, + isCompleted: { type: Boolean }, + toggleState: { Function }, + removeTodo: { Function } + }; + + toggleState() { + this.props.toggleState(this.props.id); + } + + removeTodo() { + this.props.removeTodo(this.props.id); + } +} diff --git a/awesome_owl/static/src/todolist/todoitem.xml b/awesome_owl/static/src/todolist/todoitem.xml new file mode 100644 index 0000000000..b1163bb1ec --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.xml @@ -0,0 +1,11 @@ + + + +
+ + . + + +
+
+
diff --git a/awesome_owl/static/src/todolist/todolist.js b/awesome_owl/static/src/todolist/todolist.js new file mode 100644 index 0000000000..775ef732ed --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.js @@ -0,0 +1,50 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todoitem"; +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem } + + setup() { + this.todos = useState([ + { id: 0, description: "Buy house", isCompleted: true }, + { id: 1, description: "Buy milk", isCompleted: false }, + { id: 2, description: "Buy chocolate", isCompleted: true }, + { id: 3, description: "Clean house", isCompleted: false }, + { id: 4, description: "Pay bills", isCompleted: false }, + { id: 5, description: "Call mom", isCompleted: true }, + { id: 6, description: "Go on holiday", isCompleted: false }, + ]); + this.id_counter = this.todos.length; + useAutofocus("todolist_input"); + }; + + addTodo(ev) { + if (ev.keyCode === 13) { + let desc = ev.target.value; + if (desc) { + this.todos.push({ + id: this.id_counter++, + description: desc, + isCompleted: false + }); + ev.target.value = ""; + } + } + }; + + toggleState(id) { + const index = this.todos.findIndex((elem) => elem.id === id); + this.todos[index].isCompleted = !this.todos[index].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/todolist/todolist.xml b/awesome_owl/static/src/todolist/todolist.xml new file mode 100644 index 0000000000..50f93a0d6a --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.xml @@ -0,0 +1,11 @@ + + + + + +

+ +

+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 0000000000..5ca09cbef6 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,9 @@ +import { useRef, useEffect } from "@odoo/owl" + +export function useAutofocus(name) { + let ref = useRef(name); + useEffect( + (el) => el && el.focus(), + () => [ref.el] + ) +} diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml index aa54c1a724..82442cb2ef 100644 --- a/awesome_owl/views/templates.xml +++ b/awesome_owl/views/templates.xml @@ -1,15 +1,14 @@ - From f1c575108c3e07a4d9af59f0c4926d3b99158922 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Mon, 23 Dec 2024 14:12:51 +0100 Subject: [PATCH 20/25] [IMP] estate: add tests --- estate/models/estate_property.py | 2 + estate/models/estate_property_offer.py | 2 + estate/tests/__init__.py | 1 + estate/tests/test_estate.py | 99 ++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 03960d2dda..3a6502fbd4 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -110,6 +110,8 @@ def action_property_sold(self): self.ensure_one() if self.state == "cancelled": raise UserError("Cancelled properties cannot be sold.") + if not self.offer_ids.filtered(lambda offer: offer.status == "accepted"): + raise UserError("Cannot sell a property with no accepted offer on it.") self.state = "sold" return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index beed26e6b3..73e0fa5587 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -53,6 +53,8 @@ def create(self, vals_list): property_offers = offers_per_property[vals["property_id"]] property_model = self.env["estate.property"].browse(vals["property_id"]) if property_model.exists(): + if property_model.state == "sold": + raise UserError("Cannot create an offer for a sold property.") property_model.state = "offer_received" if any(record.price > vals["price"] for record in property_offers): diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 0000000000..dfd37f0be1 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate diff --git a/estate/tests/test_estate.py b/estate/tests/test_estate.py new file mode 100644 index 0000000000..d0b366b4b7 --- /dev/null +++ b/estate/tests/test_estate.py @@ -0,0 +1,99 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import tagged, Form + + +@tagged("post_install", "-at_install") +class EstateTestCase(TransactionCase): + + @classmethod + def setUpClass(cls): + + super(EstateTestCase, cls).setUpClass() + + cls.properties = cls.env["estate.property"].create( + [ + { + "name": "Property1", + "expected_price": 1000000, + "living_area": 20, + "garden_area": 30, + }, + { + "name": "Property2", + "expected_price": 2000000, + "living_area": 0, + "garden_area": 20, + }, + { + "name": "Property3", + "expected_price": 3000000, + "living_area": 20, + "garden_area": 0, + }, + ] + ) + + def test_creation_area(self): + """Test that the total_area is computed like it should.""" + self.assertRecordValues( + self.properties, + [ + {"name": "Property1", "total_area": 50}, + {"name": "Property2", "total_area": 20}, + {"name": "Property3", "total_area": 20}, + ], + ) + + def test_sold_then_create_offer(self): + """Test that we cannot create a new offer after having sold a property.""" + property1 = self.properties[0] + self.env["estate.property.offer"].create( + [ + { + "price": 400000, + "partner_id": 1, + "property_id": property1.id, + "status": "accepted", + } + ] + ) + property1.action_property_sold() + self.assertEqual(property1.state, "sold") + with self.assertRaises(UserError): + self.env["estate.property.offer"].create( + [{"price": 400000, "partner_id": 1, "property_id": property1.id}] + ) + + def test_sell_no_accepted_offers(self): + """Test that we cannot sell a property with no accepted offer.""" + for property in self.properties: + with self.assertRaises(UserError): + property.action_property_sold() + + def test_garden_checkbox(self): + """Test the reset of the garden area and orientation when garden checkbox is ticked in the property form.""" + form = Form(self.env["estate.property"]) + form.name = "TestPropertyGarden" + form.expected_price = 10 + # Untick garden + form.garden = False + record = form.save() + self.assertEqual(record.garden, False) + self.assertEqual(record.garden_area, 0) + self.assertEqual(record.garden_orientation, False) + # Tick garden + form.garden = True + record = form.save() + self.assertEqual(record.garden, True) + self.assertEqual(record.garden_area, 10) + self.assertEqual(record.garden_orientation, "north") + form.garden_area = 20 + form.garden_orientation = "south" + record = form.save() + # Untick garden + form.garden = False + record = form.save() + self.assertEqual(record.garden, False) + self.assertEqual(record.garden_area, 0) + self.assertEqual(record.garden_orientation, False) From 6704364c6ef69a8827fd3e9cb9aeb26fdf60d10d Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Thu, 26 Dec 2024 10:50:44 +0100 Subject: [PATCH 21/25] [ADD] awesome dashboard: chapter 2 --- .../static/src/cards/number_card.js | 18 +++++ .../static/src/cards/piechart_card.js | 18 +++++ awesome_dashboard/static/src/dashboard.js | 10 --- awesome_dashboard/static/src/dashboard.xml | 8 --- .../static/src/dashboard/dashboard.js | 57 +++++++++++++++ .../static/src/dashboard/dashboard.scss | 3 + .../static/src/dashboard/dashboard.xml | 28 ++++++++ .../static/src/dashboard/dashboard_dialog.js | 38 ++++++++++ .../static/src/dashboard/dashboard_dialog.xml | 20 ++++++ .../static/src/dashboard/dashboard_item.js | 13 ++++ .../static/src/dashboard/dashboard_item.xml | 10 +++ .../static/src/dashboard_action.js | 14 ++++ .../src/dashboard_items/dashboard_items.js | 69 +++++++++++++++++++ .../static/src/piechart/piechart.js | 65 +++++++++++++++++ .../static/src/piechart/piechart.xml | 10 +++ .../static/src/statistics_service.js | 25 +++++++ 16 files changed, 388 insertions(+), 18 deletions(-) create mode 100644 awesome_dashboard/static/src/cards/number_card.js create mode 100644 awesome_dashboard/static/src/cards/piechart_card.js delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_dialog.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_dialog.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard_action.js create mode 100644 awesome_dashboard/static/src/dashboard_items/dashboard_items.js create mode 100644 awesome_dashboard/static/src/piechart/piechart.js create mode 100644 awesome_dashboard/static/src/piechart/piechart.xml create mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/awesome_dashboard/static/src/cards/number_card.js b/awesome_dashboard/static/src/cards/number_card.js new file mode 100644 index 0000000000..913494531d --- /dev/null +++ b/awesome_dashboard/static/src/cards/number_card.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = xml` +

+ +

+

+ +

+ `; + static props = { + title: { type: String }, + value: { type: Number } + }; +} diff --git a/awesome_dashboard/static/src/cards/piechart_card.js b/awesome_dashboard/static/src/cards/piechart_card.js new file mode 100644 index 0000000000..0b32542501 --- /dev/null +++ b/awesome_dashboard/static/src/cards/piechart_card.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; +import { PieChart } from "../piechart/piechart" + +export class PieChartCard extends Component { + static components = { PieChart } + static template = xml` +

+ +

+ + `; + static props = { + title: { type: String }, + values: { type: Object } + }; +} diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 637fa4bb97..0000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @odoo-module **/ - -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fe..0000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 0000000000..20d419187a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,57 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { Layout } from "@web/search/layout"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item"; +import { PieChart } from "../piechart/piechart"; +import { DashboardDialog } from "./dashboard_dialog"; +import { browser } from "@web/core/browser/browser"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, PieChart, DashboardDialog }; + + setup() { + this.items = registry.category("awesome_dashboard").get("awesome_dashboard_items"); + this.action = useService("action"); + this.dialog = useService("dialog"); + this.statistics = useState(useService("statistics")); + this.inactive_items = useState({ + items: browser.localStorage.getItem("inactive_items")?.split(",") || [] + }); + } + + openCustomersKanban() { + this.action.doAction("base.action_partner_form"); + } + + openLeadsView() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: 'CRM Leads', + target: 'current', + res_model: 'crm.lead', + views: [[false, 'list'], [false, 'form']], + }); + } + + openDialog() { + this.dialog.add(DashboardDialog, { + items: this.items, + inactive_items: this.inactive_items.items, + updateInactiveItems: this.setInactiveItems.bind(this) + }); + } + + setInactiveItems(inactive_items) { + browser.localStorage.setItem( + "inactive_items", + inactive_items, + ); + this.inactive_items.items = inactive_items; + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 0000000000..a78fb255a2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: rgb(234, 234, 234); +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 0000000000..555d51fc41 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_dialog.js b/awesome_dashboard/static/src/dashboard/dashboard_dialog.js new file mode 100644 index 0000000000..2b568ba8b3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_dialog.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; + + +export class DashboardDialog extends Component { + static template = "awesome_dashboard.DashboardDialog"; + static components = { Dialog, CheckBox }; + static props = { + close: { type: Function }, + items: { type: Array }, + inactive_items: { type: Array }, + updateInactiveItems: { type: Function } + }; + + setup() { + this.items = useState([]); + this.props.items.forEach(item => { + let active_item = { + ...item, + is_active: !this.props.inactive_items.includes(item.id) + } + this.items.push(active_item); + }); + } + + onChecked(checked, item) { + item.is_active = checked; + } + + close_dialog() { + this.props.close(); + this.props.updateInactiveItems( + this.items.filter((item) => !item.is_active).map((item) => item.id) + ) + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_dialog.xml b/awesome_dashboard/static/src/dashboard/dashboard_dialog.xml new file mode 100644 index 0000000000..99a57a327c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_dialog.xml @@ -0,0 +1,20 @@ + + + + +

Which cards do you wish to see ?

+ +
+ +
+ +
+
+
+
+ + + +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 0000000000..88b1dc23e0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static defaultProps = { + size: 2, + }; + static props = { + size: { type: Number } + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item.xml new file mode 100644 index 0000000000..2e06cc16c1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 0000000000..c49c4a2c43 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); diff --git a/awesome_dashboard/static/src/dashboard_items/dashboard_items.js b/awesome_dashboard/static/src/dashboard_items/dashboard_items.js new file mode 100644 index 0000000000..44a067804d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_items/dashboard_items.js @@ -0,0 +1,69 @@ +/** @odoo-module **/ +import { NumberCard } from "../cards/number_card" +import { PieChartCard } from "../cards/piechart_card" +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity + }) + }, + { + id: "average_time", + description: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + Component: NumberCard, + size: 3, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time + }) + }, + { + id: "nb_new_orders", + description: "Number of new orders this month", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders + }) + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders this month", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders + }) + }, + { + id: "total_amount", + description: "Total amount of new orders this month", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount + }) + }, + { + id: "orders_by_size", + description: "Shirt orders by size", + Component: PieChartCard, + size: 1, + props: (data) => ({ + title: "Shirt orders by size", + values: data.orders_by_size + }) + }, +] + +registry.category("awesome_dashboard").add("awesome_dashboard_items", items); diff --git a/awesome_dashboard/static/src/piechart/piechart.js b/awesome_dashboard/static/src/piechart/piechart.js new file mode 100644 index 0000000000..2fff1136d3 --- /dev/null +++ b/awesome_dashboard/static/src/piechart/piechart.js @@ -0,0 +1,65 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useRef, useEffect } from "@odoo/owl" +import { loadJS } from "@web/core/assets" + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + statistics: { type: Object } + } + + setup() { + this.chart = null; + this.canvasRef = useRef("canvas"); + onWillStart(async () => { + await loadJS(["/web/static/lib/Chart/Chart.js"]); + }); + + useEffect(() => { + this.renderChart() + return () => { + if (this.chart) { + this.chart.destroy(); + } + } + }); + } + + getPieChartData() { + return { + labels: [ + "S", "M", "XL" + ], + datasets: [{ + data: [ + this.props.statistics["s"], + this.props.statistics["m"], + this.props.statistics["xl"], + ], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)'], + hoverOffset: 4 + }] + } + } + + getChartConfig() { + let config = { + type: "pie", + data: this.getPieChartData() + } + return config; + } + + + renderChart() { + if (this.chart) { + this.chart.destroy(); + } + const config = this.getChartConfig(); + this.chart = new Chart(this.canvasRef.el, config); + } +} diff --git a/awesome_dashboard/static/src/piechart/piechart.xml b/awesome_dashboard/static/src/piechart/piechart.xml new file mode 100644 index 0000000000..fce281eda5 --- /dev/null +++ b/awesome_dashboard/static/src/piechart/piechart.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 0000000000..28e71ea19e --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,25 @@ +/** @odoo-module **/ +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl" + +export async function loadStatistics() { + return await rpc("/awesome_dashboard/statistics"); +} + +export const statisticsService = { + + start() { + const statistics = reactive({ isReady: false }); + setInterval(async () => { + statistics.stats = await loadStatistics(); + }, 10 * 1000); + loadStatistics().then((stats) => { + statistics.stats = stats; + statistics.isReady = true; + }) + return statistics; + }, +}; + +registry.category("services").add("statistics", statisticsService); From 2e5e8f1cf36e9db6224cbaf461a2560c3d2ce87c Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Thu, 26 Dec 2024 10:55:56 +0100 Subject: [PATCH 22/25] [IMP] awesome owl: refactor code --- awesome_owl/static/src/counter/counter.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index 0f04cdd9d9..b48e82727e 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -16,8 +16,6 @@ export class Counter extends Component { increment() { this.state.value++; - if (this.props.onChange) { - this.props.onChange(); - } + this.props.onChange?.(); }; } From ccfc291ad9eac4eace8d243264cc3c397b0b3035 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Thu, 26 Dec 2024 10:58:34 +0100 Subject: [PATCH 23/25] [IMP] estate: follow coding guidelines --- estate/models/estate_property_offer.py | 9 +++++---- estate/tests/test_estate.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 73e0fa5587..e23cc4d177 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -51,11 +51,12 @@ def create(self, vals_list): ) property_offers = offers_per_property[vals["property_id"]] - property_model = self.env["estate.property"].browse(vals["property_id"]) - if property_model.exists(): - if property_model.state == "sold": + EstateProperty = self.env["estate.property"] + property = EstateProperty.browse(vals["property_id"]) + if property.exists(): + if property.state == "sold": raise UserError("Cannot create an offer for a sold property.") - property_model.state = "offer_received" + property.state = "offer_received" if any(record.price > vals["price"] for record in property_offers): raise ValidationError( diff --git a/estate/tests/test_estate.py b/estate/tests/test_estate.py index d0b366b4b7..8a8db9279a 100644 --- a/estate/tests/test_estate.py +++ b/estate/tests/test_estate.py @@ -9,7 +9,7 @@ class EstateTestCase(TransactionCase): @classmethod def setUpClass(cls): - super(EstateTestCase, cls).setUpClass() + super().setUpClass() cls.properties = cls.env["estate.property"].create( [ From e34aacb0f6dd97b97a9191310dde74bda5ed5ebf Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Thu, 26 Dec 2024 15:44:17 +0100 Subject: [PATCH 24/25] [IMP] estate: add demo data --- estate/__manifest__.py | 5 +- estate/data/estate_property_data.xml | 59 ++++++++++++++++++++++ estate/data/estate_property_offer_data.xml | 31 ++++++++++++ estate/data/estate_property_type_data.xml | 16 ++++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 estate/data/estate_property_data.xml create mode 100644 estate/data/estate_property_offer_data.xml create mode 100644 estate/data/estate_property_type_data.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 91a6153745..10d41b2e8c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,10 +7,13 @@ "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_offer_views.xml", "views/estate_property_type_views.xml", "views/estate_property_tag_views.xml", - "views/estate_property_offer_views.xml", "views/res_users_views.xml", "views/estate_menus.xml", + "data/estate_property_type_data.xml", + "data/estate_property_data.xml", + "data/estate_property_offer_data.xml", ], } diff --git a/estate/data/estate_property_data.xml b/estate/data/estate_property_data.xml new file mode 100644 index 0000000000..77dea7b890 --- /dev/null +++ b/estate/data/estate_property_data.xml @@ -0,0 +1,59 @@ + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 6 + 100 + 4 + True + True + 100000 + south + + + + Trailer home + cancelled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + False + + + + Tiny House + new + This will be your cosy home <3 + 5555 + 2025-01-01 + 10000 + 1 + 30 + 4 + False + + + + + diff --git a/estate/data/estate_property_offer_data.xml b/estate/data/estate_property_offer_data.xml new file mode 100644 index 0000000000..695ec87201 --- /dev/null +++ b/estate/data/estate_property_offer_data.xml @@ -0,0 +1,31 @@ + + + + + + 10000 + 14 + + + + + 1500000 + 14 + + + + + 1500001 + 14 + + + + + + + + + + + + diff --git a/estate/data/estate_property_type_data.xml b/estate/data/estate_property_type_data.xml new file mode 100644 index 0000000000..dc9a474fd5 --- /dev/null +++ b/estate/data/estate_property_type_data.xml @@ -0,0 +1,16 @@ + + + + Residential + + + Commercial + + + Industrial + + + Land + + + From 488345eb3e788ce86ee40317c6aea30f6020b059 Mon Sep 17 00:00:00 2001 From: romc-odoo Date: Fri, 27 Dec 2024 14:23:18 +0100 Subject: [PATCH 25/25] [FIX] estate: fix demo data --- estate/data/estate_property_data.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/estate/data/estate_property_data.xml b/estate/data/estate_property_data.xml index 77dea7b890..8f7e5a6845 100644 --- a/estate/data/estate_property_data.xml +++ b/estate/data/estate_property_data.xml @@ -45,12 +45,10 @@