From 96ef7be6a0055c8610e998c5c8a6546ee7d4c8c6 Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 19 Jun 2025 09:19:41 +0700 Subject: [PATCH 01/26] [ADD] estate: create new real estate property management module Kickstart development of a new real estate property management module by creating init and manifest file Server Framework 101 Chapter 2 --- estate/__init__.py | 0 estate/__manifest__.py | 8 ++++++++ 2 files changed, 8 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 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..8fdce654c78 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': 'Real Estate', + 'depends': [ + 'base', + ], + 'application': True, + 'license': 'AGPL-3' +} \ No newline at end of file From 48651ba3e79e8f30d82d3857b37139921326255e Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 19 Jun 2025 10:49:39 +0700 Subject: [PATCH 02/26] [IMP] estate: create estate_property table to store properties data estate_property table contains information about properties (name, selling price, specs, etc.) Server Framework 101 Chapter 3 --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+) 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 e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +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 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..b8c42c2a4fd --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,19 @@ +from odoo import fields, models + +class Property(models.Model): + _name = "estate.property" + _description = "Estate Property" + + name = fields.Char('Property Name', required=True, translate=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([('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) From b28bb684fd62d35cef7bec7dde3df8ded08a262e Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 19 Jun 2025 11:32:29 +0700 Subject: [PATCH 03/26] [IMP] estate: configure access rights for estate_property model created ir.model.access.csv add access rights (read, write, create, unlink) for base.group_user on estate_property model Server Framework 101 Chapter 4 --- .gitignore | 3 +++ estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/.gitignore b/.gitignore index b6e47617de1..58710ec238a 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# VS Code +.vscode \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8fdce654c78..cee5be3ca7b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,6 +3,9 @@ 'depends': [ 'base', ], + 'data': [ + 'security/ir.model.access.csv', + ], 'application': True, 'license': 'AGPL-3' } \ 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..03d51a24555 --- /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 +access_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From a1bbac3ee1752631597cc4923c2ffbe49b6c157d Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 19 Jun 2025 14:31:34 +0700 Subject: [PATCH 04/26] [IMP] estate: create basic views for estate module create menu to better navigate the estate module create form to interact with estate property model Server Framework 101 Chapter 5 --- estate/__manifest__.py | 3 +++ estate/models/estate_property.py | 29 +++++++++++++++++++++----- estate/views/estate_menus.xml | 8 +++++++ estate/views/estate_property_views.xml | 8 +++++++ 4 files changed, 43 insertions(+), 5 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 cee5be3ca7b..8b5e47fc05a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,9 @@ ], 'data': [ 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'application': True, 'license': 'AGPL-3' diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b8c42c2a4fd..962d2b3a787 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -4,16 +4,35 @@ class Property(models.Model): _name = "estate.property" _description = "Estate Property" - name = fields.Char('Property Name', required=True, translate=True) + # misc + name = fields.Char('Property Name', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date(default=fields.Date.add(fields.Date.today(), months=3), copy=False) + + # price expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + + # rooms + 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([('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West')]) + + # reserved + active = fields.Boolean(default=True) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled')], + required=True, default='new', copy=False) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..edfda3aa8a7 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + \ 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 00000000000..e9f760bff34 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property Action + estate.property + list,form + + \ No newline at end of file From fb86dce86cca0fd694df48fccc0fcd8beb873a76 Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 19 Jun 2025 17:07:48 +0700 Subject: [PATCH 05/26] [IMP] estate: create basic list, product, and search view for estate property create list view to show more fields in estate property create form view that group fields in estate property based on their context create search view that has custom filters and group by Server Framework 101 Chapter 6 --- estate/models/estate_property.py | 8 +-- estate/views/estate_property_views.xml | 85 ++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 962d2b3a787..a581f72d89e 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,10 +5,10 @@ class Property(models.Model): _description = "Estate Property" # misc - name = fields.Char('Property Name', required=True) + name = fields.Char(string='Title', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(default=fields.Date.add(fields.Date.today(), months=3), copy=False) + date_availability = fields.Date(string='Available From', default=fields.Date.add(fields.Date.today(), months=3), copy=False) # price expected_price = fields.Float(required=True) @@ -16,11 +16,11 @@ class Property(models.Model): # rooms bedrooms = fields.Integer(default=2) - living_area = fields.Integer() + living_area = fields.Integer(string='Living Area (sqm)') facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer(string='Garden Area (sqm)') garden_orientation = fields.Selection([ ('north', 'North'), ('south', 'South'), diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e9f760bff34..db93b6ce8cb 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,90 @@ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + Estate Property Action estate.property From e793550a4ce3ced2ee9f6b7853b7284fb5745f69 Mon Sep 17 00:00:00 2001 From: rhri Date: Fri, 20 Jun 2025 10:32:44 +0700 Subject: [PATCH 06/26] [IMP] estate: create property type, property tags, and property offers model Create property type model and linked it to property using Many2One Create property tags model and linked it to property using Many2Many Create property offers model and linked it to property using One2Many Create views for property type, tags, and offers Create salesman and buyer field in property which uses Many2One Server Framework 101 Chapter 7 --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 5 ++- estate/models/estate_property.py | 11 +++++ estate/models/estate_property_offer.py | 14 ++++++ estate/models/estate_property_tag.py | 8 ++++ estate/models/estate_property_type.py | 8 ++++ estate/security/ir.model.access.csv | 5 ++- estate/views/estate_menus.xml | 9 +++- estate/views/estate_property_offer_views.xml | 33 ++++++++++++++ estate/views/estate_property_tag_views.xml | 46 ++++++++++++++++++++ estate/views/estate_property_type_views.xml | 46 ++++++++++++++++++++ estate/views/estate_property_views.xml | 43 +++++++++++------- 12 files changed, 212 insertions(+), 19 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 8b5e47fc05a..81023e8dc19 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,7 +6,10 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', 'views/estate_property_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..09b2099fe84 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ -from . import estate_property \ No newline at end of file +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a581f72d89e..7dd2b816f90 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -36,3 +36,14 @@ class Property(models.Model): ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, default='new', copy=False) + + # many2one + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + salesman = fields.Many2one('res.users', default=lambda self: self.env.user) + buyer = fields.Many2one('res.partner', copy=False) + + # many2many + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + + #one2many + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..5150bf75711 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,14 @@ +from odoo import fields, models + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + + # misc + price = fields.Float() + status = fields.Selection([ + ('accepted', 'Accepted'), + ('refused', 'Refused')], + copy=False) + 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..271b4557289 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + + # misc + name = fields.Char(required=True) \ No newline at end of file diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..4e948c99f67 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + + # misc + name = fields.Char(string='Type', required=True) \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 03d51a24555..eda618fb79c 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 -access_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,estate_property_offer,model_estate_property_offer,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 index edfda3aa8a7..4b84351db1f 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,13 @@ - - + + + + + + + \ 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 00000000000..28e8982cb58 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,33 @@ + + + + + Property Offer Form + estate.property.offer + +
+ + + + + + + +
+
+
+ + + + Property Offer List + estate.property.offer + + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..a76480fae42 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,46 @@ + + + + + Property Tag Search + estate.property.tag + + + + + + + + + + Property Tag Form + estate.property.tag + +
+ + + + + +
+
+
+ + + + Property Tag List + estate.property.tag + + + + + + + + + + Property Tag + estate.property.tag + list,form + +
\ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..a9b04c7f4be --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,46 @@ + + + + + Property Type Search + estate.property.type + + + + + + + + + + Property Type Form + estate.property.type + +
+ +

