From 88181a303d2d15ff4c409d825d451583d16ccef2 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Tue, 10 Mar 2026 18:37:55 +0530 Subject: [PATCH 01/24] [ADD] Chapter 2&3 (Estate) Completed the initial setup for the new Real Estate module. Created the base directory structure and manifest. Successfully installed the module. Initialized model and model fields which generates tables using odoo ORM --- estate/__init__.py | 1 + estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 31 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 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..606035919fb --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1 @@ +{'name': 'Real Estate', 'depends': ['base']} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..2b1f75517b3 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,28 @@ +from odoo import fields, models + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" # Removes the terminal warning + + # Required fields (not null in database) + name = fields.Char(required=True) + expected_price = fields.Float(required=True) + + # Basic fields + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + + # Selection field (Dropdown) + garden_orientation = fields.Selection( + string='Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + help="The direction the garden faces." + ) \ No newline at end of file From 15ba5f87ef0a2c7cdc93cada8d290103aa61eb90 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Wed, 11 Mar 2026 18:01:02 +0530 Subject: [PATCH 02/24] [IMP] Estate:Fixed Issues and Completed Chapter-4 Fixed warnings and errors raised by the first push. Created security/ir.model.access.csv file in estate for defining access rights. Added the data in csv file and defined the csv file in manifest. --- estate/__manifest__.py | 12 +++++++++++- estate/models/estate_property.py | 11 ++++------- estate/security/ir.model.access.csv | 2 ++ 3 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 606035919fb..be547d5fdba 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1 +1,11 @@ -{'name': 'Real Estate', 'depends': ['base']} +{ + 'name': 'Real Estate', + 'author': 'soham', + 'license': 'LGPL-3', + 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + ], + 'installable': True, + 'application': True, +} diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2b1f75517b3..9556516ad4d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,14 +1,12 @@ from odoo import fields, models + class EstateProperty(models.Model): _name = "estate.property" - _description = "Real Estate Property" # Removes the terminal warning + _description = "Real Estate Property" - # Required fields (not null in database) name = fields.Char(required=True) expected_price = fields.Float(required=True) - - # Basic fields description = fields.Text() postcode = fields.Char() date_availability = fields.Date() @@ -19,10 +17,9 @@ class EstateProperty(models.Model): garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() - - # Selection field (Dropdown) garden_orientation = fields.Selection( string='Orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], help="The direction the garden faces." - ) \ 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..0e11f47e58d --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 1039a390b46dfaaf46f8d4d6ce263006ac13793a Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Thu, 12 Mar 2026 18:38:16 +0530 Subject: [PATCH 03/24] [IMP] Estate:Partially Completed Chapter-5 Created estate_property_views.xml for the window action. Implemented 3-level menu structure: Root, Advertisements, and Properties. Linked the menu to the window action to enable UI navigation. --- estate/__manifest__.py | 2 ++ estate/views/estate_menus.xml | 7 +++++++ estate/views/estate_property_views.xml | 7 +++++++ 3 files changed, 16 insertions(+) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index be547d5fdba..b6699025785 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,8 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'installable': True, 'application': True, diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..1c6d53ac7bd --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..4d544c18597 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,7 @@ + + + Properties + estate.property + list,form + + From aab2bd161f0f5d40bf88b474de95e244458440ff Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Fri, 13 Mar 2026 19:09:33 +0530 Subject: [PATCH 04/24] [IMP] Estate:Completed Chapter-5 Added readonly and copy=False attributes to selling_price field. Set default value of 2 for bedrooms field. Set default availability date to 3 months from today using Date.add(). Added active reserved field with default=True for record visibility. Added state reserved field with specific values. --- estate/models/estate_property.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9556516ad4d..6e87ec1cb91 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,10 +9,11 @@ class EstateProperty(models.Model): expected_price = fields.Float(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() - selling_price = fields.Float() - bedrooms = fields.Integer() + date_availability = fields.Date(copy=False) + selling_price = fields.Float(readonly=True,copy=False) + bedrooms = fields.Integer(default= 2) living_area = fields.Integer() + active = fields.Boolean(default=True) facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() @@ -23,3 +24,17 @@ class EstateProperty(models.Model): help="The direction the garden faces." ) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Cancelled'), + ], + string="Status", + required=True, + copy=False, + default='new', + ) + \ No newline at end of file From ae34671000caecfc0fe00068541bc709c7c05684 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Mon, 16 Mar 2026 23:14:14 +0530 Subject: [PATCH 05/24] [IMP] Estate:Errors Fixed & Completed Chapter-6 Added list view with key property fields for display. Added form view with grouped fields and description tab. Added search view with title and postcode search fields. Added Available filter using date_availability domain. Added Group By postcode option in search view. --- estate/models/estate_property.py | 22 ++++--- estate/views/estate_menus.xml | 4 +- estate/views/estate_property_views.xml | 83 ++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6e87ec1cb91..0fb1e631e83 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,9 +9,9 @@ class EstateProperty(models.Model): expected_price = fields.Float(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(copy=False) - selling_price = fields.Float(readonly=True,copy=False) - bedrooms = fields.Integer(default= 2) + date_availability = fields.Date(copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3)) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() active = fields.Boolean(default=True) facades = fields.Integer() @@ -19,22 +19,20 @@ class EstateProperty(models.Model): garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( - string='Orientation', - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + string="Orientation", + selection=[('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")], help="The direction the garden faces." - ) state = fields.Selection( selection=[ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('canceled', 'Cancelled'), + ('new', "New"), + ('offer_received', "Offer Received"), + ('offer_accepted', "Offer Accepted"), + ('sold', "Sold"), + ('canceled', "Cancelled"), ], string="Status", required=True, copy=False, default='new', ) - \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 1c6d53ac7bd..5d240e46a1a 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -2,6 +2,6 @@ + action="estate_property_action" + parent="estate_first_level_menu"/> diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4d544c18597..1e3ef74349f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,4 +4,87 @@ estate.property list,form + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + From 2db9029671811520bc77dc232bc1268630e0a972 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Wed, 18 Mar 2026 18:44:31 +0530 Subject: [PATCH 06/24] [IMP] Estate: Completed Chapter-7 Added Many2one fields for property type, buyer and salesman to main model. Added Many2many tag_ids field and created estate.property.tag model. Added One2many offer_ids field and created estate.property.offer model. Added corresponding menus,actions,views and access rights for all new models. --- estate/__manifest__.py | 3 + estate/models/__init__.py | 3 + estate/models/estate_property.py | 26 +++- estate/models/estate_property_offer.py | 25 ++++ estate/models/estate_property_tag.py | 8 ++ estate/models/estate_property_type.py | 8 ++ estate/security/ir.model.access.csv | 5 +- estate/views/estate_menus.xml | 16 ++- estate/views/estate_property_offer_views.xml | 13 ++ estate/views/estate_property_tag_views.xml | 33 +++++ estate/views/estate_property_type_views.xml | 33 +++++ estate/views/estate_property_views.xml | 133 +++++++++++-------- 12 files changed, 242 insertions(+), 64 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b6699025785..708f7a1e773 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,9 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', ], diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0fb1e631e83..e657a2b1d1b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -2,7 +2,7 @@ class EstateProperty(models.Model): - _name = "estate.property" + _name = 'estate.property' _description = "Real Estate Property" name = fields.Char(required=True) @@ -36,3 +36,27 @@ class EstateProperty(models.Model): copy=False, default='new', ) + property_type_id = fields.Many2one( + "estate.property.type", + string="Property Type", +) + + buyer_id = fields.Many2one( + "res.partner", + string="Buyer", + copy=False, +) + salesman_id = fields.Many2one( + "res.users", + string="Salesman", + default=lambda self: self.env.user, +) + tag_ids = fields.Many2many( + "estate.property.tag", + string="Tags", +) + offer_ids = fields.One2many( + "estate.property.offer", + "property_id", + string="Offers", +) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..9765b373d00 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,25 @@ +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="Buyer", + required=True + ) + property_id = fields.Many2one( + "estate.property", + string="Property", + required=True + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..198c1037c41 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + + name = fields.Char(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..ced40ef01cc --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0e11f47e58d..05bd9eefba4 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access_estate_property,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,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 5d240e46a1a..becc72c64fb 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,17 @@ - - + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..92ab9007f2b --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,13 @@ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..056fbe8b61e --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,33 @@ + + + Property Tags + estate.property.tag + list,form + + + + estate.property.tag.list + estate.property.tag + + + + + + + + + estate.property.tag.form + estate.property.tag + +
+ +
+

