From 1b57e98b1a324d363b4850c9deb0efefba6c5691 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Wed, 7 May 2025 18:00:46 +0530
Subject: [PATCH 01/15] [ADD] estate: initial Real Estate module with models,
views, and relations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduce the base structure of the Real Estate application as per Odoo 18
developer tutorial, covering the foundational data model, views, menus,
access rights, and key relationships.
This commit adds:
- A new module named `estate`.
- The main model `estate.property` with relevant business fields (name, price,
expected price, description, living area, garden, garage, etc.).
- Custom menus, actions, form and list views for `estate.property`.
- Field attributes like `readonly`, `copy`, `required`, and default values.
- Access rights through `ir.model.access.csv` for developers.
- Multiple record filtering using `domain`, `search`, and view buttons.
- Related models:
- estate.property.type`: Many2one for property types.
- estate.property.tag`: Many2many tags with selection UI.
- estate.property.offer`: One2many offer list per property with inline editing
- Links to existing models: `res.partner` (buyer), `res.users` (salesperson).
- Navigation enhancements through related fields and smart buttons.
- Search enhancements using filters for related fields like tags and type.
The goal of this commit is to build a working foundation of the real estate
sales module including a robust data model, basic UI, and relations required
for future business logic and workflow implementation.
task-001 (Chapter 1–7 Odoo 18 Developer Tutorial)
---
estate/__init__.py | 1 +
estate/__manifest__.py | 19 +++
estate/models/__init__.py | 4 +
estate/models/estate_property.py | 76 +++++++++++
estate/models/estate_property_offer.py | 13 ++
estate/models/estate_property_tag.py | 7 +
estate/models/estate_property_type.py | 9 ++
estate/security/ir.model.access.csv | 5 +
estate/views/estate_menus.xml | 51 ++++++++
estate/views/estate_property_offer_views.xml | 30 +++++
estate/views/estate_property_tag_views.xml | 32 +++++
estate/views/estate_property_type_views.xml | 32 +++++
estate/views/estate_property_views.xml | 128 +++++++++++++++++++
13 files changed, 407 insertions(+)
create mode 100644 estate/__init__.py
create mode 100644 estate/__manifest__.py
create mode 100644 estate/models/__init__.py
create mode 100644 estate/models/estate_property.py
create mode 100644 estate/models/estate_property_offer.py
create mode 100644 estate/models/estate_property_tag.py
create mode 100644 estate/models/estate_property_type.py
create mode 100644 estate/security/ir.model.access.csv
create mode 100644 estate/views/estate_menus.xml
create mode 100644 estate/views/estate_property_offer_views.xml
create mode 100644 estate/views/estate_property_tag_views.xml
create mode 100644 estate/views/estate_property_type_views.xml
create mode 100644 estate/views/estate_property_views.xml
diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/estate/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
new file mode 100644
index 00000000000..acfbc610376
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ 'name': 'Real Estate',
+ 'version': '1.0',
+ 'depends': ['base'],
+ 'author': 'Rajeev Aanjana',
+ 'category': 'Real Estate',
+ 'description': 'A module for managing real estate properties',
+ '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',
+ ],
+ 'license': 'LGPL-3',
+ 'application': True,
+ 'installable': True,
+}
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..09b2099fe84
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,4 @@
+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
new file mode 100644
index 00000000000..6b0f394cbef
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,76 @@
+from odoo import fields, models
+from dateutil.relativedelta import relativedelta
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "Real Estate Property"
+
+ # Basic Fields
+ name = fields.Char(string="Name", required=True)
+ description = fields.Text(string="Description")
+ postcode = fields.Char(string="Postcode")
+
+ # Date Fields
+ date_availability = fields.Date(
+ string="Available From",
+ copy=False,
+ default=lambda self: fields.Date.today() + relativedelta(months=3)
+ )
+
+ # Price Fields
+ expected_price = fields.Float(string="Expected Price", required=True)
+ selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
+
+ # Property Details
+ bedrooms = fields.Integer(string="Bedrooms", default=2)
+ living_area = fields.Integer(string="Living Area (sqm)")
+ facades = fields.Integer(string="Facades")
+ garage = fields.Boolean(string="Garage")
+ garden = fields.Boolean(string="Garden")
+ garden_area = fields.Integer(string="Garden Area (sqm)")
+
+ # Selection Fields
+ garden_orientation = fields.Selection(
+ selection=[
+ ('north', 'North'),
+ ('south', 'South'),
+ ('east', 'East'),
+ ('west', 'West'),
+ ],
+ string="Garden Orientation"
+ )
+
+ state = fields.Selection(
+ selection=[
+ ('new', 'New'),
+ ('offer_received', 'Offer Received'),
+ ('offer_accepted', 'Offer Accepted'),
+ ('sold', 'Sold'),
+ ('cancelled', 'Cancelled'),
+ ],
+ string="Status",
+ required=True,
+ copy=False,
+ default='new'
+ )
+
+ active = fields.Boolean(string="Active", default=True)
+
+ # Many2one, Many2many, One2many Relation to Property Type
+ property_type_id = fields.Many2one("estate.property.type", string="Property Type")
+
+ buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
+
+ salesperson_id = fields.Many2one(
+ "res.users",
+ string="Salesperson",
+ default=lambda self: self.env.user
+ )
+
+ offer_ids = fields.One2many(
+ "estate.property.offer",
+ "property_id",
+ string="Offers"
+ )
+
+ tag_ids = fields.Many2many("estate.property.tag", string="Tags", widget="many2many_tags" )
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..767d755be98
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,13 @@
+from odoo import fields, models
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Real Estate Property Offer"
+
+ price = fields.Float()
+ status = fields.Selection(
+ selection=[('accepted', 'Accepted'), ('refused', 'Refused')],
+ copy=False
+ )
+ partner_id = fields.Many2one("res.partner", string="Partner", required=True)
+ property_id = fields.Many2one("estate.property", string="Property", required=True)
\ No newline at end of file
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..20cc687c86b
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _description = "Real Estate Property Tag"
+
+ 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..83b629bc687
--- /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"
+
+ # Basic Fields
+ name = fields.Char(required=True)
+
\ No newline at end of file
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..238e44969b0
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_estate_property_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1
+access_estate_property_type,access_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
new file mode 100644
index 00000000000..3947837b8bc
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..f6e1ddfa5c4
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,30 @@
+
+
+
+ estate.property.offer.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form
+ 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..81a14cfac64
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,32 @@
+
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
+
+ estate.property.tag.form
+ estate.property.tag
+
+
+
+
+
\ 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..35bcb0c0a5e
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,32 @@
+
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
\ 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..612413bc5fe
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,128 @@
+
+
+ Properties
+ estate.property
+ list,form
+
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From e65a6c3fbfc41bdb29f2817e1a2939bc42894035 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Thu, 8 May 2025 20:16:45 +0530
Subject: [PATCH 02/15] [ADD] estate: constraints, compute fields, and offer
logic enhancements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Enhance the Real Estate module by implementing computed fields, onchange
behavior, SQL constraints, and business logic for handling property offers as
outlined in Chapters 9 and 10 of the Odoo 18 Developer Tutorial.
This commit adds:
- Computed fields:
- `total_area`: combines living and garden areas.
- `best_price`: dynamically shows the highest offer on a property.
- Onchange method to auto-fill garden attributes when the garden is checked
- Python constraints using `@api.constrains` to:
- Ensure selling price is at least 90% of expected price.
- Validate date availability is not in the past.
- SQL-level check constraints to enforce:
- `expected_price > 0` on estate.property.
- `price > 0` on estate.property.offer.
- Action methods for accepting and refusing property offers:
- Accepting sets offer status and updates property selling price/buyer.
- Refusing only updates offer status.
- Constraints ensure only one offer can be accepted per property.
- Improved inline editing experience in offer list view.
- Readonly fields, computed badges, and smart button usability refinements.
These changes enforce critical business rules and improve user feedback,
ensuring data consistency and meaningful interactions within the Real
Estate flow.
task-002 (Chapter 9–10 Odoo 18 Developer Tutorial)
---
estate/models/estate_property.py | 99 +++++++++++++-
estate/models/estate_property_offer.py | 53 +++++++-
estate/models/estate_property_tag.py | 12 +-
estate/models/estate_property_type.py | 7 +-
estate/views/estate_property_offer_views.xml | 8 +-
estate/views/estate_property_views.xml | 136 +++++++++++--------
6 files changed, 252 insertions(+), 63 deletions(-)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 6b0f394cbef..578408b1cc4 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,5 +1,8 @@
-from odoo import fields, models
+from odoo import fields, api, models
from dateutil.relativedelta import relativedelta
+from odoo.exceptions import UserError
+from odoo.exceptions import ValidationError
+from odoo.tools.float_utils import float_compare
class EstateProperty(models.Model):
_name = "estate.property"
@@ -74,3 +77,97 @@ class EstateProperty(models.Model):
)
tag_ids = fields.Many2many("estate.property.tag", string="Tags", widget="many2many_tags" )
+
+ # Computed Fields & Onchange
+
+ total_area = fields.Float(string="Total Area",compute="_compute_total_area")
+
+ @api.depends("living_area", "garden_area")
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = record.living_area + record.garden_area
+
+ best_price = fields.Float("Best Offer", compute="_compute_best_price", store=True)
+
+ @api.depends("offer_ids.price")
+ def _compute_best_price(self):
+ for record in self:
+ if record.offer_ids:
+ record.best_price = max(record.offer_ids.mapped("price"))
+ else:
+ record.best_price = 0.0
+
+ # Onchange
+
+ @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
+
+
+ # Add Action Logic of "Cancel" & "Sold"
+
+ def action_set_sold(self):
+ for record in self:
+ if record.state == "cancelled":
+ raise UserError("Canceled property cannot be sold.")
+ record.state = "sold"
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Success',
+ 'message': 'The property has been marked as sold.',
+ 'sticky': False,
+ 'type': 'success',
+ }
+ }
+
+ def action_set_canceled(self):
+ for record in self:
+ if record.state == "sold":
+ raise UserError("Sold property cannot be canceled.")
+ record.state = "cancelled"
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Cancelled',
+ 'message': 'The property has been marked as canceled.',
+ 'sticky': False,
+ 'type': 'warning',
+ }
+ }
+
+
+ # SQL Constraints
+
+ _sql_constraints = [
+ (
+ "check_expected_price_positive",
+ "CHECK(expected_price > 0)",
+ "The expected price must be strictly positive.",
+ ),
+ (
+ "check_selling_price_positive",
+ "CHECK(selling_price >= 0)",
+ "The selling price must be positive.",
+ ),
+ ]
+
+ @api.constrains('selling_price', 'expected_price')
+ def _check_selling_price_threshold(self):
+ for record in self:
+ if record.selling_price:
+ if float_compare(
+ record.selling_price,
+ record.expected_price * 0.9,
+ precision_digits=2
+ ) < 0:
+ raise ValidationError(
+ "The selling price cannot be lower than 90% of the expected price."
+ )
\ No newline at end of file
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 767d755be98..75128a4e0f9 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -1,4 +1,6 @@
-from odoo import fields, models
+from odoo import fields, api, models
+from datetime import timedelta
+from odoo.exceptions import UserError
class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
@@ -10,4 +12,51 @@ class EstatePropertyOffer(models.Model):
copy=False
)
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
- property_id = fields.Many2one("estate.property", string="Property", required=True)
\ No newline at end of file
+ property_id = fields.Many2one("estate.property", string="Property", required=True)
+
+ validity = fields.Integer(string="Validity (days)", default=7)
+ date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline", store=True)
+
+ @api.depends("create_date", "validity")
+ def _compute_date_deadline(self):
+ for record in self:
+ if record.create_date:
+ record.date_deadline = record.create_date.date() + timedelta(days=record.validity)
+ else:
+ record.date_deadline = fields.Date.today() + timedelta(days=record.validity)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ if record.create_date and record.date_deadline:
+ record.validity = (record.date_deadline - record.create_date.date()).days
+ else:
+ record.validity = (record.date_deadline - fields.Date.today()).days
+
+ def action_accept(self):
+ for record in self:
+ if record.status == 'refused':
+ raise UserError("A refused offer cannot be accepted.")
+ record.status = 'accepted'
+ record.property_id.selling_price = record.price
+ record.property_id.buyer_id = record.partner_id
+ record.property_id.state = 'offer_accepted'
+ # Refuse other offers
+ for offer in record.property_id.offer_ids:
+ if offer != record and offer.status != 'accepted':
+ offer.status = 'refused'
+ return True
+
+ def action_refuse(self):
+ for record in self:
+ record.status = 'refused'
+ return True
+
+ # SQL Constraints
+
+ _sql_constraints = [
+ (
+ "check_offer_price_positive",
+ "CHECK(price > 0)",
+ "The offer price must be strictly positive.",
+ ),
+ ]
\ No newline at end of file
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index 20cc687c86b..e764cb83ecf 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -4,4 +4,14 @@ class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Real Estate Property Tag"
- name = fields.Char(required=True)
\ No newline at end of file
+ name = fields.Char(required=True)
+
+ # SQL Constraints
+
+ sql_constraints = [
+ (
+ "unique_property_tag_name",
+ "UNIQUE(name)",
+ "The name of the tag must be unique.",
+ ),
+ ]
\ No newline at end of file
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 83b629bc687..4cb97d29548 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -6,4 +6,9 @@ class EstatePropertyType(models.Model):
# Basic Fields
name = fields.Char(required=True)
-
\ No newline at end of file
+
+ # SQL Constraints
+
+ _sql_constraints = [
+ ('name_unique', 'UNIQUE(name)', 'Type name must be unique.'),
+ ]
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
index f6e1ddfa5c4..861e9e9ef08 100644
--- a/estate/views/estate_property_offer_views.xml
+++ b/estate/views/estate_property_offer_views.xml
@@ -7,7 +7,9 @@
-
+
+
+
@@ -17,11 +19,15 @@
estate.property.offer
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 612413bc5fe..4a23f87b372 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -10,19 +10,19 @@
estate.property.list
estate.property
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -34,69 +34,89 @@
estate.property
@@ -109,20 +129,22 @@
estate.property
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
\ No newline at end of file
From d2659654ac8bf98368261c7943b33612edb5fd78 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Thu, 29 May 2025 11:27:29 +0530
Subject: [PATCH 03/15] [IMP] estate: implement business logic for offers,
constraints, state transitions This commit completes the enhancements from
chapters 11 to 15 of the Odoo 18 Real Estate module. It introduces a range of
improvements:
- Added state field with custom transitions (`new`, `offer received`,
`offer accepted`, `sold`, `cancelled`), including buttons and logic for
Sold/Cancelled.
- Added computed fields: total_area (living + garden), best_offer, and
selling_price.
- Added SQL constraints and Python constraints to ensure offers are above
a threshold and selling price is logically correct.
- Used computed and inverse fields to manage garden area visibility based on the
garden boolean.
- Refactored UI behavior (e.g., readonly fields) based on the state of the
property.
- Automatically assigns partner to offers upon creation.
- Used Inheritance for inheriting the class and use the features.
- Created kanban view while using drag and drop.
These changes reinforce business rules and improve usability for real estate
agents managing listings and offers.
---
estate/__init__.py | 1 +
estate/__manifest__.py | 7 +-
estate/models/__init__.py | 12 +-
estate/models/estate_property.py | 212 ++++++++++--------
estate/models/estate_property_offer.py | 122 ++++++----
estate/models/estate_property_tag.py | 16 +-
estate/models/estate_property_type.py | 28 ++-
estate/models/res_users.py | 12 +
estate/views/estate_menus.xml | 29 ---
estate/views/estate_property_offer_views.xml | 41 ++--
estate/views/estate_property_tag_views.xml | 19 +-
estate/views/estate_property_type_views.xml | 51 ++++-
estate/views/estate_property_views.xml | 201 ++++++++++-------
estate/views/res_users_views.xml | 14 ++
estate_account/__init__.py | 1 +
estate_account/__manifest__.py | 15 ++
estate_account/models/__init__.py | 1 +
.../models/inherited_estate_property.py | 41 ++++
18 files changed, 536 insertions(+), 287 deletions(-)
create mode 100644 estate/models/res_users.py
create mode 100644 estate/views/res_users_views.xml
create mode 100644 estate_account/__init__.py
create mode 100644 estate_account/__manifest__.py
create mode 100644 estate_account/models/__init__.py
create mode 100644 estate_account/models/inherited_estate_property.py
diff --git a/estate/__init__.py b/estate/__init__.py
index 0650744f6bc..a9e3372262c 100644
--- a/estate/__init__.py
+++ b/estate/__init__.py
@@ -1 +1,2 @@
+
from . import models
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index acfbc610376..f8df7ad4b40 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -1,19 +1,22 @@
{
'name': 'Real Estate',
'version': '1.0',
- 'depends': ['base'],
+ 'depends': ['base', 'sale', 'mail'],
'author': 'Rajeev Aanjana',
'category': 'Real Estate',
'description': 'A module for managing real estate properties',
'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',
],
'license': 'LGPL-3',
'application': True,
'installable': True,
}
+
+
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
index 09b2099fe84..61cd27a37b3 100644
--- a/estate/models/__init__.py
+++ b/estate/models/__init__.py
@@ -1,4 +1,8 @@
-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
+from . import (
+ estate_property,
+ estate_property_type,
+ estate_property_offer,
+ estate_property_tag,
+ res_users,
+)
+
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 578408b1cc4..73b3e2c45db 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,13 +1,16 @@
-from odoo import fields, api, models
from dateutil.relativedelta import relativedelta
-from odoo.exceptions import UserError
-from odoo.exceptions import ValidationError
-from odoo.tools.float_utils import float_compare
+
+from odoo import fields, models,api
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.float_utils import float_compare, float_is_zero
+
class EstateProperty(models.Model):
_name = "estate.property"
_description = "Real Estate Property"
-
+ _order = "id desc"
+ _inherit = ["mail.thread"]
+
# Basic Fields
name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
@@ -26,48 +29,42 @@ class EstateProperty(models.Model):
# Property Details
bedrooms = fields.Integer(string="Bedrooms", default=2)
- living_area = fields.Integer(string="Living Area (sqm)")
+ living_area = fields.Float(string="Living Area (sqm)")
facades = fields.Integer(string="Facades")
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
- garden_area = fields.Integer(string="Garden Area (sqm)")
+ garden_area = fields.Float(string="Garden Area (sqm)")
- # Selection Fields
- garden_orientation = fields.Selection(
- selection=[
- ('north', 'North'),
- ('south', 'South'),
- ('east', 'East'),
- ('west', 'West'),
- ],
- string="Garden Orientation"
- )
+ best_price = fields.Float("Best Offer", compute="_compute_best_price", store=True)
+ # Selection Fields
+ garden_orientation = fields.Selection([('north', 'North'),('south', 'South'),('east', 'East'),('west', 'West')], string='Garden Orientation')
state = fields.Selection(
- selection=[
- ('new', 'New'),
- ('offer_received', 'Offer Received'),
- ('offer_accepted', 'Offer Accepted'),
- ('sold', 'Sold'),
- ('cancelled', 'Cancelled'),
+ [
+ ("new", "New"),
+ ("offer_received", "Offer Received"),
+ ("offer_accepted", "Offer Accepted"),
+ ("sold", "Sold"),
+ ("canceled", "Canceled"),
],
string="Status",
required=True,
copy=False,
- default='new'
+ default="new",
)
+
active = fields.Boolean(string="Active", default=True)
# Many2one, Many2many, One2many Relation to Property Type
- property_type_id = fields.Many2one("estate.property.type", string="Property Type")
-
+ property_type_id = fields.Many2one('estate.property.type', string="Property Type", required=True)
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
salesperson_id = fields.Many2one(
- "res.users",
- string="Salesperson",
- default=lambda self: self.env.user
+ "res.users",
+ string="Sales Person",
+ default=lambda self: self.env.user,
+ required=True,
)
offer_ids = fields.One2many(
@@ -76,98 +73,123 @@ class EstateProperty(models.Model):
string="Offers"
)
- tag_ids = fields.Many2many("estate.property.tag", string="Tags", widget="many2many_tags" )
+ tag_ids = fields.Many2many("estate.property.tag", string="Tags" )
+ total_area = fields.Float(string='Total Area (sqm)',compute='_compute_total_area',store=True,help='Sum of living area and garden area')
+
+ # SQL Constraints
+
+ _sql_constraints = [
+ (
+ "check_expected_price_positive",
+ "CHECK(expected_price >= 0)",
+ "The expected price must be strictly positive.",
+ ),
+ (
+ "check_selling_price_positive",
+ "CHECK(selling_price >= 0)",
+ "The selling price must be strictly positive.",
+ ),
+ (
+ "check_bedrooms_positive",
+ "CHECK(bedrooms >= 0)",
+ "The number of bedrooms must be zero or positive.",
+ ),
+ ]
# Computed Fields & Onchange
- total_area = fields.Float(string="Total Area",compute="_compute_total_area")
-
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
record.total_area = record.living_area + record.garden_area
- best_price = fields.Float("Best Offer", compute="_compute_best_price", store=True)
- @api.depends("offer_ids.price")
+
+
+ @api.depends("offer_ids.offer_price")
def _compute_best_price(self):
for record in self:
- if record.offer_ids:
- record.best_price = max(record.offer_ids.mapped("price"))
- else:
- record.best_price = 0.0
+ record.best_price = max(record.offer_ids.mapped("offer_price"), default=0.0)
# Onchange
- @api.onchange("garden")
+ @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
-
-
+ for record in self:
+ if record.garden:
+ record.garden_area = 10
+ record.garden_orientation = 'north'
+ else:
+ record.garden_area = 0
+ record.garden_orientation = ''
+
# Add Action Logic of "Cancel" & "Sold"
def action_set_sold(self):
for record in self:
- if record.state == "cancelled":
- raise UserError("Canceled property cannot be sold.")
+ if record.state == "canceled":
+ raise UserError("A cancelled property cannot be sold.")
+
+ if record.state != "offer_accepted":
+ raise UserError(
+ "You cannot mark a property as sold without accepting an offer."
+ )
+
record.state = "sold"
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': 'Success',
- 'message': 'The property has been marked as sold.',
- 'sticky': False,
- 'type': 'success',
- }
- }
+
+ return True
+
+ # return {
+ # 'type': 'ir.actions.client',
+ # 'tag': 'display_notification',
+ # 'params': {
+ # 'title': 'Success',
+ # 'message': 'The property has been marked as sold.',
+ # 'sticky': False,
+ # 'type': 'success',
+ # }
+ # }
def action_set_canceled(self):
for record in self:
if record.state == "sold":
raise UserError("Sold property cannot be canceled.")
- record.state = "cancelled"
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': 'Cancelled',
- 'message': 'The property has been marked as canceled.',
- 'sticky': False,
- 'type': 'warning',
- }
- }
-
+ record.state = "canceled"
+ # return {
+ # 'type': 'ir.actions.client',
+ # 'tag': 'display_notification',
+ # 'params': {
+ # 'title': 'canceled',
+ # 'message': 'The property has been marked as canceled.',
+ # 'sticky': False,
+ # 'type': 'warning',
+ # }
+ # }
- # SQL Constraints
-
- _sql_constraints = [
- (
- "check_expected_price_positive",
- "CHECK(expected_price > 0)",
- "The expected price must be strictly positive.",
- ),
- (
- "check_selling_price_positive",
- "CHECK(selling_price >= 0)",
- "The selling price must be positive.",
- ),
- ]
@api.constrains('selling_price', 'expected_price')
- def _check_selling_price_threshold(self):
+ def _check_selling_price(self):
+ for record in self:
+ if float_is_zero(record.selling_price, precision_digits=2):
+ continue
+ min_acceptable_price = 0.9 * record.expected_price
+ if float_compare(record.selling_price, min_acceptable_price, precision_digits=2) < 0:
+ raise ValidationError("The selling price cannot be lower than 90% of the expected price.")
+
+ @api.ondelete(at_uninstall=False)
+ def _check_state_before_delete(self):
for record in self:
- if record.selling_price:
- if float_compare(
- record.selling_price,
- record.expected_price * 0.9,
- precision_digits=2
- ) < 0:
- raise ValidationError(
- "The selling price cannot be lower than 90% of the expected price."
- )
\ No newline at end of file
+ if record.state not in ('new', 'canceled'):
+ raise UserError("You can only delete properties in 'New' or 'canceled' state.")
+
+
+ # def check_offer(self, price):
+ # curr_offers = []
+ # for record in self:
+ # for offer in record.offer_ids:
+ # curr_offers.append(offer.offer_price)
+ # if curr_offers:
+ # if price < min(curr_offers):
+ # raise exceptions.UserError("New offer cannot be lower than other offers")
+ # record.state = 'offer_received'
+
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 75128a4e0f9..884742ec578 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -1,62 +1,96 @@
-from odoo import fields, api, models
+from odoo import models, fields, api
+from odoo.exceptions import UserError, ValidationError
from datetime import timedelta
-from odoo.exceptions import UserError
+
class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Real Estate Property Offer"
-
- price = fields.Float()
+ _order = "offer_price desc"
+
+ offer_price = fields.Float(string="Price")
status = fields.Selection(
- selection=[('accepted', 'Accepted'), ('refused', 'Refused')],
- copy=False
+ [("accepted", "Accepted"), ("refused", "Refused")], string="Status", copy=False
+ )
+ partner_id = fields.Many2one("res.partner", string="Buyer", required=True)
+ property_id = fields.Many2one(
+ "estate.property", string="Property", required=True, ondelete="cascade"
)
- partner_id = fields.Many2one("res.partner", string="Partner", required=True)
- property_id = fields.Many2one("estate.property", string="Property", required=True)
-
validity = fields.Integer(string="Validity (days)", default=7)
- date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline", store=True)
-
- @api.depends("create_date", "validity")
+ date_deadline = fields.Date(
+ string="Date Deadline",
+ compute="_compute_date_deadline",
+ inverse="_inverse_date_deadline",
+ store=True,
+ )
+ property_type_id = fields.Many2one(
+ related="property_id.property_type_id", store=True
+ )
+
+ _sql_constraints = [
+ (
+ "check_offer_price_positive",
+ "CHECK(offer_price > 0)",
+ "The offer price must be strictly positive.",
+ ),
+ ]
+
+ @api.depends("validity")
def _compute_date_deadline(self):
for record in self:
- if record.create_date:
- record.date_deadline = record.create_date.date() + timedelta(days=record.validity)
- else:
- record.date_deadline = fields.Date.today() + timedelta(days=record.validity)
-
+ create_date = record.create_date or fields.Date.context_today(record)
+ record.date_deadline = create_date + timedelta(days=record.validity)
+
def _inverse_date_deadline(self):
for record in self:
- if record.create_date and record.date_deadline:
- record.validity = (record.date_deadline - record.create_date.date()).days
- else:
- record.validity = (record.date_deadline - fields.Date.today()).days
-
+ create_date = record.create_date or fields.Datetime.now()
+ delta = record.date_deadline - create_date.date()
+ record.validity = delta.days
+
def action_accept(self):
for record in self:
- if record.status == 'refused':
- raise UserError("A refused offer cannot be accepted.")
- record.status = 'accepted'
- record.property_id.selling_price = record.price
+ if record.property_id.state in ["sold", "canceled"]:
+ raise UserError(
+ "You cannot accept an offer for a sold or cancelled property."
+ )
+ if record.property_id.offer_ids.filtered(lambda o: o.status == "accepted"):
+ raise UserError("An offer has already been accepted for this property.")
+ record.status = "accepted"
record.property_id.buyer_id = record.partner_id
- record.property_id.state = 'offer_accepted'
- # Refuse other offers
- for offer in record.property_id.offer_ids:
- if offer != record and offer.status != 'accepted':
- offer.status = 'refused'
- return True
+ record.property_id.selling_price = record.offer_price
+ record.property_id.state = "offer_accepted"
def action_refuse(self):
for record in self:
- record.status = 'refused'
- return True
-
- # SQL Constraints
-
- _sql_constraints = [
- (
- "check_offer_price_positive",
- "CHECK(price > 0)",
- "The offer price must be strictly positive.",
- ),
- ]
\ No newline at end of file
+ record.status = "refused"
+
+ @api.model_create_multi
+ def create(self, offers):
+ # Extract property_id from the first offer
+ property_id = offers[0].get("property_id")
+ if not property_id:
+ raise ValidationError("Property ID is required.")
+
+ # Fetch the related property record
+ estate = self.env["estate.property"].browse(property_id)
+ if not estate.exists():
+ raise ValidationError("The specified property does not exist.")
+
+ if estate.state in ["sold", "canceled"]:
+ raise UserError("Cannot create an offer on a sold or canceled property.")
+ if estate.state == "offer_accepted":
+ raise UserError(
+ "Cannot create an offer on a property with an accepted offer."
+ )
+
+ curr_max_price = estate.best_price or 0.0
+
+ for offer in offers:
+ if curr_max_price >= offer["offer_price"]:
+ raise UserError(
+ "The offer price must be higher than the current best price."
+ )
+ curr_max_price = max(curr_max_price, offer["offer_price"])
+
+ estate.state = "offer_received"
+ return super().create(offers)
\ No newline at end of file
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index e764cb83ecf..0cbdac94f92 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -1,17 +1,17 @@
from odoo import fields, models
+
class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Real Estate Property Tag"
+ _order = "name asc"
- name = fields.Char(required=True)
+ name = fields.Char(string='Name', required=True)
+ color = fields.Integer(string="Color")
# SQL Constraints
- sql_constraints = [
- (
- "unique_property_tag_name",
- "UNIQUE(name)",
- "The name of the tag must be unique.",
- ),
- ]
\ No newline at end of file
+ _sql_constraints = [
+ ('unique_tag_name', 'UNIQUE(name)', 'The tag name must be unique.'),
+ ]
+
\ No newline at end of file
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 4cb97d29548..6ec5d1a3059 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -1,14 +1,38 @@
-from odoo import fields, models
+from odoo import fields,api, models
+
class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type"
+ _order = "sequence, name"
+
+ sequence = fields.Integer(string="Sequence")
# Basic Fields
name = fields.Char(required=True)
+ status = fields.Selection([('active', 'Active'),('inactive', 'Inactive'),], string='Status', default='active', required=True)
+ description = fields.Text(string="Description")
+
+ # One2Many
+ property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
+ offer_ids = fields.One2many(
+ 'estate.property.offer',
+ 'property_type_id',
+ string='Offers',
+ )
+
+ offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count')
+
+
# SQL Constraints
_sql_constraints = [
- ('name_unique', 'UNIQUE(name)', 'Type name must be unique.'),
+ ('unique_type_name', 'UNIQUE(name)', 'The property type name must be unique.'),
]
+
+ @api.depends("property_ids.offer_ids")
+ def _compute_offer_count(self):
+ for prop_type in self:
+ prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
+
\ No newline at end of file
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..e75b79a0402
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,12 @@
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ property_ids = fields.One2many(
+ 'estate.property',
+ 'salesperson_id',
+ string='Properties',
+ domain=[('state', '=', 'available')]
+ )
\ No newline at end of file
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
index 3947837b8bc..99695130d84 100644
--- a/estate/views/estate_menus.xml
+++ b/estate/views/estate_menus.xml
@@ -20,32 +20,3 @@
-
-
-
-
-
-
\ 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 861e9e9ef08..e7d7a854fff 100644
--- a/estate/views/estate_property_offer_views.xml
+++ b/estate/views/estate_property_offer_views.xml
@@ -1,33 +1,40 @@
-
-
+
+ Property Offers
+ estate.property.offer
+ list,form
+ [('property_type_id', '=', active_id)]
+
+
+
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
index 81a14cfac64..afb85749005 100644
--- a/estate/views/estate_property_tag_views.xml
+++ b/estate/views/estate_property_tag_views.xml
@@ -1,4 +1,4 @@
-
+
Property Tags
@@ -10,8 +10,9 @@
estate.property.tag.list
estate.property.tag
-
-
+
+
+
@@ -22,8 +23,18 @@
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
index 35bcb0c0a5e..83c5c9c6fb8 100644
--- a/estate/views/estate_property_type_views.xml
+++ b/estate/views/estate_property_type_views.xml
@@ -1,32 +1,77 @@
-
+
Property Types
estate.property.type
list,form
+ {'search_default_active_filter': True}
+
estate.property.type.list
estate.property.type
-
+
+
+
+
+
+
estate.property.type.form
estate.property.type
-
+
+
+
+ estate.property.type.search
+ estate.property.type
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 4a23f87b372..3a27431d1f7 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -2,7 +2,16 @@
Properties
estate.property
- list,form
+ list,form,kanban
+ {'search_default_available_property_filter': True}
+
+
+ Properties
+
+
+ Create your properties here.
+
+
@@ -10,19 +19,20 @@
estate.property.list
estate.property
-
-
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
@@ -35,94 +45,128 @@
+
+
+
+ estate.property.view.kanban
+ estate.property
+
+
+
+
+
+
+
+
+ Expected Price :
+
+
+ Best Price :
+
+
+ Selling Price:
+
+
+
+
+
+
+
+
+
+ Estate Create Form
+ estate.property
+ 17
+
+
+
+
+
estate.property.search
@@ -130,21 +174,20 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
-
-
\ No newline at end of file
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
new file mode 100644
index 00000000000..fe30c14d66e
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,14 @@
+
+
+ inherited.user.view.form
+ res.users
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
new file mode 100644
index 00000000000..9a7e03eded3
--- /dev/null
+++ b/estate_account/__init__.py
@@ -0,0 +1 @@
+from . import models
\ No newline at end of file
diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py
new file mode 100644
index 00000000000..eb10c91be5b
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,15 @@
+{
+ 'name': 'Real Estate Account',
+ 'version': '1.0',
+ 'summary': 'Link between Real Estate and Accounting',
+ 'description': """
+ This module links the Real Estate module with Accounting,
+ automatically creating invoices when properties are sold.
+ """,
+ 'depends': ['estate', 'account'],
+ "category": "Sales",
+ 'installable': True,
+ 'application': True,
+ 'auto_install': True,
+ "license": "LGPL-3",
+}
\ No newline at end of file
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..9e11045196d
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import inherited_estate_property
\ No newline at end of file
diff --git a/estate_account/models/inherited_estate_property.py b/estate_account/models/inherited_estate_property.py
new file mode 100644
index 00000000000..09bad029319
--- /dev/null
+++ b/estate_account/models/inherited_estate_property.py
@@ -0,0 +1,41 @@
+from datetime import datetime
+
+from odoo import models, Command
+
+
+class EstateModel(models.Model):
+ _inherit = "estate.property"
+
+ # Adding a new field to store the invoice reference
+ # Creates an invoice in Accounting (account.move)
+ def action_set_sold(self):
+ if super().action_set_sold() is True:
+ for record in self:
+ invoice_vals = record._prepare_invoice()
+
+ self.env["account.move"].create(invoice_vals)
+
+ # move_type means types of invoice
+ def _prepare_invoice(self):
+ invoice_vals = {
+ "partner_id": self.buyer_id.id,
+ "move_type": "out_invoice",
+ "invoice_date": datetime.today(),
+ "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,
+ }
+ ),
+ ],
+ }
+ return invoice_vals
\ No newline at end of file
From 734a36147250447d31809cbf8415819c1543f436 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Mon, 12 May 2025 18:50:29 +0530
Subject: [PATCH 04/15] [IMP] estate: apply styling improvements to code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refactored the code to enhance readability and maintainability by improving
indentation, spacing, and overall formatting. No functional or logic changes
were made—only visual/code style adjustments for clarity.
This improves code quality and aligns with best practices for clean code.
---
estate/__init__.py | 1 -
estate/__manifest__.py | 6 +-
estate/models/__init__.py | 1 -
estate/models/estate_property.py | 64 ++-----------------
estate/models/estate_property_offer.py | 7 +-
estate/models/estate_property_tag.py | 2 -
estate/models/estate_property_type.py | 18 ++----
estate/models/res_users.py | 5 +-
estate_account/__init__.py | 2 +-
estate_account/models/__init__.py | 2 +-
.../models/inherited_estate_property.py | 3 +-
11 files changed, 21 insertions(+), 90 deletions(-)
diff --git a/estate/__init__.py b/estate/__init__.py
index a9e3372262c..0650744f6bc 100644
--- a/estate/__init__.py
+++ b/estate/__init__.py
@@ -1,2 +1 @@
-
from . import models
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index f8df7ad4b40..81a07738c2f 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -5,7 +5,7 @@
'author': 'Rajeev Aanjana',
'category': 'Real Estate',
'description': 'A module for managing real estate properties',
- 'data':[
+ 'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
@@ -17,6 +17,4 @@
'license': 'LGPL-3',
'application': True,
'installable': True,
-}
-
-
+}
\ No newline at end of file
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
index 61cd27a37b3..0103b221de9 100644
--- a/estate/models/__init__.py
+++ b/estate/models/__init__.py
@@ -5,4 +5,3 @@
estate_property_tag,
res_users,
)
-
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 73b3e2c45db..0cc0d60da79 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,6 +1,6 @@
from dateutil.relativedelta import relativedelta
-from odoo import fields, models,api
+from odoo import fields, models, api
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero
@@ -10,23 +10,19 @@ class EstateProperty(models.Model):
_description = "Real Estate Property"
_order = "id desc"
_inherit = ["mail.thread"]
-
# Basic Fields
name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
postcode = fields.Char(string="Postcode")
-
# Date Fields
date_availability = fields.Date(
string="Available From",
copy=False,
default=lambda self: fields.Date.today() + relativedelta(months=3)
)
-
# Price Fields
expected_price = fields.Float(string="Expected Price", required=True)
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
-
# Property Details
bedrooms = fields.Integer(string="Bedrooms", default=2)
living_area = fields.Float(string="Living Area (sqm)")
@@ -34,11 +30,9 @@ class EstateProperty(models.Model):
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Float(string="Garden Area (sqm)")
-
best_price = fields.Float("Best Offer", compute="_compute_best_price", store=True)
-
# Selection Fields
- garden_orientation = fields.Selection([('north', 'North'),('south', 'South'),('east', 'East'),('west', 'West')], string='Garden Orientation')
+ garden_orientation = fields.Selection([('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], string='Garden Orientation')
state = fields.Selection(
[
("new", "New"),
@@ -52,33 +46,24 @@ class EstateProperty(models.Model):
copy=False,
default="new",
)
-
-
active = fields.Boolean(string="Active", default=True)
-
# Many2one, Many2many, One2many Relation to Property Type
property_type_id = fields.Many2one('estate.property.type', string="Property Type", required=True)
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
-
salesperson_id = fields.Many2one(
"res.users",
string="Sales Person",
default=lambda self: self.env.user,
required=True,
)
-
offer_ids = fields.One2many(
"estate.property.offer",
- "property_id",
+ "property_id",
string="Offers"
)
-
- tag_ids = fields.Many2many("estate.property.tag", string="Tags" )
- total_area = fields.Float(string='Total Area (sqm)',compute='_compute_total_area',store=True,help='Sum of living area and garden area')
-
-
+ tag_ids = fields.Many2many("estate.property.tag", string="Tags")
+ total_area = fields.Float(string='Total Area (sqm)', compute='_compute_total_area', store=True, help='Sum of living area and garden area')
# SQL Constraints
-
_sql_constraints = [
(
"check_expected_price_positive",
@@ -97,14 +82,10 @@ class EstateProperty(models.Model):
),
]
# Computed Fields & Onchange
-
@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.offer_price")
def _compute_best_price(self):
@@ -112,7 +93,6 @@ def _compute_best_price(self):
record.best_price = max(record.offer_ids.mapped("offer_price"), default=0.0)
# Onchange
-
@api.onchange('garden')
def _onchange_garden(self):
for record in self:
@@ -124,7 +104,6 @@ def _onchange_garden(self):
record.garden_orientation = ''
# Add Action Logic of "Cancel" & "Sold"
-
def action_set_sold(self):
for record in self:
if record.state == "canceled":
@@ -139,34 +118,12 @@ def action_set_sold(self):
return True
- # return {
- # 'type': 'ir.actions.client',
- # 'tag': 'display_notification',
- # 'params': {
- # 'title': 'Success',
- # 'message': 'The property has been marked as sold.',
- # 'sticky': False,
- # 'type': 'success',
- # }
- # }
-
def action_set_canceled(self):
for record in self:
if record.state == "sold":
raise UserError("Sold property cannot be canceled.")
record.state = "canceled"
- # return {
- # 'type': 'ir.actions.client',
- # 'tag': 'display_notification',
- # 'params': {
- # 'title': 'canceled',
- # 'message': 'The property has been marked as canceled.',
- # 'sticky': False,
- # 'type': 'warning',
- # }
- # }
-
@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for record in self:
@@ -182,14 +139,3 @@ def _check_state_before_delete(self):
if record.state not in ('new', 'canceled'):
raise UserError("You can only delete properties in 'New' or 'canceled' state.")
-
- # def check_offer(self, price):
- # curr_offers = []
- # for record in self:
- # for offer in record.offer_ids:
- # curr_offers.append(offer.offer_price)
- # if curr_offers:
- # if price < min(curr_offers):
- # raise exceptions.UserError("New offer cannot be lower than other offers")
- # record.state = 'offer_received'
-
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 884742ec578..6ea7f540b7f 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -70,7 +70,6 @@ def create(self, offers):
property_id = offers[0].get("property_id")
if not property_id:
raise ValidationError("Property ID is required.")
-
# Fetch the related property record
estate = self.env["estate.property"].browse(property_id)
if not estate.exists():
@@ -82,15 +81,13 @@ def create(self, offers):
raise UserError(
"Cannot create an offer on a property with an accepted offer."
)
-
curr_max_price = estate.best_price or 0.0
-
for offer in offers:
if curr_max_price >= offer["offer_price"]:
raise UserError(
"The offer price must be higher than the current best price."
)
curr_max_price = max(curr_max_price, offer["offer_price"])
-
estate.state = "offer_received"
- return super().create(offers)
\ No newline at end of file
+ return super().create(offers)
+
\ No newline at end of file
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index 0cbdac94f92..30a6ca55148 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -8,9 +8,7 @@ class EstatePropertyTag(models.Model):
name = fields.Char(string='Name', required=True)
color = fields.Integer(string="Color")
-
# SQL Constraints
-
_sql_constraints = [
('unique_tag_name', 'UNIQUE(name)', 'The tag name must be unique.'),
]
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 6ec5d1a3059..417f5cbf03c 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -1,38 +1,30 @@
-from odoo import fields,api, models
+from odoo import fields, api, models
class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type"
- _order = "sequence, name"
+ _order = "sequence, name"
sequence = fields.Integer(string="Sequence")
-
# Basic Fields
name = fields.Char(required=True)
- status = fields.Selection([('active', 'Active'),('inactive', 'Inactive'),], string='Status', default='active', required=True)
+ status = fields.Selection([('active', 'Active'), ('inactive', 'Inactive'),], string='Status', default='active', required=True)
description = fields.Text(string="Description")
-
# One2Many
-
property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
offer_ids = fields.One2many(
'estate.property.offer',
'property_type_id',
string='Offers',
)
-
- offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count')
-
-
+ offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count')
# SQL Constraints
-
_sql_constraints = [
('unique_type_name', 'UNIQUE(name)', 'The property type name must be unique.'),
]
-
@api.depends("property_ids.offer_ids")
def _compute_offer_count(self):
for prop_type in self:
- prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
+ prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
\ No newline at end of file
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
index e75b79a0402..4b179db4d09 100644
--- a/estate/models/res_users.py
+++ b/estate/models/res_users.py
@@ -5,8 +5,9 @@ class ResUsers(models.Model):
_inherit = 'res.users'
property_ids = fields.One2many(
- 'estate.property',
+ 'estate.property',
'salesperson_id',
string='Properties',
domain=[('state', '=', 'available')]
- )
\ No newline at end of file
+ )
+
\ No newline at end of file
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
index 9a7e03eded3..0650744f6bc 100644
--- a/estate_account/__init__.py
+++ b/estate_account/__init__.py
@@ -1 +1 @@
-from . import models
\ No newline at end of file
+from . import models
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
index 9e11045196d..93b13d1fc71 100644
--- a/estate_account/models/__init__.py
+++ b/estate_account/models/__init__.py
@@ -1 +1 @@
-from . import inherited_estate_property
\ No newline at end of file
+from . import inherited_estate_property
diff --git a/estate_account/models/inherited_estate_property.py b/estate_account/models/inherited_estate_property.py
index 09bad029319..95c5e3d1863 100644
--- a/estate_account/models/inherited_estate_property.py
+++ b/estate_account/models/inherited_estate_property.py
@@ -38,4 +38,5 @@ def _prepare_invoice(self):
),
],
}
- return invoice_vals
\ No newline at end of file
+ return invoice_vals
+
\ No newline at end of file
From 35f9b50a223293618ac781c72fa575edf52f2193 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Tue, 13 May 2025 11:26:29 +0530
Subject: [PATCH 05/15] [IMP] estate: Again apply styling improvements to code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refactored the code to enhance readability and maintainability by improving
indentation, spacing, and overall formatting. No functional or logic changes
were made—only visual/code style adjustments for clarity.
This improves code quality and aligns with best practices for clean code.
---
estate/__manifest__.py | 2 +-
estate/models/estate_property.py | 8 ++++----
estate/models/estate_property_offer.py | 1 -
estate/models/estate_property_tag.py | 3 +--
estate/models/estate_property_type.py | 10 +++++-----
estate/models/res_users.py | 1 -
estate_account/models/inherited_estate_property.py | 1 -
7 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index 81a07738c2f..d6fd02ec7e4 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -17,4 +17,4 @@
'license': 'LGPL-3',
'application': True,
'installable': True,
-}
\ No newline at end of file
+}
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 0cc0d60da79..0f2ab7330b9 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -57,7 +57,7 @@ class EstateProperty(models.Model):
required=True,
)
offer_ids = fields.One2many(
- "estate.property.offer",
+ "estate.property.offer",
"property_id",
string="Offers"
)
@@ -82,6 +82,7 @@ class EstateProperty(models.Model):
),
]
# Computed Fields & Onchange
+
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
@@ -123,7 +124,7 @@ def action_set_canceled(self):
if record.state == "sold":
raise UserError("Sold property cannot be canceled.")
record.state = "canceled"
-
+
@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for record in self:
@@ -132,10 +133,9 @@ def _check_selling_price(self):
min_acceptable_price = 0.9 * record.expected_price
if float_compare(record.selling_price, min_acceptable_price, precision_digits=2) < 0:
raise ValidationError("The selling price cannot be lower than 90% of the expected price.")
-
+
@api.ondelete(at_uninstall=False)
def _check_state_before_delete(self):
for record in self:
if record.state not in ('new', 'canceled'):
raise UserError("You can only delete properties in 'New' or 'canceled' state.")
-
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 6ea7f540b7f..80d1c82b426 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -90,4 +90,3 @@ def create(self, offers):
curr_max_price = max(curr_max_price, offer["offer_price"])
estate.state = "offer_received"
return super().create(offers)
-
\ No newline at end of file
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index 30a6ca55148..edc6fc0fff4 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -5,11 +5,10 @@ class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Real Estate Property Tag"
_order = "name asc"
-
+
name = fields.Char(string='Name', required=True)
color = fields.Integer(string="Color")
# SQL Constraints
_sql_constraints = [
('unique_tag_name', 'UNIQUE(name)', 'The tag name must be unique.'),
]
-
\ No newline at end of file
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 417f5cbf03c..2c3d79aac41 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -9,7 +9,7 @@ class EstatePropertyType(models.Model):
sequence = fields.Integer(string="Sequence")
# Basic Fields
name = fields.Char(required=True)
- status = fields.Selection([('active', 'Active'), ('inactive', 'Inactive'),], string='Status', default='active', required=True)
+ status = fields.Selection([('active', 'Active'), ('inactive', 'Inactive')], string='Status', default='active', required=True)
description = fields.Text(string="Description")
# One2Many
property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
@@ -18,13 +18,13 @@ class EstatePropertyType(models.Model):
'property_type_id',
string='Offers',
)
- offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count')
+ offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count')
# SQL Constraints
_sql_constraints = [
('unique_type_name', 'UNIQUE(name)', 'The property type name must be unique.'),
]
+
@api.depends("property_ids.offer_ids")
def _compute_offer_count(self):
- for prop_type in self:
- prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
-
\ No newline at end of file
+ for prop_type in self:
+ prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
index 4b179db4d09..4e241e7d1ab 100644
--- a/estate/models/res_users.py
+++ b/estate/models/res_users.py
@@ -10,4 +10,3 @@ class ResUsers(models.Model):
string='Properties',
domain=[('state', '=', 'available')]
)
-
\ No newline at end of file
diff --git a/estate_account/models/inherited_estate_property.py b/estate_account/models/inherited_estate_property.py
index 95c5e3d1863..4312e5fba29 100644
--- a/estate_account/models/inherited_estate_property.py
+++ b/estate_account/models/inherited_estate_property.py
@@ -39,4 +39,3 @@ def _prepare_invoice(self):
],
}
return invoice_vals
-
\ No newline at end of file
From d52e2a399c6b10f647fa1f5051f16b28e68f1c3b Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Mon, 19 May 2025 10:19:41 +0530
Subject: [PATCH 06/15] [IMP] estate: add chatter, demo data, access rules, and
PDF report
Enhance the estate module with collaborative features, realistic demo
content, fine-grained access control, and printable PDF output to
support real-world usage, development, and testing.
- Enabled chatter by adding `mail.thread` and `mail.activity.mixin` to property
and offer models for tracking and communication.
- Added a demo data file to preload properties, offers, and users for
development and testing.
- Implemented access control using mixins and record rules to restrict
read/write/delete permissions based on ownership and roles.
- Introduced a QWeb PDF report for properties with a print button to export
property data as a downloadable document.
Improves module functionality, security, and usability for real-world use and
testing.
---
estate/__manifest__.py | 14 +-
estate/data/estate_property_demo.xml | 125 ++++++++++++++++++
estate/data/master_data.xml | 14 ++
estate/models/estate_property.py | 25 +++-
estate/models/estate_property_offer.py | 12 +-
estate/models/estate_property_type.py | 8 +-
estate/report/estate_property_reports.xml | 21 +++
estate/report/estate_property_templates.xml | 99 ++++++++++++++
estate/security/ir.model.access.csv | 14 +-
estate/security/security.xml | 36 +++++
estate/views/estate_menus.xml | 6 +-
estate/views/estate_property_offer_views.xml | 4 +-
estate/views/estate_property_tag_views.xml | 2 +-
estate/views/estate_property_type_views.xml | 2 +-
estate/views/estate_property_views.xml | 5 +-
estate/views/res_users_views.xml | 2 +-
estate_account/__manifest__.py | 2 +-
.../models/inherited_estate_property.py | 43 +++---
18 files changed, 380 insertions(+), 54 deletions(-)
create mode 100644 estate/data/estate_property_demo.xml
create mode 100644 estate/data/master_data.xml
create mode 100644 estate/report/estate_property_reports.xml
create mode 100644 estate/report/estate_property_templates.xml
create mode 100644 estate/security/security.xml
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index d6fd02ec7e4..1ccf05654ac 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -1,11 +1,13 @@
{
'name': 'Real Estate',
'version': '1.0',
- 'depends': ['base', 'sale', 'mail'],
+ 'depends': ['base', 'mail'],
'author': 'Rajeev Aanjana',
- 'category': 'Real Estate',
+ 'category': 'Real Estate/Brokerage',
'description': 'A module for managing real estate properties',
+ 'application': True,
'data': [
+ 'security/security.xml',
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
@@ -13,8 +15,14 @@
'views/estate_property_tag_views.xml',
'views/res_users_views.xml',
'views/estate_menus.xml',
+ # 'data/master_data.xml',
+ 'data/estate_property_demo.xml',
+ 'report/estate_property_templates.xml',
+ 'report/estate_property_reports.xml',
],
+ # 'demo': [
+ # 'demo/demo_data.xml',
+ # ],
'license': 'LGPL-3',
- 'application': True,
'installable': True,
}
diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml
new file mode 100644
index 00000000000..635bd7a720f
--- /dev/null
+++ b/estate/data/estate_property_demo.xml
@@ -0,0 +1,125 @@
+
+
+ Residential
+
+
+ Commercial
+
+
+ Industrial
+
+
+ Land
+
+
+
+
+
+ Big Villa
+ new
+ A nice and big villa
+ 12345
+ 2020-02-02
+ 1600000
+ 6
+ 100
+ 4
+ True
+ True
+ 100000
+ south
+
+
+
+
+
+ 10000
+ 14
+
+
+
+
+
+ 1500000
+ 14
+
+
+
+
+
+ 1500001
+ 14
+
+
+
+
+
+ Trailer Home
+ canceled
+ Home in a trailer park
+ 54321
+ 1970-01-01
+ 100000
+ 120000
+ 1
+ 10
+ 4
+ False
+ True
+
+
+
+
+ International Space Station
+ new
+ Aliens sometimes come visit
+ 12345
+ 2030-12-31
+ 45890000
+
+
+
+
+ Cozy Cabin
+ new
+ Small cabin by lake
+ 10000
+ 2020-01-01
+ 80000
+ 1
+ 10
+ 4
+ False
+ True
+
+
+
+
+ 60000
+ 14
+
+
+
+
+
+ 75000
+ 14
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/data/master_data.xml b/estate/data/master_data.xml
new file mode 100644
index 00000000000..506df5750d1
--- /dev/null
+++ b/estate/data/master_data.xml
@@ -0,0 +1,14 @@
+
+
+ Residential
+
+
+ Commercial
+
+
+ Industrial
+
+
+ Land
+
+
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 0f2ab7330b9..e5863e7f374 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -9,7 +9,7 @@ class EstateProperty(models.Model):
_name = "estate.property"
_description = "Real Estate Property"
_order = "id desc"
- _inherit = ["mail.thread"]
+ _inherit = ['mail.thread', 'mail.activity.mixin']
# Basic Fields
name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
@@ -63,6 +63,12 @@ class EstateProperty(models.Model):
)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
total_area = fields.Float(string='Total Area (sqm)', compute='_compute_total_area', store=True, help='Sum of living area and garden area')
+ company_id = fields.Many2one(
+ 'res.company',
+ string='Company',
+ required=True,
+ default=lambda self: self.env.company
+ )
# SQL Constraints
_sql_constraints = [
(
@@ -82,7 +88,7 @@ class EstateProperty(models.Model):
),
]
# Computed Fields & Onchange
-
+
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
@@ -102,8 +108,8 @@ def _onchange_garden(self):
record.garden_orientation = 'north'
else:
record.garden_area = 0
- record.garden_orientation = ''
-
+ record.garden_orientation = False
+
# Add Action Logic of "Cancel" & "Sold"
def action_set_sold(self):
for record in self:
@@ -139,3 +145,14 @@ def _check_state_before_delete(self):
for record in self:
if record.state not in ('new', 'canceled'):
raise UserError("You can only delete properties in 'New' or 'canceled' state.")
+
+ def action_sold(self):
+ # Check the user has write access to the properties
+ self.check_access_rights('write')
+ self.check_access_rule('write')
+
+ # Create invoice with sudo to bypass access rights
+ invoice = self.env['account.move'].sudo().create({
+ # ... invoice creation data ...
+ })
+ return super().action_sold()
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 80d1c82b426..659b8cb4841 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -1,7 +1,8 @@
-from odoo import models, fields, api
-from odoo.exceptions import UserError, ValidationError
from datetime import timedelta
+from odoo import api, models, fields
+from odoo.exceptions import UserError, ValidationError
+
class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
@@ -21,7 +22,6 @@ class EstatePropertyOffer(models.Model):
string="Date Deadline",
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
- store=True,
)
property_type_id = fields.Many2one(
related="property_id.property_type_id", store=True
@@ -53,8 +53,11 @@ def action_accept(self):
raise UserError(
"You cannot accept an offer for a sold or cancelled property."
)
- if record.property_id.offer_ids.filtered(lambda o: o.status == "accepted"):
+ # if record.property_id.offer_ids.filtered(lambda o: o.status == "accepted"):
+ # raise UserError("An offer has already been accepted for this property.")
+ if record.property_id.state == 'offer_accepted':
raise UserError("An offer has already been accepted for this property.")
+
record.status = "accepted"
record.property_id.buyer_id = record.partner_id
record.property_id.selling_price = record.offer_price
@@ -88,5 +91,6 @@ def create(self, offers):
"The offer price must be higher than the current best price."
)
curr_max_price = max(curr_max_price, offer["offer_price"])
+
estate.state = "offer_received"
return super().create(offers)
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 2c3d79aac41..8de23618be7 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -1,11 +1,11 @@
-from odoo import fields, api, models
+from odoo import api, fields, models
class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type"
_order = "sequence, name"
-
+
sequence = fields.Integer(string="Sequence")
# Basic Fields
name = fields.Char(required=True)
@@ -23,8 +23,8 @@ class EstatePropertyType(models.Model):
_sql_constraints = [
('unique_type_name', 'UNIQUE(name)', 'The property type name must be unique.'),
]
-
+
@api.depends("property_ids.offer_ids")
def _compute_offer_count(self):
for prop_type in self:
- prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
+ prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml
new file mode 100644
index 00000000000..6e55125ad11
--- /dev/null
+++ b/estate/report/estate_property_reports.xml
@@ -0,0 +1,21 @@
+
+
+ Estate Property Offers Report
+ estate.property
+ qweb-pdf
+ estate.estate_property_report_offers
+ 'Property Offer Report - %s' % object.name
+
+
+
+
+
+ Salesperson Properties Report
+ res.users
+ qweb-pdf
+ estate.res_users_report_properties
+ 'Salesperson Properties Report - %s' % object.name
+
+
+
+
diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml
new file mode 100644
index 00000000000..051d86733b9
--- /dev/null
+++ b/estate/report/estate_property_templates.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+ Price |
+ Partner |
+ Validity (days) |
+ Deadline |
+ State |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ N/A
+ |
+
+
+
+
+ No offers available for this property.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Salesperson:
+
+
+ Expected Price:
+
+
+ Status:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Salesperson:
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+ Status:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
index 238e44969b0..0893d96cfcf 100644
--- a/estate/security/ir.model.access.csv
+++ b/estate/security/ir.model.access.csv
@@ -1,5 +1,11 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_estate_property_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1
-access_estate_property_type,access_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
+
+estate.access_estate_property_user,access_estate_property_user,estate.model_estate_property,estate.estate_group_user,1,1,1,0
+
+estate.access_estate_property_offer_user,access_estate_property_offer_user,estate.model_estate_property_offer,estate.estate_group_user,1,1,1,0
+
+estate.access_estate_property_type_user,access_estate_property_type_user,estate.model_estate_property_type,estate.estate_group_user,1,0,0,0
+estate.access_estate_property_type_manager,access_estate_property_type_manager,estate.model_estate_property_type,estate.estate_group_manager,1,1,1,0
+
+estate.access_estate_property_tag_user,access_estate_property_tag_user,estate.model_estate_property_tag,estate.estate_group_user,1,0,0,0
+estate.access_estate_property_tag_manager,access_estate_property_tag_manager,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,0
\ No newline at end of file
diff --git a/estate/security/security.xml b/estate/security/security.xml
new file mode 100644
index 00000000000..545ead76a5a
--- /dev/null
+++ b/estate/security/security.xml
@@ -0,0 +1,36 @@
+
+
+ Agent
+ A Real Estate Agent
+
+
+
+
+ Manager
+ A Real Estate Manager
+
+
+
+
+
+
+ Salesperson: Access to Own or Unassigned Properties
+
+
+ ['|', ('salesperson_id', '=', user.id), ('salesperson_id', '=', False)]
+
+
+
+ Manager: Full Access to All Properties
+
+
+
+
+
+
+ Company Isolation: Access Limited to User's Company Properties
+
+
+ ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
+
+
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
index 99695130d84..ade5a0aa7c6 100644
--- a/estate/views/estate_menus.xml
+++ b/estate/views/estate_menus.xml
@@ -1,6 +1,6 @@
-
+
@@ -18,5 +18,7 @@
-
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
index e7d7a854fff..059667293c3 100644
--- a/estate/views/estate_property_offer_views.xml
+++ b/estate/views/estate_property_offer_views.xml
@@ -32,7 +32,7 @@
-
+
@@ -40,4 +40,4 @@
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
index afb85749005..c5fdef1391d 100644
--- a/estate/views/estate_property_tag_views.xml
+++ b/estate/views/estate_property_tag_views.xml
@@ -40,4 +40,4 @@
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
index 83c5c9c6fb8..1f333da4cae 100644
--- a/estate/views/estate_property_type_views.xml
+++ b/estate/views/estate_property_type_views.xml
@@ -74,4 +74,4 @@
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 3a27431d1f7..d9d5943f4ba 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -115,6 +115,7 @@
+
@@ -177,7 +178,7 @@
-
+
@@ -190,4 +191,4 @@
-
\ No newline at end of file
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
index fe30c14d66e..d0845d551ed 100644
--- a/estate/views/res_users_views.xml
+++ b/estate/views/res_users_views.xml
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py
index eb10c91be5b..0624b0016ee 100644
--- a/estate_account/__manifest__.py
+++ b/estate_account/__manifest__.py
@@ -12,4 +12,4 @@
'application': True,
'auto_install': True,
"license": "LGPL-3",
-}
\ No newline at end of file
+}
diff --git a/estate_account/models/inherited_estate_property.py b/estate_account/models/inherited_estate_property.py
index 4312e5fba29..6aacfb04d6a 100644
--- a/estate_account/models/inherited_estate_property.py
+++ b/estate_account/models/inherited_estate_property.py
@@ -1,41 +1,34 @@
from datetime import datetime
-
-from odoo import models, Command
+from odoo import models, Command, api, _
+from odoo.exceptions import AccessError, UserError
class EstateModel(models.Model):
_inherit = "estate.property"
- # Adding a new field to store the invoice reference
- # Creates an invoice in Accounting (account.move)
def action_set_sold(self):
- if super().action_set_sold() is True:
- for record in self:
- invoice_vals = record._prepare_invoice()
+ self.check_access("write")
+ if super().action_sold() is True:
+ invoice_vals = self._prepare_invoice()
+ self.env["account.move"].sudo().create(invoice_vals)
- self.env["account.move"].create(invoice_vals)
- # move_type means types of invoice
def _prepare_invoice(self):
- invoice_vals = {
+ """Prepare invoice vals with strict field control"""
+ return {
"partner_id": self.buyer_id.id,
"move_type": "out_invoice",
"invoice_date": datetime.today(),
"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,
- }
- ),
+ Command.create({
+ "name": f"Commission for {self.name}",
+ "quantity": 1,
+ "price_unit": (self.selling_price * 0.06), # 6% commission
+ }),
+ Command.create({
+ "name": "Administrative Fees",
+ "quantity": 1,
+ "price_unit": 100.00, # Fixed fee
+ }),
],
}
- return invoice_vals
From f2062aa5cdcff3d88bcd3dc6a66176312c9e7d7a Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Tue, 20 May 2025 18:43:58 +0530
Subject: [PATCH 07/15] [IMP] estate: add website controller, offer wizard &
tests
This commit includes three major enhancements to the estate module:
1. Website Controller:
- Added a new website controller to render a list of available properties on
the frontend (`/properties`).
- Implemented optional filtering by min and max price using query parameters.
2. Add Offer Wizard:
- Introduced a wizard to allow adding offers to multiple properties in bulk.
- Activated via a new Add Offer button on the estate property list view.
3. Test Cases:
- Added unit tests to ensure the correctness of the code and logic.
These improvements significantly enhance usability for both website visitors
and internal users, especially salespeople handling bulk property offers.
---
estate/__init__.py | 2 +
estate/__manifest__.py | 4 +-
estate/controllers/__init__.py | 1 +
estate/controllers/main.py | 39 ++++++++
estate/models/estate_property.py | 11 ---
estate/models/estate_property_type.py | 4 +-
estate/security/ir.model.access.csv | 4 +-
estate/tests/__init__.py | 1 +
estate/tests/test_estate_property.py | 99 +++++++++++++++++++
estate/views/estate_property_templates.xml | 67 +++++++++++++
estate/views/estate_property_views.xml | 11 ++-
estate/wizard/__init__.py | 1 +
estate/wizard/estate_property_offer_wizard.py | 35 +++++++
.../wizard/estate_property_offer_wizard.xml | 28 ++++++
14 files changed, 291 insertions(+), 16 deletions(-)
create mode 100644 estate/controllers/__init__.py
create mode 100644 estate/controllers/main.py
create mode 100644 estate/tests/__init__.py
create mode 100644 estate/tests/test_estate_property.py
create mode 100644 estate/views/estate_property_templates.xml
create mode 100644 estate/wizard/__init__.py
create mode 100644 estate/wizard/estate_property_offer_wizard.py
create mode 100644 estate/wizard/estate_property_offer_wizard.xml
diff --git a/estate/__init__.py b/estate/__init__.py
index 0650744f6bc..48d9904390f 100644
--- a/estate/__init__.py
+++ b/estate/__init__.py
@@ -1 +1,3 @@
from . import models
+from . import controllers
+from . import wizard
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index 1ccf05654ac..f13c25445cb 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -1,7 +1,7 @@
{
'name': 'Real Estate',
'version': '1.0',
- 'depends': ['base', 'mail'],
+ 'depends': ['base', 'mail', 'website'],
'author': 'Rajeev Aanjana',
'category': 'Real Estate/Brokerage',
'description': 'A module for managing real estate properties',
@@ -9,12 +9,14 @@
'data': [
'security/security.xml',
'security/ir.model.access.csv',
+ 'wizard/estate_property_offer_wizard.xml',
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/res_users_views.xml',
'views/estate_menus.xml',
+ 'views/estate_property_templates.xml',
# 'data/master_data.xml',
'data/estate_property_demo.xml',
'report/estate_property_templates.xml',
diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py
new file mode 100644
index 00000000000..deec4a8b86d
--- /dev/null
+++ b/estate/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
\ No newline at end of file
diff --git a/estate/controllers/main.py b/estate/controllers/main.py
new file mode 100644
index 00000000000..e680081c4d3
--- /dev/null
+++ b/estate/controllers/main.py
@@ -0,0 +1,39 @@
+from odoo import http
+from odoo.http import request
+
+class EstateWebsiteController(http.Controller):
+
+ @http.route('/properties', type='http', auth='public', website=True)
+ def list_properties(self, min_price=0, max_price=0, **kwargs):
+ domain = []
+ try:
+ min_price = float(min_price)
+ except (ValueError, TypeError):
+ min_price = 0
+ try:
+ max_price = float(max_price)
+ except (ValueError, TypeError):
+ max_price = 0
+
+ if min_price:
+ domain.append(('selling_price', '>=', min_price))
+ if max_price:
+ domain.append(('selling_price', '<=', max_price))
+
+ properties = request.env['estate.property'].sudo().search(domain)
+
+ return request.render('estate.property_listing', {
+ 'properties': properties,
+ 'min_price': min_price,
+ 'max_price': max_price,
+ })
+
+ @http.route('/properties/', type='http', auth='public', website=True)
+ def property_detail(self, property_id, **kwargs):
+ property_rec = request.env['estate.property'].sudo().browse(property_id)
+ if not property_rec.exists():
+ return request.not_found()
+
+ return request.render('estate.property_detail', {
+ 'property': property_rec
+ })
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index e5863e7f374..d3423264f18 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -145,14 +145,3 @@ def _check_state_before_delete(self):
for record in self:
if record.state not in ('new', 'canceled'):
raise UserError("You can only delete properties in 'New' or 'canceled' state.")
-
- def action_sold(self):
- # Check the user has write access to the properties
- self.check_access_rights('write')
- self.check_access_rule('write')
-
- # Create invoice with sudo to bypass access rights
- invoice = self.env['account.move'].sudo().create({
- # ... invoice creation data ...
- })
- return super().action_sold()
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 8de23618be7..2b05ee685ca 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -26,5 +26,5 @@ class EstatePropertyType(models.Model):
@api.depends("property_ids.offer_ids")
def _compute_offer_count(self):
- for prop_type in self:
- prop_type.offer_count = len(prop_type.mapped("property_ids.offer_ids"))
+ for record in self:
+ record.offer_count = len(record.offer_ids)
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
index 0893d96cfcf..caa76898747 100644
--- a/estate/security/ir.model.access.csv
+++ b/estate/security/ir.model.access.csv
@@ -8,4 +8,6 @@ estate.access_estate_property_type_user,access_estate_property_type_user,estate.
estate.access_estate_property_type_manager,access_estate_property_type_manager,estate.model_estate_property_type,estate.estate_group_manager,1,1,1,0
estate.access_estate_property_tag_user,access_estate_property_tag_user,estate.model_estate_property_tag,estate.estate_group_user,1,0,0,0
-estate.access_estate_property_tag_manager,access_estate_property_tag_manager,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,0
\ No newline at end of file
+estate.access_estate_property_tag_manager,access_estate_property_tag_manager,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,0
+
+estate.access_estate_property_offer_wizard_user,access_estate_property_offer_wizard_user,estate.model_estate_property_offer_wizard,estate.estate_group_user,1,1,1,0
\ No newline at end of file
diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py
new file mode 100644
index 00000000000..18f3a50c3e1
--- /dev/null
+++ b/estate/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_estate_property
\ No newline at end of file
diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py
new file mode 100644
index 00000000000..5764a4aaea8
--- /dev/null
+++ b/estate/tests/test_estate_property.py
@@ -0,0 +1,99 @@
+from odoo import Command
+from odoo.exceptions import UserError
+from odoo.tests import tagged, Form
+from odoo.tests.common import TransactionCase
+
+
+@tagged("post_install", "-at_install")
+class EstateTestCase(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.properties = cls.env["estate.property"].create(
+ [
+ {
+ "name": "Sale Test Property",
+ "description": "Test Description",
+ "expected_price": 100000,
+ "living_area": 50,
+ },
+ {
+ "name": "Garden Test Property",
+ "description": "Test Description Garden",
+ "expected_price": 200000,
+ "living_area": 100,
+ },
+ ]
+ )
+
+ cls.offers = cls.env["estate.property.offer"].create(
+ [
+ {
+ "partner_id": cls.env.ref("base.res_partner_2").id,
+ "offer_price": 110000,
+ "property_id": cls.properties[0].id,
+ },
+ {
+ "partner_id": cls.env.ref("base.res_partner_12").id,
+ "offer_price": 130000,
+ "property_id": cls.properties[0].id,
+ },
+ {
+ "partner_id": cls.env.ref("base.res_partner_2").id,
+ "offer_price": 150000,
+ "property_id": cls.properties[0].id,
+ },
+ ]
+ )
+
+ def test_sell_property_without_accepted_offer(self):
+ """
+ Test selling a property without an accepted offer.
+ Ensure that a UserError is raised when trying to sell a property without an accepted offer.
+ Ensure that other offers are not allowed to be created after the property is sold.
+ """
+
+ with self.assertRaises(UserError):
+ self.properties[0].action_sold()
+
+ self.offers[1].action_accept()
+ self.properties[0].action_sold()
+
+ self.assertEqual(
+ self.properties[0].state, "sold", "Property was not marked as sold"
+ )
+
+ with self.assertRaises(UserError):
+ self.properties[0].offer_ids = [
+ Command.create(
+ {
+ "partner_id": self.env.ref("base.res_partner_2").id,
+ "price": 200000,
+ "property_id": self.properties[0].id,
+ }
+ )
+ ]
+
+ def test_garden_toggle(self):
+ """
+ Test toggling the garden field on the property.
+ Ensure that the garden area and orientation are resetting.
+ """
+
+ with Form(self.properties[1]) as form:
+ form.garden = True
+ self.assertEqual(form.garden_area, 10, "Garden area should be reset to 10")
+ self.assertEqual(
+ form.garden_orientation,
+ "north",
+ "Garden orientation should be reset to north",
+ )
+
+ form.garden = False
+ self.assertEqual(form.garden_area, 0, "Garden area should be reset to 0")
+ self.assertEqual(
+ form.garden_orientation,
+ False,
+ "Garden orientation should be reset to False",
+ )
\ No newline at end of file
diff --git a/estate/views/estate_property_templates.xml b/estate/views/estate_property_templates.xml
new file mode 100644
index 00000000000..6c4537d054b
--- /dev/null
+++ b/estate/views/estate_property_templates.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index d9d5943f4ba..7ec2373e42b 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -14,6 +14,12 @@
+
+ Open External Website
+ /properties
+ new
+
+
estate.property.list
@@ -21,7 +27,10 @@
-
+
+
diff --git a/estate/wizard/__init__.py b/estate/wizard/__init__.py
new file mode 100644
index 00000000000..78122fb9f31
--- /dev/null
+++ b/estate/wizard/__init__.py
@@ -0,0 +1 @@
+from . import estate_property_offer_wizard
\ No newline at end of file
diff --git a/estate/wizard/estate_property_offer_wizard.py b/estate/wizard/estate_property_offer_wizard.py
new file mode 100644
index 00000000000..6d07f43bf81
--- /dev/null
+++ b/estate/wizard/estate_property_offer_wizard.py
@@ -0,0 +1,35 @@
+from odoo import api, fields, models
+
+
+class MakeOfferWizard(models.TransientModel):
+ _name = 'estate.property.offer.wizard'
+ _description = 'Property Offer Wizard'
+
+ offer_price = fields.Float('Offer Price', required=True)
+ status = fields.Selection(
+ [('accepted', 'Accepted'), ('refused', 'Refused')],
+ string='Status'
+ )
+ partner_id = fields.Many2one('res.partner', 'Buyer', required=True)
+ property_ids = fields.Many2many(
+ 'estate.property',
+ string='Selected Properties',
+ )
+
+ @api.model
+ def default_get(self, fields):
+ res = super().default_get(fields)
+ property_ids = self.env.context.get("active_ids", [])
+ if property_ids:
+ res["property_ids"] = [(6, 0, property_ids)]
+ return res
+
+ def action_make_offer(self):
+ for property in self.property_ids:
+ self.env['estate.property.offer'].create({
+ "offer_price": self.offer_price,
+ 'status': self.status,
+ 'partner_id': self.partner_id.id,
+ 'property_id': property.id,
+ })
+ return {'type': 'ir.actions.act_window_close'}
\ No newline at end of file
diff --git a/estate/wizard/estate_property_offer_wizard.xml b/estate/wizard/estate_property_offer_wizard.xml
new file mode 100644
index 00000000000..6e0b4172d50
--- /dev/null
+++ b/estate/wizard/estate_property_offer_wizard.xml
@@ -0,0 +1,28 @@
+
+
+ Add Offer
+ estate.property.offer.wizard
+ form
+ new
+
+ list
+
+
+
+ estate.property.offer.wizard.form
+ estate.property.offer.wizard
+
+
+
+
+
\ No newline at end of file
From c431e831b216911d83020b4b616da031a3794214 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Tue, 20 May 2025 19:08:16 +0530
Subject: [PATCH 08/15] [IMP] estate: add website controller, offer wizard &
tests
This commit includes three major enhancements to the estate module:
1. Website Controller:
- Added a new website controller to render a list of available properties on
the frontend (`/properties`).
- Implemented optional filtering by min and max price using query parameters.
2. Add Offer Wizard:
- Introduced a wizard to allow adding offers to multiple properties in bulk.
- Activated via a new Add Offer button on the estate property list view.
3. Test Cases:
- Added unit tests to ensure the correctness of the code and logic.
These improvements significantly enhance usability for both website visitors
and internal users, especially salespeople handling bulk property offers.
---
estate/tests/test_estate_property.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py
index 5764a4aaea8..b8b1a1eec8f 100644
--- a/estate/tests/test_estate_property.py
+++ b/estate/tests/test_estate_property.py
@@ -55,10 +55,10 @@ def test_sell_property_without_accepted_offer(self):
"""
with self.assertRaises(UserError):
- self.properties[0].action_sold()
+ self.properties[0].action_set_sold()
self.offers[1].action_accept()
- self.properties[0].action_sold()
+ self.properties[0].action_set_sold()
self.assertEqual(
self.properties[0].state, "sold", "Property was not marked as sold"
From 6f6bcb871107fa172c051748ba38a2be3b21e399 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Thu, 22 May 2025 18:48:42 +0530
Subject: [PATCH 09/15] [IMP] estate, awesome_owl: update estate tests and
complete OWL exercises
Refactored test case logic in the estate module to improve clarity and
reliability.
Also finalized all exercises in the awesome_owl module using the OWL framework,
including the following key enhancements:
- Refactored the Card component to use slots instead of a content prop
- Implemented content toggling in Card with button and open/close state
- Ensured safe and correct rendering of dynamic HTML using t-raw
- Improved Playground layout with professional and colorful design
- Fixed TodoList display logic and checkbox behavior
These changes align the implementation with best practices in OWL and improve
user experience and maintainability.
---
.../static/src/components/card/card.js | 22 +++++++
.../static/src/components/card/card.xml | 19 ++++++
.../static/src/components/counter/counter.js | 28 ++++++++
.../static/src/components/counter/counter.xml | 26 ++++++++
.../src/components/playground/playground.js | 42 ++++++++++++
.../src/components/playground/playground.xml | 65 +++++++++++++++++++
.../src/components/todolist/todo_item.js | 23 +++++++
.../src/components/todolist/todo_item.xml | 27 ++++++++
.../src/components/todolist/todo_list.js | 49 ++++++++++++++
.../src/components/todolist/todo_list.xml | 36 ++++++++++
awesome_owl/static/src/main.js | 4 +-
awesome_owl/static/src/playground.js | 7 --
awesome_owl/static/src/playground.xml | 10 ---
awesome_owl/static/src/utils.js | 9 +++
estate/controllers/main.py | 16 ++---
estate/models/estate_property.py | 4 +-
estate/models/res_users.py | 2 +-
estate/tests/test_estate_property.py | 4 ++
.../models/inherited_estate_property.py | 2 +-
19 files changed, 365 insertions(+), 30 deletions(-)
create mode 100644 awesome_owl/static/src/components/card/card.js
create mode 100644 awesome_owl/static/src/components/card/card.xml
create mode 100644 awesome_owl/static/src/components/counter/counter.js
create mode 100644 awesome_owl/static/src/components/counter/counter.xml
create mode 100644 awesome_owl/static/src/components/playground/playground.js
create mode 100644 awesome_owl/static/src/components/playground/playground.xml
create mode 100644 awesome_owl/static/src/components/todolist/todo_item.js
create mode 100644 awesome_owl/static/src/components/todolist/todo_item.xml
create mode 100644 awesome_owl/static/src/components/todolist/todo_list.js
create mode 100644 awesome_owl/static/src/components/todolist/todo_list.xml
delete mode 100644 awesome_owl/static/src/playground.js
delete mode 100644 awesome_owl/static/src/playground.xml
create mode 100644 awesome_owl/static/src/utils.js
diff --git a/awesome_owl/static/src/components/card/card.js b/awesome_owl/static/src/components/card/card.js
new file mode 100644
index 00000000000..82e746d11b4
--- /dev/null
+++ b/awesome_owl/static/src/components/card/card.js
@@ -0,0 +1,22 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.card";
+ static props = {
+ title: String,
+ slots: {
+ type: Object,
+ shape: {
+ default: true
+ },
+ }
+ };
+
+ setup() {
+ this.state = useState({ isOpen: true });
+ }
+
+ toggleContent() {
+ this.state.isOpen = !this.state.isOpen;
+ }
+}
\ No newline at end of file
diff --git a/awesome_owl/static/src/components/card/card.xml b/awesome_owl/static/src/components/card/card.xml
new file mode 100644
index 00000000000..4cd71d6575f
--- /dev/null
+++ b/awesome_owl/static/src/components/card/card.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/components/counter/counter.js b/awesome_owl/static/src/components/counter/counter.js
new file mode 100644
index 00000000000..b8739b6a95a
--- /dev/null
+++ b/awesome_owl/static/src/components/counter/counter.js
@@ -0,0 +1,28 @@
+// static/src/components/counter.js
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+static template = "component_counter";
+static props = {
+ title: String,
+ onChange: { type: Function, optional: true },
+};
+
+ setup() {
+ this.state = useState({ value: 1 });
+ }
+
+ increment() {
+ this.state.value++;
+ if (this.props.onChange) {
+ this.props.onChange(this.state.value);
+ }
+ }
+
+ decrement() {
+ this.state.value--;
+ if (this.props.onChange) {
+ this.props.onChange(this.state.value);
+ }
+ }
+}
diff --git a/awesome_owl/static/src/components/counter/counter.xml b/awesome_owl/static/src/components/counter/counter.xml
new file mode 100644
index 00000000000..bdff23ca634
--- /dev/null
+++ b/awesome_owl/static/src/components/counter/counter.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+ Value:
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_owl/static/src/components/playground/playground.js b/awesome_owl/static/src/components/playground/playground.js
new file mode 100644
index 00000000000..2ccf27215f5
--- /dev/null
+++ b/awesome_owl/static/src/components/playground/playground.js
@@ -0,0 +1,42 @@
+/** @odoo-module **/
+
+import { Component, markup, useState } from "@odoo/owl";
+import { Counter } from "../counter/counter";
+import { Card } from "../card/card";
+import { TodoList } from "../todolist/todo_list";
+
+export class Playground extends Component {
+ static template = "awesome_owl.playground";
+ static components = {Counter, Card, TodoList}
+
+ setup(){
+ this.state = useState({
+ counter1: 1,
+ counter2: 1,
+ sum:2,
+ });
+ }
+
+ calculateSum=(counterName, value)=>{
+ this.state[counterName]=value
+ this.state.sum = this.state.counter1 + this.state.counter2;
+ }
+
+ cards = [
+ {
+ id:1,
+ title: "Card 1",
+ content: "Just a simple text (escaped by default)",
+ },
+ {
+ id:2,
+ title: "Card 2",
+ content: markup("Bold HTML content"),
+ },
+ {
+ id:3,
+ title: "Card 3",
+ content: markup("Red colored HTML
"),
+ },
+ ];
+}
diff --git a/awesome_owl/static/src/components/playground/playground.xml b/awesome_owl/static/src/components/playground/playground.xml
new file mode 100644
index 00000000000..70665b78594
--- /dev/null
+++ b/awesome_owl/static/src/components/playground/playground.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+ 🧮 Owl Counter Playground
+
+
+
+
+
+ The sum is:
+
+
+
+
+
+
+ 🃏 Dynamic Cards (Loop)
+
+
+
+
+
+ 🧩 Custom Cards with Slots
+
+
+
+
+
+
+
+
Hello Owl 🦉!
+
This is a card with custom slot content.
+
+
+
+
+
+ - ✅ Uses Bootstrap styling
+ - 🎨 Supports any component inside
+ - 🧩 Built using
<t-slot>
+
+
+
+
+
+
+
+ ✅ Your Task List
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/components/todolist/todo_item.js b/awesome_owl/static/src/components/todolist/todo_item.js
new file mode 100644
index 00000000000..b48a4e6eba6
--- /dev/null
+++ b/awesome_owl/static/src/components/todolist/todo_item.js
@@ -0,0 +1,23 @@
+import { Component } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.todoItem";
+ static props = {
+ todo: {
+ type: Object,
+ shape: {
+ id: Number,
+ description: String,
+ isCompleted: Boolean
+ }
+ },
+ toggleState: Function,
+ removeTodo: Function,
+ };
+ onCheckboxChange() {
+ this.props.toggleState(this.props.todo.id);
+ }
+ onRemoveClick() {
+ this.props.removeTodo(this.props.todo.id);
+ }
+}
diff --git a/awesome_owl/static/src/components/todolist/todo_item.xml b/awesome_owl/static/src/components/todolist/todo_item.xml
new file mode 100644
index 00000000000..471f448ccf9
--- /dev/null
+++ b/awesome_owl/static/src/components/todolist/todo_item.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/components/todolist/todo_list.js b/awesome_owl/static/src/components/todolist/todo_list.js
new file mode 100644
index 00000000000..b0bdfb7c67b
--- /dev/null
+++ b/awesome_owl/static/src/components/todolist/todo_list.js
@@ -0,0 +1,49 @@
+import { Component, useState, useRef } from "@odoo/owl";
+import { TodoItem } from "./todo_item";
+import { useAutofocus } from "../../utils";
+
+export class TodoList extends Component {
+ static template = "awesome_owl.todoList";
+ static components = { TodoItem};
+
+ setup() {
+ this.todos = useState([]);
+ this.nextId = 1;
+ useAutofocus("input")
+ // this.toggleTodo = this.toggleTodo.bind(this);
+ }
+
+ addTodo(item){
+ if(item.key === 'Enter'){
+ const input = item.target;
+ const description = input.value.trim();
+
+ if(!description){
+ return;
+ }
+ else{
+ this.todos.push({
+ id: this.nextId++,
+ description,
+ isCompleted : false
+ });
+ input.value = ""
+ }
+ }
+ }
+
+ toggleTodo=(id)=>{
+ const todo = this.todos.find(prev => prev.id === id);
+ if(todo){
+ todo.isCompleted = !todo.isCompleted
+ }
+ }
+
+ removeTodoItem=(todoId)=> {
+ const index = this.todos.findIndex(todo => todo.id === todoId);
+ if (index >= 0) {
+ this.todos.splice(index, 1);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/awesome_owl/static/src/components/todolist/todo_list.xml b/awesome_owl/static/src/components/todolist/todo_list.xml
new file mode 100644
index 00000000000..dfaede7f0ed
--- /dev/null
+++ b/awesome_owl/static/src/components/todolist/todo_list.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
📝 Todo List
+
+ Tasks
+
+
+
+
+
+
+
+
+
+
+
+ No tasks yet. Add one above ⬆️
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js
index 1af6c827e0b..7f8ec6badf2 100644
--- a/awesome_owl/static/src/main.js
+++ b/awesome_owl/static/src/main.js
@@ -1,6 +1,8 @@
import { whenReady } from "@odoo/owl";
import { mountComponent } from "@web/env";
-import { Playground } from "./playground";
+import { Playground } from "./components/playground/playground";
+import "@web/core/assets";
+
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/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..8a56d82c8bf
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,9 @@
+import { useRef, onMounted } from "@odoo/owl";
+
+export function useAutofocus(refName) {
+ const ref = useRef(refName);
+ onMounted(() => {
+ ref.el?.focus();
+ });
+ return ref;
+}
diff --git a/estate/controllers/main.py b/estate/controllers/main.py
index e680081c4d3..6a7c91b9a07 100644
--- a/estate/controllers/main.py
+++ b/estate/controllers/main.py
@@ -6,14 +6,14 @@ class EstateWebsiteController(http.Controller):
@http.route('/properties', type='http', auth='public', website=True)
def list_properties(self, min_price=0, max_price=0, **kwargs):
domain = []
- try:
- min_price = float(min_price)
- except (ValueError, TypeError):
- min_price = 0
- try:
- max_price = float(max_price)
- except (ValueError, TypeError):
- max_price = 0
+ # try:
+ # min_price = float(min_price)
+ # except (ValueError, TypeError):
+ # min_price = 0
+ # try:
+ # max_price = float(max_price)
+ # except (ValueError, TypeError):
+ # max_price = 0
if min_price:
domain.append(('selling_price', '>=', min_price))
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index d3423264f18..c76223265e3 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -30,7 +30,7 @@ class EstateProperty(models.Model):
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Float(string="Garden Area (sqm)")
- best_price = fields.Float("Best Offer", compute="_compute_best_price", store=True)
+ best_price = fields.Float("Best Offer", compute="_compute_best_price")
# Selection Fields
garden_orientation = fields.Selection([('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], string='Garden Orientation')
state = fields.Selection(
@@ -62,7 +62,7 @@ class EstateProperty(models.Model):
string="Offers"
)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
- total_area = fields.Float(string='Total Area (sqm)', compute='_compute_total_area', store=True, help='Sum of living area and garden area')
+ total_area = fields.Float(string='Total Area (sqm)', compute='_compute_total_area', help='Sum of living area and garden area')
company_id = fields.Many2one(
'res.company',
string='Company',
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
index 4e241e7d1ab..964cf103b75 100644
--- a/estate/models/res_users.py
+++ b/estate/models/res_users.py
@@ -8,5 +8,5 @@ class ResUsers(models.Model):
'estate.property',
'salesperson_id',
string='Properties',
- domain=[('state', '=', 'available')]
+ # domain=[('state', '=', 'available')]
)
diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py
index b8b1a1eec8f..2f8df06feb3 100644
--- a/estate/tests/test_estate_property.py
+++ b/estate/tests/test_estate_property.py
@@ -9,6 +9,8 @@ class EstateTestCase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
+
+ cls.property_type = cls.env["estate.property.type"].create({"name": "House Test Type"})
cls.properties = cls.env["estate.property"].create(
[
@@ -17,12 +19,14 @@ def setUpClass(cls):
"description": "Test Description",
"expected_price": 100000,
"living_area": 50,
+ "property_type_id": cls.property_type.id,
},
{
"name": "Garden Test Property",
"description": "Test Description Garden",
"expected_price": 200000,
"living_area": 100,
+ "property_type_id": cls.property_type.id,
},
]
)
diff --git a/estate_account/models/inherited_estate_property.py b/estate_account/models/inherited_estate_property.py
index 6aacfb04d6a..ff457ac8806 100644
--- a/estate_account/models/inherited_estate_property.py
+++ b/estate_account/models/inherited_estate_property.py
@@ -8,7 +8,7 @@ class EstateModel(models.Model):
def action_set_sold(self):
self.check_access("write")
- if super().action_sold() is True:
+ if super().action_set_sold() is True:
invoice_vals = self._prepare_invoice()
self.env["account.move"].sudo().create(invoice_vals)
From 1cbf9c38fa755a5f2944bff1792a1be33874bbf0 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Fri, 23 May 2025 10:40:12 +0530
Subject: [PATCH 10/15] [IMP] estate: update the test cases logic
---
estate/controllers/__init__.py | 2 +-
estate/controllers/main.py | 1 +
estate/models/estate_property.py | 8 ++++----
estate/models/estate_property_offer.py | 18 ++++++++++++------
estate/tests/__init__.py | 2 +-
estate/tests/test_estate_property.py | 4 ++--
estate/wizard/__init__.py | 2 +-
estate/wizard/estate_property_offer_wizard.py | 4 ++--
.../models/inherited_estate_property.py | 4 +---
9 files changed, 25 insertions(+), 20 deletions(-)
diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py
index deec4a8b86d..12a7e529b67 100644
--- a/estate/controllers/__init__.py
+++ b/estate/controllers/__init__.py
@@ -1 +1 @@
-from . import main
\ No newline at end of file
+from . import main
diff --git a/estate/controllers/main.py b/estate/controllers/main.py
index 6a7c91b9a07..791d9744faa 100644
--- a/estate/controllers/main.py
+++ b/estate/controllers/main.py
@@ -1,6 +1,7 @@
from odoo import http
from odoo.http import request
+
class EstateWebsiteController(http.Controller):
@http.route('/properties', type='http', auth='public', website=True)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index c76223265e3..1d04c95fec7 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -64,9 +64,9 @@ class EstateProperty(models.Model):
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
total_area = fields.Float(string='Total Area (sqm)', compute='_compute_total_area', help='Sum of living area and garden area')
company_id = fields.Many2one(
- 'res.company',
- string='Company',
- required=True,
+ 'res.company',
+ string='Company',
+ required=True,
default=lambda self: self.env.company
)
# SQL Constraints
@@ -109,7 +109,7 @@ def _onchange_garden(self):
else:
record.garden_area = 0
record.garden_orientation = False
-
+
# Add Action Logic of "Cancel" & "Sold"
def action_set_sold(self):
for record in self:
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 659b8cb4841..0714f79a7f9 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -69,28 +69,34 @@ def action_refuse(self):
@api.model_create_multi
def create(self, offers):
+ # Guard clause for empty input
+ if not offers:
+ return super().create(offers)
+
# Extract property_id from the first offer
property_id = offers[0].get("property_id")
if not property_id:
raise ValidationError("Property ID is required.")
+
# Fetch the related property record
estate = self.env["estate.property"].browse(property_id)
if not estate.exists():
raise ValidationError("The specified property does not exist.")
+ # Business rules
if estate.state in ["sold", "canceled"]:
raise UserError("Cannot create an offer on a sold or canceled property.")
if estate.state == "offer_accepted":
- raise UserError(
- "Cannot create an offer on a property with an accepted offer."
- )
+ raise UserError("Cannot create an offer on a property with an accepted offer.")
+
+ # Offer price validation
curr_max_price = estate.best_price or 0.0
for offer in offers:
if curr_max_price >= offer["offer_price"]:
- raise UserError(
- "The offer price must be higher than the current best price."
- )
+ raise UserError("The offer price must be higher than the current best price.")
curr_max_price = max(curr_max_price, offer["offer_price"])
+ # Update property state
estate.state = "offer_received"
+
return super().create(offers)
diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py
index 18f3a50c3e1..576617cccff 100644
--- a/estate/tests/__init__.py
+++ b/estate/tests/__init__.py
@@ -1 +1 @@
-from . import test_estate_property
\ No newline at end of file
+from . import test_estate_property
diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py
index 2f8df06feb3..6cd509d6d09 100644
--- a/estate/tests/test_estate_property.py
+++ b/estate/tests/test_estate_property.py
@@ -9,7 +9,7 @@ class EstateTestCase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
-
+
cls.property_type = cls.env["estate.property.type"].create({"name": "House Test Type"})
cls.properties = cls.env["estate.property"].create(
@@ -100,4 +100,4 @@ def test_garden_toggle(self):
form.garden_orientation,
False,
"Garden orientation should be reset to False",
- )
\ No newline at end of file
+ )
diff --git a/estate/wizard/__init__.py b/estate/wizard/__init__.py
index 78122fb9f31..e9926bcd3ec 100644
--- a/estate/wizard/__init__.py
+++ b/estate/wizard/__init__.py
@@ -1 +1 @@
-from . import estate_property_offer_wizard
\ No newline at end of file
+from . import estate_property_offer_wizard
diff --git a/estate/wizard/estate_property_offer_wizard.py b/estate/wizard/estate_property_offer_wizard.py
index 6d07f43bf81..6b7bf1b8a60 100644
--- a/estate/wizard/estate_property_offer_wizard.py
+++ b/estate/wizard/estate_property_offer_wizard.py
@@ -12,7 +12,7 @@ class MakeOfferWizard(models.TransientModel):
)
partner_id = fields.Many2one('res.partner', 'Buyer', required=True)
property_ids = fields.Many2many(
- 'estate.property',
+ 'estate.property',
string='Selected Properties',
)
@@ -32,4 +32,4 @@ def action_make_offer(self):
'partner_id': self.partner_id.id,
'property_id': property.id,
})
- return {'type': 'ir.actions.act_window_close'}
\ No newline at end of file
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/estate_account/models/inherited_estate_property.py b/estate_account/models/inherited_estate_property.py
index ff457ac8806..83f31fb382b 100644
--- a/estate_account/models/inherited_estate_property.py
+++ b/estate_account/models/inherited_estate_property.py
@@ -1,6 +1,5 @@
from datetime import datetime
-from odoo import models, Command, api, _
-from odoo.exceptions import AccessError, UserError
+from odoo import models, Command
class EstateModel(models.Model):
@@ -12,7 +11,6 @@ def action_set_sold(self):
invoice_vals = self._prepare_invoice()
self.env["account.move"].sudo().create(invoice_vals)
-
def _prepare_invoice(self):
"""Prepare invoice vals with strict field control"""
return {
From 4c06dbdff8aa014a5ffe4842b4c6923b2b963068 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Mon, 26 May 2025 18:39:08 +0530
Subject: [PATCH 11/15] [IMP] awesome_dashboard: make dashboard items dynamic
and reusable
This commit restructures the dashboard to render items dynamically based on a
centralized configuration list. This approach enables easier extension and
reuse of dashboard item types, improving maintainability and scalability.
Key changes include:
Introduction of dashboard_items.js to define a list of item configurations,
including type, title, and props. This enables dynamic rendering of
dashboard widgets like NumberCard and PieChartCard.
The DashboardItem component now acts as a dynamic wrapper that loads and
renders the appropriate child component based on its type. This is achieved
using Owl's dynamic component syntax and t-component.
Separation of each card type into its own file (NumberCard, PieChartCard)
to promote single-responsibility design and reuse across modules.
All components continue to use Bootstrap utility classes and maintain
responsive, clean, and modern styling.
This change lays the foundation for a fully extensible dashboard architecture,
where new visualizations can be added without modifying the core dashboard view.
---
awesome_dashboard/static/src/dashboard.js | 10 ---
awesome_dashboard/static/src/dashboard.scss | 15 ++++
awesome_dashboard/static/src/dashboard.xml | 8 --
.../components/dashboard_item/dashboard.scss | 29 ++++++++
.../dashboard_item/dashboard_item.js | 6 ++
.../dashboard_item/dashboard_item.xml | 10 +++
.../components/number_card/number_card.js | 9 +++
.../components/number_card/number_card.xml | 10 +++
.../components/pie_chart/pie_chart.js | 62 ++++++++++++++++
.../components/pie_chart/pie_chart.xml | 5 ++
.../static/src/dashboard/dashboard.js | 55 ++++++++++++++
.../static/src/dashboard/dashboard.xml | 26 +++++++
.../static/src/dashboard/dashboard_items.js | 73 +++++++++++++++++++
.../src/dashboard/services/statistics.js | 36 +++++++++
.../src/dashboard/setting/setting_dialog.js | 35 +++++++++
.../src/dashboard/setting/setting_dialog.xml | 21 ++++++
.../static/src/dashboard_action.js | 11 +++
.../static/src/dashboard_action.xml | 5 ++
awesome_dashboard/views/views.xml | 2 +-
estate/__manifest__.py | 2 +-
20 files changed, 410 insertions(+), 20 deletions(-)
delete mode 100644 awesome_dashboard/static/src/dashboard.js
create mode 100644 awesome_dashboard/static/src/dashboard.scss
delete mode 100644 awesome_dashboard/static/src/dashboard.xml
create mode 100644 awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard.scss
create mode 100644 awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.js
create mode 100644 awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.xml
create mode 100644 awesome_dashboard/static/src/dashboard/components/number_card/number_card.js
create mode 100644 awesome_dashboard/static/src/dashboard/components/number_card/number_card.xml
create mode 100644 awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.js
create mode 100644 awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.xml
create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js
create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml
create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js
create mode 100644 awesome_dashboard/static/src/dashboard/services/statistics.js
create mode 100644 awesome_dashboard/static/src/dashboard/setting/setting_dialog.js
create mode 100644 awesome_dashboard/static/src/dashboard/setting/setting_dialog.xml
create mode 100644 awesome_dashboard/static/src/dashboard_action.js
create mode 100644 awesome_dashboard/static/src/dashboard_action.xml
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.scss b/awesome_dashboard/static/src/dashboard.scss
new file mode 100644
index 00000000000..9d2b00304fc
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard.scss
@@ -0,0 +1,15 @@
+.dashboard-card-wrapper {
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ }
+
+ .dashboard-card {
+ border-radius: 1rem;
+ transition: all 0.3s ease-in-out;
+ cursor: pointer;
+ }
+
+ .dashboard-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.12);
+ }
+
\ No newline at end of file
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/components/dashboard_item/dashboard.scss b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard.scss
new file mode 100644
index 00000000000..eb88c8dbb56
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard.scss
@@ -0,0 +1,29 @@
+// .o_dashboard_cards {
+// display: flex;
+// flex-wrap: wrap;
+// gap: 1rem;
+// }
+
+.dashboard-item {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 0.75rem;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+ min-height: 150px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ h5 {
+ font-size: 1rem;
+ margin-bottom: 0.5rem;
+ text-align: center;
+ }
+
+ p {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: green;
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.js
new file mode 100644
index 00000000000..d09af3eae16
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.js
@@ -0,0 +1,6 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.DashboardItem";
+ static props = { size: { type: Number, optional: true, default: 1 } };
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.xml
new file mode 100644
index 00000000000..b5cf5ea9f37
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/components/dashboard_item/dashboard_item.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/components/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.js
new file mode 100644
index 00000000000..7183ecf0966
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.js
@@ -0,0 +1,9 @@
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.NumberCard";
+ static props = {
+ label: String,
+ value: [Number, String],
+ };
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/components/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.xml
new file mode 100644
index 00000000000..ad4d2a9dea5
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/components/number_card/number_card.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.js
new file mode 100644
index 00000000000..83458025f30
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.js
@@ -0,0 +1,62 @@
+/** @odoo-module **/
+
+import { Component, useRef, onWillStart, onMounted, onWillUpdateProps } from '@odoo/owl';
+import { loadJS } from "@web/core/assets";
+
+export class PieChart extends Component {
+ static template = 'awesome_dashboard.PieChart';
+
+ static props = {
+ data: Object,
+ };
+
+ setup() {
+ this.canvasRef = useRef('chartCanvas');
+ this.chart = null;
+
+ onWillStart(async () => {
+ await loadJS("/web/static/lib/Chart/Chart.js");
+ });
+
+ onMounted(() => {
+ this.renderChart();
+ });
+
+ onWillUpdateProps((nextProps) => {
+ if (this.chart) {
+ // Update dataset data with new props
+ this.chart.data.labels = Object.keys(nextProps.data);
+ this.chart.data.datasets[0].data = Object.values(nextProps.data);
+ this.chart.update(); // ✅ Trigger re-render
+ }
+ });
+ }
+
+ renderChart() {
+ const ctx = this.canvasRef.el.getContext('2d');
+ this.chart = new Chart(ctx, {
+ type: "pie",
+ data: {
+ labels: Object.keys(this.props.data),
+ datasets: [
+ {
+ label: "Orders by T-shirt Size",
+ data: Object.values(this.props.data),
+ backgroundColor: [
+ "#3498db", "#2ecc71", "#f1c40f", "#e67e22", "#9b59b6"
+ ],
+ borderWidth: 1
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: "right"
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.xml
new file mode 100644
index 00000000000..38d6e3cc98c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/components/pie_chart/pie_chart.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..d3aed40682c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,55 @@
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { useService } from "@web/core/utils/hooks";
+import { registry } from "@web/core/registry";
+import { Layout } from "@web/search/layout";
+import { DashboardItem } from "./components/dashboard_item/dashboard_item";
+import { NumberCard } from "./components/number_card/number_card";
+import { PieChart } from "./components/pie_chart/pie_chart";
+import { SettingDialog } from "./setting/setting_dialog";
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem, NumberCard, PieChart };
+
+ setup() {
+ this.dialogService = useService("dialog");
+ this.action = useService("action");
+ this.statisticsService = useService("awesome_dashboard.statistics");
+ this.statistics = useState(this.statisticsService.data);
+ this.state = useState({ hiddenItems: JSON.parse(localStorage.getItem("awesome_dashboard_hidden_items")) || [] });
+ }
+
+ openCustomers = () => {
+ this.action.doAction("base.action_partner_customer_form", {
+ views: [[false, "kanban"]],
+ });
+ }
+
+ openLeads = () => {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "Leads",
+ res_model: "crm.lead",
+ views: [
+ [false, "list"],
+ [false, "form"]
+ ]
+ });
+ }
+
+ getFilteredItems = () => {
+ const hiddenSet = new Set(this.state.hiddenItems);
+ return registry.category("awesome_dashboard.items").getAll().filter(item => !hiddenSet.has(item.id));
+ }
+
+ openSettings = () => {
+ this.dialogService.add(SettingDialog, {
+ onApply: (hiddenItems) => {
+ localStorage.setItem("awesome_dashboard_hidden_items", JSON.stringify(hiddenItems));
+ this.state.hiddenItems = hiddenItems;
+ }
+ });
+ }
+}
+
+registry.category("lazy_components").add("awesome_dashboard.dashboard", AwesomeDashboard);
\ 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..cb6a7b7a0c2
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..6bba5560dfa
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,73 @@
+import { registry } from "@web/core/registry"
+import { NumberCard } from "./components/number_card/number_card";
+import { PieChart } from "./components/pie_chart/pie_chart";
+import { _t } from "@web/core/l10n/translation";
+
+const dashboardRegistry = registry.category("awesome_dashboard.items");
+
+
+dashboardRegistry.add("average_quantity", {
+ id: "average_quantity",
+ description: _t("Average amount of t-shirt"),
+ component: NumberCard,
+ size: 2,
+ props: (data) => ({
+ label: _t("Average Quantity"),
+ value: data.average_quantity,
+ }),
+});
+
+dashboardRegistry.add("average_time", {
+ id: "average_time",
+ description: _t("Average order processing time"),
+ component: NumberCard,
+ size: 2,
+ props: (data) => ({
+ label: _t("Average time"),
+ value: data.average_time,
+ }),
+});
+
+dashboardRegistry.add("nb_new_orders", {
+ id: "nb_new_orders",
+ description: _t("New orders this month"),
+ component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ label: _t("New orders this month"),
+ value: data.nb_new_orders,
+ }),
+});
+
+dashboardRegistry.add("nb_cancelled_orders", {
+ id: "nb_cancelled_orders",
+ description: _t("Cancelled orders this month"),
+ component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ label: _t("Number of Cancelled Orders this month"),
+ value: data.nb_cancelled_orders,
+ }),
+});
+
+dashboardRegistry.add("total_amount", {
+ id: "total_amount",
+ description: _t("Total sales amount"),
+ component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ label: _t("Total amount of new Orders This Month"),
+ value: data.total_amount,
+ }),
+});
+
+dashboardRegistry.add("orders_by_size", {
+ id: "orders_by_size",
+ description: _t("Shirt orders by size"),
+ component: PieChart,
+ size: 2,
+ props: (data) => ({
+ label: _t("Shirt orders by size"),
+ data: data.orders_by_size,
+ }),
+});
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/services/statistics.js b/awesome_dashboard/static/src/dashboard/services/statistics.js
new file mode 100644
index 00000000000..51a91bc5939
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/services/statistics.js
@@ -0,0 +1,36 @@
+import { registry } from "@web/core/registry";
+import { rpc } from "@web/core/network/rpc";
+import { reactive } from "@odoo/owl";
+import { memoize } from "@web/core/utils/functions";
+
+
+export const statisticsService = {
+
+ start() {
+ const stats = reactive({
+ nb_new_orders: 0,
+ total_amount: 0,
+ average_quantity: 0,
+ nb_cancelled_orders: 0,
+ average_time: 0,
+ orders_by_size: { m: 0, s: 0, xl: 0 }
+ });
+
+ const loadStatistics = memoize(async () => {
+ const result = await rpc("/awesome_dashboard/statistics", {});
+ if (result) {
+ Object.assign(stats, result);
+ }
+ });
+
+ loadStatistics();
+
+ setInterval(() => {
+ loadStatistics();
+ }, 1000);
+
+ return { data: stats };
+ },
+};
+
+registry.category("services").add("awesome_dashboard.statistics", statisticsService);
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/setting/setting_dialog.js b/awesome_dashboard/static/src/dashboard/setting/setting_dialog.js
new file mode 100644
index 00000000000..3fb6dec11a9
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/setting/setting_dialog.js
@@ -0,0 +1,35 @@
+import { Component, useState } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { Dialog } from "@web/core/dialog/dialog";
+
+export class SettingDialog extends Component {
+ static template = "awesome_dashboard.SettingDialog";
+ static components = { Dialog };
+ static props = {
+ close: { type: Function, optional: false },
+ onApply: { type: Function, optional: true },
+ };
+
+ setup() {
+ this.items = registry.category("awesome_dashboard.items").getAll();
+ const hiddenItems = JSON.parse(localStorage.getItem("awesome_dashboard_hidden_items")) || [];
+ this.state = useState({
+ items: this.items.map(item => ({
+ id: item.id,
+ description: item.description,
+ enabled: !hiddenItems.includes(item.id),
+ })),
+ });
+ }
+
+ applySettings() {
+ const uncheckedItemIds = this.state.items
+ .filter(item => !item.enabled)
+ .map(item => item.id);
+
+ if (this.props.onApply) {
+ this.props.onApply(uncheckedItemIds);
+ }
+ this.props.close();
+ }
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/setting/setting_dialog.xml b/awesome_dashboard/static/src/dashboard/setting/setting_dialog.xml
new file mode 100644
index 00000000000..f4d3eddd9e0
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/setting/setting_dialog.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ 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..0f922763efa
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,11 @@
+import { Component } from "@odoo/owl";
+import { LazyComponent } from "@web/core/assets";
+import { registry } from "@web/core/registry";
+
+
+export class DashboardLoader extends Component {
+ static components = { LazyComponent };
+ static template = "awesome_dashboard.DashboardLoader"
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard_action", DashboardLoader);
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard_action.xml b/awesome_dashboard/static/src/dashboard_action.xml
new file mode 100644
index 00000000000..1c626a8e8d6
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/views/views.xml b/awesome_dashboard/views/views.xml
index 47fb2b6f258..1729ef86951 100644
--- a/awesome_dashboard/views/views.xml
+++ b/awesome_dashboard/views/views.xml
@@ -2,7 +2,7 @@
Dashboard
- awesome_dashboard.dashboard
+ awesome_dashboard.dashboard_action
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index f13c25445cb..a0a3bbc3583 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -1,7 +1,7 @@
{
'name': 'Real Estate',
'version': '1.0',
- 'depends': ['base', 'mail', 'website'],
+ 'depends': ['mail', 'website'],
'author': 'Rajeev Aanjana',
'category': 'Real Estate/Brokerage',
'description': 'A module for managing real estate properties',
From 8b6372c6b1c61fd15e4d3eb703ec44e0c9a472f8 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Wed, 28 May 2025 15:34:50 +0530
Subject: [PATCH 12/15] [ADD] rental,sale: implement mandatory rental deposit
system
This commit introduces a mandatory deposit system for rental products to
secure rentals against potential damages or late returns. The implementation
includes:
1. System configuration to set a deposit product that will be used across
all rental deposits
2. Product-level settings to enable deposits and specify amounts per unit
3. Automatic addition of deposit lines to rental orders
4. Frontend display of deposit information for customer transparency
The deposit system works by:
- Allowing administrators to configure a deposit product in Settings
- Enabling per-product deposit requirements with customizable amounts
- Automatically adding deposit lines when rental products are ordered
- Calculating deposit totals based on product quantity
- Maintaining clear relationships between rentals and their deposits
This feature was implemented to reduce financial risk for rental businesses
while maintaining a smooth customer experience. The system ensures deposits
are always collected when required without manual intervention from sales
staff.
task- Add rental deposit functionality
---
rental_deposit/__init__.py | 1 +
rental_deposit/__manifest__.py | 17 ++++++++++
rental_deposit/models/__init__.py | 4 +++
rental_deposit/models/product_template.py | 12 +++++++
rental_deposit/models/res_config_settings.py | 26 ++++++++++++++
rental_deposit/models/sale_order.py | 34 +++++++++++++++++++
rental_deposit/models/sale_order_line.py | 22 ++++++++++++
.../static/src/website_deposit_amount.js | 21 ++++++++++++
.../views/product_template_views.xml | 15 ++++++++
.../views/product_webiste_template_views.xml | 23 +++++++++++++
.../views/res_config_settings_views.xml | 16 +++++++++
11 files changed, 191 insertions(+)
create mode 100644 rental_deposit/__init__.py
create mode 100644 rental_deposit/__manifest__.py
create mode 100644 rental_deposit/models/__init__.py
create mode 100644 rental_deposit/models/product_template.py
create mode 100644 rental_deposit/models/res_config_settings.py
create mode 100644 rental_deposit/models/sale_order.py
create mode 100644 rental_deposit/models/sale_order_line.py
create mode 100644 rental_deposit/static/src/website_deposit_amount.js
create mode 100644 rental_deposit/views/product_template_views.xml
create mode 100644 rental_deposit/views/product_webiste_template_views.xml
create mode 100644 rental_deposit/views/res_config_settings_views.xml
diff --git a/rental_deposit/__init__.py b/rental_deposit/__init__.py
new file mode 100644
index 00000000000..9a7e03eded3
--- /dev/null
+++ b/rental_deposit/__init__.py
@@ -0,0 +1 @@
+from . import models
\ No newline at end of file
diff --git a/rental_deposit/__manifest__.py b/rental_deposit/__manifest__.py
new file mode 100644
index 00000000000..3155ae82224
--- /dev/null
+++ b/rental_deposit/__manifest__.py
@@ -0,0 +1,17 @@
+{
+ 'name': 'Rental Deposit',
+ 'version': '1.0',
+ 'depends': ['sale_renting', 'website_sale'],
+ 'category': 'Sales',
+ 'Summary': 'Add deposit logic to rental products on sale order and webshop',
+ 'data': [
+ 'views/res_config_settings_views.xml',
+ 'views/product_template_views.xml',
+ 'views/product_webiste_template_views.xml',
+ ],
+ 'assets': {
+ 'web.assets_frontend': {
+ 'deposit_rental/static/src/website_deposit_amount.js',
+ }
+ },
+}
\ No newline at end of file
diff --git a/rental_deposit/models/__init__.py b/rental_deposit/models/__init__.py
new file mode 100644
index 00000000000..5781e09c853
--- /dev/null
+++ b/rental_deposit/models/__init__.py
@@ -0,0 +1,4 @@
+from . import res_config_settings
+from . import product_template
+from . import sale_order
+from . import sale_order_line
\ No newline at end of file
diff --git a/rental_deposit/models/product_template.py b/rental_deposit/models/product_template.py
new file mode 100644
index 00000000000..b4f84b0ba66
--- /dev/null
+++ b/rental_deposit/models/product_template.py
@@ -0,0 +1,12 @@
+from odoo import api, fields, models
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ deposit_require = fields.Boolean(string='Require Deposit')
+ deposit_amount = fields.Monetary(string='Deposit Amount')
+ currency_id = fields.Many2one(
+ 'res.currency', string='Currency',
+ required=True,
+ default=lambda self: self.env.company.currency_id,
+ )
diff --git a/rental_deposit/models/res_config_settings.py b/rental_deposit/models/res_config_settings.py
new file mode 100644
index 00000000000..105ef73a19c
--- /dev/null
+++ b/rental_deposit/models/res_config_settings.py
@@ -0,0 +1,26 @@
+from odoo import api, fields, models
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ deposit_product = fields.Many2one('product.product', string='Deposit Product')
+
+ def set_values(self):
+ super().set_values()
+ # Save the product ID or 0 if no product is selected
+ self.env['ir.config_parameter'].sudo().set_param(
+ 'rental_deposit.deposit_product',
+ self.deposit_product.id if self.deposit_product else 0
+ )
+
+ @api.model
+ def get_values(self):
+ res = super().get_values()
+ # Read the deposit product ID as string
+ config_value = self.env['ir.config_parameter'].sudo().get_param(
+ 'rental_deposit.deposit_product', default=0
+ )
+ # Convert to int and browse the product; if empty or invalid, return empty recordset
+ product = self.env['product.product'].browse(int(config_value)) if config_value else self.env['product.product']
+ res.update(deposit_product=product)
+ return res
diff --git a/rental_deposit/models/sale_order.py b/rental_deposit/models/sale_order.py
new file mode 100644
index 00000000000..0e33bc57a62
--- /dev/null
+++ b/rental_deposit/models/sale_order.py
@@ -0,0 +1,34 @@
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ def _add_deposit_product(self, order_line):
+ config_product = int(self.env['ir.config_parameter'].sudo().get_param('rental_deposit.deposit_product', default=0))
+ if not config_product:
+ raise UserError(_("No deposit product configured. Please configure it in Rental Settings."))
+
+ deposit_product = self.env['product.product'].browse(config_product)
+ if not deposit_product.exists():
+ raise UserError(_("The configured deposit product does not exist."))
+
+ existing_line = self.order_line.filtered(lambda l: l.linked_line_id.id == order_line.id)
+ amount = order_line.product_id.deposit_amount * order_line.product_uom_qty
+
+ if existing_line:
+ existing_line.write({
+ 'product_uom_qty': order_line.product_uom_qty,
+ 'price_unit': order_line.product_id.deposit_amount,
+ 'name': f"Deposit for {order_line.product_id.display_name}"
+ })
+ else:
+ self.env['sale.order.line'].create({
+ 'order_id': self.id,
+ 'product_id': deposit_product.id,
+ 'product_uom_qty': order_line.product_uom_qty,
+ 'product_uom': deposit_product.uom_id.id,
+ 'price_unit': order_line.product_id.deposit_amount,
+ 'linked_line_id': order_line.id,
+ 'name': f"Deposit for {order_line.product_id.display_name}",
+ })
\ No newline at end of file
diff --git a/rental_deposit/models/sale_order_line.py b/rental_deposit/models/sale_order_line.py
new file mode 100644
index 00000000000..595196e8d1f
--- /dev/null
+++ b/rental_deposit/models/sale_order_line.py
@@ -0,0 +1,22 @@
+from odoo import api, fields, models
+
+class SaleOrderLine(models.Model):
+ _inherit = 'sale.order.line'
+
+ linked_line_id = fields.Many2one('sale.order.line', string='Linked Product Line')
+
+ @api.model_create_multi
+ def create(self, vals):
+ lines = super().create(vals)
+ for line in lines:
+ deposit_product_id = int(self.env['ir.config_parameter'].sudo().get_param('rental_deposit.deposit_product', default=0))
+ if line.product_id.id != deposit_product_id and line.product_id.rent_ok and line.product_id.deposit_require:
+ line.order_id._add_deposit_product(line)
+ return lines
+
+ def write(self, vals):
+ res = super().write(vals)
+ for line in self:
+ if 'product_uom_qty' in vals and line.product_id.deposit_require:
+ line.order_id._add_deposit_product(line)
+ return res
diff --git a/rental_deposit/static/src/website_deposit_amount.js b/rental_deposit/static/src/website_deposit_amount.js
new file mode 100644
index 00000000000..8047719fb35
--- /dev/null
+++ b/rental_deposit/static/src/website_deposit_amount.js
@@ -0,0 +1,21 @@
+import publicWidget from "@web/legacy/js/public/public_widget";
+
+publicWidget.registry.DepositRental = publicWidget.Widget.extend({
+ selector: "#product_detail",
+ events: {
+ 'change input[name="add_qty"]': '_updateDepositAmount',
+ },
+ start: function () {
+ this._super.apply(this, arguments);
+ if ($("#deposit_amount").length && $("#deposit_amount").data("base-amount") > 0) {
+ this._updateDepositAmount();
+ } else {
+ this.$el.off('change input[name="add_qty"]');
+ }
+ },
+ _updateDepositAmount: function () {
+ var qty = parseFloat($("#o_wsale_cta_wrapper").find("input[name='add_qty']").val()) || 1;
+ var depositAmount = parseFloat($("#deposit_amount").data("base-amount")) || 0;
+ $("#deposit_amount").text(depositAmount * qty);
+ }
+});
diff --git a/rental_deposit/views/product_template_views.xml b/rental_deposit/views/product_template_views.xml
new file mode 100644
index 00000000000..d4bc0d02481
--- /dev/null
+++ b/rental_deposit/views/product_template_views.xml
@@ -0,0 +1,15 @@
+
+
+ product.template.form.deposit.rental
+ product.template
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rental_deposit/views/product_webiste_template_views.xml b/rental_deposit/views/product_webiste_template_views.xml
new file mode 100644
index 00000000000..2904abb0fb2
--- /dev/null
+++ b/rental_deposit/views/product_webiste_template_views.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Deposit Required:
+
+
+
+
+
+
+
+
+
+
+ Deposit for :
+
+
+
+
+
+
diff --git a/rental_deposit/views/res_config_settings_views.xml b/rental_deposit/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..80867a801d1
--- /dev/null
+++ b/rental_deposit/views/res_config_settings_views.xml
@@ -0,0 +1,16 @@
+
+
+
+ res.config.settings.deposit.rental
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 0f69fe999a4590464f30e8b4a6c9f71646b84464 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Thu, 29 May 2025 11:20:00 +0530
Subject: [PATCH 13/15] [IMP] estate: implement business logic for offers,
constraints, state transitions This commit completes the enhancements from
chapters 11 to 15 of the Odoo 18 Real Estate module. It introduces a range of
improvements:
- Added state field with custom transitions (`new`, `offer received`,
`offer accepted`, `sold`, `cancelled`), including buttons and logic for
Sold/Cancelled.
- Added computed fields: total_area (living + garden), best_offer, and
selling_price.
- Added SQL constraints and Python constraints to ensure offers are above
a threshold and selling price is logically correct.
- Used computed and inverse fields to manage garden area visibility based on the
garden boolean.
- Refactored UI behavior (e.g., readonly fields) based on the state of the
property.
- Automatically assigns partner to offers upon creation.
- Used Inheritance for inheriting the class and use the features.
- Created kanban view while using drag and drop.
These changes reinforce business rules and improve usability for real estate
agents managing listings and offers.
---
rental_deposit/__init__.py | 2 +-
rental_deposit/__manifest__.py | 2 +-
rental_deposit/models/__init__.py | 2 +-
rental_deposit/models/product_template.py | 3 ++-
rental_deposit/models/res_config_settings.py | 1 +
rental_deposit/models/sale_order.py | 7 ++++---
rental_deposit/models/sale_order_line.py | 1 +
rental_deposit/views/res_config_settings_views.xml | 2 +-
8 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/rental_deposit/__init__.py b/rental_deposit/__init__.py
index 9a7e03eded3..0650744f6bc 100644
--- a/rental_deposit/__init__.py
+++ b/rental_deposit/__init__.py
@@ -1 +1 @@
-from . import models
\ No newline at end of file
+from . import models
diff --git a/rental_deposit/__manifest__.py b/rental_deposit/__manifest__.py
index 3155ae82224..ae2c4b64ee2 100644
--- a/rental_deposit/__manifest__.py
+++ b/rental_deposit/__manifest__.py
@@ -14,4 +14,4 @@
'deposit_rental/static/src/website_deposit_amount.js',
}
},
-}
\ No newline at end of file
+}
diff --git a/rental_deposit/models/__init__.py b/rental_deposit/models/__init__.py
index 5781e09c853..d5e3c47b5c4 100644
--- a/rental_deposit/models/__init__.py
+++ b/rental_deposit/models/__init__.py
@@ -1,4 +1,4 @@
from . import res_config_settings
from . import product_template
from . import sale_order
-from . import sale_order_line
\ No newline at end of file
+from . import sale_order_line
diff --git a/rental_deposit/models/product_template.py b/rental_deposit/models/product_template.py
index b4f84b0ba66..1057bb0f585 100644
--- a/rental_deposit/models/product_template.py
+++ b/rental_deposit/models/product_template.py
@@ -1,4 +1,5 @@
-from odoo import api, fields, models
+from odoo import fields, models
+
class ProductTemplate(models.Model):
_inherit = 'product.template'
diff --git a/rental_deposit/models/res_config_settings.py b/rental_deposit/models/res_config_settings.py
index 105ef73a19c..c62f2d9ee94 100644
--- a/rental_deposit/models/res_config_settings.py
+++ b/rental_deposit/models/res_config_settings.py
@@ -1,5 +1,6 @@
from odoo import api, fields, models
+
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
diff --git a/rental_deposit/models/sale_order.py b/rental_deposit/models/sale_order.py
index 0e33bc57a62..a8cc00023cb 100644
--- a/rental_deposit/models/sale_order.py
+++ b/rental_deposit/models/sale_order.py
@@ -1,6 +1,7 @@
-from odoo import _, api, fields, models
+from odoo import _, models
from odoo.exceptions import UserError
+
class SaleOrder(models.Model):
_inherit = 'sale.order'
@@ -14,7 +15,7 @@ def _add_deposit_product(self, order_line):
raise UserError(_("The configured deposit product does not exist."))
existing_line = self.order_line.filtered(lambda l: l.linked_line_id.id == order_line.id)
- amount = order_line.product_id.deposit_amount * order_line.product_uom_qty
+ # amount = order_line.product_id.deposit_amount * order_line.product_uom_qty
if existing_line:
existing_line.write({
@@ -31,4 +32,4 @@ def _add_deposit_product(self, order_line):
'price_unit': order_line.product_id.deposit_amount,
'linked_line_id': order_line.id,
'name': f"Deposit for {order_line.product_id.display_name}",
- })
\ No newline at end of file
+ })
diff --git a/rental_deposit/models/sale_order_line.py b/rental_deposit/models/sale_order_line.py
index 595196e8d1f..04dc267a381 100644
--- a/rental_deposit/models/sale_order_line.py
+++ b/rental_deposit/models/sale_order_line.py
@@ -1,5 +1,6 @@
from odoo import api, fields, models
+
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
diff --git a/rental_deposit/views/res_config_settings_views.xml b/rental_deposit/views/res_config_settings_views.xml
index 80867a801d1..5fb87c6bd8a 100644
--- a/rental_deposit/views/res_config_settings_views.xml
+++ b/rental_deposit/views/res_config_settings_views.xml
@@ -13,4 +13,4 @@
-
\ No newline at end of file
+
From a656668f12d9b2ba90ac6f70faeea1df0d6f6c0c Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Thu, 29 May 2025 12:37:25 +0530
Subject: [PATCH 14/15] [IMP] rental_deposit: Fix the the error.
---
rental_deposit/__manifest__.py | 1 +
rental_deposit/static/src/website_deposit_amount.js | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/rental_deposit/__manifest__.py b/rental_deposit/__manifest__.py
index ae2c4b64ee2..49b3b3b565b 100644
--- a/rental_deposit/__manifest__.py
+++ b/rental_deposit/__manifest__.py
@@ -14,4 +14,5 @@
'deposit_rental/static/src/website_deposit_amount.js',
}
},
+ 'license':'LGPL-3',
}
diff --git a/rental_deposit/static/src/website_deposit_amount.js b/rental_deposit/static/src/website_deposit_amount.js
index 8047719fb35..7da3e3a25b8 100644
--- a/rental_deposit/static/src/website_deposit_amount.js
+++ b/rental_deposit/static/src/website_deposit_amount.js
@@ -8,7 +8,7 @@ publicWidget.registry.DepositRental = publicWidget.Widget.extend({
start: function () {
this._super.apply(this, arguments);
if ($("#deposit_amount").length && $("#deposit_amount").data("base-amount") > 0) {
- this._updateDepositAmount();
+ this._updateDepositAmount();
} else {
this.$el.off('change input[name="add_qty"]');
}
From bc2e9aaf37d40782cf1a1f8ea09389f0e2e42ae7 Mon Sep 17 00:00:00 2001
From: raaa-odoo
Date: Thu, 29 May 2025 12:45:55 +0530
Subject: [PATCH 15/15] [IMP] rental_deposit: Fix the the error.
---
rental_deposit/__manifest__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rental_deposit/__manifest__.py b/rental_deposit/__manifest__.py
index 49b3b3b565b..1624de3036e 100644
--- a/rental_deposit/__manifest__.py
+++ b/rental_deposit/__manifest__.py
@@ -14,5 +14,5 @@
'deposit_rental/static/src/website_deposit_amount.js',
}
},
- 'license':'LGPL-3',
+ 'license': 'LGPL-3',
}