+ +

+
+
+
+
+ + + + Property Type List + estate.property.type + + + + + + + + + + Property Type + estate.property.type + list,form + +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index db93b6ce8cb..9390ab63525 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,11 +2,12 @@ - estate.property.search + Property Search estate.property - + + @@ -20,21 +21,22 @@ - - estate.property.form + Property Form estate.property -
+

+ + @@ -58,6 +60,15 @@ + + + + + + + + +
@@ -66,17 +77,19 @@ - estate.property.list + Property List estate.property - - - - - - - - + + + + + + + + + + @@ -86,7 +99,7 @@ - Estate Property Action + Property estate.property list,form From 6ab47099c86ac62219640f8a3ec86d4c4933b88e Mon Sep 17 00:00:00 2001 From: rhri Date: Fri, 20 Jun 2025 13:42:05 +0700 Subject: [PATCH 07/26] [IMP] estate: create computed values and ochanges on property and property offer Created total area computed values using living area and garden area on property Created deadline computed values using validity on property with its inverse function also Created default values for garden area and orientation using onchanges for garden field on property Server Framework 101 Chapter 8 --- estate/models/estate_property.py | 29 ++++++++++++++++++-- estate/models/estate_property_offer.py | 18 +++++++++++- estate/views/estate_property_offer_views.xml | 4 +++ estate/views/estate_property_views.xml | 2 ++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 7dd2b816f90..0619722d82a 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 Property(models.Model): _name = "estate.property" @@ -14,7 +14,7 @@ class Property(models.Model): expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) - # rooms + # area bedrooms = fields.Integer(default=2) living_area = fields.Integer(string='Living Area (sqm)') facades = fields.Integer() @@ -45,5 +45,28 @@ class Property(models.Model): # many2many tag_ids = fields.Many2many('estate.property.tag', string='Tags') - #one2many + # one2many offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + + # computed + total_area = fields.Integer(compute='_compute_total_area', string='Total Area (sqm)') + best_offer = fields.Float(compute='_compute_best_offer') + + @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('offer_ids') + def _compute_best_offer(self): + for record in self: + record.best_offer = max(record.offer_ids.mapped('price'), default=0) + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5150bf75711..986aa9f82e6 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class PropertyOffer(models.Model): _name = "estate.property.offer" @@ -12,3 +12,19 @@ class PropertyOffer(models.Model): copy=False) partner_id = fields.Many2one('res.partner', required=True) property_id = fields.Many2one('estate.property', required=True) + + # computed + validity = fields.Integer(default=7, string='Validity (days)') + date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', string='Deadline') + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + safe_create_date = record.create_date or fields.Date.today() + record.date_deadline = fields.Date.add(safe_create_date, days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + safe_create_date = record.create_date or fields.Date.today() + delta = record.date_deadline - fields.Date.to_date(safe_create_date) + record.validity = delta.days \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 28e8982cb58..4c8b6bbd09d 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -10,6 +10,8 @@ + + @@ -25,6 +27,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9390ab63525..fb5401376cb 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -42,6 +42,7 @@ + @@ -56,6 +57,7 @@ + From a915adc264452d46a70c42cd779cacd35fe0ea97 Mon Sep 17 00:00:00 2001 From: rhri Date: Mon, 23 Jun 2025 09:51:21 +0700 Subject: [PATCH 08/26] [IMP] estate: add sold, cancel, accept offer, and refuse offer button Add sold property button to change property state to 'sold' Add cancel property button to change property state to 'cancelled' Add error handler for sold and cancel situation Add accept & refuse button for property offer Add automatic status change to other offers if an offer is accepted Automatically configure selling price and buyer after an offer is accepted Server Framework 101 Chapter 9 --- estate/models/estate_property.py | 21 ++++++++++++++-- estate/models/estate_property_offer.py | 25 ++++++++++++++++++-- estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 7 ++++-- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0619722d82a..38ae198369e 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 Property(models.Model): _name = "estate.property" @@ -35,7 +36,7 @@ class Property(models.Model): ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], - required=True, default='new', copy=False) + string="Status", required=True, default='new', copy=False) # many2one property_type_id = fields.Many2one('estate.property.type', string='Property Type') @@ -69,4 +70,20 @@ def _onchange_garden(self): self.garden_orientation = 'north' else: self.garden_area = 0 - self.garden_orientation = False \ No newline at end of file + self.garden_orientation = False + + def action_sell_property(self): + for record in self: + if record.state == 'cancelled': + raise UserError('Cancelled property cannot be sold.') + + record.state = 'sold' + return True + + def action_cancel_property(self): + for record in self: + if record.state == 'sold': + raise UserError('Sold property cannot be cancelled.') + + record.state = 'cancelled' + return True \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 986aa9f82e6..ed246579d15 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -14,7 +14,7 @@ class PropertyOffer(models.Model): property_id = fields.Many2one('estate.property', required=True) # computed - validity = fields.Integer(default=7, string='Validity (days)') + validity = fields.Integer(string='Validity (days)', default=7) date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', string='Deadline') @api.depends('validity') @@ -27,4 +27,25 @@ def _inverse_date_deadline(self): for record in self: safe_create_date = record.create_date or fields.Date.today() delta = record.date_deadline - fields.Date.to_date(safe_create_date) - record.validity = delta.days \ No newline at end of file + record.validity = delta.days + + def action_accept_offer(self): + for record in self: + # refuse other offers + other_offers = self.search([ + ('property_id', '=', record.property_id.id), + ('id', '!=', record.id) + ]) + other_offers.write({'status': 'refused'}) + + record.property_id.selling_price = record.price + record.property_id.buyer = record.partner_id + record.property_id.state = 'offer_accepted' + + record.status = 'accepted' + return True + + def action_refuse_offer(self): + for record in self: + record.status = 'refused' + return True \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 4c8b6bbd09d..b7f839a498f 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -29,6 +29,8 @@ + + + + +