+ +

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

+ +

+
+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1e3ef74349f..88768d4a3f3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,8 +5,7 @@ list,form - + estate.property.list estate.property @@ -22,69 +21,85 @@ - - estate.property.form - estate.property - -
- -
-

- -

-
- - - - - + + estate.property.form + estate.property + + + +
+

+ +

+
+ - - - -
- - - - - - - - - - + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-
+
-
+
- - estate.property.search - estate.property - - - - - - - - - - + + estate.property.search + estate.property + + + + + + + + - + string="Available" + name="available" + domain="[('date_availability','<=', context_today())]"/> + + + - - + + From 1f35744e35ab842ced55db8a6eaecb9e5b2cd420 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Mon, 30 Mar 2026 22:41:24 +0530 Subject: [PATCH 07/24] [IMP] estate: Partially Completed Chapter-8 --- estate/models/estate_property.py | 28 ++++++++++++++++++- estate/models/estate_property_offer.py | 37 +++++++++++++++++++++++--- estate/views/estate_menus.xml | 22 +++++++-------- estate/views/estate_property_views.xml | 19 ++++++++++++- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e657a2b1d1b..ce886472de6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -60,3 +60,29 @@ class EstateProperty(models.Model): "property_id", string="Offers", ) + total_area = fields.Integer( + compute="_compute_total_area", + string="Total Area (sqm)", +) + + @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( + compute="_compute_best_price", + string="Best Offer", +) + + @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 diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9765b373d00..9f8d6b30763 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -8,8 +8,8 @@ class EstatePropertyOffer(models.Model): price = fields.Float() status = fields.Selection( selection=[ - ('accepted', 'Accepted'), - ('refused', 'Refused'), + ('accepted', "Accepted"), + ('refused', "Refused"), ], copy=False ) @@ -23,3 +23,34 @@ class EstatePropertyOffer(models.Model): string="Property", required=True ) + validity = fields.Integer( + string="Validity (days)", + default=7, + ) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_set_date_deadline", + ) + + @api.depends("validity", "create_date") + def _compute_date_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = fields.Date.add( + record.create_date, + days=record.validity, + ) + else: + record.date_deadline = fields.Date.add( + fields.Date.today(), + days=record.validity, + ) + + def _set_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 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index becc72c64fb..63393aa0d61 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,17 +1,17 @@ - - + + - + - + - + - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 88768d4a3f3..3b805c98c1c 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -17,6 +17,12 @@ + +

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 18e217d3477..a40367673df 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,20 +3,25 @@ Properties estate.property list,form + {'search_default_available': 1} estate.property.list estate.property - + - + +

