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
- +
+
+ + -
-

- -

-
- -
-
+
+ + + + +
- - - - + + + + - - - - + + + + - + - - - - + + + + - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + + +
+ + + + 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 @@
-
- - -
- - - - -
+
+

+ +

+
+ - - - - - - - + + + - - + + - + - - - - - + + + + + - - - - + + + - - - - - - - - + +
+ +
+ + + + 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 @@ + + + + +
+

Which cards do you want to show?

+ +
  • + + +
  • +
    +
    + + + +
    +
    +
    \ 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 @@ + + + + + + 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', }