+ +

+ + + + + diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..fca5db1af2f --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,18 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static props = ["onChange?"]; + static template = "awesome_owl.counter"; + + setup() { + this.state = useState({ value: 1}); + } + + increment() { + this.state.value++; + + if (this.props.onChange) { + this.props.onChange(); + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..29bf8404d52 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,38 @@ + + + + +
+

Counter:

+ +
+
+ +
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1af6c827e0b..1ba7b61f346 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -1,6 +1,6 @@ import { whenReady } from "@odoo/owl"; import { mountComponent } from "@web/env"; -import { Playground } from "./playground"; +import { Playground } from "./playground/playground"; const config = { dev: true, diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js deleted file mode 100644 index 657fb8b07bb..00000000000 --- a/awesome_owl/static/src/playground.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @odoo-module **/ - -import { Component } from "@odoo/owl"; - -export class Playground extends Component { - static template = "awesome_owl.playground"; -} diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml deleted file mode 100644 index 4fb905d59f9..00000000000 --- a/awesome_owl/static/src/playground.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - -
- hello world -
-
- -
diff --git a/awesome_owl/static/src/playground/playground.js b/awesome_owl/static/src/playground/playground.js new file mode 100644 index 00000000000..37626ceb4f0 --- /dev/null +++ b/awesome_owl/static/src/playground/playground.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +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 = { Counter, Card, TodoList }; + + setup() { + this.state = useState({ + sum: 2 + }); + + this.title1 = "Card 1"; + this.title2 = "Card 2"; + } + + incrementSum() { + this.state.sum++; + } +} diff --git a/awesome_owl/static/src/playground/playground.xml b/awesome_owl/static/src/playground/playground.xml new file mode 100644 index 00000000000..25c110e7748 --- /dev/null +++ b/awesome_owl/static/src/playground/playground.xml @@ -0,0 +1,36 @@ + + + + +
+
+ hello world +
+ + + +
+ The sum is: +
+ + + + + + + +
some other content
+
+
+
+ +
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..ac85691e2c9 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,15 @@ +import { Component, useState } from "@odoo/owl"; +import { useAutoFocus } from "../utils/utils"; + +export class TodoItem extends Component { + static props = ["todo", "toggleState", "removeTodo"]; + static template = "awesome_owl.todo_item"; + + change() { + this.props.toggleState(this.props.todo.id); + } + + remove() { + this.props.removeTodo(this.props.todo.id); + } +} \ No newline at end of file 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..5bed0b51a1d --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,29 @@ + + + + +
+ +

+ . +

+ +
+
+ +
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..6ba60064156 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,42 @@ +import { Component, useState, useRef, onMounted } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutoFocus } from "../utils/utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + + setup() { + this.state = useState({ + todos: [] + }); + + this.inputRef = useAutoFocus("todo_input"); + + this.idCount = 0; + } + + addTodo(ev) { + const description = ev.target.value.trim(); + if (ev.keyCode === 13 && description) { + const newTodo = { + id: this.idCount++, + description: description, + isCompleted: false + }; + this.state.todos.push(newTodo); + ev.target.value = ""; + } + } + + toggleState(id) { + const todo = this.state.todos.find(todo => todo.id === id); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(id) { + this.state.todos = this.state.todos.filter(todo => todo.id !== id); + } +} \ No newline at end of file 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..c570f0fe656 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,29 @@ + + + + +
+ + + + +
+
+ +
diff --git a/awesome_owl/static/src/utils/utils.js b/awesome_owl/static/src/utils/utils.js new file mode 100644 index 00000000000..c4c38b119e2 --- /dev/null +++ b/awesome_owl/static/src/utils/utils.js @@ -0,0 +1,13 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutoFocus(refName = "input") { + const inputRef = useRef(refName); + + onMounted(() => { + if (inputRef.el) { + inputRef.el.focus(); + } + }); + + return inputRef; +} \ No newline at end of file From 02e4c3cceb8fa65563316493d821611f0b6b6d83 Mon Sep 17 00:00:00 2001 From: rhri Date: Tue, 1 Jul 2025 15:33:31 +0700 Subject: [PATCH 21/26] [IMP] awesome_dashboard: create simple dashboard create customers button that leads to customers kanban view create leads button that leads to leads list and form view create cards in the dashboard that display real life updated data create pie chart that shows sales numbers lazy load the dashboard items create add and remove dashboard items feature save the current config to browser make responsive views Web Framework Chapter 2 --- awesome_dashboard/__manifest__.py | 3 + awesome_dashboard/controllers/controllers.py | 4 +- awesome_dashboard/static/src/dashboard.js | 10 --- awesome_dashboard/static/src/dashboard.xml | 8 -- .../static/src/dashboard/card/number_card.js | 9 +++ .../static/src/dashboard/card/number_card.xml | 13 +++ .../src/dashboard/card/pie_chart_card.js | 11 +++ .../src/dashboard/card/pie_chart_card.xml | 11 +++ .../static/src/dashboard/dashboard.js | 79 +++++++++++++++++++ .../static/src/dashboard/dashboard.scss | 18 +++++ .../static/src/dashboard/dashboard.xml | 32 ++++++++ .../dashboard_item/dashboard_item.js | 12 +++ .../dashboard_item/dashboard_item.xml | 12 +++ .../static/src/dashboard/dashboard_items.js | 65 +++++++++++++++ .../static/src/dashboard/dialog/dialog.js | 26 ++++++ .../static/src/dashboard/dialog/dialog.xml | 31 ++++++++ .../src/dashboard/pie_chart/pie_chart.js | 56 +++++++++++++ .../src/dashboard/pie_chart/pie_chart.xml | 8 ++ .../services/dashboard_statistics_service.js | 27 +++++++ .../static/src/dashboard_action.js | 12 +++ 20 files changed, 427 insertions(+), 20 deletions(-) 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/card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/card/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/card/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/card/pie_chart_card.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_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/dialog/dialog.js create mode 100644 awesome_dashboard/static/src/dashboard/dialog/dialog.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml create mode 100644 awesome_dashboard/static/src/dashboard/services/dashboard_statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..c5b63b98530 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,9 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', + ], }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 56d4a051287..ab12c1acdfc 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -4,10 +4,11 @@ import random from odoo import http -from odoo.http import request +# from odoo.http import request logger = logging.getLogger(__name__) + class AwesomeDashboard(http.Controller): @http.route('/awesome_dashboard/statistics', type='json', auth='user') def get_statistics(self): @@ -33,4 +34,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 deleted file mode 100644 index 637fa4bb972..00000000000 --- 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 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/card/number_card.js b/awesome_dashboard/static/src/dashboard/card/number_card.js new file mode 100644 index 00000000000..7dbe11671cb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/card/number_card.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.number_card"; + static props = { + title: { type: String }, + data: { type: Number } + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/card/number_card.xml b/awesome_dashboard/static/src/dashboard/card/number_card.xml new file mode 100644 index 00000000000..57e7466f14e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/card/number_card.xml @@ -0,0 +1,13 @@ + + + + +
+
+

+ +

+
+
+ +
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/card/pie_chart_card.js new file mode 100644 index 00000000000..e14086eb359 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/card/pie_chart_card.js @@ -0,0 +1,11 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.pie_chart_card"; + static components = { PieChart }; + static props = { + title: { type: String }, + data: { type: Object }, + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/card/pie_chart_card.xml new file mode 100644 index 00000000000..15451757c87 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/card/pie_chart_card.xml @@ -0,0 +1,11 @@ + + + + +
+
+ +
+
+ +
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..9de6e817ff2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,79 @@ +import { Component, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; +import { browser } from "@web/core/browser/browser"; + +import { Layout } from "@web/search/layout"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { PieChart } from "./pie_chart/pie_chart"; +import { DashboardDialog } from "./dialog/dialog"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.dashboard"; + static components = { Layout, DashboardItem, PieChart }; + + setup() { + this.display = { + controlPanel: {} + }; + + this.action = useService("action"); + this.dialog = useService("dialog"); + + this.statisticsService = useService("awesome_dashboard.statistics"); + + this.statistics = useState(this.statisticsService.statistics); + this.items = registry.category("awesome_dashboard").getAll(); + + this.storageKey = ["awesome_dashboard_item"]; + this.setupActiveDashboardItem(); + }; + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + target: "current", + }); + } + + openDialog() { + this.dialog.add(DashboardDialog, { + items: this.items, + activeDashboardItem: this.activeDashboardItem, + storageKey: this.storageKey, + }); + } + + get activeItems() { + return this.items.filter( + (item) => this.activeDashboardItem[item.id] + ); + } + + setupActiveDashboardItem() { + const activeDashboardItemList = browser.localStorage.getItem(this.storageKey)?.split(","); + + this.activeDashboardItem = useState({}); + for (const item of this.items) { + if (activeDashboardItemList) { + this.activeDashboardItem[item.id] = activeDashboardItemList.includes( + item.id.toString() + ); + } else { + this.activeDashboardItem[item.id] = true; + } + } + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..aa380c4834d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,18 @@ +.o_dashboard { + background-color: gray; + overflow-y: auto; +} + +.pie-chart-container { + width: 100%; + aspect-ratio: 1 / 1; + max-width: 350px; + margin: 0 auto; +} + +.pie-chart-container canvas { + display: block; + width: 100% !important; + height: 100% !important; + max-width: 100% !important; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..31003c4f6f7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + +
+
+ +
+ + + + +
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..d930062f2a5 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,12 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.dashboard_item"; + static props = { + size: { optional: true}, + slots: { optional: true } + }; + static defaultProps = { + size: 1 + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..b7bb0ee0835 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,12 @@ + + + + +
+
+ +
+
+
+ +
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js new file mode 100644 index 00000000000..e6298ef645b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,65 @@ +import { registry } from "@web/core/registry"; +import { NumberCard } from "./card/number_card"; +import { PieChartCard } from "./card/pie_chart_card"; + +const items = [ + { + id: "avg_amount", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + data: data.average_quantity, + }), + }, + { + id: "avg_time", + description: "Average time for an order to go", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + data: data.average_time, + }), + }, + { + id: "new_order", + description: "Number of new orders", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + data: data.nb_new_orders, + }), + }, + { + id: "cancelled_order", + description: "Number of cancelled orders", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + data: data.nb_cancelled_orders, + }), + }, + { + id: "total_order", + description: "Total amount of new orders", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + data: data.total_amount, + }), + }, + { + id: "order_by_size", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + data: data.orders_by_size, + }), + }, +]; + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dialog/dialog.js b/awesome_dashboard/static/src/dashboard/dialog/dialog.js new file mode 100644 index 00000000000..8beb96b0880 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dialog/dialog.js @@ -0,0 +1,26 @@ +import { Component } from "@odoo/owl"; +import { browser } from "@web/core/browser/browser"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class DashboardDialog extends Component { + static template = "awesome_dashboard.dialog"; + static components = { CheckBox, Dialog }; + static props = [ "title", "items", "storageKey", "activeDashboardItem" ]; + + setup() { + this.items = this.props.items; + this.storageKey = this.props.storageKey; + this.activeDashboardItem = this.props.activeDashboardItem; + } + + toggleActiveItem(itemId) { + this.activeDashboardItem[itemId] = !this.activeDashboardItem[itemId]; + browser.localStorage.setItem( + this.storageKey.join(","), + Object.keys(this.activeDashboardItem).filter( + (itemId) => this.activeDashboardItem[itemId] + ) + ); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dialog/dialog.xml b/awesome_dashboard/static/src/dashboard/dialog/dialog.xml new file mode 100644 index 00000000000..38946838984 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dialog/dialog.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..2118634b880 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,56 @@ +import { Component, onWillStart, onMounted, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.pie_chart"; + static props = { + data: { type: Object } + }; + + chart = null; + + setup() { + this.chartRef = useRef("pieChartCanvas"); + // Lazy load + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + this.shouldRenderChart = true; + }); + + onMounted(() => { + if (this.shouldRenderChart && window.Chart) { + this.renderChart(); + } else { + console.warn("Chart.js not loaded, skipping render."); + } + }); + } + + renderChart() { + const ctx = this.chartRef.el.getContext("2d"); + + if (this.chart) { + this.chart.destroy(); + } + this.chart = new window.Chart(ctx, { + type: "pie", + data: { + labels: Object.keys(this.props.data), + datasets: [{ + data: Object.values(this.props.data), + backgroundColor: [ + "#42A5F5", "#66BB6A", "#AB47BC" + ], + }] + }, + options: { + responsive: false, + plugins: { + legend: { + position: "bottom" + } + } + } + }); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..9c4828b0c87 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/services/dashboard_statistics_service.js b/awesome_dashboard/static/src/dashboard/services/dashboard_statistics_service.js new file mode 100644 index 00000000000..d35949f5351 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/services/dashboard_statistics_service.js @@ -0,0 +1,27 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +export const statisticsService = { + dependencies: [], + start() { + const statistics = reactive({}); + + async function loadStatistics() { + const result = await rpc("/awesome_dashboard/statistics"); + + Object.keys(statistics).forEach((k) => delete statistics[k]); + Object.assign(statistics, result); + } + + loadStatistics(); + setInterval(loadStatistics, 600_000); + + return { + statistics, + reload: loadStatistics, + }; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..ad191ce2856 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,12 @@ +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class DashboardComponentLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", DashboardComponentLoader); \ No newline at end of file From 652cfa11901892448ea05d6ce229da7d5a9d040e Mon Sep 17 00:00:00 2001 From: rhri Date: Wed, 2 Jul 2025 14:27:57 +0700 Subject: [PATCH 22/26] [ADD] case_javascript: create case_javascript module to answer case study questions add volume and weight field into product information tab by inheriting product_info_popup.xml and get_product_info_pos method add remove button after control button by inheriting product_screen.xml and product_screen.js add congratulary_text field in pos_config and adding it of order_receipt using inherit Case Study: Javascript (PoS) --- case_javascript/__init__.py | 1 + case_javascript/__manifest__.py | 17 +++++++++++++ case_javascript/models/__init__.py | 2 ++ case_javascript/models/pos_config.py | 9 +++++++ case_javascript/models/product_product.py | 19 +++++++++++++++ case_javascript/static/src/order_receipt.xml | 12 ++++++++++ case_javascript/static/src/pos_store.js | 11 +++++++++ .../static/src/product_info_popup.xml | 24 +++++++++++++++++++ case_javascript/static/src/product_screen.js | 10 ++++++++ case_javascript/static/src/product_screen.xml | 16 +++++++++++++ case_javascript/views/pos_config_views.xml | 17 +++++++++++++ 11 files changed, 138 insertions(+) create mode 100644 case_javascript/__init__.py create mode 100644 case_javascript/__manifest__.py create mode 100644 case_javascript/models/__init__.py create mode 100644 case_javascript/models/pos_config.py create mode 100644 case_javascript/models/product_product.py create mode 100644 case_javascript/static/src/order_receipt.xml create mode 100644 case_javascript/static/src/pos_store.js create mode 100644 case_javascript/static/src/product_info_popup.xml create mode 100644 case_javascript/static/src/product_screen.js create mode 100644 case_javascript/static/src/product_screen.xml create mode 100644 case_javascript/views/pos_config_views.xml diff --git a/case_javascript/__init__.py b/case_javascript/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/case_javascript/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/case_javascript/__manifest__.py b/case_javascript/__manifest__.py new file mode 100644 index 00000000000..4eaa4535294 --- /dev/null +++ b/case_javascript/__manifest__.py @@ -0,0 +1,17 @@ +{ + 'name': 'Case Study: Javascript', + 'depends': [ + 'point_of_sale', + ], + 'application': True, + 'installable': True, + 'data': [ + 'views/pos_config_views.xml' + ], + 'assets': { + 'point_of_sale._assets_pos': [ + 'case_javascript/static/src/**/*', + ] + }, + 'license': 'AGPL-3', +} diff --git a/case_javascript/models/__init__.py b/case_javascript/models/__init__.py new file mode 100644 index 00000000000..6b0845d1923 --- /dev/null +++ b/case_javascript/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_product +from . import pos_config \ No newline at end of file diff --git a/case_javascript/models/pos_config.py b/case_javascript/models/pos_config.py new file mode 100644 index 00000000000..e3982f7b17a --- /dev/null +++ b/case_javascript/models/pos_config.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class PosConfig(models.Model): + _inherit = "pos.config" + + congratulatory_text = fields.Char( + string='Congratulatory Text', + default='Congratulations!') diff --git a/case_javascript/models/product_product.py b/case_javascript/models/product_product.py new file mode 100644 index 00000000000..62ac8c932dd --- /dev/null +++ b/case_javascript/models/product_product.py @@ -0,0 +1,19 @@ +from odoo import models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def get_product_info_pos(self, price, quantity, pos_config_id): + product_info = super().get_product_info_pos( + price, + quantity, + pos_config_id) + + product_info.update( + weight=self.weight, + weight_uom_name=self.weight_uom_name, + volume=self.volume, + volume_uom_name=self.volume_uom_name + ) + return product_info diff --git a/case_javascript/static/src/order_receipt.xml b/case_javascript/static/src/order_receipt.xml new file mode 100644 index 00000000000..6c6f6f24ebb --- /dev/null +++ b/case_javascript/static/src/order_receipt.xml @@ -0,0 +1,12 @@ + + + + + +
+

+
+
+
+ +
\ No newline at end of file diff --git a/case_javascript/static/src/pos_store.js b/case_javascript/static/src/pos_store.js new file mode 100644 index 00000000000..5f3dd0b5156 --- /dev/null +++ b/case_javascript/static/src/pos_store.js @@ -0,0 +1,11 @@ +import { patch } from "@web/core/utils/patch"; +import { PosStore } from "@point_of_sale/app/store/pos_store"; + +patch(PosStore.prototype, { + getReceiptHeaderData(order) { + return { + ...super.getReceiptHeaderData(...arguments), + congratulatory_text: this.config.congratulatory_text, + } + } +}); \ No newline at end of file diff --git a/case_javascript/static/src/product_info_popup.xml b/case_javascript/static/src/product_info_popup.xml new file mode 100644 index 00000000000..9fb5297fd9a --- /dev/null +++ b/case_javascript/static/src/product_info_popup.xml @@ -0,0 +1,24 @@ + + + + + +
+

Logistics

+
+ + + + + + + + + +
Weight:
Volume:
+
+
+
+
+ +
\ No newline at end of file diff --git a/case_javascript/static/src/product_screen.js b/case_javascript/static/src/product_screen.js new file mode 100644 index 00000000000..13faa58efbb --- /dev/null +++ b/case_javascript/static/src/product_screen.js @@ -0,0 +1,10 @@ +import { patch } from "@web/core/utils/patch"; +import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; + +patch(ProductScreen.prototype, { + onRemoveClick() { + if (this.currentOrder.get_selected_orderline()) { + this.currentOrder.removeOrderline(this.currentOrder.get_selected_orderline()); + } + } +}); \ No newline at end of file diff --git a/case_javascript/static/src/product_screen.xml b/case_javascript/static/src/product_screen.xml new file mode 100644 index 00000000000..1aa46b8147c --- /dev/null +++ b/case_javascript/static/src/product_screen.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/case_javascript/views/pos_config_views.xml b/case_javascript/views/pos_config_views.xml new file mode 100644 index 00000000000..eef2c78780b --- /dev/null +++ b/case_javascript/views/pos_config_views.xml @@ -0,0 +1,17 @@ + + + + + pos.config.view.form.inherit.case_javascript + pos.config + + + + + + + + + + + \ No newline at end of file From e59766e619ce187fd8a2dedcac9f3a53f06e930c Mon Sep 17 00:00:00 2001 From: rhri Date: Wed, 2 Jul 2025 17:58:18 +0700 Subject: [PATCH 23/26] [IMP] estate: add additional security rules to estate create two groups 'Agent' and 'Manager' give full access to all objects to your Real Estate Manager group give agents (real estate users) only read access to types and tags give nobody the right to delete properties make sure agent only can access properties they are assigned to or not assigned property create security bypass for estate_account make sure everyone only able to access property in their company make settings invisible to user --- case_javascript/__init__.py | 2 +- case_javascript/models/__init__.py | 2 +- estate/__manifest__.py | 2 ++ estate/models/estate_property.py | 4 +++ estate/security/ir.model.access.csv | 10 +++++--- estate/security/security.xml | 31 ++++++++++++++++++++++++ estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 1 + estate_account/models/estate_property.py | 3 ++- 9 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 estate/security/security.xml diff --git a/case_javascript/__init__.py b/case_javascript/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/case_javascript/__init__.py +++ b/case_javascript/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/case_javascript/models/__init__.py b/case_javascript/models/__init__.py index 6b0845d1923..39c258af875 100644 --- a/case_javascript/models/__init__.py +++ b/case_javascript/models/__init__.py @@ -1,2 +1,2 @@ from . import product_product -from . import pos_config \ No newline at end of file +from . import pos_config diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6b871d10f77..2faedfc7bf8 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,10 +1,12 @@ { 'name': 'Real Estate', + 'category': 'Real Estate/Brokerage', 'depends': [ 'base', ], 'data': [ 'security/ir.model.access.csv', + 'security/security.xml', 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 84e074b7c98..6b111cbf5cb 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -56,6 +56,10 @@ class Property(models.Model): string='Property Type') salesman = fields.Many2one('res.users', default=lambda self: self.env.user) buyer = fields.Many2one('res.partner', copy=False) + company_id = fields.Many2one('res.company', + string='Company', + default=lambda self: self.env.company, + required=True) # many2many tag_ids = fields.Many2many('estate.property.tag', string='Tags') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index eda618fb79c..88923f0328d 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,1 -access_estate_property_type,estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 -access_estate_property_tag,estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 -access_estate_property_offer,estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,estate_property,model_estate_property,estate_group_user,1,1,1,0 +access_user_estate_property_type,user_estate_property_type,model_estate_property_type,estate_group_user,1,0,0,0 +access_manager_estate_property_type,manager_estate_property_type,model_estate_property_type,estate_group_manager,1,1,1,1 +access_user_estate_property_tag,user_estate_property_tag,model_estate_property_tag,estate_group_user,1,0,0,0 +access_manager_estate_property_tag,manager_estate_property_tag,model_estate_property_tag,estate_group_manager,1,1,1,1 +access_estate_property_offer,estate_property_offer,model_estate_property_offer,estate_group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 00000000000..cb3f16f5bbc --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,31 @@ + + + + Agent + + + + + Manager + + + + + + Agent Rule + + + [ + '|', '&', + ('salesman', '=', user.id), ('salesman', '=', False), + ('company_id', 'in', company_ids) + ] + + + + Manager Rule + + + [(1, '=', 1)] + + \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 51f0758a2b7..4609abd3c08 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -5,7 +5,7 @@ - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4c0ee5947d5..43a7f7f251d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -72,6 +72,7 @@ + diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 15e4f399df3..cb08dff088a 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -8,7 +8,8 @@ def action_sell_property(self): res = super().action_sell_property() for record in self: - self.env['account.move'].create({ + record.check_access("write") + self.sudo().env['account.move'].create({ 'partner_id': record.buyer.id, 'move_type': 'out_invoice', 'invoice_line_ids': [ From a73759f7919a5c6a2f5de49c3349ad13f61a3141 Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 3 Jul 2025 14:18:48 +0700 Subject: [PATCH 24/26] [ADD] case_data_access: create sales team leader groups create team leader groups which are able to create, write, and read any sales order done by his team member team leader groups implies own documents only groups with additional record rules Case Study : Data Access --- case_data_access/__init__.py | 0 case_data_access/__manifest__.py | 14 ++++++++++++++ .../security/sales_team_security.xml | 17 +++++++++++++++++ estate/__manifest__.py | 2 +- estate/security/ir.model.access.csv | 4 +++- 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 case_data_access/__init__.py create mode 100644 case_data_access/__manifest__.py create mode 100644 case_data_access/security/sales_team_security.xml diff --git a/case_data_access/__init__.py b/case_data_access/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/case_data_access/__manifest__.py b/case_data_access/__manifest__.py new file mode 100644 index 00000000000..6f83ed5d923 --- /dev/null +++ b/case_data_access/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Case Study: Data Access', + 'category': 'Sales/Sales', + 'depends': [ + 'sale', + 'sales_team', + ], + 'application': True, + 'installable': True, + 'data': [ + 'security/sales_team_security.xml' + ], + 'license': 'AGPL-3', +} diff --git a/case_data_access/security/sales_team_security.xml b/case_data_access/security/sales_team_security.xml new file mode 100644 index 00000000000..7b11f6b9a39 --- /dev/null +++ b/case_data_access/security/sales_team_security.xml @@ -0,0 +1,17 @@ + + + + Sales Team Leader + + + the user will have access to all of his own team's data in the sales application + + + + Sales Team Leader Rule + + + + [('team_id.id', '=', user.sale_team_id.id)] + + \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 2faedfc7bf8..3e01ef51900 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,8 +5,8 @@ 'base', ], 'data': [ - 'security/ir.model.access.csv', 'security/security.xml', + 'security/ir.model.access.csv', 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 88923f0328d..7e5658a23a5 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -4,4 +4,6 @@ access_user_estate_property_type,user_estate_property_type,model_estate_property access_manager_estate_property_type,manager_estate_property_type,model_estate_property_type,estate_group_manager,1,1,1,1 access_user_estate_property_tag,user_estate_property_tag,model_estate_property_tag,estate_group_user,1,0,0,0 access_manager_estate_property_tag,manager_estate_property_tag,model_estate_property_tag,estate_group_manager,1,1,1,1 -access_estate_property_offer,estate_property_offer,model_estate_property_offer,estate_group_user,1,1,1,1 \ No newline at end of file +access_estate_property_offer,estate_property_offer,model_estate_property_offer,estate_group_user,1,1,1,1 +base_access_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,0 +base_access_estate_property_offer,estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 From 37a5b15d0762dc6c51649464247ec61cd1ab5920 Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 3 Jul 2025 17:12:02 +0700 Subject: [PATCH 25/26] [IMP] estate: add unit test for estate property and estate property offer update code so no one can create an offer for a sold property and sell property with no accepted offer add unit test for sell property action and create property offer add unit test for onchange garden in estate property using odoo.test.form Onboarding Chapter 7: Unit Testing --- estate/models/estate_property.py | 4 +++ estate/models/estate_property_offer.py | 7 +++- estate/tests/__init__.py | 2 ++ estate/tests/common.py | 42 ++++++++++++++++++++++ estate/tests/test_estate_property.py | 37 +++++++++++++++++++ estate/tests/test_estate_property_offer.py | 38 ++++++++++++++++++++ 6 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/common.py create mode 100644 estate/tests/test_estate_property.py create mode 100644 estate/tests/test_estate_property_offer.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6b111cbf5cb..efa742faf80 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -121,6 +121,10 @@ def action_sell_property(self): if record.state == 'cancelled': raise UserError('Cancelled property cannot be sold.') + if not any( + offer.status == 'accepted' for offer in record.offer_ids): + raise UserError('There is no accepted offer.') + record.state = 'sold' return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 62299e927fa..a9d7bec882d 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,5 @@ from odoo import api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError class PropertyOffer(models.Model): @@ -52,6 +52,11 @@ def create(self, vals_list): if property_id: property = self.env['estate.property'].browse(property_id) + if property.state == 'sold': + raise UserError( + 'The property %s is already sold.' % property.name + ) + max_offer = max(property.offer_ids.mapped('price'), default=0.0) if new_price < max_offer: diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..d6724ad4c71 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_estate_property +from . import test_estate_property_offer diff --git a/estate/tests/common.py b/estate/tests/common.py new file mode 100644 index 00000000000..2eda93e8c31 --- /dev/null +++ b/estate/tests/common.py @@ -0,0 +1,42 @@ +from odoo.tests import TransactionCase + + +class TestEstateCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # PARTNER + cls.partner = cls.env['res.partner'].create({ + 'name': 'Test Partner' + }) + + # PROPERTIES + cls.property_offer_received = cls.env['estate.property'].create({ + 'name': 'Test Property Offer Received', + 'expected_price': 100000, + 'state': 'offer_received' + }) + cls.property_offer_accepted = cls.env['estate.property'].create({ + 'name': 'Test Property Offer Accepted', + 'expected_price': 200000, + 'state': 'offer_accepted' + }) + cls.property_cancelled = cls.env['estate.property'].create({ + 'name': 'Test Property Cancelled', + 'expected_price': 200000, + 'state': 'cancelled' + }) + + # OFFERS + cls.offer = cls.env['estate.property.offer'].create({ + 'property_id': cls.property_offer_received.id, + 'partner_id': cls.partner.id, + 'price': 110000 + }) + cls.offer_accepted = cls.env['estate.property.offer'].create({ + 'property_id': cls.property_offer_accepted.id, + 'partner_id': cls.partner.id, + 'price': 220000, + 'status': 'accepted' + }) diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..656a3038734 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,37 @@ +from odoo.tests import Form, tagged +from odoo.exceptions import UserError +from odoo.addons.estate.tests.common import TestEstateCommon + + +@tagged('post_install', '-at_install') +class TestEstateProperty(TestEstateCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_action_sell_property(self): + # Test failed case: Cancelled state + with self.assertRaises(UserError): + self.property_cancelled.action_sell_property() + + # Test failed case: No accepted offer + with self.assertRaises(UserError): + self.property_offer_received.action_sell_property() + + # Test successful case + result = self.property_offer_accepted.action_sell_property() + self.assertTrue(result) + self.assertEqual(self.property_offer_accepted.state, 'sold') + + def test_onchange_garden(self): + property_form = Form(self.env['estate.property']) + + # Test when garden is True + property_form.garden = True + self.assertEqual(property_form.garden_area, 10) + self.assertEqual(property_form.garden_orientation, 'north') + + # Test when garden is False + property_form.garden = False + self.assertFalse(property_form.garden_area) + self.assertFalse(property_form.garden_orientation) diff --git a/estate/tests/test_estate_property_offer.py b/estate/tests/test_estate_property_offer.py new file mode 100644 index 00000000000..a37c0524e95 --- /dev/null +++ b/estate/tests/test_estate_property_offer.py @@ -0,0 +1,38 @@ +from odoo.tests import tagged +from odoo.exceptions import UserError, ValidationError +from odoo.addons.estate.tests.common import TestEstateCommon + + +@tagged('post_install', '-at_install') +class TestEstatePropertyOffer(TestEstateCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_create_offer(self): + # Test failed case: Property is already sold + self.property_offer_accepted.action_sell_property() + + with self.assertRaises(UserError): + self.env['estate.property.offer'].create({ + 'property_id': self.property_offer_accepted.id, + 'partner_id': self.partner.id, + 'price': 250000 + }) + + # Test failed case: Offer price is less than maximum offer + with self.assertRaises(ValidationError): + self.env['estate.property.offer'].create({ + 'property_id': self.property_offer_received.id, + 'partner_id': self.partner.id, + 'price': 90000 # Less than the expected price + }) + + # Test successful case + offer = self.env['estate.property.offer'].create({ + 'property_id': self.property_offer_received.id, + 'partner_id': self.partner.id, + 'price': 120000 # Valid offer price + }) + self.assertTrue(offer) + self.assertEqual(offer.property_id, self.property_offer_received) From daefa077a3cc282e6ebcc23dd73dead74718038c Mon Sep 17 00:00:00 2001 From: rhri Date: Thu, 3 Jul 2025 17:15:30 +0700 Subject: [PATCH 26/26] [REM] estate: remove access rights for base user group these access rights are only used for dev purposes --- estate/security/ir.model.access.csv | 2 -- 1 file changed, 2 deletions(-) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 7e5658a23a5..36b6e2d0b43 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -5,5 +5,3 @@ access_manager_estate_property_type,manager_estate_property_type,model_estate_pr access_user_estate_property_tag,user_estate_property_tag,model_estate_property_tag,estate_group_user,1,0,0,0 access_manager_estate_property_tag,manager_estate_property_tag,model_estate_property_tag,estate_group_manager,1,1,1,1 access_estate_property_offer,estate_property_offer,model_estate_property_offer,estate_group_user,1,1,1,1 -base_access_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,0 -base_access_estate_property_offer,estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1