- + - + @@ -66,9 +71,9 @@ - + - + @@ -79,7 +84,8 @@ - +
@@ -98,10 +104,18 @@ + + + Date: Mon, 13 Apr 2026 19:36:56 +0530 Subject: [PATCH 13/24] [IMP] estate: Completed Ch-12 - Add ondelete constraint to prevent deletion of non-new/cancelled properties - Override create() in offers to validate price and set offer_received state - Extend res.users model with property_ids One2many field via model inheritance - Add Real Estate Properties tab to user form view via view inheritance --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 53 ++++++++++++++++++++ estate/models/estate_property_offer.py | 48 ++++++++++++++++++ estate/models/res_users.py | 12 +++++ estate/views/estate_menus.xml | 3 ++ estate/views/estate_property_offer_views.xml | 35 +++++++++++-- estate/views/estate_property_type_views.xml | 2 +- estate/views/estate_property_views.xml | 4 ++ estate/views/res_users_views.xml | 30 +++++++++++ 10 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 02840fad2e4..527f0da580c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,6 +10,7 @@ 'views/estate_property_tag_views.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', + 'views/res_users_views.xml', ], 'installable': True, 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 61cb130b9eb..7dfa9cd7ffc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -58,6 +58,8 @@ class EstateProperty(models.Model): tag_ids = fields.Many2many( "estate.property.tag", string="Tags", + compute="_compute_tags", + store=True, ) offer_ids = fields.One2many( "estate.property.offer", @@ -69,6 +71,10 @@ class EstateProperty(models.Model): compute="_compute_total_area", string="Total Area (sqm)", ) + has_suspicious_offers = fields.Boolean( + string="Has Suspicious Offers", + compute="_compute_has_suspicious_offers" +) @api.depends("living_area", "garden_area") def _compute_total_area(self): @@ -145,3 +151,50 @@ def _check_seling_price(self): ) < 0: raise ValidationError("Selling price cannot be lower than 90%" "of expected price!") + + def _search_tag(self, tag_name): + return self.env['estate.property.tag'].search( + [('name', '=', tag_name)], limit=1 + ) + + def _create_tag(self, tag_name): + return self.env['estate.property.tag'].create( + {'name': tag_name} + ) + + def _get_or_create_tag(self, tag_name): + return self._search_tag(tag_name) or self._create_tag(tag_name) + + @api.depends('expected_price', 'offer_ids', 'sold_date', 'date_availability', 'state') + def _compute_tags(self): + for record in self: + tag_records = self.env['estate.property.tag'] + + if record.expected_price > 200000: + tag_records |= self._get_or_create_tag('High Value') + + if (record.state == 'sold' + and record.sold_date + and record.date_availability + and (record.sold_date - record.date_availability).days <= 10): + tag_records |= self._get_or_create_tag('Quick Sale') + + if len(record.offer_ids) < 2: + tag_records |= self._get_or_create_tag('Low Interest') + + record.tag_ids = tag_records + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_canceled(self): + for record in self: + if record.state not in ('new', 'canceled'): + raise UserError( + "Only new and cancelled properties can be deleted!" + ) + + @api.depends("offer_ids.is_suspicious") + def _compute_has_suspicious_offers(self): + for record in self: + record.has_suspicious_offers = any( + offer.is_suspicious for offer in record.offer_ids + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a980e38f73b..79403ffa9b7 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ from odoo import api, fields, models from odoo.exceptions import UserError +from datetime import timedelta class EstatePropertyOffer(models.Model): @@ -40,6 +41,14 @@ class EstatePropertyOffer(models.Model): compute="_compute_date_deadline", inverse="_set_date_deadline", ) + is_suspicious = fields.Boolean( + string="Suspicious", + default=False, + readonly=True, + copy=False, + compute="_compute_is_suspicious", + store=True, +) @api.depends("validity", "create_date") def _compute_date_deadline(self): @@ -70,6 +79,10 @@ def action_accept(self): self.property_id.selling_price = self.price self.property_id.buyer_id = self.partner_id self.property_id.state = 'offer_accepted' + + for offer in self.property_id.offer_ids: + if offer.id != self.id: + offer.status = 'refused' return True def action_refuse(self): @@ -81,3 +94,38 @@ def action_refuse(self): 'CHECK(price > 0)', 'Offer price must be strictly positive!', ) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + property_id = self.env['estate.property'].browse( + vals.get('property_id', False) + ) + if property_id.offer_ids: + max_offer = max(property_id.offer_ids.mapped('price')) + if vals.get('price', 0) < max_offer: + raise UserError( + "You cannot create an offer lower " + "than an existing offer of %.2f" % max_offer + ) + + property_id.state = 'offer_received' + + return super().create(vals_list) + + @api.depends("partner_id", "create_date") + def _compute_is_suspicious(self): + for record in self: + if not record.create_date: + record.is_suspicious = False + continue + + start = (record.create_date - timedelta(minutes=5)) + end = (record.create_date + timedelta(minutes=5)) + recent_offers = self.search([ + ('partner_id', '=', record.partner_id.id), + ('create_date', '>=', start), + ('create_date', '<=', end), + ]) + + record.is_suspicious = len(recent_offers) >= 3 diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..f88e1bb18cc --- /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", + "salesman_id", + string="Assigned Properties", + domain=[('state', 'in', ['new', 'offer_received'])] + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 63393aa0d61..2d0f697fb14 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -14,4 +14,7 @@ + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 3549a5bbc7b..468bb108830 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,10 +1,8 @@ - Property Offers estate.property.offer list,form - [('property_type_id', '=', active_id)] @@ -12,8 +10,7 @@ estate.property.offer + decoration-success="status=='accepted'"> @@ -21,6 +18,7 @@ + +

@@ -89,7 +97,32 @@ + readonly="state in ('offer_accepted','sold','canceled')"> + + + + + + + + + -

+ +

@@ -101,28 +107,27 @@ - - - - - - - - + +

+ + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..dbb268c4d18 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,20 @@ -import { Component } from "@odoo/owl"; +/** @odoo-module **/ +import { Component, useState, markup } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { - static template = "awesome_owl.playground"; + static template = "awesome_owl.Playground"; + static components = { Counter, Card, TodoList }; + + setup() { + this.state = useState({ sum: 0 }); + this.safeHtml = markup("
some content
"); + this.unsafeHtml = "
some content
"; + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..b06c0be9cb3 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,32 @@ - + + + +
+ +

Hello Counter

+ + + + +

+ The sum is: +

+ +
+ + This is simple text inside card + + + + +
+ +
+ +
- -
- hello world
-
+ diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..efc3053a547 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ +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: { type: Function }, + removeTodo: { type: Function }, + }; +} diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..14126c9e69d --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,26 @@ + + + +
+ + + + + . + + + + + +
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..04dfb0f57c7 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,45 @@ +/** @odoo-module **/ +import { Component, useState, useRef, onMounted } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.todos = useState([]); + this.nextId = 1; + this.inputRef = useRef("todoInput"); + + onMounted(() => { + this.inputRef.el.focus(); + }); + } + + addTodo(ev) { + if (ev.keyCode === 13) { + const description = ev.target.value.trim(); + if (!description) return; + this.todos.push({ + id: this.nextId++, + description: description, + isCompleted: false, + }); + ev.target.value = ""; + } + } + + toggleState(todoId) { + const todo = this.todos.find(t => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + const index = this.todos.findIndex(t => t.id === todoId); + if (index >= 0) { + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..7b19de79303 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,26 @@ + + + +
+ +

+ Todo List +

+ + + + + + + +
+
+
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a3aa2bf275d..4bca52a575e 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -44,7 +44,6 @@ class EstateProperty(models.Model): "estate.property.type", string="Property Type", ) - buyer_id = fields.Many2one( "res.partner", string="Buyer", @@ -112,9 +111,7 @@ def _compute_visit_count(self): @api.depends("living_area", "garden_area") def _compute_total_area(self): for record in self: - record.total_area = ( - record.living_area + - record.garden_area + record.total_area = (record.living_area + record.garden_area ) @api.depends("offer_ids.price") @@ -186,9 +183,11 @@ def action_sold(self): raise UserError("Cancelled property cannot be sold!") for rec in record.issue_ids: if rec.staty != 'resolved' and rec.priority == 'high': - raise UserError("Cannot sell the property pls solve the issues") + raise UserError( + "Cannot sell the property, please solve the issues" + ) record.state = 'sold' - record.sold_date = fields.Datetime.now() + record.sold_date = fields.Date.today() return True def action_cancel(self): diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 546554e3765..a055585ecb4 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -4,4 +4,4 @@ access_estate_property_type,access_estate_property_type,model_estate_property_ty access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 access_estate_property_visit,estate.property.visit,model_estate_property_visit,base.group_user,1,1,1,1 -access_estate_property_issue,estate.property.issue,model_estate_property_issue,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property_issue,estate.property.issue,model_estate_property_issue,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index f4c71b1ff9e..aa87e68e43d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -26,7 +26,8 @@ name="unlink" string="Delete" type="object" - icon="fa-trash"/> + icon="fa-trash" + /> From d290f4c29d326b37ba3b0472c56b81f8511855df Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Wed, 6 May 2026 14:16:09 +0530 Subject: [PATCH 20/24] [IMP] estate:add chatter in form view and email template on sold - Add chatter to estate.property form view - Inherit mail.thread and mail.activity.mixin in estate.property - Add mail template for property sold - Auto-fill buyer and salesman in partner To field - action_sold opens mail compose wizard with template loaded --- estate/__manifest__.py | 3 ++- estate/data/mail_template_data.xml | 27 ++++++++++++++++++++++++++ estate/models/estate_property.py | 22 +++++++++++++++++++-- estate/views/estate_property_views.xml | 1 + 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 estate/data/mail_template_data.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 07805908998..b2b12f09676 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,7 +2,7 @@ 'name': 'Real Estate', 'author': 'soham', 'license': 'LGPL-3', - 'depends': ['base', 'calendar'], + 'depends': ['base', 'calendar', 'mail'], 'data': [ 'security/ir.model.access.csv', 'views/estate_property_visit_views.xml', @@ -13,6 +13,7 @@ 'views/estate_property_views.xml', 'views/res_users_views.xml', 'views/estate_menus.xml', + 'data/mail_template_data.xml', ], 'installable': True, 'application': True, diff --git a/estate/data/mail_template_data.xml b/estate/data/mail_template_data.xml new file mode 100644 index 00000000000..dac0617beb4 --- /dev/null +++ b/estate/data/mail_template_data.xml @@ -0,0 +1,27 @@ + + + + + Estate: Property Sold + + {{object.name}} Sold + + {{ object.salesman_id.email_formatted }} + + +
+

Dear ,

+

Property + + has been sold.

+

Seller:

+

Selling Price:

+

Description:

+

Thank you!

+
+
+
+
+
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4bca52a575e..25cd16b8e3b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,13 +5,14 @@ class EstateProperty(models.Model): _name = 'estate.property' + _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Real Estate Property" _order = "id desc" name = fields.Char(required=True) expected_price = fields.Float(required=True) description = fields.Text() - postcode = fields.Char() + postcode = fields.Char(tracking=1) date_availability = fields.Date(copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3)) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) @@ -188,7 +189,24 @@ def action_sold(self): ) record.state = 'sold' record.sold_date = fields.Date.today() - return True + + ctx = { + 'default_model': 'estate.property', + 'default_res_ids': self.ids, + 'default_template_id': self.env.ref('estate.email_template_estate_property_sold').id, + 'default_partner_ids': [self.buyer_id.id, self.salesman_id.partner_id.id], + } + + return { + 'name': 'Sold', + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(False, 'form')], + 'view_id': False, + 'target': 'new', + 'context': ctx, + } def action_cancel(self): for record in self: diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index aa87e68e43d..93093a6eacb 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -132,6 +132,7 @@ + From 6b3a11d4d8e2d5d8aeea662d761a2ba751533c83 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Thu, 7 May 2026 10:54:34 +0530 Subject: [PATCH 21/24] [ADD] estate_crm:auto generate crm lead on offer and update stage - Create estate_crm module - Create CRM lead on offer creation with property name, buyer and price - Mark lead as won when offer is accepted - Mark lead as lost when offer is refused - Update action_accept to call action_refuse on other offers so refused leads automatically move to lost stage --- estate_crm/__init__.py | 1 + estate_crm/__manifest__.py | 14 ++++++++++ estate_crm/models/__init__.py | 1 + estate_crm/models/estate_property.py | 41 ++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 estate_crm/__init__.py create mode 100644 estate_crm/__manifest__.py create mode 100644 estate_crm/models/__init__.py create mode 100644 estate_crm/models/estate_property.py diff --git a/estate_crm/__init__.py b/estate_crm/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_crm/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_crm/__manifest__.py b/estate_crm/__manifest__.py new file mode 100644 index 00000000000..75ac55d6fb8 --- /dev/null +++ b/estate_crm/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Real Estate Lead', + 'version': '1.0', + 'author': 'soham', + 'license': 'LGPL-3', + 'category': 'Real Estate', + 'depends': [ + 'estate', + 'crm', + ], + 'data': [], + 'installable': True, + 'application': True, +} diff --git a/estate_crm/models/__init__.py b/estate_crm/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_crm/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_crm/models/estate_property.py b/estate_crm/models/estate_property.py new file mode 100644 index 00000000000..ab95af50c87 --- /dev/null +++ b/estate_crm/models/estate_property.py @@ -0,0 +1,41 @@ +from odoo import api, fields, models + + +class EstatePropertyOffer(models.Model): + _inherit = "estate.property.offer" + + lead_id = fields.Many2one( + "crm.lead", + string="CRM Lead", + readonly=True, + copy=False + ) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + for record in records: + lead = self.env["crm.lead"].create({ + "name": record.property_id.name, + "partner_id": record.partner_id.id, + "expected_revenue": record.price, + }) + record.lead_id = lead.id + return records + + def action_accept(self): + result = super().action_accept() + for offer in self: + if offer.lead_id: + offer.lead_id.action_set_won() + for offer in self.property_id.offer_ids: + if offer.id != self.id: + offer.action_refuse() + return result + + def action_refuse(self): + result = super().action_refuse() + for offer in self: + if offer.lead_id: + offer.lead_id.action_set_lost() + return result From 8879c0ecb0d414ddca1d5a7e59fd776637261280 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Sat, 9 May 2026 17:03:44 +0530 Subject: [PATCH 22/24] [ADD] awesome_dashboard:build a dynamic dashboard - Add Layout component with control panel buttons - Add Customers and Leads navigation buttons using action service - Add statistics service with reactive data and auto-refresh every 10 mins - Call /awesome_dashboard/statistics rpc to fetch server data - Add NumberCard component to display stat values - Add PieChartCard component with lazy loaded Chart.js - Add DashboardItem wrapper component with size support - Add dashboard_items registry with all stat cards - Add dashboard_registry using awesome_dashboard.items category - Add ConfigurationDialog with CheckBox to show/hide cards - Persist hidden cards in localStorage via browser service - Add lazy loading via LazyComponent and lazy_components registry - Add statistics service caching with reactive object --- awesome_dashboard/__manifest__.py | 7 +- awesome_dashboard/static/src/dashboard.js | 8 -- awesome_dashboard/static/src/dashboard.xml | 8 -- .../static/src/dashboard/dashboard.js | 102 ++++++++++++++++++ .../static/src/dashboard/dashboard.scss | 10 ++ .../static/src/dashboard/dashboard.xml | 58 ++++++++++ .../dashboard_item/dashboard_item.js | 9 ++ .../dashboard_item/dashboard_item.xml | 11 ++ .../static/src/dashboard/dashboard_items.js | 71 ++++++++++++ .../src/dashboard/dashboard_registry.js | 3 + .../src/dashboard/number_card/number_card.js | 9 ++ .../src/dashboard/number_card/number_card.xml | 9 ++ .../src/dashboard/pie_chart/pie_chart.js | 34 ++++++ .../src/dashboard/pie_chart/pie_chart.xml | 7 ++ .../pie_chart_card/pie_chart_card.js | 11 ++ .../pie_chart_card/pie_chart_card.xml | 11 ++ .../src/dashboard/statistics_service.js | 26 +++++ .../static/src/dashboard_action.js | 10 ++ .../static/src/dashboard_action.xml | 9 ++ 19 files changed, 396 insertions(+), 17 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_registry.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js 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/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..58817d5fbd4 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- { 'name': "Awesome Dashboard", @@ -21,10 +20,16 @@ 'data': [ 'views/views.xml', ], + 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*' + ], }, + 'license': 'AGPL-3' } diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..2c988a966f1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,102 @@ +/** @odoo-module **/ +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; +import "./statistics_service"; +import "./dashboard_items"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem }; + + setup() { + this.action = useService("action"); + this.dialog = useService("dialog"); + this.display = { controlPanel: {} }; + this.stats = useService("awesome_dashboard.statistics"); + + this.state = useState({ + disabledItems: JSON.parse( + browser.localStorage.getItem("disabledDashboardItems") || "[]" + ) + }); + + this.allItems = registry + .category("awesome_dashboard.items") + .getAll(); + } + + get items() { + return this.allItems.filter( + item => !this.state.disabledItems.includes(item.id) + ); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.allItems, + disabledItems: this.state.disabledItems, + onUpdate: this.updateConfiguration.bind(this), + }); + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + browser.localStorage.setItem( + "disabledDashboardItems", + JSON.stringify(newDisabledItems) + ); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + }); + } +} + +class ConfigurationDialog extends Component { + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { Dialog, CheckBox }; + static props = ["close", "items", "disabledItems", "onUpdate"]; + + setup() { + const disabled = Array.isArray(this.props.disabledItems) + ? this.props.disabledItems + : []; + + this.items = useState( + this.props.items.map(item => ({ + ...item, + enabled: !disabled.includes(item.id), + })) + ); + } + + onChange(checked, item) { + item.enabled = checked; + const disabled = this.items + .filter(i => !i.enabled) + .map(i => i.id); + this.props.onUpdate(disabled); + } + + done() { + this.props.close(); + } +} +registry + .category("lazy_components") + .add("awesome_dashboard.AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..ffd265b1982 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,10 @@ +.o_dashboard { + background-color: rgb(16, 18, 20); +} + +@media (max-width: 768px) { + + .o_dashboard_item { + width: 100% !important; + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..4df87c37ab1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..818c69c580f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static slots = { + default: {}, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..82e3f8055d3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,11 @@ + + + + + +
+ +
+ +
+
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..695b557c2f8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,71 @@ +/** @odoo-module **/ +import { dashboardItemRegistry } from "./dashboard_registry"; +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import { _t } from "@web/core/l10n/translation"; + +dashboardItemRegistry.add("new_orders", { + id: "new_orders", + description: _t("New orders this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Number of new orders"), + value: data?.nb_new_orders || 0, + }), +}); + +dashboardItemRegistry.add("total_amount", { + id: "total_amount", + description: _t("Total amount orders this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Total amount"), + value: data?.total_amount || 0, + }), +}); + +dashboardItemRegistry.add("avg_quantity", { + id: "avg_quantity", + description: _t("Average amount of t-shirt"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Average quantity"), + value: data?.average_quantity || 0, + }), +}); + +dashboardItemRegistry.add("average_time", { + id: "average_time", + description: _t("Average time new to sent/cancelled"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Average time"), + value: data?.average_time || 0, + }), +}); + +dashboardItemRegistry.add("nb_cancelled_orders", { + id: "nb_cancelled_orders", + description: _t("Cancelled orders this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Number of cancelled orders"), + value: data?.nb_cancelled_orders || 0, + }), +}); + +dashboardItemRegistry.add("pie_chart", { + id: "pie_chart", + description: _t("Shirt orders by size"), + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: _t("Orders by size"), + data: data?.orders_by_size || {}, + }), +}); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_registry.js b/awesome_dashboard/static/src/dashboard/dashboard_registry.js new file mode 100644 index 00000000000..e3fe596f7ad --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_registry.js @@ -0,0 +1,3 @@ +import { registry } from "@web/core/registry"; + +export const dashboardItemRegistry = registry.category("awesome_dashboard.items"); diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..95cc39c8b33 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/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 = { + title: String, + value: { type: Number, optional: true }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..1da288b98a9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ + + + +
+
+

+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..4fb8c3bacaa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,34 @@ +import { Component, onWillStart, onMounted, useRef } 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("chart"); + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + onMounted(() => { + this.renderChart(); + }); + } + + renderChart() { + const ctx = this.canvasRef.el.getContext("2d"); + + new Chart(ctx, { + type: "pie", + data: { + labels: Object.keys(this.props.data), + datasets: [{ + data: Object.values(this.props.data), + }], + }, + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..6d356ee58fb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js new file mode 100644 index 00000000000..23aa93ec2c3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,11 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart } + static props = { + title: String, + data: Object, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..f22aac77842 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,11 @@ + + + + +
+
+ +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..fe717b5e350 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,26 @@ +/** @odoo-module **/ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const statisticsService = { + name: "awesome_dashboard.statistics", + + start() { + const stats = reactive({}); + + async function loadStatistics() { + const data = await rpc("/awesome_dashboard/statistics"); + Object.assign(stats, data); + } + + loadStatistics(); + setInterval(loadStatistics, 10 * 60 * 1000); + + return stats; + }, +}; + +registry + .category("services") + .add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..52decad3f86 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class DashboardAction extends Component { + static components = { LazyComponent }; + static template = "awesome_dashboard.DashboardLoader"; +} + +registry.category("actions").add("awesome_dashboard.dashboard", DashboardAction); diff --git a/awesome_dashboard/static/src/dashboard_action.xml b/awesome_dashboard/static/src/dashboard_action.xml new file mode 100644 index 00000000000..5beaede125b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.xml @@ -0,0 +1,9 @@ + + + + + + From ca889d2bd25e27af0e571688169ae83e28f101a1 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Thu, 14 May 2026 16:00:08 +0530 Subject: [PATCH 23/24] [IMP] estate: Added review task --- estate/__manifest__.py | 6 ++-- estate/data/ir_cron.xml | 12 +++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 24 ++++++++++++- estate/models/estate_property_offer.py | 18 ++++++++++ estate/models/estate_property_wizard.py | 27 +++++++++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menus.xml | 3 ++ estate/views/estate_property_views.xml | 8 +++++ estate/views/estate_property_wizard_views.xml | 34 +++++++++++++++++++ 10 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 estate/data/ir_cron.xml create mode 100644 estate/models/estate_property_wizard.py create mode 100644 estate/views/estate_property_wizard_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b2b12f09676..5cdf7aed65f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,18 +2,20 @@ 'name': 'Real Estate', 'author': 'soham', 'license': 'LGPL-3', - 'depends': ['base', 'calendar', 'mail'], + 'depends': ['base', 'calendar', 'mail', 'sale'], 'data': [ 'security/ir.model.access.csv', + 'data/mail_template_data.xml', + 'data/ir_cron.xml', 'views/estate_property_visit_views.xml', 'views/estate_property_issue_views.xml', 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', + 'views/estate_property_wizard_views.xml', 'views/estate_property_views.xml', 'views/res_users_views.xml', 'views/estate_menus.xml', - 'data/mail_template_data.xml', ], 'installable': True, 'application': True, diff --git a/estate/data/ir_cron.xml b/estate/data/ir_cron.xml new file mode 100644 index 00000000000..9d1cf480adc --- /dev/null +++ b/estate/data/ir_cron.xml @@ -0,0 +1,12 @@ + + + + Offer: Expired Offers Refused + + code + model._cron_automatic_refuse() + + 5 + minutes + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 15f719c5492..dfacdea237e 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -5,3 +5,4 @@ from . import res_users from . import estate_property_visit from . import estate_property_issue +from . import estate_property_wizard diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 25cd16b8e3b..41b538fe1e7 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, Command, fields, models from odoo.exceptions import UserError, ValidationError from odoo.tools.float_utils import float_compare, float_is_zero @@ -217,6 +217,28 @@ def action_cancel(self): record.state = 'canceled' return True + def action_create_quotations(self): + if not self.offer_ids: + raise UserError("No offers found") + + for offer in self.offer_ids: + if offer.quotation_id: + continue + quotation = self.env["sale.order"].create({ + "partner_id": offer.partner_id.id, + "origin": self.name, + "order_line": [ + Command.create({ + "name": self.name, + "product_uom_qty": 1, + "price_unit": offer.price, + }) + ], + }) + + offer.quotation_id = quotation.id + return True + def _search_tag(self, tag_name): return self.env['estate.property.tag'].search( [('name', '=', tag_name)], limit=1 diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 64413a413d5..e9de6781318 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -36,6 +36,12 @@ class EstatePropertyOffer(models.Model): string="Validity (days)", default=7, ) + quotation_id = fields.Many2one( + "sale.order", + string="Quotation", + readonly=True, + copy=False + ) date_deadline = fields.Date( string="Deadline", compute="_compute_date_deadline", @@ -129,3 +135,15 @@ def action_refuse(self): for record in self: record.status = 'refused' return True + + def _cron_automatic_refuse(self): + today = fields.Datetime.now() + offers = self.search([ + ('status', "not in", ["accepted", "refused"]), + ]) + for offer in offers: + expiry_date = offer.create_date + timedelta(days=offer.validity) + if expiry_date < today: + offer.write({ + 'status': 'refused' + }) diff --git a/estate/models/estate_property_wizard.py b/estate/models/estate_property_wizard.py new file mode 100644 index 00000000000..42cb0528067 --- /dev/null +++ b/estate/models/estate_property_wizard.py @@ -0,0 +1,27 @@ +from odoo import fields, models + + +class EstatePropertyWizard(models.TransientModel): + _name = "estate.property.wizard" + _description = "Create Offers" + + price = fields.Float() + + partner_id = fields.Many2one( + "res.partner", + string="Buyer", + copy=False, + ) + + def action_create_offers(self): + + properties = self.env["estate.property"].search([ + ("state", "not in", ["sold", "canceled", "offer_accepted"]) + ]) + for property in properties: + self.env["estate.property.offer"].create({ + "price": self.price, + "partner_id": self.partner_id.id, + "property_id": property.id, + }) + return True diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index a055585ecb4..051e7c3e2c4 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -5,3 +5,4 @@ access_estate_property_tag,access_estate_property_tag,model_estate_property_tag, access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 access_estate_property_visit,estate.property.visit,model_estate_property_visit,base.group_user,1,1,1,1 access_estate_property_issue,estate.property.issue,model_estate_property_issue,base.group_user,1,1,1,1 +access_estate_property_wizard,estate.property.wizard,model_estate_property_wizard,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 290958fd750..25fad3f77c3 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -20,4 +20,7 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 93093a6eacb..658f344eee8 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -14,6 +14,13 @@ decoration-bf="state == 'offer_accepted'" decoration-muted="state == 'sold'" delete="True"> +
+
@@ -40,6 +47,7 @@
diff --git a/estate/views/estate_property_wizard_views.xml b/estate/views/estate_property_wizard_views.xml new file mode 100644 index 00000000000..df41da94874 --- /dev/null +++ b/estate/views/estate_property_wizard_views.xml @@ -0,0 +1,34 @@ + + + + + Create Offers + estate.property.wizard + form + new + + + + estate.property.offer.wizard.form + estate.property.wizard + +
+ + + + + +
+
+
+
+
+
From b71c0c450206389a004ad466daf04b938a821525 Mon Sep 17 00:00:00 2001 From: "Soham Patil (sopat)" Date: Fri, 15 May 2026 10:41:48 +0530 Subject: [PATCH 24/24] [ADD] bom_forecast: clean bom overview forecast view --- bom_forecast/__init__.py | 1 + bom_forecast/__manifest__.py | 15 ++++++++ bom_forecast/model/__init__.py | 1 + .../model/report_mrp_bom_structure.py | 15 ++++++++ bom_forecast/static/src/bom_overview_line.js | 28 ++++++++++++++ bom_forecast/static/src/bom_overview_line.xml | 38 +++++++++++++++++++ .../static/src/mrp_bom_overview_table.xml | 7 ++++ 7 files changed, 105 insertions(+) create mode 100644 bom_forecast/__init__.py create mode 100644 bom_forecast/__manifest__.py create mode 100644 bom_forecast/model/__init__.py create mode 100644 bom_forecast/model/report_mrp_bom_structure.py create mode 100644 bom_forecast/static/src/bom_overview_line.js create mode 100644 bom_forecast/static/src/bom_overview_line.xml create mode 100644 bom_forecast/static/src/mrp_bom_overview_table.xml diff --git a/bom_forecast/__init__.py b/bom_forecast/__init__.py new file mode 100644 index 00000000000..9186ee3ad24 --- /dev/null +++ b/bom_forecast/__init__.py @@ -0,0 +1 @@ +from . import model diff --git a/bom_forecast/__manifest__.py b/bom_forecast/__manifest__.py new file mode 100644 index 00000000000..53a96141049 --- /dev/null +++ b/bom_forecast/__manifest__.py @@ -0,0 +1,15 @@ +{ + "name": "bom_forecast", + "version": "1.0", + "depends": ["mrp", "purchase"], + "author": "soham", + "category": "Tutorials", + "license": "LGPL-3", + 'installable': True, + 'application': True, + "assets": { + "web.assets_backend": [ + "bom_forecast/static/src/**/*", + ], + }, +} diff --git a/bom_forecast/model/__init__.py b/bom_forecast/model/__init__.py new file mode 100644 index 00000000000..98974148e0e --- /dev/null +++ b/bom_forecast/model/__init__.py @@ -0,0 +1 @@ +from . import report_mrp_bom_structure diff --git a/bom_forecast/model/report_mrp_bom_structure.py b/bom_forecast/model/report_mrp_bom_structure.py new file mode 100644 index 00000000000..a406162251c --- /dev/null +++ b/bom_forecast/model/report_mrp_bom_structure.py @@ -0,0 +1,15 @@ +from odoo import _, models + + +class ReportMrpReportBomStructure(models.AbstractModel): + _inherit = "report.mrp.report_bom_structure" + + def _get_bom_data(self, *args, **kwargs): + result = super()._get_bom_data(*args, **kwargs) + if result.get("level") == 0: + qty = int(result.get("producible_qty") or 0) + if qty > 0: + result["status"] = _("%(qty)s Ready To Produce", qty=qty) + else: + result["status"] = _("No Ready To Produce") + return result diff --git a/bom_forecast/static/src/bom_overview_line.js b/bom_forecast/static/src/bom_overview_line.js new file mode 100644 index 00000000000..058723a2187 --- /dev/null +++ b/bom_forecast/static/src/bom_overview_line.js @@ -0,0 +1,28 @@ +import { patch } from "@web/core/utils/patch"; +import { BomOverviewLine } from "@mrp/components/bom_overview_line/mrp_bom_overview_line"; + +patch(BomOverviewLine.prototype, { + + get statusBackgroundColor() { + switch (this.data.availability_state) { + case "available": return "text-bg-success"; + case "expected": + case "estimated": + if (this.data.level === 0) return "text-bg-dark"; + return "border border-warning text-warning"; + case "unavailable": return "text-bg-danger"; + default: return "text-bg-dark"; + } + }, + + get statusDisplay() { + if (this.data.level > 0 && + this.data.availability_display && + this.data.availability_display.includes("Estimated")) { + return this.data.availability_display.replace( + "Estimated", "Expected" + ); + } + return this.data.availability_display; + } +}); diff --git a/bom_forecast/static/src/bom_overview_line.xml b/bom_forecast/static/src/bom_overview_line.xml new file mode 100644 index 00000000000..c3dc59fb628 --- /dev/null +++ b/bom_forecast/static/src/bom_overview_line.xml @@ -0,0 +1,38 @@ + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/bom_forecast/static/src/mrp_bom_overview_table.xml b/bom_forecast/static/src/mrp_bom_overview_table.xml new file mode 100644 index 00000000000..151a81b4662 --- /dev/null +++ b/bom_forecast/static/src/mrp_bom_overview_table.xml @@ -0,0 +1,7 @@ + + + + + + +