From 5d116bfa6ba0b8a312bfc8fd2ebd383a90dc6a4c Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Tue, 20 May 2025 13:59:01 +0530 Subject: [PATCH 01/13] [ADD] estate: Introducing new real estate property management module Created a new real estate property management module User will be able to use this module for managing workflow related to properties Managing properties, creating and updating properties, buying - selling --- estate/__init__.py | 1 + estate/__manifest__.py | 17 +++++++++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 28 ++++++++++++++++++++++++++++ estate/security/ir.model.access.csv | 2 ++ 5 files changed, 49 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/security/ir.model.access.csv 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..baf556d7f06 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,17 @@ +{ + 'name': "estate", + 'description': """ + This module is used to manage the Real estate and properties. + """, + + 'author': "ayush", + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base'], + 'license': 'LGPL-3', + 'category': 'Real Estate/Brokerage', + 'data': [ + 'security/ir.model.access.csv', + ], +} 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..cebec322b38 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,28 @@ +from odoo import models, fields + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property" + + name = fields.Char(string="Property Name", required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") + date_availability = fields.Date(string="Available From") + expected_price = fields.Float(string="Expected Price") + selling_price = fields.Float(string="Selling Price") + bedrooms = fields.Integer(string="Bedrooms") + 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)") + garden_orientation = fields.Selection( + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], + string="Garden Orientation" + ) 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 576332a8d26687ed448f5b5cb4bddee071756642 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Wed, 21 May 2025 17:43:32 +0530 Subject: [PATCH 02/13] [IMP] estate: add offer management and property enhancements This commit introduces a system for managing property offers, including tracking validity and deadlines, and automatically calculating the best offer price. It also adds an onchange to set default garden details, improving data entry efficiency and consistency. These changes enhance the module's utility for real estate agents by providing better tools for offer tracking and property information management. --- estate/__manifest__.py | 35 +++-- estate/data/estate_property_demo.xml | 65 +++++++++ estate/data/estate_property_offer_demo.xml | 28 ++++ estate/data/estate_property_type_demo.xml | 18 +++ estate/models/__init__.py | 4 + estate/models/estate_property.py | 137 +++++++++++++++++-- estate/models/estate_property_offer.py | 137 +++++++++++++++++++ estate/models/estate_property_tags.py | 18 +++ estate/models/estate_property_type.py | 28 ++++ estate/models/res_users.py | 12 ++ estate/security/estate_security.xml | 44 ++++++ estate/security/ir.model.access.csv | 9 +- estate/views/estate_menus.xml | 12 ++ estate/views/estate_property_offer_views.xml | 41 ++++++ estate/views/estate_property_tags_views.xml | 34 +++++ estate/views/estate_property_type_views.xml | 60 ++++++++ estate/views/estate_property_views.xml | 110 +++++++++++++++ estate/views/res_users_views.xml | 14 ++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 14 ++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 31 +++++ 22 files changed, 831 insertions(+), 22 deletions(-) create mode 100644 estate/data/estate_property_demo.xml create mode 100644 estate/data/estate_property_offer_demo.xml create mode 100644 estate/data/estate_property_type_demo.xml create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tags.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/models/res_users.py create mode 100644 estate/security/estate_security.xml 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_tags_views.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/estate_property_views.xml create mode 100644 estate/views/res_users_views.xml create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate/__manifest__.py b/estate/__manifest__.py index baf556d7f06..7ef106bb020 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,17 +1,28 @@ { - 'name': "estate", - 'description': """ + "name": "estate", + "description": """ This module is used to manage the Real estate and properties. """, - - 'author': "ayush", - 'version': '0.1', - 'application': True, - 'installable': True, - 'depends': ['base'], - 'license': 'LGPL-3', - 'category': 'Real Estate/Brokerage', - 'data': [ - 'security/ir.model.access.csv', + "author": "ayush", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["base"], + "license": "LGPL-3", + "category": "Real Estate/Brokerage", + "data": [ + "security/estate_security.xml", + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tags_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_menus.xml", + "views/res_users_views.xml", + "data/estate_property_type_demo.xml", + ], + "demo": [ + "data/estate_property_demo.xml", + "data/estate_property_offer_demo.xml", ], } diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml new file mode 100644 index 00000000000..b63f37af490 --- /dev/null +++ b/estate/data/estate_property_demo.xml @@ -0,0 +1,65 @@ +<odoo> + <data noupdate="1"> + <record id="demo_property_1" model="estate.property"> + <field name="name">Big Villa</field> + <field name="state">new</field> + <field name="description">A nice and big Villa</field> + <field name="postcode">30050</field> + <field name="date_availability" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d')"/> + <field name="expected_price">1600000</field> + <field name="bedrooms">6</field> + <field name="living_area">500</field> + <field name="facades">4</field> + <field name="garage">True</field> + <field name="garden">True</field> + <field name="garden_area">1200</field> + <field name="garden_orientation">south</field> + </record> + + <record id="demo_property_2" model="estate.property"> + <field name="name">Ocean side Mansion</field> + <field name="state">cancelled</field> + <field name="description">Grand ocean side mansion with stunning views of ocean</field> + <field name="postcode">50052</field> + <field name="date_availability" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d')"/> + <field name="expected_price">1200000</field> + <field name="selling_price">0</field> + <field name="bedrooms">4</field> + <field name="living_area">1000</field> + <field name="facades">4</field> + <field name="garage">True</field> + </record> + + <record id="demo_property_3" model="estate.property"> + <field name="name">Empire tower's Luxurious Penthouse</field> + <field name="state">offer_received</field> + <field name="description">A luxurious penthouse with views of central park</field> + <field name="postcode">65065</field> + <field name="date_availability" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d')"/> + <field name="expected_price">1800000</field> + <field name="bedrooms">6</field> + <field name="living_area">5000</field> + <field name="facades">2</field> + <field name="garage">False</field> + <field name="garden">False</field> + <field name="buyer_id" ref="base.res_partner_2"/> + <field name="offer_ids" eval="[ + Command.create({ + 'partner_id': ref('base.res_partner_2'), + 'price': 2000000, + 'validity': 21 + }), + Command.create({ + 'partner_id': ref('base.res_partner_1'), + 'price': 2150000, + 'validity': 14 + }), + Command.create({ + 'partner_id': ref('base.res_partner_2'), + 'price': 2300000, + 'validity': 14 + }) + ]"/> + </record> + </data> +</odoo> \ No newline at end of file diff --git a/estate/data/estate_property_offer_demo.xml b/estate/data/estate_property_offer_demo.xml new file mode 100644 index 00000000000..cde5f12ae12 --- /dev/null +++ b/estate/data/estate_property_offer_demo.xml @@ -0,0 +1,28 @@ +<odoo> + <data noupdate="1"> + <record id="demo_big_villa_offer_1" model="estate.property.offer"> + <field name="property_id" ref="estate.demo_property_1"/> + <field name="partner_id" ref="base.res_partner_1"/> + <field name="price">1440000</field> + <field name="validity">14</field> + </record> + + <record id="demo_big_villa_offer_2" model="estate.property.offer"> + <field name="property_id" ref="estate.demo_property_1"/> + <field name="partner_id" ref="base.res_partner_1"/> + <field name="price">1500000</field> + <field name="validity">14</field> + </record> + + <record id="demo_big_villa_offer_3" model="estate.property.offer"> + <field name="property_id" ref="estate.demo_property_1"/> + <field name="partner_id" ref="base.res_partner_2"/> + <field name="price">1550000</field> + <field name="validity">14</field> + </record> + + <function name="action_accept_offer" model="estate.property.offer"> + <value eval="ref('demo_big_villa_offer_2')"/> + </function> + </data> +</odoo> \ No newline at end of file diff --git a/estate/data/estate_property_type_demo.xml b/estate/data/estate_property_type_demo.xml new file mode 100644 index 00000000000..603a20874bc --- /dev/null +++ b/estate/data/estate_property_type_demo.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <!-- Property Types --> + <record id="estate_property_type_residential" model="estate.property.type"> + <field name="name">Residential</field> + </record> + <record id="estate_property_type_commercial" model="estate.property.type"> + <field name="name">Commercial</field> + </record> + <record id="estate_property_type_land" model="estate.property.type"> + <field name="name">Land</field> + </record> + <record id="estate_property_type_industrial" model="estate.property.type"> + <field name="name">Industrial</field> + </record> + </data> +</odoo> \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..6315ba30deb 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,5 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tags +from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cebec322b38..5d3ecc30446 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,17 +1,37 @@ -from odoo import models, fields +from odoo import models, fields, api +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError, ValidationError class EstateProperty(models.Model): _name = "estate.property" _description = "Estate Property" + _sql_constraints = [ + ( + "estate_property_expected_price_positive", + "CHECK(expected_price > 0)", + "The expected price must be positive.", + ), + ( + "estate_property_selling_price_non_negative", + "CHECK(selling_price >= 0)", + "The selling price must be non negative.", + ), + ] + + _order = "id desc" name = fields.Char(string="Property Name", required=True) description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") - date_availability = fields.Date(string="Available From") + date_availability = fields.Date( + string="Available From", + copy=False, + default=fields.Date.today() + relativedelta(months=4), + ) expected_price = fields.Float(string="Expected Price") - selling_price = fields.Float(string="Selling Price") - bedrooms = fields.Integer(string="Bedrooms") + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + 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") @@ -19,10 +39,109 @@ class EstateProperty(models.Model): garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( selection=[ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West'), + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + string="Garden Orientation", + ) + active = fields.Boolean(string="Active", default=True) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), ], - string="Garden Orientation" + string="Status", + required=True, + 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) + salesperson_id = fields.Many2one( + "res.users", string="Salesperson", default=lambda self: self.env.user + ) + tag_ids = fields.Many2many( + "estate.property.tags", string="Tags", help="Tags for the property" + ) + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + total_area = fields.Integer( + string="Total Area (sqm)", + compute="_compute_total_area", + store=True, + ) + + company_id = fields.Many2one( + "res.company", required=True, default=lambda self: self.env.company ) + + @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( + string="Best Price", + 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 + + @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 = "" + + def action_set_sold(self): + for offer in self: + if offer.state == "sold": + raise UserError("This property is already sold.") + if offer.state == "offer_accepted": + offer.state = "sold" + return True + else: + raise UserError("Only accepted offers can set the property as sold.") + + def action_set_cancel(self): + for offer in self: + if offer.state == "new": + offer.state = "cancelled" + return True + else: + raise UserError( + "Only refused offers can set the property as cancelled." + ) + + # we will not accept the offer if the price is less than 10% of the selling price + + @api.constrains("selling_price", "expected_price") + def _check_price(self): + for record in self: + if record.selling_price and record.expected_price: + if record.selling_price < 0.9 * record.expected_price: + raise ValidationError( + "The selling price must be at least 90% of the expected price." + ) + + @api.ondelete(at_uninstall=False) + def _ondelete_property(self): + for record in self: + if record.state not in ["new", "cancelled"]: + raise UserError( + "You cannot delete a property that is not new or cancelled." + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..0f9d4653ef0 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,137 @@ +from odoo import models, fields, api +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError, ValidationError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _sql_constraints = [ + ( + "estate_property_offer_price_positive", + "CHECK(price > 0)", + "The offer price must be strictly positive.", + ) + ] + + _order = "price desc" + + price = fields.Float(string="Price", required=True) + status = fields.Selection( + [ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + string="Status", + copy=False, + ) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True, + ) + create_date = fields.Datetime( + string="Creation Date", readonly=True, default=fields.Datetime.now + ) + + @api.depends("create_date", "validity") + def _compute_date_deadline(self): + for offer in self: + creation_date = ( + offer.create_date.date() if offer.create_date else fields.Date.today() + ) + if offer.validity: + offer.date_deadline = creation_date + relativedelta(days=offer.validity) + else: + offer.date_deadline = creation_date + + def _inverse_date_deadline(self): + for offer in self: + creation_date = ( + offer.create_date.date() if offer.create_date else fields.Date.today() + ) + if offer.date_deadline: + delta = offer.date_deadline - creation_date + offer.validity = delta.days + else: + offer.validity = 0 + + def action_accept_offer(self): + for record in self: + record_property = record.property_id + property_state = record_property.state + + if property_state == "offer_accepted": + raise UserError("You can only accept one offer at a time.") + if property_state == "sold": + raise UserError("You cannot accept an offer on a sold property.") + if property_state == "cancelled": + raise UserError("You cannot accept an offer on a cancelled property.") + + other_offers = record_property.offer_ids.filtered( + lambda o: o.id != record.id + ) + other_offers.write({"status": "refused"}) + + record.status = "accepted" + record_property.write( + { + "buyer_id": record.partner_id.id, + "selling_price": record.price, + "state": "offer_accepted", + } + ) + + return True + + def action_refuse_offer(self): + for record in self: + record_property = record.property_id + property_state = record_property.state + + if property_state in ["sold", "cancelled"]: + raise UserError( + "You cannot refuse an offer on a sold or cancelled property." + ) + if record.status == "accepted": + raise UserError("You cannot refuse an already accepted offer.") + + record.status = "refused" + return True + + property_type_id = fields.Many2one( + "estate.property.type", + string="Property Type", + related="property_id.property_type_id", + store=True, + ) + + @api.model_create_multi + def create(self, vals_list): + estate_property_model_instance = self.env["estate.property"] + for vals in vals_list: + property_id = vals.get("property_id") + estate_property = estate_property_model_instance.browse(property_id) + best_price = estate_property.best_price + if not estate_property: + raise ValidationError("Property not found.") + + if estate_property.state in ["sold", "cancelled"]: + raise ValidationError( + "You cannot create an offer for a sold or cancelled property." + ) + + if best_price >= vals.get("price", 0.0): + raise ValidationError( + "The offer price must be strictly higher than the previous offers." + ) + best_price = max(best_price, vals.get("price", 0.0)) + + estate_property_model_instance.state = "offer_received" + + return super().create(vals_list) diff --git a/estate/models/estate_property_tags.py b/estate/models/estate_property_tags.py new file mode 100644 index 00000000000..211c2f18d1b --- /dev/null +++ b/estate/models/estate_property_tags.py @@ -0,0 +1,18 @@ +from odoo import models, fields + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tags" + _description = "Estate Property Tags" + _sql_constraints = [ + ( + "estate_property_tag_name_unique", + "UNIQUE(name)", + "The tag names must be unique.", + ) + ] + + _order = "name" + + name = fields.Char(string="Tag Name", required=True) + color = fields.Integer(string="Color Index") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..f8b77b9a7c0 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,28 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + _sql_constraints = [ + ( + "estate_property_type_name_unique", + "UNIQUE(name)", + "The type names must be unique.", + ) + ] + + _order = "sequence, name" + + name = fields.Char(required=True) + sequence = fields.Integer("Sequence") + property_ids = fields.One2many("estate.property", "property_type_id") + offer_ids = fields.One2many("estate.property.offer", "property_type_id") + offer_count = fields.Integer( + compute="_compute_offer_count", string="Offer Count", readonly=True, copy=False + ) + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..a051f025c06 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson_id", + string="Properties", + domain=[("state", "in", ["new", "offer_received"])], + ) diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..dd37969f7bd --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,44 @@ +<odoo> + <!--Security groups for Estate Module--> + <record id="estate_group_user" model="res.groups"> + <field name="name">Agent</field> + <field name="category_id" ref="base.module_category_real_estate_brokerage"/> + <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/> + </record> + + <record id="estate_group_manager" model="res.groups"> + <field name="name">Estate Manager</field> + <field name="category_id" ref="base.module_category_real_estate_brokerage"/> + <field name="implied_ids" eval="[(4, ref('estate_group_user'))]"/> + <field name="users" eval="[(4, ref('base.user_admin')), (4, ref('base.user_root'))]"/> + </record> + + <!-- Record Rule for Agents: Access own or unassigned properties --> + <record id="estate_group_user_access_rule" model="ir.rule"> + <field name="name">Estate Property Agent Access</field> + <field name="model_id" ref="model_estate_property"/> + <field name="groups" eval="[(4, ref('estate_group_user'))]"/> + <field name="perm_unlink" eval="False"/> + <field name="perm_create" eval="False"/> + <field name="domain_force">['|', ('salesperson_id', '=', user.id), ('salesperson_id', '=', False)]</field> + </record> + + <!-- Record Rule for Managers: Access all properties --> + <record id="estate_group_manager_rule" model="ir.rule"> + <field name="name">Estate Property Manager All Access</field> + <field name="model_id" ref="model_estate_property"/> + <field name="perm_unlink" eval="False"/> + <field name="groups" eval="[(4, ref('estate_group_manager'))]"/> + </record> + + <!-- Record Rule for Company specific access to agent --> + <record id="estate_company_access_rule" model="ir.rule"> + <field name="name">Agents can see only their company's data</field> + <field name="model_id" ref="model_estate_property"/> + <field name="domain_force">[ + '|', ('company_id', '=', False), + ('company_id', 'in', company_ids) + ]</field> + </record> + +</odoo> \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0e11f47e58d..a321ac21c31 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,9 @@ 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_manager,access_estate_property_manager,model_estate_property,estate_group_manager,1,1,1,1 +access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate_group_manager,1,1,1,1 +access_estate_property_tags_manager,access_estate_property_tags_manager,model_estate_property_tags,estate_group_manager,1,1,1,1 +access_estate_property_offer_manager,access_estate_property_offer_manager,model_estate_property_offer,estate_group_manager,1,1,1,1 +access_estate_property_agent,access_estate_property_agent,model_estate_property,estate_group_user,1,1,1,0 +access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate_group_user,1,0,0,0 +access_estate_property_tags_agent,access_estate_property_tags_agent,model_estate_property_tags,estate_group_user,1,0,0,0 +access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0 \ 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..7e6b6772106 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <menuitem id="estate_menu_root" name="Real Estate"> + <menuitem id="estate_first_level_menu" name="Advertisements"> + <menuitem id="estate_property_menu_action" action="estate_property_action"/> + </menuitem> + <menuitem id="estate_first_level_menu_settings" name="settings" groups="estate_group_manager"> + <menuitem id="estate_property_type_menu_action" action="estate_property_type_action"/> + <menuitem id="estate_property_tags_menu_action" action="estate_property_tags_action"/> + </menuitem> + </menuitem> +</odoo> \ 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..18558164d2a --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,41 @@ +<odoo> + <!--List view for Property offer--> + <record id="estate_property_offer_view_tree" model="ir.ui.view"> + <field name="name">estate.property.offer.tree</field> + <field name="model">estate.property.offer</field> + <field name="arch" type="xml"> + <list string="Property Offers" editable="bottom" decoration-danger="status == 'refused'" decoration-success="status == 'accepted'"> + <field name="price" string="Offer Price"/> + <field name="partner_id"/> + <button name="action_accept_offer" type="object" icon="fa-check" title="Accept Offer" + invisible="status in ['accepted', 'refused']"/> + <button name="action_refuse_offer" type="object" icon="fa-times" title="Refuse offer" + invisible="status in ['accepted', 'refused']"/> + <field name="status"/> + <field name="validity"/> + <field name="date_deadline"/> + <field name="property_type_id" optional="hide"/> + </list> + </field> + </record> + + <!-- Form view for Property offer --> + <record id="estate_property_offer_view_form" model="ir.ui.view"> + <field name="name">estate.property.offer.form</field> + <field name="model">estate.property.offer</field> + <field name="arch" type="xml"> + <form string="Property Offer"> + <sheet> + <group> + <field name="price"/> + <field name="partner_id"/> + <field name="status"/> + <field name="validity"/> + <field name="date_deadline"/> + </group> + </sheet> + </form> + </field> + </record> + +</odoo> \ No newline at end of file diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml new file mode 100644 index 00000000000..96184876b3d --- /dev/null +++ b/estate/views/estate_property_tags_views.xml @@ -0,0 +1,34 @@ +<odoo> + <!--Action for Property tags--> + <record id="estate_property_tags_action" model="ir.actions.act_window"> + <field name="name">Property Tags</field> + <field name="res_model">estate.property.tags</field> + <field name="view_mode">list,form</field> + </record> + + <!-- list View for Property Tags --> + <record id="estate_property_tags_view_list" model="ir.ui.view"> + <field name="name">estate.property.tags.list</field> + <field name="model">estate.property.tags</field> + <field name="arch" type="xml"> + <list string="Property Tags" editable="bottom"> + <field name="name"/> + </list> + </field> + </record> + + <!-- Form View for Property Tags --> + <record id="estate_property_tags_view_form" model="ir.ui.view"> + <field name="name">estate.property.tags.form</field> + <field name="model">estate.property.tags</field> + <field name="arch" type="xml"> + <form string="Property Tags"> + <sheet> + <group> + <field name="name"/> + </group> + </sheet> + </form> + </field> + </record> +</odoo> diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..47db00ef7c7 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!-- Action for Property Types --> + <record id="estate_property_type_action" model="ir.actions.act_window"> + <field name="name">Property Types</field> + <field name="res_model">estate.property.type</field> + <field name="view_mode">list,form</field> + </record> + + <!-- list View for Property Types --> + <record id="estate_property_type_view_list" model="ir.ui.view"> + <field name="name">estate.property.type.list</field> + <field name="model">estate.property.type</field> + <field name="arch" type="xml"> + <list string="Property Types"> + <field name="sequence" widget="handle"/> + <field name="name"/> + </list> + </field> + </record> + + + <record id="estate_property_type_offers_action" model="ir.actions.act_window"> + <field name="name">Property Type Offers</field> + <field name="res_model">estate.property.offer</field> + <field name="view_mode">list</field> + <field name="domain">[('property_type_id', '=', active_id)]</field> + </record> + + <!-- Form View for Property Types --> + <record id="estate_property_type_view_form" model="ir.ui.view"> + <field name="name">estate.property.type.form</field> + <field name="model">estate.property.type</field> + <field name="arch" type="xml"> + <form string="Property Type"> + <sheet> + <div name="button_box" class="oe_button_box"> + <button name="%(estate_property_type_offers_action)d" type="action" icon="fa-money" + class="oe_stat_button"> + <field name="offer_count" string="Offers" widget="statinfo"/> + </button> + </div> + <h2><field name="name"/></h2> + <notebook> + <page string="Properties"> + <field name="property_ids" widget="one2many_tags"> + <list> + <field name="name"/> + <field name="state"/> + <field name="expected_price"/> + <field name="selling_price"/> + </list> + </field> + </page> + </notebook> + </sheet> + </form> + </field> + </record> +</odoo> \ 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..1d46eb0b5fa --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="estate_property_action" model="ir.actions.act_window"> + <field name="name">Properties</field> + <field name="res_model">estate.property</field> + <field name="view_mode">list,form</field> + <field name="context">{'search_default_available_properties': True}</field> + </record> + + <!-- Estate Property List View --> + <record id="estate_property_view_list" model="ir.ui.view"> + <field name="name">estate.property.list</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <list string="Properties" decoration-success="state in ['offer_received', 'offer_accepted']" + decoration-bf="state == 'offer_accepted'" decoration-muted="state == 'sold'"> + <field name="name" string="Title"/> + <field name="postcode" string="Postcode"/> + <field name="property_type_id"/> + <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/> + <field name="bedrooms" string="Bedrooms"/> + <field name="living_area" string="Living Area (sqm)"/> + <field name="expected_price" string="Expected Price"/> + <field name="selling_price" string="Selling Price"/> + <field name="date_availability" string="Available From" optional="hide"/> + </list> + </field> + </record> + + <!--Estate Property Form View--> + <record id="estate_property_view_form" model="ir.ui.view"> + <field name="name">estate.property.form</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <form string="Property"> + <header> + <button name="action_set_sold" type="object" string="Sold" class="btn-primary" invisible="state != 'offer_accepted'"/> + <button name="action_set_sold" type="object" string="Sold" class="btn-secondary" invisible="state in ('offer_accepted','sold','cancelled')"/> + <button name="action_set_cancel" type="object" string="Cancel" class="btn-secondary" invisible="state in ('sold', 'cancelled')"/> + <field name="state" widget="statusbar" statusbar_visible="new,offer_received,offer_accepted,sold"/> + </header> + <sheet> + <h1> + <field name="name" placeholder="Property Title"/> + </h1> + <field name="tag_ids" widget="many2many_tags" options="{'no_create': True, 'color_field': 'color'}" placeholder="Tags"/> + <br/> + <br/> + <group> + <group> + <field name="state" string="Status"/> + <field name="property_type_id" placeholder="Property Type" options="{'no_create': True}"/> + <field name="postcode"/> + <field name="date_availability"/> + </group> + <group> + <field name="expected_price"/> + <field name="selling_price"/> + <field name="best_price"/> + </group> + </group> + <notebook> + <page string="Description"> + <group> + <field name="description"/> + <field name="bedrooms"/> + <field name="living_area"/> + <field name="facades"/> + <field name="garage"/> + <field name="garden"/> + <field name="garden_area" invisible="garden==False"/> + <field name="garden_orientation" invisible="garden==False"/> + <field name="total_area"/> + </group> + </page> + <page name="offers" string="Offers"> + <field name="offer_ids" readonly="state in ('offer_accepted', 'sold', 'cancelled')"/> + </page> + <page string="Other Info"> + <group> + <field name="salesperson_id"/> + <field name="buyer_id"/> + </group> + </page> + </notebook> + </sheet> + </form> + </field> + </record> + + <!-- Estate Property Search view --> + <record id="estate_property_view_search" model="ir.ui.view"> + <field name="name">estate.property.search</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <search string="Search Properties"> + <field name="name" string="Title"/> + <field name="postcode" string="Postcode"/> + <field name="expected_price" string="Expected Price"/> + <field name="bedrooms" string="Bedrooms"/> + <field name="facades" string="Facades"/> + <filter name="available_properties" string="Available Properties" domain="[('state', 'in', ['new', 'offer_received'])]"/> + <field name="living_area" string="Living Area" filter_domain="[('living_area', '>=', self)]"/> + <group expand="1" string="Group By"> + <filter string="Postcode" name="groupby_postcode" context="{'group_by':'postcode'}"/> + </group> + </search> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..050b9273457 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,14 @@ +<odoo> + <record id="res_users_view_form" model="ir.ui.view"> + <field name="name">res.users.view.form.inherit.estate</field> + <field name="model">res.users</field> + <field name="inherit_id" ref="base.view_users_form"/> + <field name="arch" type="xml"> + <notebook position="inside"> + <page name="estate_properties" string="Real Estate Properties"> + <field name="property_ids"/> + </page> + </notebook> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..91246fb4329 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "estate account", + "version": "0.1", + "depends": ["base", "estate", "account"], + "author": "Ayush Patel", + "category": "Real Estate", + "description": """ + This module links Estate and Accounting. + """, + "application": True, + "auto_install": True, + "data": [], + "license": "LGPL-3", +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..513e8374866 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,31 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_set_sold(self): + self.check_access("write") + self.env["account.move"].sudo().create( + { + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create( + { + "name": f"Sale of property {self.name}", + "quantity": 1, + "price_unit": self.selling_price * 0.06, + } + ), + Command.create( + { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": 100.00, + } + ), + ], + } + ) + return super().action_set_sold() From bfdfe8c4cd43cbb63e4942e822f416edc4f45053 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Thu, 29 May 2025 18:48:11 +0530 Subject: [PATCH 03/13] [IMP] estate: implemented QWeb reports This commit introduces two new QWeb reports for the estate module: Property Offers Report: Displays detailed information about a specific property including salesperson details, expected price, status, and a comprehensive table of all offers made on the property with price, partner, validity, deadline, and status information. Salesperson Properties Report: Shows all properties managed by a specific salesperson, with each property displaying its basic information and associated offers in a tabular format. --- estate/__manifest__.py | 2 + estate/reports/estate_property_reports.xml | 25 ++++++ estate/reports/estate_property_templates.xml | 89 +++++++++++++++++++ estate_account/__manifest__.py | 4 +- .../reports/estate_account_templates.xml | 13 +++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 estate/reports/estate_property_reports.xml create mode 100644 estate/reports/estate_property_templates.xml create mode 100644 estate_account/reports/estate_account_templates.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 7ef106bb020..2e8515d484d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -20,6 +20,8 @@ "views/estate_menus.xml", "views/res_users_views.xml", "data/estate_property_type_demo.xml", + "reports/estate_property_templates.xml", + "reports/estate_property_reports.xml", ], "demo": [ "data/estate_property_demo.xml", diff --git a/estate/reports/estate_property_reports.xml b/estate/reports/estate_property_reports.xml new file mode 100644 index 00000000000..8b6292b84df --- /dev/null +++ b/estate/reports/estate_property_reports.xml @@ -0,0 +1,25 @@ +<?xml version ="1.0" encoding="utf-8"?> +<odoo> + <!--Property sale and offer report view--> + <record id="action_report_property_sale" model="ir.actions.report"> + <field name="name">Estate Property Sale Report</field> + <field name="model">estate.property</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">estate.report_property_offers</field> + <field name="report_file">estate.report_property_offers</field> + <field name="print_report_name">'Property Offers - ' + object.name</field> + <field name="binding_model_id" ref="model_estate_property"/> + <field name="binding_type">report</field> + </record> + + <!-- Salespersons all properties report view --> + <record id="action_salesperson_property" model="ir.actions.report"> + <field name="name">Salesperson Properties</field> + <field name="model">res.users</field> + <field name="report_name">estate.report_salesperson_property</field> + <field name="report_file">estate.report_salesperson_property</field> + <field name="print_report_name">'Salesperson Properties - ' + object.name</field> + <field name="binding_model_id" ref="base.model_res_users"/> + <field name="binding_type">report</field> + </record> +</odoo> \ No newline at end of file diff --git a/estate/reports/estate_property_templates.xml b/estate/reports/estate_property_templates.xml new file mode 100644 index 00000000000..fa6004fdc18 --- /dev/null +++ b/estate/reports/estate_property_templates.xml @@ -0,0 +1,89 @@ +<odoo> + <!--Template for listing offers for a property--> + <!--When calling templates keep in mind about 1-M, M-1 definations keep field vairable constants--> + <template id="property_offers_table_template"> + <table t-if="property.mapped('offer_ids')" class="table"> + <thead> + <tr> + <th>Offer price</th> + <th>Partner</th> + <th>Validity (days)</th> + <th>Deadline</th> + <th>State</th> + </tr> + </thead> + <tbody> + <tr t-foreach="property.mapped('offer_ids')" t-as="offer"> + <td><span t-field="offer.price"/></td> + <td><span t-field="offer.partner_id.name"/></td> + <td><span t-field="offer.validity"/></td> + <td><span t-field="offer.date_deadline"/></td> + <td> + <span t-if="offer.status" t-field="offer.status"/> + <span t-else="">Pending</span> + </td> + </tr> + </tbody> + </table> + <p t-else=""> + <strong>No offers available for this property.</strong> + </p> + </template> + + <!--Main Template for viewing property wise offers--> + <template id="report_property_offers"> + <t t-foreach="docs" t-as="property"> + <t t-call="web.html_container"> + <t t-call="web.external_layout"> + <div class="page"> + <h2 t-field="property.name"/> + <div id="property_details"> + <strong>Salesperson:</strong> + <span t-field="property.salesperson_id.name"/> + <br/> + <strong>Expected Price:</strong> + <span t-field="property.expected_price"/> + <br/> + <strong>Status:</strong> + <span t-field="property.state"/> + </div> + <br/> + <!-- Calling template for listing offers of a property--> + <t t-call="estate.property_offers_table_template"/> + </div> + </t> + </t> + </t> + </template> + + <!--Main Template for viewing all properties of a salesperson--> + <template id="report_salesperson_property"> + <t t-foreach="docs" t-as="salesperson"> + <t t-call="web.html_container"> + <t t-call="web.external_layout"> + <div class="page"> + <h2> + <strong>Salesperson: </strong> + <span t-field="salesperson.name"/> + </h2> + <br/> + <div t-foreach="salesperson.property_ids" t-as="property"> + <h3 t-field="property.name"/> + <div> + <strong>Expected Price:</strong> + <span t-field="property.expected_price"/> + <br/> + <strong>Status:</strong> + <span t-field="property.state"/> + </div> + <br/> + <!-- Calling template for listing offers of a property--> + <t t-call="estate.property_offers_table_template"/> + <br/><br/> + </div> + </div> + </t> + </t> + </t> + </template> +</odoo> \ No newline at end of file diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 91246fb4329..93dff616702 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -9,6 +9,8 @@ """, "application": True, "auto_install": True, - "data": [], + "data": [ + "reports/estate_account_templates.xml", + ], "license": "LGPL-3", } diff --git a/estate_account/reports/estate_account_templates.xml b/estate_account/reports/estate_account_templates.xml new file mode 100644 index 00000000000..79556a16cbc --- /dev/null +++ b/estate_account/reports/estate_account_templates.xml @@ -0,0 +1,13 @@ +<!--Odoo fetches the primary template estate.report_property_offers. +It then looks for any "extension" views that inherit from it. It finds your report_property_offers_inherit_account. +It applies the <xpath> from report_property_offers_inherit_account to the arch of estate.report_property_offers. +The resulting, modified arch is then rendered.--> +<odoo> + <template id="report_property_offers_inherit_account" inherit_id="estate.report_property_offers"> + <xpath expr="//div[@id='property_details']" position="inside"> + <t t-if="property.state == 'sold'"> + <p><strong>Invoice has already been created!!!</strong></p> + </t> + </xpath> + </template> +</odoo> \ No newline at end of file From 99043e55e4ce7196719268e1d6a535f1390dace6 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Fri, 30 May 2025 18:48:28 +0530 Subject: [PATCH 04/13] [IMP] estate: validations and test cases added To provided best user experience to users of estate module we have added improved validations on property form to prevent any illogical manual update in data. As well as added test cases to keep it mind to avoid breaking any logic while making any future changes --- estate/models/estate_property.py | 28 ++++++++++ estate/tests/__init__.py | 1 + estate/tests/estate_property_tests.py | 79 +++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/estate_property_tests.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5d3ecc30446..6ad922513c1 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -145,3 +145,31 @@ def _ondelete_property(self): raise UserError( "You cannot delete a property that is not new or cancelled." ) + + @api.onchange("offer_ids") + def _onchange_offer_ids(self): + if self.offer_ids: + self.state = "offer_received" + if not self.offer_ids and self.state != "new": + self.state = "new" + + @api.onchange("state") + def _onchange_state(self): + if self.state == "offer_received" and not self.offer_ids: + raise UserError("No offers available yet!.") + elif self.state == "offer_received" and self.offer_ids.filtered( + lambda o: o.status == "accepted" + ): + raise UserError( + "You cannot set the property as offer received when there is an accepted offer." + ) + elif self.state == "offer_accepted" and not self.offer_ids: + raise UserError("You cannot accept an offer without any offers.") + elif self.state == "sold" and not self.offer_ids: + raise UserError("You cannot sell a property without any offers.") + elif self.state == "offer_accepted" and not self.offer_ids.filtered( + lambda o: o.status == "accepted" + ): + raise UserError( + "You cannot set the property as offer accepted without an accepted offer." + ) diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..524f7bf1293 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import estate_property_tests diff --git a/estate/tests/estate_property_tests.py b/estate/tests/estate_property_tests.py new file mode 100644 index 00000000000..b692795af5b --- /dev/null +++ b/estate/tests/estate_property_tests.py @@ -0,0 +1,79 @@ +from odoo import Command # noqa: F401 +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tests import Form, TransactionCase + + +@tagged("post_install", "-at_install") +class EstateTestCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.properties = cls.env["estate.property"].create( + [ + { + "name": "Test Property", + "description": "Some Description", + "expected_price": 100000, + "living_area": 50, + }, + { + "name": "Test Property Garden", + "description": "property with garden", + "expected_price": 200000, + "living_area": 100, + }, + ] + ) + + cls.offers = cls.env["estate.property.offer"].create( + [ + { + "partner_id": cls.env.ref("base.res_partner_2").id, + "price": 110000, + "property_id": cls.properties[0].id, + }, + { + "partner_id": cls.env.ref("base.res_partner_1").id, + "price": 120000, + "property_id": cls.properties[0].id, + }, + { + "partner_id": cls.env.ref("base.res_partner_3").id, + "price": 125000, + "property_id": cls.properties[0].id, + }, + ] + ) + + def test_property_sale(self): + with self.assertRaises(UserError): + self.properties[0].action_set_sold() + + self.offers[1].action_accept_offer() + + self.properties[0].action_set_sold() + self.assertEqual(self.properties[0].state, "sold", "Property was not sold") + + with self.assertRaises(UserError): + self.env["estate.property.offer"].create( + { + "partner_id": self.env.ref("base.res_partner_4").id, + "price": 200000, + "property_id": self.properties[0].id, + } + ) + + def test_garden_reset(self): + with Form(self.properties[1]) as form: + form.garden = True + self.assertEqual(form.garden_area, 10) + self.assertEqual(form.garden_orientation, "north") + + form.garden = False + self.assertEqual(form.garden_area, 0, "Garden area should be reset to 0") + self.assertEqual( + form.garden_orientation, + False, + "Garden orientation should be reset to False", + ) From 54cf205edf1fd2a1abc87b9e0cee5a18206b8f8c Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Tue, 3 Jun 2025 19:00:41 +0530 Subject: [PATCH 05/13] [IMP] awsomeowl: implemetned various owl concepts This commit has various tutorial completions of OWL concepts tutorial --- awesome_owl/static/src/card/card.js | 11 +++++ awesome_owl/static/src/card/card.xml | 11 +++++ awesome_owl/static/src/counter/counter.js | 23 ++++++++++ awesome_owl/static/src/counter/counter.xml | 7 +++ awesome_owl/static/src/playground.js | 19 +++++++- awesome_owl/static/src/playground.xml | 15 +++++-- .../static/src/todo_list/todo_items.js | 30 +++++++++++++ .../static/src/todo_list/todo_items.xml | 13 ++++++ awesome_owl/static/src/todo_list/todo_list.js | 43 +++++++++++++++++++ .../static/src/todo_list/todo_list.xml | 15 +++++++ awesome_owl/static/src/utils.js | 18 ++++++++ 11 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todo_list/todo_items.js create mode 100644 awesome_owl/static/src/todo_list/todo_items.xml create mode 100644 awesome_owl/static/src/todo_list/todo_list.js create mode 100644 awesome_owl/static/src/todo_list/todo_list.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..a7009563fa5 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: { type: String, optional: true, default: "Card Title" }, + content: { type: String, optional: true, default: "Card Content" }, + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..549325bda73 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_owl.card"> + <div class="card d-inline-block m-2" style="width: 18rem;"> + <div class="card-body"> + <h5 class="card-title" t-out="props.title"/> + <p class="card-text" t-out="props.content"/> + </div> + </div> + </t> +</templates> \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..00d89b6e6ed --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + setup() { + this.state = useState({ value: 0 }); + } + + + static props = { + onChange: { type: Function, optional: true }, + }; + + increment() { + this.state.value += 1; + if (this.props.onChange) { + this.props.onChange(); + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..2451ae002f6 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_owl.counter"> + <p> Counter :<t t-esc="state.value"/></p> + <button class="btn btn-primary" t-on-click="increment" style="border: 2px solid #007bff; background-color: #28a745; color: #fff;">Increment</button> + </t> +</templates> \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..3f01f13d3e6 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,22 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState } 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 components = { Counter, Card, TodoList}; + + setup() { + this.state = useState({sum: 0}) + } + + incrementSum() { + console.log("Incrementing sum function called!"); + console.log("Current sum:", this.state.sum); + this.state.sum += 1; + console.log("New sum:", this.state.sum); + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..8752a3e3d94 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,17 @@ <?xml version="1.0" encoding="UTF-8" ?> <templates xml:space="preserve"> - <t t-name="awesome_owl.playground"> + <div> + <h1>Owl Playground</h1> + </div> <div class="p-3"> - hello world + <!-- <p>Sum: <t t-out="state.sum"/></p> + <Counter onChange.bind="incrementSum"/> + <Counter onChange.bind="incrementSum"/> --> + <!-- <Card title="'First Card'" content="'This is the first card's content.'"/> + <Card title="'Second Card'" content="'Another card with different content.'"/> --> + <!-- <Card/> --> + <TodoList/> </div> </t> - -</templates> +</templates> \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_items.js b/awesome_owl/static/src/todo_list/todo_items.js new file mode 100644 index 00000000000..d7c75acce75 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_items.js @@ -0,0 +1,30 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + static props = { + todo: { + type: Object, + shape: { + id: { type: Number, optional: false }, + description: { type: String, optional: false }, + isCompleted: { type: Boolean, optional: false }, + }, + optional: false, + }, + toggleState: { type: Function, optional: false }, + removeTodo: { type: Function, optional: false }, // New callback prop for deletion + }; + + toggleState() { + // Call the parent's toggleState function with the todo id + this.props.toggleState(this.props.todo.id); + } + + removeTodo() { + // Call the parent's removeTodo function with the todo id + this.props.removeTodo(this.props.todo.id); + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_items.xml b/awesome_owl/static/src/todo_list/todo_items.xml new file mode 100644 index 00000000000..3dbd4ca744a --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_items.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_owl.todo_item"> + <div class="todo-item" + t-att-class="props.todo.isCompleted ? 'text-muted text-decoration-line-through' : ''"> + <input type="checkbox" + t-att-checked="props.todo.isCompleted" + t-on-change="toggleState"/> + <span t-out="props.todo.id" style="padding:5px"/>. <span t-out="props.todo.description"/> + <span class="fa fa-remove" t-on-click="removeTodo" title="Delete Todo" style="color: #dc3545; padding:5px"/> + </div> + </t> +</templates> \ No newline at end of file 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..b94c7ec2e7b --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,43 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_items"; +import { useAutoFocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + + setup() { + useAutoFocus("todoInput"); + this.todos = useState([]); + } + + addTodo(event) { + if (event.keyCode === 13) { + const description = event.target.value.trim(); + if (description) { + this.todos.push({ + id: this.todos.length + 1, + description: description, + isCompleted: false, + }); + event.target.value = ""; + } + } + } + + toggleTodo(todoId) { + const todo = this.todos.find(t => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + // Find the index of the todo to remove + const index = this.todos.findIndex(t => t.id === todoId); + if (index !== -1) { + // Remove the todo from the array + this.todos.splice(index, 1); + } + } +} \ No newline at end of file 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..b2254f3a73b --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_owl.todo_list"> + <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 5px;"> + <h3>Todo List</h3> + <input type="text" t-ref="todoInput" + placeholder="Enter a new task" + t-on-keyup="addTodo" + style="width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 4px;"/> + <t t-foreach="todos" t-as="item" t-key="item.id"> + <TodoItem todo="item" toggleState.bind="toggleTodo" removeTodo.bind="removeTodo"/> + </t> + </div> + </t> +</templates> \ No newline at end of file diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..db22ff4b1d0 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ + +import { onMounted, useRef } from "@odoo/owl"; + +export function useAutoFocus(refname) { + console.log(`useAutoFocus called with refname: ${refname}`); + const inputRef = useRef(refname) + + onMounted(() => { + if (inputRef.el) { + inputRef.el.focus(); + console.log(`Input with ref '${refname}' has been focused.`); + } + else { + console.warn(`Input with ref '${refname}' not found.`); + } + }); +} \ No newline at end of file From 693facf1978b226580ad605c26b994aade0110c7 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Wed, 4 Jun 2025 19:08:32 +0530 Subject: [PATCH 06/13] [IMP] awesome_owl: improvements in existing components This commit add concept of generic card with slots and minimizing . Also improved logic for assigning id while adding new todo --- awesome_owl/static/src/card/card.js | 14 ++++++- awesome_owl/static/src/card/card.xml | 13 +++++- awesome_owl/static/src/playground.js | 3 -- awesome_owl/static/src/playground.xml | 41 ++++++++++++++----- awesome_owl/static/src/todo_list/todo_list.js | 4 +- awesome_owl/static/src/utils.js | 5 --- 6 files changed, 55 insertions(+), 25 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index a7009563fa5..0de00c74eb8 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -1,11 +1,21 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; export class Card extends Component { static template = "awesome_owl.card"; static props = { title: { type: String, optional: true, default: "Card Title" }, - content: { type: String, optional: true, default: "Card Content" }, + slots: {type: Object, optional: true}, }; + + setup() { + // Add state to track if card is open (default: true) + this.state = useState({ isOpen: true }); + } + + toggleOpen() { + // Toggle the open/closed state + this.state.isOpen = !this.state.isOpen; + } } \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 549325bda73..65859fb318b 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -3,8 +3,17 @@ <t t-name="awesome_owl.card"> <div class="card d-inline-block m-2" style="width: 18rem;"> <div class="card-body"> - <h5 class="card-title" t-out="props.title"/> - <p class="card-text" t-out="props.content"/> + <div class="d-flex justify-content-between align-items-center mb-2"> + <h5 class="card-title mb-0" t-out="props.title"/> + <button class="btn btn-sm btn-outline-secondary" t-on-click="toggleOpen" type="button"> + <span t-if="state.isOpen" class="fa fa-chevron-up"/> + <span t-else="" class="fa fa-chevron-down"/> + </button> + </div> + + <div class="card-text" t-att-hidden="!this.state.isOpen"> + <t t-slot="default"/> + </div> </div> </div> </t> diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 3f01f13d3e6..27fb7f12461 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -14,9 +14,6 @@ export class Playground extends Component { } incrementSum() { - console.log("Incrementing sum function called!"); - console.log("Current sum:", this.state.sum); this.state.sum += 1; - console.log("New sum:", this.state.sum); } } \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 8752a3e3d94..4906ac4c4fc 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,17 +1,36 @@ <?xml version="1.0" encoding="UTF-8" ?> <templates xml:space="preserve"> <t t-name="awesome_owl.playground"> - <div> - <h1>Owl Playground</h1> - </div> - <div class="p-3"> - <!-- <p>Sum: <t t-out="state.sum"/></p> - <Counter onChange.bind="incrementSum"/> - <Counter onChange.bind="incrementSum"/> --> - <!-- <Card title="'First Card'" content="'This is the first card's content.'"/> - <Card title="'Second Card'" content="'Another card with different content.'"/> --> - <!-- <Card/> --> - <TodoList/> + <div class="container py-4"> + <h1 class="mb-4 text-primary text-center">Owl Playground</h1> + <div class="row mb-4"> + <div class="col-md-6 mb-3"> + <div class="bg-light rounded shadow-sm p-3"> + <h5 class="mb-3">Counters</h5> + <p class="mb-2">Sum: <span class="fw-bold" t-out="state.sum"/></p> + <div class="d-flex gap-2"> + <Counter onChange.bind="incrementSum"/> + <Counter onChange.bind="incrementSum"/> + </div> + </div> + </div> + <div class="col-md-6 mb-3"> + <Card title="'First Card'"> + <Counter/> + </Card> + <Card title="'Second Card'"> + <p class="mb-0">This is another card. Add your content here!</p> + </Card> + </div> + </div> + <div class="row"> + <div class="col-12"> + <div class="bg-white rounded shadow-sm p-3"> + <h5 class="mb-3">Todo List</h5> + <TodoList/> + </div> + </div> + </div> </div> </t> </templates> \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js index b94c7ec2e7b..5bc44f92322 100644 --- a/awesome_owl/static/src/todo_list/todo_list.js +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -5,10 +5,10 @@ import { useAutoFocus } from "../utils"; export class TodoList extends Component { static template = "awesome_owl.todo_list"; static components = { TodoItem }; - setup() { useAutoFocus("todoInput"); this.todos = useState([]); + this.nextId = 1; } addTodo(event) { @@ -16,7 +16,7 @@ export class TodoList extends Component { const description = event.target.value.trim(); if (description) { this.todos.push({ - id: this.todos.length + 1, + id: this.nextId++, description: description, isCompleted: false, }); diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js index db22ff4b1d0..17bab19505a 100644 --- a/awesome_owl/static/src/utils.js +++ b/awesome_owl/static/src/utils.js @@ -3,16 +3,11 @@ import { onMounted, useRef } from "@odoo/owl"; export function useAutoFocus(refname) { - console.log(`useAutoFocus called with refname: ${refname}`); const inputRef = useRef(refname) onMounted(() => { if (inputRef.el) { inputRef.el.focus(); - console.log(`Input with ref '${refname}' has been focused.`); - } - else { - console.warn(`Input with ref '${refname}' not found.`); } }); } \ No newline at end of file From 6bac6a336a44e577466c99dd8f6e3f819d48578a Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Thu, 5 Jun 2025 19:00:02 +0530 Subject: [PATCH 07/13] [IMP] awesome_dashboard: dashboard added This commit introduces dashboard for viewing data This commit also consists caching logic to avoid calling api every time we view dashboard --- awesome_dashboard/static/src/dashboard.js | 32 ++++++++++++++- awesome_dashboard/static/src/dashboard.scss | 3 ++ awesome_dashboard/static/src/dashboard.xml | 39 +++++++++++++++++-- .../src/dashboard_item/dashboard_item.js | 19 +++++++++ .../src/dashboard_item/dashboard_item.xml | 10 +++++ .../static/src/statistics_service.js | 22 +++++++++++ 6 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 637fa4bb972..e142c2a2c4a 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,10 +1,38 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState, onWillStart } 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"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { Layout, DashboardItem }; + + setup() { + this.actionService = useService("action"); + this.statisticsService = useService("awesome_dashboard.statistics"); + this.statistics = useState({}); + onWillStart(async () => { + const data = await this.statisticsService.loadStatistics(); + Object.assign(this.statistics, data); + }); + } + + openCustomers() { + this.actionService.doAction("base.action_partner_form"); + } + + openLeads() { + this.actionService.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + }); + } } -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss new file mode 100644 index 00000000000..a1281d8303e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: grey; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 1a2ac9a2fed..f84d37994f8 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -2,7 +2,40 @@ <templates xml:space="preserve"> <t t-name="awesome_dashboard.AwesomeDashboard"> - hello dashboard - </t> + <Layout display="{ controlPanel: {} }" className="'o_dashboard h-100'"> + <t t-set-slot="control-panel-create-button"> + <button t-on-click="openCustomers" type="button" class="btn btn-primary me-2" title="Customers">Customers</button> + <button t-on-click="openLeads" type="button" class="btn btn-primary" title="Leads">Leads</button> + </t> + <div class="p-3"> + <div class="d-flex flex-wrap"> + <DashboardItem> + <h5>Number of new orders this month</h5> + <p class="fs-4 fw-bold text-success" t-out="statistics.nb_new_orders"/> + </DashboardItem> + + <DashboardItem> + <h5>Total amount of new orders this month</h5> + <p class="fs-4 fw-bold text-success" t-out="statistics.total_amount"/> + </DashboardItem> + + <DashboardItem> + <h5>Average amount of t-shirt by order this month</h5> + <p class="fs-4 fw-bold text-success" t-out="statistics.average_quantity"/> + </DashboardItem> -</templates> + <DashboardItem> + <h5>Number of cancelled orders this month</h5> + <p class="fs-4 fw-bold text-success" t-out="statistics.nb_cancelled_orders"/> + </DashboardItem> + + <DashboardItem> + <h5>Average time for an order to go from new to sent or cancelled</h5> + <p class="fs-4 fw-bold text-success"><t t-out="statistics.average_time"/> hours</p> + </DashboardItem> + + </div> + </div> + </Layout> + </t> +</templates> \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..b5d0a82161b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + size: { + type: Number, + optional: true, + default: 1, + }, + slots: { + type: Object, + optional: true, + shape: {default: Object} + } + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..aed541e2ad2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_dashboard.DashboardItem"> + <div class="card m-2 d-inline-block shadow-sm" t-attf-style="width: {{(props.size || 1) * 18}}rem;"> + <div class="card-body"> + <t t-slot="default"/> + </div> + </div> + </t> +</templates> \ No newline at end of file diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..a6d071c0e01 --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; + +const statisticsService = { + start() { + const loadStatistics = memoize(async () => { + console.log("Loading statistics from server..."); + const data = await rpc("/awesome_dashboard/statistics"); + console.log("Statistics loaded:", data); + return data; + }); + + return { + loadStatistics, + }; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); \ No newline at end of file From 7c6f7baf821577aaae5e43744e5a22d828856737 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Fri, 6 Jun 2025 17:59:24 +0530 Subject: [PATCH 08/13] [IMP] awesome_dashboard: dashboard improvements Improved dashboard by adding pie chart for better visual representation of data Real time updates to avoid refreshing dashboard every time. added lazy loading to lazy load dashboard component --- awesome_dashboard/__manifest__.py | 7 +- awesome_dashboard/static/src/dashboard.xml | 41 --------- .../static/src/{ => dashboard}/dashboard.js | 15 ++- .../static/src/{ => dashboard}/dashboard.scss | 0 .../static/src/dashboard/dashboard.xml | 22 +++++ .../dashboard_item/dashboard_item.js | 0 .../dashboard_item/dashboard_item.xml | 0 .../static/src/dashboard/dashboard_items.js | 67 ++++++++++++++ .../src/dashboard/number_card/number_card.js | 11 +++ .../src/dashboard/number_card/number_card.xml | 9 ++ .../src/dashboard/pie_chart/pie_chart.js | 91 +++++++++++++++++++ .../src/dashboard/pie_chart/pie_chart.xml | 8 ++ .../dashboard/piechart_card/piechart_card.js | 13 +++ .../dashboard/piechart_card/piechart_card.xml | 9 ++ .../src/dashboard/statistics_service.js | 32 +++++++ .../static/src/dashboard_action.js | 14 +++ .../static/src/statistics_service.js | 22 ----- 17 files changed, 288 insertions(+), 73 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.xml rename awesome_dashboard/static/src/{ => dashboard}/dashboard.js (65%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.scss (100%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.js (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.xml (100%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.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/piechart_card/piechart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js delete mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..5241ee41868 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -23,8 +23,11 @@ ], 'assets': { 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + 'awesome_dashboard/static/src/dashboard_action.js', + ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', ], }, 'license': 'AGPL-3' -} +} \ 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 f84d37994f8..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,41 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<templates xml:space="preserve"> - - <t t-name="awesome_dashboard.AwesomeDashboard"> - <Layout display="{ controlPanel: {} }" className="'o_dashboard h-100'"> - <t t-set-slot="control-panel-create-button"> - <button t-on-click="openCustomers" type="button" class="btn btn-primary me-2" title="Customers">Customers</button> - <button t-on-click="openLeads" type="button" class="btn btn-primary" title="Leads">Leads</button> - </t> - <div class="p-3"> - <div class="d-flex flex-wrap"> - <DashboardItem> - <h5>Number of new orders this month</h5> - <p class="fs-4 fw-bold text-success" t-out="statistics.nb_new_orders"/> - </DashboardItem> - - <DashboardItem> - <h5>Total amount of new orders this month</h5> - <p class="fs-4 fw-bold text-success" t-out="statistics.total_amount"/> - </DashboardItem> - - <DashboardItem> - <h5>Average amount of t-shirt by order this month</h5> - <p class="fs-4 fw-bold text-success" t-out="statistics.average_quantity"/> - </DashboardItem> - - <DashboardItem> - <h5>Number of cancelled orders this month</h5> - <p class="fs-4 fw-bold text-success" t-out="statistics.nb_cancelled_orders"/> - </DashboardItem> - - <DashboardItem> - <h5>Average time for an order to go from new to sent or cancelled</h5> - <p class="fs-4 fw-bold text-success"><t t-out="statistics.average_time"/> hours</p> - </DashboardItem> - - </div> - </div> - </Layout> - </t> -</templates> \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js similarity index 65% rename from awesome_dashboard/static/src/dashboard.js rename to awesome_dashboard/static/src/dashboard/dashboard.js index e142c2a2c4a..0bd6bcdf62f 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -1,24 +1,23 @@ /** @odoo-module **/ -import { Component, useState, onWillStart } from "@odoo/owl"; +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 { PieChart } from "./pie_chart/pie_chart"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem }; + static components = { Layout, DashboardItem, PieChart}; setup() { this.actionService = useService("action"); this.statisticsService = useService("awesome_dashboard.statistics"); - this.statistics = useState({}); - onWillStart(async () => { - const data = await this.statisticsService.loadStatistics(); - Object.assign(this.statistics, data); - }); + this.statistics = useState(this.statisticsService.data); + const dashboardItemsRegistry = registry.category("awesome_dashboard"); + this.items = dashboardItemsRegistry.getAll(); } openCustomers() { @@ -35,4 +34,4 @@ class AwesomeDashboard extends Component { } } -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); \ No newline at end of file +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss similarity index 100% rename from awesome_dashboard/static/src/dashboard.scss rename to awesome_dashboard/static/src/dashboard/dashboard.scss diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..a266baef0c6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + + <t t-name="awesome_dashboard.AwesomeDashboard"> + <Layout display="{ controlPanel: {} }" className="'o_dashboard h-100'"> + <t t-set-slot="control-panel-create-button"> + <button t-on-click="openCustomers" type="button" class="btn btn-primary me-2" title="Customers">Customers</button> + <button t-on-click="openLeads" type="button" class="btn btn-primary" title="Leads">Leads</button> + </t> + <div class="p-3"> + <div class="d-flex flex-wrap"> + <t t-foreach="items" t-as="item" t-key="item.id"> + <DashboardItem size="item.size || 1"> + <t t-set="itemProp" t-value="item.props ? item.props(statistics) : {'data': statistics}"/> + <t t-component="item.Component" t-props="itemProp" /> + </DashboardItem> + </t> + </div> + </div> + </Layout> + </t> +</templates> \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js similarity index 100% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.js rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml similarity index 100% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.xml rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml 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..3ca604081af --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,67 @@ +/** @odoo-module **/ + +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./piechart_card/piechart_card"; +import { registry } from "@web/core/registry"; + +export const items = [ + { + id: "nb_new_orders", + description: "Number of new orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders + }), + }, + { + id: "total_amount", + description: "Total amount of new orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount + }), + }, + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders + }), + }, + { + id: "average_time", + description: "Average time for order processing", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from new to sent or cancelled", + value: `${data.average_time} hours` + }), + }, + { + id: "orders_by_size_chart", + description: "T-Shirt Sales by Size Chart", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "T-Shirt Sales by Size", + data: data + }), + }, +]; + +items.forEach((item) => { + registry.category("awesome_dashboard").add(item.id, item) +}); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..3fc37f36551 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { type: String }, + value: { type: [String, Number] }, + }; +} \ No newline at end of file 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..79d8c0176be --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_dashboard.NumberCard"> + <div> + <h5 t-out="props.title"/> + <p class="fs-4 fw-bold text-success" t-out="props.value"/> + </div> + </t> +</templates> \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..09afbc6204e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,91 @@ +/** @odoo-module **/ +import { Component, onWillStart, onMounted, onWillUnmount, useRef, useEffect } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + data: { type: Object }, + }; + + setup() { + this.chart = null; + this.canvasRef = useRef("canvas"); + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + onMounted(() => { + this.renderChart(); + }); + + // Re-render chart when data changes + useEffect(() => { + if (this.canvasRef.el) { + this.renderChart(); + } + }, () => [this.props.data.orders_by_size]); + + + onWillUnmount(() => { + if (this.chart) { + this.chart.destroy(); + } + }); + } + + renderChart() { + if (this.chart) { + this.chart.destroy(); + } + + const ordersBySizeData = this.props.data?.orders_by_size || {}; + const labels = Object.keys(ordersBySizeData); + const values = Object.values(ordersBySizeData); + + if (labels.length === 0) { + return; + } + + const colors = [ + '#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40' + ]; + + this.chart = new Chart(this.canvasRef.el, { + type: 'pie', + data: { + labels: labels.map(label => label.toUpperCase()), + datasets: [{ + data: values, + backgroundColor: colors.slice(0, labels.length), + borderWidth: 2, + borderColor: '#fff' + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + padding: 15, + usePointStyle: true, + pointStyle: 'rect' + } + }, + title: { + display: true, + text: 'T-Shirt Sales by Size', + font: { + size: 16, + weight: 'bold' + }, + padding: 20 + } + } + } + }); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..0eaea84bbcd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_dashboard.PieChart"> + <div class="pie-chart-container" style="position: relative; height: 300px; width: 100%;"> + <canvas t-ref="canvas"/> + </div> + </t> +</templates> \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js new file mode 100644 index 00000000000..916d24c4d04 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +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: { type: String, optional: true }, + data: { type: Object }, + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml new file mode 100644 index 00000000000..a5d046c0d5a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_dashboard.PieChartCard"> + <div> + <h5 t-if="props.title" t-out="props.title"/> + <PieChart data="props.data"/> + </div> + </t> +</templates> \ No newline at end of file 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..72a9b8a9d80 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,32 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; +import { reactive } from "@odoo/owl"; + + +const statisticsService = { + start() { + const data = reactive({}); + const loadStatistics = async () => { + try { + const result = await rpc("/awesome_dashboard/statistics"); + Object.keys(data).forEach(key => delete data[key]); + Object.assign(data, result); + + } catch (error) { + console.error('Error loading statistics:', error); + } + }; + loadStatistics(); + + setInterval(loadStatistics, 50000); + + return { + data, + }; + }, +}; + +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..c696799b416 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + <LazyComponent bundle="'awesome_dashboard.dashboard'" Component="'AwesomeDashboard'" /> + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); \ No newline at end of file diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js deleted file mode 100644 index a6d071c0e01..00000000000 --- a/awesome_dashboard/static/src/statistics_service.js +++ /dev/null @@ -1,22 +0,0 @@ -/** @odoo-module **/ - -import { registry } from "@web/core/registry"; -import { rpc } from "@web/core/network/rpc"; -import { memoize } from "@web/core/utils/functions"; - -const statisticsService = { - start() { - const loadStatistics = memoize(async () => { - console.log("Loading statistics from server..."); - const data = await rpc("/awesome_dashboard/statistics"); - console.log("Statistics loaded:", data); - return data; - }); - - return { - loadStatistics, - }; - }, -}; - -registry.category("services").add("awesome_dashboard.statistics", statisticsService); \ No newline at end of file From eb10d6ee53fc23ad6e807bd11d184d649e68bc81 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Mon, 9 Jun 2025 16:27:33 +0530 Subject: [PATCH 09/13] [IMP] awesome_dashboard: added selective display of data This commit introduces display of selective charts via check boxes This allows users to see only preferred charts and data points local storage enables users to automatically save selected preference of charts in browser --- awesome_dashboard/__manifest__.py | 2 +- .../static/src/dashboard/dashboard.js | 28 ++++++++++-- .../static/src/dashboard/dashboard.scss | 2 +- .../static/src/dashboard/dashboard.xml | 18 +++++--- .../dashboard_item/dashboard_item.js | 2 +- .../dashboard_item/dashboard_item.xml | 2 +- .../static/src/dashboard/dashboard_items.js | 2 +- .../dashboard_settings/dashboard_settings.js | 44 +++++++++++++++++++ .../dashboard_settings/dashboard_settings.xml | 26 +++++++++++ .../src/dashboard/number_card/number_card.js | 2 +- .../src/dashboard/number_card/number_card.xml | 2 +- .../src/dashboard/pie_chart/pie_chart.js | 2 +- .../src/dashboard/pie_chart/pie_chart.xml | 2 +- .../dashboard/piechart_card/piechart_card.js | 2 +- .../dashboard/piechart_card/piechart_card.xml | 2 +- .../static/src/dashboard_action.js | 2 +- awesome_owl/static/src/card/card.js | 2 +- awesome_owl/static/src/card/card.xml | 2 +- awesome_owl/static/src/counter/counter.js | 2 +- awesome_owl/static/src/counter/counter.xml | 2 +- awesome_owl/static/src/playground.js | 2 +- awesome_owl/static/src/playground.xml | 3 +- .../static/src/todo_list/todo_items.js | 2 +- .../static/src/todo_list/todo_items.xml | 2 +- awesome_owl/static/src/todo_list/todo_list.js | 2 +- .../static/src/todo_list/todo_list.xml | 2 +- awesome_owl/static/src/utils.js | 2 +- estate/data/estate_property_demo.xml | 2 +- estate/data/estate_property_offer_demo.xml | 2 +- estate/data/estate_property_type_demo.xml | 2 +- estate/models/estate_property.py | 16 +++---- estate/models/estate_property_offer.py | 20 ++++----- estate/reports/estate_property_reports.xml | 2 +- estate/reports/estate_property_templates.xml | 2 +- estate/security/estate_security.xml | 2 +- estate/security/ir.model.access.csv | 2 +- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_offer_views.xml | 2 +- estate/views/estate_property_type_views.xml | 2 +- estate/views/estate_property_views.xml | 2 +- estate/views/res_users_views.xml | 2 +- .../reports/estate_account_templates.xml | 2 +- 42 files changed, 156 insertions(+), 69 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 5241ee41868..ca5d5092218 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -30,4 +30,4 @@ ], }, 'license': 'AGPL-3' -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index 0bd6bcdf62f..caafe61dedb 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -5,19 +5,39 @@ 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 { PieChart } from "./pie_chart/pie_chart"; +import { DashboardSettings } from "./dashboard_settings/dashboard_settings"; +import { browser } from "@web/core/browser/browser"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - - static components = { Layout, DashboardItem, PieChart}; + static components = { Layout, DashboardItem }; setup() { this.actionService = useService("action"); this.statisticsService = useService("awesome_dashboard.statistics"); this.statistics = useState(this.statisticsService.data); + + this.dialogService = useService("dialog"); + const dashboardItemsRegistry = registry.category("awesome_dashboard"); this.items = dashboardItemsRegistry.getAll(); + + this.state = useState({ + uncheckedItems: browser.localStorage.getItem("uncheckedItems")?.split(",").filter(id => id) || [], + }); + } + + updateConfiguration(newUncheckedItems) { + this.state.uncheckedItems.length = 0; + this.state.uncheckedItems.push(...newUncheckedItems); + } + + openConfiguration() { + this.dialogService.add(DashboardSettings, { + items: this.items, + initialUncheckedItems: this.state.uncheckedItems, + updateConfiguration: this.updateConfiguration.bind(this), + }); } openCustomers() { @@ -34,4 +54,4 @@ class AwesomeDashboard extends Component { } } -registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); \ No newline at end of file +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss index a1281d8303e..90e1493325f 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.scss +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -1,3 +1,3 @@ .o_dashboard { background-color: grey; -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index a266baef0c6..eae4f3faf9e 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -7,16 +7,22 @@ <button t-on-click="openCustomers" type="button" class="btn btn-primary me-2" title="Customers">Customers</button> <button t-on-click="openLeads" type="button" class="btn btn-primary" title="Leads">Leads</button> </t> + + <t t-set-slot="control-panel-additional-actions"> + <button class="btn btn-light" title="Dashboard Settings" t-on-click="openConfiguration"> + <i class="fa fa-cog"></i> + </button> + </t> <div class="p-3"> <div class="d-flex flex-wrap"> - <t t-foreach="items" t-as="item" t-key="item.id"> - <DashboardItem size="item.size || 1"> - <t t-set="itemProp" t-value="item.props ? item.props(statistics) : {'data': statistics}"/> - <t t-component="item.Component" t-props="itemProp" /> + <t t-foreach="Object.values(items)" t-as="item" t-key="item.id"> + <DashboardItem t-if="!state.uncheckedItems.includes(item.id)" size="item.size || 1"> + <t t-set="itemProp" t-value="item.props ? item.props(statistics) : {'data': statistics}"/> + <t t-component="item.Component" t-props="itemProp" /> </DashboardItem> - </t> + </t> </div> </div> </Layout> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js index b5d0a82161b..6aeb93e2446 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -16,4 +16,4 @@ export class DashboardItem extends Component { shape: {default: Object} } } -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml index aed541e2ad2..213ca6e0b67 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -7,4 +7,4 @@ </div> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js index 3ca604081af..067739d5770 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_items.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -64,4 +64,4 @@ export const items = [ items.forEach((item) => { registry.category("awesome_dashboard").add(item.id, item) -}); \ No newline at end of file +}); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js new file mode 100644 index 00000000000..45e85f2205e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js @@ -0,0 +1,44 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { browser } from "@web/core/browser/browser"; + +export class DashboardSettings extends Component { + static template = "awesome_dashboard.DashboardSettings"; + static components = { Dialog }; + + static props = { + close: { type: Function }, + }; + + setup() { + const items = this.props.items || {}; + const initialUncheckedItems = this.props.initialUncheckedItems || []; + + this.dialogDisplayItems = useState( + Object.values(items).map((item) => ({ + ...item, + checked: !initialUncheckedItems.includes(item.id), + })) + ); + } + + onChange(checked, itemInDialog) { + const targetItem = this.dialogDisplayItems.find(i => i.id === itemInDialog.id); + if (targetItem) { + targetItem.checked = checked; + } + } + + confirmChanges() { + const newUncheckedItems = this.dialogDisplayItems.filter((item) => !item.checked).map((item) => item.id); + + browser.localStorage.setItem("uncheckedItems", newUncheckedItems.join(",")); + + if (this.props.updateConfiguration) { + this.props.updateConfiguration(newUncheckedItems); + } + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml new file mode 100644 index 00000000000..a5b02c81169 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-name="awesome_dashboard.DashboardSettings"> + <Dialog title="'Dashboard Items Configuration'"> + <div class="p-3"> + <p>Select items to display on your dashboard:</p> + <div t-foreach="dialogDisplayItems" t-as="item" t-key="item.id" class="form-check mb-2"> + <input + type="checkbox" + class="form-check-input" + t-att-id="'settings_item_' + item.id" + t-att-checked="item.checked" + t-on-change="(ev) => this.onChange(ev.target.checked, item)" + /> + <label class="form-check-label" t-att-for="'settings_item_' + item.id"> + <t t-out="item.description"/> + </label> + </div> + </div> + <t t-set-slot="footer"> + <button class="btn btn-primary" t-on-click="confirmChanges">Apply</button> + <button class="btn btn-secondary ms-2" t-on-click="props.close">Cancel</button> + </t> + </Dialog> + </t> +</templates> diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js index 3fc37f36551..60e661b99d3 100644 --- a/awesome_dashboard/static/src/dashboard/number_card/number_card.js +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -8,4 +8,4 @@ export class NumberCard extends Component { title: { type: String }, value: { type: [String, Number] }, }; -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml index 79d8c0176be..f7d50ce9e90 100644 --- a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -6,4 +6,4 @@ <p class="fs-4 fw-bold text-success" t-out="props.value"/> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js index 09afbc6204e..5580d146fb5 100644 --- a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -88,4 +88,4 @@ export class PieChart extends Component { } }); } -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml index 0eaea84bbcd..a04df27478b 100644 --- a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -5,4 +5,4 @@ <canvas t-ref="canvas"/> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js index 916d24c4d04..033586f7d81 100644 --- a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js @@ -10,4 +10,4 @@ export class PieChartCard extends Component { title: { type: String, optional: true }, data: { type: Object }, }; -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml index a5d046c0d5a..2803b633f47 100644 --- a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml @@ -6,4 +6,4 @@ <PieChart data="props.data"/> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js index c696799b416..763f7fc55da 100644 --- a/awesome_dashboard/static/src/dashboard_action.js +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -11,4 +11,4 @@ export class AwesomeDashboardLoader extends Component { `; } -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); \ No newline at end of file +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 0de00c74eb8..f86fbab84c0 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -18,4 +18,4 @@ export class Card extends Component { // Toggle the open/closed state this.state.isOpen = !this.state.isOpen; } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 65859fb318b..48aca8440c3 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -17,4 +17,4 @@ </div> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index 00d89b6e6ed..aec4b1fda77 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -20,4 +20,4 @@ export class Counter extends Component { this.props.onChange(); } } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml index 2451ae002f6..eab8b0e2c95 100644 --- a/awesome_owl/static/src/counter/counter.xml +++ b/awesome_owl/static/src/counter/counter.xml @@ -4,4 +4,4 @@ <p> Counter :<t t-esc="state.value"/></p> <button class="btn btn-primary" t-on-click="increment" style="border: 2px solid #007bff; background-color: #28a745; color: #fff;">Increment</button> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 27fb7f12461..6a8d5564e14 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -16,4 +16,4 @@ export class Playground extends Component { incrementSum() { this.state.sum += 1; } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4906ac4c4fc..d326d381eb1 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8" ?> <templates xml:space="preserve"> + <t t-name="awesome_owl.playground"> <div class="container py-4"> <h1 class="mb-4 text-primary text-center">Owl Playground</h1> @@ -33,4 +34,4 @@ </div> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_owl/static/src/todo_list/todo_items.js b/awesome_owl/static/src/todo_list/todo_items.js index d7c75acce75..85630f8c450 100644 --- a/awesome_owl/static/src/todo_list/todo_items.js +++ b/awesome_owl/static/src/todo_list/todo_items.js @@ -27,4 +27,4 @@ export class TodoItem extends Component { // Call the parent's removeTodo function with the todo id this.props.removeTodo(this.props.todo.id); } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/todo_list/todo_items.xml b/awesome_owl/static/src/todo_list/todo_items.xml index 3dbd4ca744a..bfc953e9c3d 100644 --- a/awesome_owl/static/src/todo_list/todo_items.xml +++ b/awesome_owl/static/src/todo_list/todo_items.xml @@ -10,4 +10,4 @@ <span class="fa fa-remove" t-on-click="removeTodo" title="Delete Todo" style="color: #dc3545; padding:5px"/> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js index 5bc44f92322..ccefb543ba1 100644 --- a/awesome_owl/static/src/todo_list/todo_list.js +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -40,4 +40,4 @@ export class TodoList extends Component { this.todos.splice(index, 1); } } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml index b2254f3a73b..69ae97918f8 100644 --- a/awesome_owl/static/src/todo_list/todo_list.xml +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -12,4 +12,4 @@ </t> </div> </t> -</templates> \ No newline at end of file +</templates> diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js index 17bab19505a..02a5eba74db 100644 --- a/awesome_owl/static/src/utils.js +++ b/awesome_owl/static/src/utils.js @@ -10,4 +10,4 @@ export function useAutoFocus(refname) { inputRef.el.focus(); } }); -} \ No newline at end of file +} diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml index b63f37af490..eae66d14300 100644 --- a/estate/data/estate_property_demo.xml +++ b/estate/data/estate_property_demo.xml @@ -62,4 +62,4 @@ ]"/> </record> </data> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/data/estate_property_offer_demo.xml b/estate/data/estate_property_offer_demo.xml index cde5f12ae12..46b06f9756a 100644 --- a/estate/data/estate_property_offer_demo.xml +++ b/estate/data/estate_property_offer_demo.xml @@ -25,4 +25,4 @@ <value eval="ref('demo_big_villa_offer_2')"/> </function> </data> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/data/estate_property_type_demo.xml b/estate/data/estate_property_type_demo.xml index 603a20874bc..47f34ede85a 100644 --- a/estate/data/estate_property_type_demo.xml +++ b/estate/data/estate_property_type_demo.xml @@ -15,4 +15,4 @@ <field name="name">Industrial</field> </record> </data> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6ad922513c1..9279304fa42 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -74,22 +74,20 @@ class EstateProperty(models.Model): compute="_compute_total_area", store=True, ) - company_id = fields.Many2one( "res.company", required=True, default=lambda self: self.env.company ) - - @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( string="Best Price", compute="_compute_best_price", store=True, ) + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + @api.depends("offer_ids.price") def _compute_best_price(self): for record in self: @@ -127,8 +125,6 @@ def action_set_cancel(self): "Only refused offers can set the property as cancelled." ) - # we will not accept the offer if the price is less than 10% of the selling price - @api.constrains("selling_price", "expected_price") def _check_price(self): for record in self: @@ -148,8 +144,6 @@ def _ondelete_property(self): @api.onchange("offer_ids") def _onchange_offer_ids(self): - if self.offer_ids: - self.state = "offer_received" if not self.offer_ids and self.state != "new": self.state = "new" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 0f9d4653ef0..626cdf4462e 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -27,7 +27,6 @@ class EstatePropertyOffer(models.Model): ) partner_id = fields.Many2one("res.partner", string="Partner", required=True) property_id = fields.Many2one("estate.property", string="Property", required=True) - validity = fields.Integer(string="Validity (days)", default=7) date_deadline = fields.Date( string="Deadline", @@ -38,6 +37,12 @@ class EstatePropertyOffer(models.Model): create_date = fields.Datetime( string="Creation Date", readonly=True, default=fields.Datetime.now ) + property_type_id = fields.Many2one( + "estate.property.type", + string="Property Type", + related="property_id.property_type_id", + store=True, + ) @api.depends("create_date", "validity") def _compute_date_deadline(self): @@ -56,8 +61,7 @@ def _inverse_date_deadline(self): offer.create_date.date() if offer.create_date else fields.Date.today() ) if offer.date_deadline: - delta = offer.date_deadline - creation_date - offer.validity = delta.days + offer.validity = (offer.date_deadline - creation_date).days else: offer.validity = 0 @@ -104,13 +108,6 @@ def action_refuse_offer(self): record.status = "refused" return True - property_type_id = fields.Many2one( - "estate.property.type", - string="Property Type", - related="property_id.property_type_id", - store=True, - ) - @api.model_create_multi def create(self, vals_list): estate_property_model_instance = self.env["estate.property"] @@ -131,7 +128,6 @@ def create(self, vals_list): "The offer price must be strictly higher than the previous offers." ) best_price = max(best_price, vals.get("price", 0.0)) - - estate_property_model_instance.state = "offer_received" + estate_property.state = "offer_received" return super().create(vals_list) diff --git a/estate/reports/estate_property_reports.xml b/estate/reports/estate_property_reports.xml index 8b6292b84df..3312c911e42 100644 --- a/estate/reports/estate_property_reports.xml +++ b/estate/reports/estate_property_reports.xml @@ -22,4 +22,4 @@ <field name="binding_model_id" ref="base.model_res_users"/> <field name="binding_type">report</field> </record> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/reports/estate_property_templates.xml b/estate/reports/estate_property_templates.xml index fa6004fdc18..acbddef08f2 100644 --- a/estate/reports/estate_property_templates.xml +++ b/estate/reports/estate_property_templates.xml @@ -86,4 +86,4 @@ </t> </t> </template> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml index dd37969f7bd..43a06965654 100644 --- a/estate/security/estate_security.xml +++ b/estate/security/estate_security.xml @@ -41,4 +41,4 @@ ]</field> </record> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index a321ac21c31..eaa33aaaf2f 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -6,4 +6,4 @@ access_estate_property_offer_manager,access_estate_property_offer_manager,model_ access_estate_property_agent,access_estate_property_agent,model_estate_property,estate_group_user,1,1,1,0 access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate_group_user,1,0,0,0 access_estate_property_tags_agent,access_estate_property_tags_agent,model_estate_property_tags,estate_group_user,1,0,0,0 -access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0 \ No newline at end of file +access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 7e6b6772106..c2265bae90e 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -9,4 +9,4 @@ <menuitem id="estate_property_tags_menu_action" action="estate_property_tags_action"/> </menuitem> </menuitem> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 18558164d2a..493caa3167d 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -38,4 +38,4 @@ </field> </record> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 47db00ef7c7..4ebd46b9b9b 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -57,4 +57,4 @@ </form> </field> </record> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1d46eb0b5fa..49c56412f8b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -107,4 +107,4 @@ </search> </field> </record> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml index 050b9273457..a18583359db 100644 --- a/estate/views/res_users_views.xml +++ b/estate/views/res_users_views.xml @@ -11,4 +11,4 @@ </notebook> </field> </record> -</odoo> \ No newline at end of file +</odoo> diff --git a/estate_account/reports/estate_account_templates.xml b/estate_account/reports/estate_account_templates.xml index 79556a16cbc..fa9eba70534 100644 --- a/estate_account/reports/estate_account_templates.xml +++ b/estate_account/reports/estate_account_templates.xml @@ -10,4 +10,4 @@ The resulting, modified arch is then rendered.--> </t> </xpath> </template> -</odoo> \ No newline at end of file +</odoo> From 3aa1ee1d6e43e170f5c9d3ef1d44098fc3dd7909 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Mon, 23 Jun 2025 16:53:38 +0530 Subject: [PATCH 10/13] [ADD] odoo_self_order_details: new module to improve ux during self order This commit introduces a ux change and more information in the Self-Order functionality. It changes its default functionality of directly adding product into order when clicking to opening a view to show large image and detailed product description so that users can see more details about product whole view page will be scroll-able allowing to configure product depending on their options available Addition of cancel button in footer for better mobile experience --- odoo_self_order_details/__init__.py | 1 + odoo_self_order_details/__manifest__.py | 20 ++++++++ odoo_self_order_details/models/__init__.py | 1 + .../models/product_template.py | 7 +++ .../static/src/product_card/product_card.js | 48 +++++++++++++++++++ .../static/src/product_card/product_card.xml | 43 +++++++++++++++++ .../views/product_template_view.xml | 15 ++++++ 7 files changed, 135 insertions(+) create mode 100644 odoo_self_order_details/__init__.py create mode 100644 odoo_self_order_details/__manifest__.py create mode 100644 odoo_self_order_details/models/__init__.py create mode 100644 odoo_self_order_details/models/product_template.py create mode 100644 odoo_self_order_details/static/src/product_card/product_card.js create mode 100644 odoo_self_order_details/static/src/product_card/product_card.xml create mode 100644 odoo_self_order_details/views/product_template_view.xml diff --git a/odoo_self_order_details/__init__.py b/odoo_self_order_details/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/odoo_self_order_details/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo_self_order_details/__manifest__.py b/odoo_self_order_details/__manifest__.py new file mode 100644 index 00000000000..c044ed1ea18 --- /dev/null +++ b/odoo_self_order_details/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Self Order Details", + "description": """ + Self order details for products in POS + """, + "author": "Ayush Patel", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["pos_self_order"], + "license": "LGPL-3", + "assets": { + "pos_self_order.assets": [ + "odoo_self_order_details/static/src/**/*", + ], + }, + "data": [ + "views/product_template_view.xml", + ], +} diff --git a/odoo_self_order_details/models/__init__.py b/odoo_self_order_details/models/__init__.py new file mode 100644 index 00000000000..e8fa8f6bf1e --- /dev/null +++ b/odoo_self_order_details/models/__init__.py @@ -0,0 +1 @@ +from . import product_template diff --git a/odoo_self_order_details/models/product_template.py b/odoo_self_order_details/models/product_template.py new file mode 100644 index 00000000000..d9ad6e34cae --- /dev/null +++ b/odoo_self_order_details/models/product_template.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + self_order_description = fields.Html(string="Self-Order Description") diff --git a/odoo_self_order_details/static/src/product_card/product_card.js b/odoo_self_order_details/static/src/product_card/product_card.js new file mode 100644 index 00000000000..10f2fc60170 --- /dev/null +++ b/odoo_self_order_details/static/src/product_card/product_card.js @@ -0,0 +1,48 @@ +import { patch } from "@web/core/utils/patch"; +import { ProductCard } from "@pos_self_order/app/components/product_card/product_card"; +import { ProductPage } from "@pos_self_order/app/pages/product_page/product_page"; +import { markup } from "@odoo/owl"; + +// Patch ProductCard to always navigate to the product page on selection, +// enabling display of self_order_description and large image for all products. +patch(ProductCard.prototype, { + async selectProduct(qty = 1) { + const product = this.props.product; + + if (!product.self_order_available || !this.isAvailable) { + return; + } + + // For combo products, we use the default behavior + if (product.isCombo()) { + return super.selectProduct(qty); + } + + // For other products, navigate to the product page + this.router.navigate("product", { id: product.id }); + } +}); + +// Patch ProductPage component to fetch and display self_order_description +patch(ProductPage.prototype, { + async setup() { + // call the original setup method to ensure the component is initialized properly + super.setup(); + + // This ensures that the product's self_order_description is fetched + const product = this.props.product; + if (product && !product.self_order_description) { + try { + const orm = this.env.services.orm; + // orm.read() returns all fields of product.product, including those added by other modules via _inherit = "product.product". + const [record] = await orm.read("product.product",[product.id]); + if (record && record.self_order_description) { + // markup is used to safely render HTML content + product.self_order_description = markup(record.self_order_description); + } + } catch (err) { + console.error("Failed to fetch self_order_description via ORM:", err); + } + } + }, +}); diff --git a/odoo_self_order_details/static/src/product_card/product_card.xml b/odoo_self_order_details/static/src/product_card/product_card.xml new file mode 100644 index 00000000000..c1471ad6f4b --- /dev/null +++ b/odoo_self_order_details/static/src/product_card/product_card.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates xml:space="preserve"> + <!--Inheriting ProductPage and defining our changes--> + <t t-inherit="pos_self_order.ProductPage" t-inherit-mode="extension"> + <!--'options' string after product name only when Product is Configurable --> + <xpath expr="//h1[@class='mb-0 text-nowrap']" position="replace"> + <h1 class="mb-0 text-nowrap"><strong t-out="product.name"/> <span t-if="product.isConfigurable()">options</span></h1> + </xpath> + <!-- changes in product details section to have a new layout with a large image and description --> + <xpath expr="//div[hasclass('o-so-product-details')]" position="replace"> + <div class="o-so-product-details d-flex flex-column align-items-center p-3 gap-3"> + <!-- Large Product Image --> + <div class="o-so-product-details-image text-center mb-3" style="max-width: 300px;"> + <img + class="img-fluid rounded shadow-sm" + t-attf-src="/web/image/product.product/{{ product.id }}/image_512" + alt="Product image" + loading="lazy"/> + </div> + + <!-- Product related Information --> + <div class="o-so-product-details-description text-center w-100"> + <h2 t-out="product.name"/> + <span class="fs-3 d-block my-3" t-out="selfOrder.formatMonetary(selfOrder.getProductDisplayPrice(product))"/> + </div> + + <!-- Product Description --> + <div class="o-so-product-details-description text-center w-100"> + <t t-if="product.self_order_description"> + <div t-out="product.self_order_description"/> + </t> + </div> + </div> + </xpath> + + <!--Cancel button next to the Add to cart button --> + <xpath expr="//div[hasclass('page-buttons')]/button[hasclass('btn-primary')]" position="before"> + <button class="btn btn-secondary btn-lg" t-on-click="() => router.back()"> + Cancel + </button> + </xpath> + </t> +</templates> diff --git a/odoo_self_order_details/views/product_template_view.xml b/odoo_self_order_details/views/product_template_view.xml new file mode 100644 index 00000000000..aff114f9b2e --- /dev/null +++ b/odoo_self_order_details/views/product_template_view.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <!--Configures self order description field in 'point of sale' page of product form view--> + <record id="product_template_form_view_inherit_self_order" model="ir.ui.view"> + <field name="name">product.template.form.inherit.self.order</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="product.product_template_form_view"/> + <field name="arch" type="xml"> + <!-- This XPath targets the 'Point of Sale' page and the 'Point of Sale' group inside it --> + <xpath expr="//page[@name='pos']//group[@name='pos']" position="inside"> + <field name="self_order_description"/> + </xpath> + </field> + </record> +</odoo> From 718cf993f8e7dec25198fda1b26592f0b66d5ea7 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Mon, 30 Jun 2025 17:50:24 +0530 Subject: [PATCH 11/13] [ADD] new product type: Added module for kit product type This commit introduces a lightweight "Product Kit" functionality, allowing users to sell a single product that is composed of several sub-products without needing the Manufacturing application or Bills of Materials. This is useful for simple product bundling where component quantities and prices might need to be adjusted on a per-order basis. Key Features: - Adds an "Is Kit" checkbox and a "Sub-products" field to the Product form. - A configuration wizard is available on the sales order line for kit products. This wizard allows adjusting the quantity and price of each component, with a live-calculated total price. - On confirmation, the wizard updates the sales order by creating corresponding sub-product lines and updating the main kit's price to the total cost of its components. - Sub-product lines are read-only in the sales order form to prevent inconsistencies. - Deleting the main kit product automatically removes all its sub-product lines. - Adds a "Print Sub-products in Report" option to the Sales Order to conditionally show or hide the kit components --- new_product_type/__init__.py | 2 + new_product_type/__manifest__.py | 14 +++ new_product_type/models/__init__.py | 3 + new_product_type/models/product_template.py | 13 +++ new_product_type/models/sale_order.py | 41 +++++++ new_product_type/models/sale_order_line.py | 32 +++++ new_product_type/security/ir.model.access.csv | 3 + .../views/product_template_views.xml | 13 +++ new_product_type/views/sale_order_views.xml | 40 +++++++ new_product_type/wizards/__init__.py | 2 + new_product_type/wizards/sub_products_line.py | 28 +++++ .../wizards/sub_products_wizard.py | 109 ++++++++++++++++++ .../wizards/sub_products_wizard.xml | 32 +++++ 13 files changed, 332 insertions(+) create mode 100644 new_product_type/__init__.py create mode 100644 new_product_type/__manifest__.py create mode 100644 new_product_type/models/__init__.py create mode 100644 new_product_type/models/product_template.py create mode 100644 new_product_type/models/sale_order.py create mode 100644 new_product_type/models/sale_order_line.py create mode 100644 new_product_type/security/ir.model.access.csv create mode 100644 new_product_type/views/product_template_views.xml create mode 100644 new_product_type/views/sale_order_views.xml create mode 100644 new_product_type/wizards/__init__.py create mode 100644 new_product_type/wizards/sub_products_line.py create mode 100644 new_product_type/wizards/sub_products_wizard.py create mode 100644 new_product_type/wizards/sub_products_wizard.xml diff --git a/new_product_type/__init__.py b/new_product_type/__init__.py new file mode 100644 index 00000000000..aee8895e7a3 --- /dev/null +++ b/new_product_type/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/new_product_type/__manifest__.py b/new_product_type/__manifest__.py new file mode 100644 index 00000000000..d8be7483028 --- /dev/null +++ b/new_product_type/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "New Kit Product", + "version": "1.0", + "depends": ["sale_management"], + "license": "LGPL-3", + "data": [ + "security/ir.model.access.csv", + "views/product_template_views.xml", + "views/sale_order_views.xml", + "wizards/sub_products_wizard.xml", + ], + "installable": True, + "application": True, +} diff --git a/new_product_type/models/__init__.py b/new_product_type/models/__init__.py new file mode 100644 index 00000000000..8f2f8c0cbc1 --- /dev/null +++ b/new_product_type/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import sale_order_line +from . import sale_order diff --git a/new_product_type/models/product_template.py b/new_product_type/models/product_template.py new file mode 100644 index 00000000000..d6d750f9d51 --- /dev/null +++ b/new_product_type/models/product_template.py @@ -0,0 +1,13 @@ +from odoo import models, fields + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_kit = fields.Boolean(string="Is Kit", default=False) + sub_products = fields.Many2many( + "product.product", + string="Sub Products", + help="Select the products that are part of this kit", + domain="[('is_kit', '=', False)]", + ) diff --git a/new_product_type/models/sale_order.py b/new_product_type/models/sale_order.py new file mode 100644 index 00000000000..37a6f0466be --- /dev/null +++ b/new_product_type/models/sale_order.py @@ -0,0 +1,41 @@ +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + print_in_report = fields.Boolean( + string="Print Sub-products in Report", + default=False, + help="If checked, the individual sub-product components will be printed on the quotation/order report.", + ) + + @api.onchange("order_line") + def _onchange_order_line(self): + current_kit_ids = [ + line._origin.id for line in self.order_line if line.product_is_kit + ] + + new_order_lines = self.order_line.filtered( + lambda line: not line.parent_kit_line_id.id + or (line.parent_kit_line_id.id in current_kit_ids) + ) + + self.order_line = new_order_lines + + def _get_order_lines_to_report(self): + order_lines = super()._get_order_lines_to_report() + if self.print_in_report: + return order_lines + else: + return order_lines.filtered(lambda line: not line.parent_kit_line_id) + + def _get_invoiceable_lines(self, final=False): + invoicable_lines = super()._get_invoiceable_lines(final=final) + print(len(invoicable_lines), "invoicable lines before filter") + if self.print_in_report: + print(len(invoicable_lines), "invoicable lines after filter if true") + return invoicable_lines + else: + print(len(invoicable_lines), "invoicable lines after filter") + return invoicable_lines.filtered(lambda line: not line.parent_kit_line_id) diff --git a/new_product_type/models/sale_order_line.py b/new_product_type/models/sale_order_line.py new file mode 100644 index 00000000000..86c43d28291 --- /dev/null +++ b/new_product_type/models/sale_order_line.py @@ -0,0 +1,32 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + product_is_kit = fields.Boolean( + related="product_id.product_tmpl_id.is_kit", + ) + + parent_kit_line_id = fields.Many2one( + "sale.order.line", + string="Parent Kit Line", + ondelete="cascade", + copy=False, + ) + + sub_product_line_ids = fields.One2many( + "sale.order.line", "parent_kit_line_id", string="Sub-product Lines", copy=False + ) + + is_kit_sub_product = fields.Boolean(string="Is a Kit Sub-product", copy=False) + + def open_sub_product_wizard(self): + return { + "name": f"Product : {self.product_id.display_name}", + "type": "ir.actions.act_window", + "res_model": "sub.products.wizard", + "view_mode": "form", + "target": "new", + "context": {"active_id": self.id}, + } diff --git a/new_product_type/security/ir.model.access.csv b/new_product_type/security/ir.model.access.csv new file mode 100644 index 00000000000..6a08720d626 --- /dev/null +++ b/new_product_type/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sub_products_wizard_user,access.sub.products.wizard.user,model_sub_products_wizard,base.group_user,1,1,1,1 +access_sub_products_line_wizard_user,access.sub.products.line.wizard.user,model_sub_products_line_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/new_product_type/views/product_template_views.xml b/new_product_type/views/product_template_views.xml new file mode 100644 index 00000000000..dcd8d4f59e8 --- /dev/null +++ b/new_product_type/views/product_template_views.xml @@ -0,0 +1,13 @@ +<odoo> + <record id="view_product_template_form_kit" model="ir.ui.view"> + <field name="name">product.template.form.kit.inherit</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="product.product_template_only_form_view"/> + <field name="arch" type="xml"> + <xpath expr="//page[@name='general_information']//group[@name='group_general']" position="inside"> + <field name="is_kit"/> + <field name="sub_products" widget="many2many_tags" options="{'color_field': 'color'}" invisible="is_kit==False"/> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/new_product_type/views/sale_order_views.xml b/new_product_type/views/sale_order_views.xml new file mode 100644 index 00000000000..794e87a314c --- /dev/null +++ b/new_product_type/views/sale_order_views.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="sale_order_form_view_inherited" model="ir.ui.view"> + <field name="name">sale.order.form.inherited</field> + <field name="model">sale.order</field> + <field name="inherit_id" ref="sale.view_order_form" /> + <field name="arch" type="xml"> + + <xpath expr="//group[@name='order_details']" position="inside"> + <field name="print_in_report" /> + </xpath> + + <xpath expr="//field[@name='product_template_id']" position="after"> + <button type="object" name="open_sub_product_wizard" icon="fa-cubes" string="Sub Products" + invisible="not product_is_kit or state == 'sale'" /> + </xpath> + + <!-- make attributes read only inside Order Lines Page of sales order form view --> + + <xpath expr="//field[@name='product_template_id']" position="attributes"> + <attribute name="readonly" add="parent_kit_line_id" separator=" or " /> + </xpath> + <xpath expr="(//field[@name='product_id'])[2]" position="attributes"> + <attribute name="readonly" add="parent_kit_line_id" separator=" or " /> + </xpath> + <xpath expr="(//field[@name='product_uom_qty'])[2]" position="attributes"> + <attribute name="readonly" add="parent_kit_line_id" separator=" or " /> + </xpath> + <xpath expr="(//field[@name='customer_lead'])[2]" position="attributes"> + <attribute name="readonly" add="parent_kit_line_id" separator=" or " /> + </xpath> + <xpath expr="(//field[@name='price_unit'])[2]" position="attributes"> + <attribute name="readonly" add="parent_kit_line_id" separator=" or " /> + </xpath> + <xpath expr="(//field[@name='tax_id'])[2]" position="attributes"> + <attribute name="readonly" add="parent_kit_line_id" separator=" or " /> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/new_product_type/wizards/__init__.py b/new_product_type/wizards/__init__.py new file mode 100644 index 00000000000..da398f90783 --- /dev/null +++ b/new_product_type/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import sub_products_wizard +from . import sub_products_line diff --git a/new_product_type/wizards/sub_products_line.py b/new_product_type/wizards/sub_products_line.py new file mode 100644 index 00000000000..30f901f0a5f --- /dev/null +++ b/new_product_type/wizards/sub_products_line.py @@ -0,0 +1,28 @@ +from odoo import api, fields, models + + +class SubProductsLineWizard(models.TransientModel): + _name = "sub.products.line.wizard" + + product_id = fields.Many2one( + "product.product", + string="Product", + required=True, + help="The product for which sub-products are being selected", + ) + quantity = fields.Float( + string="Quantity", + required=True, + default=1.0, + help="The quantity of the sub-product to be added", + ) + price = fields.Float( + string="Price", required=True, help="The price of the sub-product" + ) + sub_products_wizard_id = fields.Many2one( + "sub.products.wizard", + string="Sub Products Wizard", + required=True, + ondelete="cascade", + help="The wizard from which this line is being created", + ) diff --git a/new_product_type/wizards/sub_products_wizard.py b/new_product_type/wizards/sub_products_wizard.py new file mode 100644 index 00000000000..ec2ca8c0353 --- /dev/null +++ b/new_product_type/wizards/sub_products_wizard.py @@ -0,0 +1,109 @@ +from odoo import api, fields, models + + +class SubProductsWizard(models.TransientModel): + _name = "sub.products.wizard" + _description = "Wizard to Configure Kit Sub-Products" + + order_line_id = fields.Many2one( + "sale.order.line", + string="Sale Order Line", + required=True, + readonly=True, + help="The main sale order line for the kit product.", + ) + order_id = fields.Many2one( + related="order_line_id.order_id", + string="Sale Order", + ) + product_id = fields.Many2one( + related="order_line_id.product_id", + string="Kit Product", + ) + + sub_product_line_ids = fields.One2many( + "sub.products.line.wizard", + "sub_products_wizard_id", + string="Sub-Products", + ) + + total_price = fields.Float( + string="Total Kit Price", + compute="_compute_total_price", + digits="Product Price", + help="The final price of the main kit product based on the components.", + ) + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + if self.env.context.get("active_id"): + order_line = self.env["sale.order.line"].browse( + self.env.context.get("active_id") + ) + res["order_line_id"] = order_line.id + + default_sub_products = order_line.product_id.sub_products + + existing_sub_lines_map = { + line.product_id: line for line in order_line.sub_product_line_ids + } + + wizard_lines = [] + for sub_product in default_sub_products: + existing_line = existing_sub_lines_map.get(sub_product) + if existing_line: + quantity = existing_line.product_uom_qty + price = ( + existing_line.price_unit + if existing_line.price_unit > 0 + else sub_product.lst_price + ) + else: + quantity = 1.0 + price = sub_product.lst_price + + wizard_lines.append( + ( + 0, + 0, + { + "product_id": sub_product.id, + "quantity": quantity, + "price": price, + }, + ) + ) + + res["sub_product_line_ids"] = wizard_lines + return res + + def action_confirm(self): + self.ensure_one() + + self.order_line_id.sub_product_line_ids.unlink() + + new_lines_vals = [] + for line in self.sub_product_line_ids: + new_lines_vals.append( + { + "order_id": self.order_id.id, + "product_id": line.product_id.id, + "product_uom_qty": line.quantity, + "price_unit": 0, + "parent_kit_line_id": self.order_line_id.id, + "is_kit_sub_product": True, + } + ) + self.env["sale.order.line"].create(new_lines_vals) + + self.order_line_id.price_unit = self.total_price + + return {"type": "ir.actions.act_window_close"} + + @api.depends("sub_product_line_ids.quantity", "sub_product_line_ids.price") + def _compute_total_price(self): + for wizard in self: + wizard.total_price = sum( + line.quantity * line.price for line in wizard.sub_product_line_ids + ) diff --git a/new_product_type/wizards/sub_products_wizard.xml b/new_product_type/wizards/sub_products_wizard.xml new file mode 100644 index 00000000000..0513d96b95c --- /dev/null +++ b/new_product_type/wizards/sub_products_wizard.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="kit_sub_products_wizard_view_form" model="ir.ui.view"> + <field name="name">sub.products.wizard</field> + <field name="model">sub.products.wizard</field> + <field name="arch" type="xml"> + <form string="Configure Kit Sub-Products"> + <group> + <field name="product_id" readonly="1" force_save="1"/> + </group> + + <separator string="Sub-Products"/> + <field name="sub_product_line_ids" nolabel="1"> + <list editable="bottom" create="false"> + <field name="product_id" readonly="1" force_save="1"/> + <field name="quantity"/> + <field name="price"/> + </list> + </field> + + <group class="oe_subtotal_footer oe_right"> + <field name="total_price"/> + </group> + + <footer> + <button name="action_confirm" string="Confirm" type="object" class="btn-primary"/> + <button string="Cancel" class="btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> +</odoo> \ No newline at end of file From 7b5069610a2eb89e0522bbccfa9124da11ea756f Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Thu, 3 Jul 2025 14:07:45 +0530 Subject: [PATCH 12/13] [ADD] sale_person: new module for Sales Person Attendance management - Introduce a new custom model for tracking sales person attendance. - Add fields for check-in, check-out, customer, agenda, conversion possibility, worked hours, and tags. - Implement admin-only visibility for check-in and check-out location fields. - Add server actions and automation for automatic check-in/check-out time - Provide user-friendly display names and improved --- sale_person/__init__.py | 1 + sale_person/__manifest__.py | 20 ++ sale_person/security/ir.model.access.csv | 4 + sale_person/views/contact_model_fields.xml | 53 ++++++ sale_person/views/contact_views.xml | 49 +++++ sale_person/views/sale_person_menu.xml | 26 +++ .../views/sale_person_model_fields.xml | 173 ++++++++++++++++++ sale_person/views/sale_person_views.xml | 76 ++++++++ sale_person/views/tag_model_fields.xml | 30 +++ sale_person/views/tag_views.xml | 39 ++++ 10 files changed, 471 insertions(+) create mode 100644 sale_person/__init__.py create mode 100644 sale_person/__manifest__.py create mode 100644 sale_person/security/ir.model.access.csv create mode 100644 sale_person/views/contact_model_fields.xml create mode 100644 sale_person/views/contact_views.xml create mode 100644 sale_person/views/sale_person_menu.xml create mode 100644 sale_person/views/sale_person_model_fields.xml create mode 100644 sale_person/views/sale_person_views.xml create mode 100644 sale_person/views/tag_model_fields.xml create mode 100644 sale_person/views/tag_views.xml diff --git a/sale_person/__init__.py b/sale_person/__init__.py new file mode 100644 index 00000000000..366188d46b3 --- /dev/null +++ b/sale_person/__init__.py @@ -0,0 +1 @@ +# Odoo module marker \ No newline at end of file diff --git a/sale_person/__manifest__.py b/sale_person/__manifest__.py new file mode 100644 index 00000000000..4d9c8cc1b35 --- /dev/null +++ b/sale_person/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Sale Person Attendance", + "version": "1.0", + "summary": "Track salesperson attendance and customer visits.", + "author": "Ayush Patel", + "depends": ["base", "base_automation"], + "license": "LGPL-3", + "data": [ + "views/tag_model_fields.xml", + "views/contact_model_fields.xml", + "views/sale_person_model_fields.xml", + "views/tag_views.xml", + "views/contact_views.xml", + "views/sale_person_views.xml", + "views/sale_person_menu.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": True, +} diff --git a/sale_person/security/ir.model.access.csv b/sale_person/security/ir.model.access.csv new file mode 100644 index 00000000000..52d25b5b373 --- /dev/null +++ b/sale_person/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_x_contact,x_contact,model_x_contact,base.group_user,1,1,1,1 +access_x_sale_person,x_sale_person,model_x_sale_person,base.group_user,1,1,1,1 +access_x_sale_person_tag,x_sale_person_tag,model_x_sale_person_tag,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/sale_person/views/contact_model_fields.xml b/sale_person/views/contact_model_fields.xml new file mode 100644 index 00000000000..546b39a127e --- /dev/null +++ b/sale_person/views/contact_model_fields.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!--This file defines model and its fields similar to model's .py file--> + + <!--Contact model--> + <record id="model_x_contact" model="ir.model"> + <field name="name">Contact</field> + <field name="model">x_contact</field> + <field name="state">manual</field> + </record> + + <!--Fields for Contact model--> + + <!--contact name--> + <record id="field_x_contact_name" model="ir.model.fields"> + <field name="model_id" ref="model_x_contact"/> + <field name="name">x_name</field> + <field name="ttype">char</field> + <field name="field_description">Name</field> + </record> + + <!--customer type--> + <record id="field_x_contact_customer_type" model="ir.model.fields"> + <field name="model_id" ref="model_x_contact"/> + <field name="name">x_customer_type</field> + <field name="ttype">char</field> + <field name="field_description">Customer Type</field> + </record> + + <!--city--> + <record id="field_x_contact_city" model="ir.model.fields"> + <field name="model_id" ref="model_x_contact"/> + <field name="name">x_city</field> + <field name="ttype">char</field> + <field name="field_description">City</field> + </record> + + <!--state--> + <record id="field_x_contact_area" model="ir.model.fields"> + <field name="model_id" ref="model_x_contact"/> + <field name="name">x_area</field> + <field name="ttype">char</field> + <field name="field_description">Area</field> + </record> + + <!--pincode--> + <record id="field_x_contact_pin_code" model="ir.model.fields"> + <field name="model_id" ref="model_x_contact"/> + <field name="name">x_pin_code</field> + <field name="ttype">char</field> + <field name="field_description">Pin Code</field> + </record> +</odoo> \ No newline at end of file diff --git a/sale_person/views/contact_views.xml b/sale_person/views/contact_views.xml new file mode 100644 index 00000000000..7e5fafaeac2 --- /dev/null +++ b/sale_person/views/contact_views.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!--This file acts as a view.xml that we define for models--> + + <!-- Action for contact view --> + <record id="action_x_contact" model="ir.actions.act_window"> + <field name="name">Contacts</field> + <field name="res_model">x_contact</field> + <field name="view_mode">list,form</field> + </record> + + <!-- Contact Form View --> + <record id="view_x_contact_form" model="ir.ui.view"> + <field name="name">x.contact.form</field> + <field name="model">x_contact</field> + <field name="arch" type="xml"> + <form string="Contact"> + <sheet> + <group> + <group> + <field name="x_name" placeholder="Contact Name"/> + <field name="x_customer_type" placeholder="Customer Type"/> + </group> + <group> + <field name="x_city" placeholder="City"/> + <field name="x_area" placeholder="Area"/> + <field name="x_pin_code" placeholder="Pin Code"/> + </group> + </group> + </sheet> + </form> + </field> + </record> + + <!-- Contact list View --> + <record id="view_x_contact_list" model="ir.ui.view"> + <field name="name">x.contact.list</field> + <field name="model">x_contact</field> + <field name="arch" type="xml"> + <list string="Contacts"> + <field name="x_name"/> + <field name="x_customer_type"/> + <field name="x_city"/> + <field name="x_area"/> + <field name="x_pin_code"/> + </list> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/sale_person/views/sale_person_menu.xml b/sale_person/views/sale_person_menu.xml new file mode 100644 index 00000000000..f8744d13267 --- /dev/null +++ b/sale_person/views/sale_person_menu.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!-- Main Menu --> + <menuitem id="menu_sale_person_root" + name="Sale Person" + sequence="10"/> + + <!-- Sub Menus --> + <menuitem id="menu_sale_person_attendance" + name="Sale Person" + parent="menu_sale_person_root" + action="action_x_sale_person" + sequence="10"/> + + <menuitem id="menu_sale_person_contact" + name="Contact" + parent="menu_sale_person_root" + action="action_x_contact" + sequence="20"/> + + <menuitem id="menu_sale_person_tags" + name="Tags" + parent="menu_sale_person_root" + action="action_x_sale_person_tag" + sequence="30"/> +</odoo> \ No newline at end of file diff --git a/sale_person/views/sale_person_model_fields.xml b/sale_person/views/sale_person_model_fields.xml new file mode 100644 index 00000000000..fba72ac567c --- /dev/null +++ b/sale_person/views/sale_person_model_fields.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!--This file defines model, fields and other logic similar to model's .py file--> + + <!--Sale person model--> + <record id="model_x_sale_person" model="ir.model"> <!--Use syntax model_x_modelname--> + <field name="name">Sale Person</field> + <field name="model">x_sale_person</field> + <field name="state">manual</field> + </record> + + <!--Fields for Sale Person model--> + + <!-- Sales Person (related to res.users, auto-filled, readonly) --> + <record id="field_x_sale_person_user_id" model="ir.model.fields"> <!--Use syntax model_x_fieldname--> + <field name="name">x_user_id</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">many2one</field> + <field name="relation">res.users</field> + <field name="required">1</field> + <field name="on_delete">restrict</field> + </record> + + <!-- Check-in (datetime, auto-filled using automation) --> + <record id="field_x_sale_person_check_in" model="ir.model.fields"> + <field name="name">x_check_in</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">datetime</field> + <field name="field_description">Check-in Time</field> + </record> + + <!-- Check-out (datetime, set by button) --> + <record id="field_x_sale_person_check_out" model="ir.model.fields"> + <field name="name">x_check_out</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">datetime</field> + <field name="field_description">Check-out Time</field> + </record> + + <!-- Customer (many2one, required) --> + <record id="field_x_sale_person_customer_id" model="ir.model.fields"> + <field name="name">x_customer_id</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">many2one</field> + <field name="relation">x_contact</field> + <field name="required">1</field> + <field name="on_delete">restrict</field> + <field name="field_description">Customer</field> + </record> + + <!-- Related fields for Customer details t display in sale person form--> + <record id="field_x_sale_person_city" model="ir.model.fields"> + <field name="name">x_city</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">char</field> + <field name="field_description">City</field> + <field name="related">x_customer_id.x_city</field> + <field name="readonly">1</field> + </record> + + <record id="field_x_sale_person_area" model="ir.model.fields"> + <field name="name">x_area</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">char</field> + <field name="field_description">Area</field> + <field name="related">x_customer_id.x_area</field> + <field name="readonly">1</field> + </record> + + <record id="field_x_sale_person_pin_code" model="ir.model.fields"> + <field name="name">x_pin_code</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">char</field> + <field name="field_description">Pin Code</field> + <field name="related">x_customer_id.x_pin_code</field> + <field name="readonly">1</field> + </record> + + <!-- Agenda (char) --> + <record id="field_x_sale_person_agenda" model="ir.model.fields"> + <field name="name">x_agenda</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">char</field> + <field name="field_description">Agenda</field> + </record> + + <!-- Conversion Possibilities (selection) --> + <record id="field_x_sale_person_conversion_possibility" model="ir.model.fields"> + <field name="name">x_conversion_possibility</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">selection</field> + <field name="selection">[('high','High'),('moderate','Moderate'),('low','Low')]</field> + <field name="field_description">Conversion Possibility</field> + </record> + + <!-- Worked hours (float, computed) --> + <record id="field_x_sale_person_worked_hours" model="ir.model.fields"> + <field name="name">x_worked_hours</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">float</field> + <field name="field_description">Worked Hours</field> + </record> + + <!-- Tags Field (many2many) --> + <record id="field_x_sale_person_tag_ids" model="ir.model.fields"> + <field name="name">x_tag_ids</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">many2many</field> + <field name="relation">x_sale_person_tag</field> + <field name="field_description">Tags</field> + </record> + + <!-- Check-in Location Field (readonly, admin only) --> + <record id="field_x_sale_person_checkin_location" model="ir.model.fields"> + <field name="name">x_checkin_location</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">char</field> + <field name="field_description">Check-in Location</field> + </record> + + <!-- Check-out Location Field (readonly, admin only) --> + <record id="field_x_sale_person_checkout_location" model="ir.model.fields"> + <field name="name">x_checkout_location</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="ttype">char</field> + <field name="field_description">Check-out Location</field> + </record> + + <!-- Server Action for Check Out Button --> + <record id="action_checkout" model="ir.actions.server"> + <field name="name">Check Out</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="state">code</field> + <field name="code"> +for record in records: + if not record.x_check_out: + checkout_time = datetime.datetime.now() + vals = { + 'x_check_out': checkout_time, + 'x_checkout_location': 'Auto-detected location (Checkout)' + } + if record.x_check_in: + delta = checkout_time - record.x_check_in + vals['x_worked_hours'] = delta.total_seconds() / 3600.0 + record.write(vals) + </field> + </record> + + <!--Server action to get checkin time i.e current time--> + <record id="automate_check_in_time" model="ir.actions.server"> + <field name="name">Auto Set Check In Time</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="state">code</field> + <field name="code"> +record.write( + { + 'x_check_in': datetime.datetime.now(), + 'x_checkin_location': 'Auto-detected location (checkin)' + } +) + </field> + </record> + + <!-- Automation rule to set check-in time on change by triggering server action--> + <record id="automate_check_in_time_rule" model="base.automation"> + <field name="name">Set Check In Time on Create</field> + <field name="model_id" ref="model_x_sale_person"/> + <field name="trigger">on_change</field> + <field name="action_server_ids" eval="[(4, ref('automate_check_in_time'))]"/> + <field name="on_change_field_ids" eval="[(6,0,[ref('sale_person.field_x_sale_person_user_id')])]"/> + <field name="active">1</field> + </record> +</odoo> \ No newline at end of file diff --git a/sale_person/views/sale_person_views.xml b/sale_person/views/sale_person_views.xml new file mode 100644 index 00000000000..4c413d799c4 --- /dev/null +++ b/sale_person/views/sale_person_views.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!--This file acts as a view.xml that we define for models--> + + <!-- Action for sale person--> + <record id="action_x_sale_person" model="ir.actions.act_window"> + <field name="name">Sales Person Attendance</field> + <field name="res_model">x_sale_person</field> + <field name="view_mode">list,form</field> + <field name="context">{'default_x_user_id': uid}</field> + </record> + + <!-- Sale Person List View --> + <record id="view_x_sale_person_list" model="ir.ui.view"> + <field name="name">x.sale.person.list</field> + <field name="model">x_sale_person</field> + <field name="arch" type="xml"> + <list> + <field name="x_user_id" string="Sales person"/> + <field name="x_check_in"/> + <field name="x_check_out"/> + <field name="x_customer_id"/> + <field name="x_agenda"/> + <field name="x_conversion_possibility"/> + <field name="x_worked_hours"/> + </list> + </field> + </record> + + <!-- Sale Person Form View --> + <record id="view_x_sale_person_form" model="ir.ui.view"> + <field name="name">x.sale.person.form</field> + <field name="model">x_sale_person</field> + <field name="arch" type="xml"> + <form string="Sales Person Attendance"> + <header> + <!--calling server action using button--> + <button name="%(action_checkout)d" string="Check Out" type="action" class="btn-primary" invisible="x_check_out != False"/> + <field name="x_check_out" invisible="1"/> + </header> + <sheet> + <h1> + <field name="x_user_id" readonly="1"/> + </h1> + <group> + <group string="Time Information"> + <field name="x_check_in" readonly="1" force_save="1"/> + <field name="x_check_out" readonly="1" force_save="1"/> + <field name="x_worked_hours" widget="float_time" readonly="1" force_save="1"/> + </group> + <group string="Customer Information"> + <field name="x_customer_id"/> + <field name="x_city" readonly="1"/> + <field name="x_area" readonly="1"/> + <field name="x_pin_code" readonly="1"/> + </group> + </group> + <group> + <group string="Visit Details"> + <field name="x_agenda"/> + <field name="x_conversion_possibility"/> + </group> + <group string="Location Information"> + <!--Using base.group_system to define access for admins only--> + <field name="x_checkin_location" readonly="1" force_save="1" groups="base.group_system"/> + <field name="x_checkout_location" readonly="1" force_save="1" groups="base.group_system"/> + </group> + </group> + <group string="Tags"> + <field name="x_tag_ids" widget="many2many_tags" options="{'color_field': 'x_color'}"/> + </group> + </sheet> + </form> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/sale_person/views/tag_model_fields.xml b/sale_person/views/tag_model_fields.xml new file mode 100644 index 00000000000..0d3ff963d01 --- /dev/null +++ b/sale_person/views/tag_model_fields.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!--This file defines model, fields and other logic similar to model's .py file--> + + <!--sale person tags model--> + <record id="model_x_sale_person_tag" model="ir.model"> + <field name="name">x_Sale Person Tag</field> + <field name="model">x_sale_person_tag</field> + <field name="state">manual</field> + </record> + + <!--Fields for sale person tag model--> + + <!--Tag name--> + <record id="field_x_sale_person_tag_name" model="ir.model.fields"> + <field name="name">x_name</field> + <field name="model_id" ref="model_x_sale_person_tag"/> + <field name="ttype">char</field> + <field name="field_description">Tag Name</field> + <field name="required">1</field> + </record> + + <!--Tag color--> + <record id="field_x_sale_person_tag_color" model="ir.model.fields"> + <field name="name">x_color</field> + <field name="model_id" ref="model_x_sale_person_tag"/> + <field name="ttype">char</field> + <field name="field_description">Color</field> + </record> +</odoo> \ No newline at end of file diff --git a/sale_person/views/tag_views.xml b/sale_person/views/tag_views.xml new file mode 100644 index 00000000000..b9d575a56b7 --- /dev/null +++ b/sale_person/views/tag_views.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!--This file acts as a view.xml that we define for models--> + + <!--Action for tag view--> + <record id="action_x_sale_person_tag" model="ir.actions.act_window"> + <field name="name">Tags</field> + <field name="res_model">x_sale_person_tag</field> + <field name="view_mode">list,form</field> + </record> + + <!-- Tag Form View --> + <record id="view_x_sale_person_tag_form" model="ir.ui.view"> + <field name="name">x.sale.person.tag.form</field> + <field name="model">x_sale_person_tag</field> + <field name="arch" type="xml"> + <form string="Tag"> + <sheet> + <group> + <field name="x_name"/> + <field name="x_color" widget="color"/> + </group> + </sheet> + </form> + </field> + </record> + + <!-- Tag list View --> + <record id="view_x_sale_person_tag_list" model="ir.ui.view"> + <field name="name">x.sale.person.tag.list</field> + <field name="model">x_sale_person_tag</field> + <field name="arch" type="xml"> + <list string="Tags"> + <field name="x_name"/> + <field name="x_color" widget="color"/> + </list> + </field> + </record> +</odoo> \ No newline at end of file From b4b0943f0d0840eba083d4be2036457abb7b2a22 Mon Sep 17 00:00:00 2001 From: paay-odoo <paay@odoo.com> Date: Fri, 4 Jul 2025 18:49:10 +0530 Subject: [PATCH 13/13] [IMP] purchase: enhanced PO report with better structure and annexure - Made the purchase order report more detailed and structured - Added dedicated section for commercial terms & conditions with improved visibility - Modified fields and layout using XML only (no Python changes) - Included employee signature and role information - Added structured Annexure section for compliance and clarity --- purchase_order_print/__init__.py | 0 purchase_order_print/__manifest__.py | 19 ++ .../custom_purchase_report_template.xml | 259 ++++++++++++++++++ .../views/hr_employee_model_fields.xml | 11 + .../views/hr_employee_views.xml | 15 + .../views/product_model_fields.xml | 31 +++ .../views/product_template_views.xml | 16 ++ .../views/purchase_order_model_fields.xml | 20 ++ .../views/purchase_order_views.xml | 29 ++ 9 files changed, 400 insertions(+) create mode 100644 purchase_order_print/__init__.py create mode 100644 purchase_order_print/__manifest__.py create mode 100644 purchase_order_print/reports/custom_purchase_report_template.xml create mode 100644 purchase_order_print/views/hr_employee_model_fields.xml create mode 100644 purchase_order_print/views/hr_employee_views.xml create mode 100644 purchase_order_print/views/product_model_fields.xml create mode 100644 purchase_order_print/views/product_template_views.xml create mode 100644 purchase_order_print/views/purchase_order_model_fields.xml create mode 100644 purchase_order_print/views/purchase_order_views.xml diff --git a/purchase_order_print/__init__.py b/purchase_order_print/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/purchase_order_print/__manifest__.py b/purchase_order_print/__manifest__.py new file mode 100644 index 00000000000..00c4f037fbc --- /dev/null +++ b/purchase_order_print/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Purchase Order Print", + "version": "1.0", + "summary": "Custom module to print purchase orders with additional fields.", + "author": "Ayush Patel", + "depends": ["purchase", "base", "hr"], + "license": "LGPL-3", + "data": [ + "reports/custom_purchase_report_template.xml", + "views/hr_employee_model_fields.xml", + "views/hr_employee_views.xml", + "views/product_model_fields.xml", + "views/product_template_views.xml", + "views/purchase_order_model_fields.xml", + "views/purchase_order_views.xml", + ], + "installable": True, + "application": True, +} \ No newline at end of file diff --git a/purchase_order_print/reports/custom_purchase_report_template.xml b/purchase_order_print/reports/custom_purchase_report_template.xml new file mode 100644 index 00000000000..3ee5e14f1e1 --- /dev/null +++ b/purchase_order_print/reports/custom_purchase_report_template.xml @@ -0,0 +1,259 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!-- Override the entire purchase order document template --> + <template id="report_purchaseorder_document_custom" inherit_id="purchase.report_purchaseorder_document"> + <xpath expr="//t[@t-call='web.external_layout']" position="replace"> + <t t-call="web.basic_layout"> + <t t-set="o" t-value="o.with_context(lang=o.partner_id.lang)"/> + + <div class="page" style="font-size: 12px; page-break-inside: avoid;"> + <h2 class="text-center mt-3">PURCHASE ORDER</h2> + <div class="d-flex justify-content-end" style="margin-top:-36px;"> + <strong>F-Admin-002</strong> + </div> + + <table class="table table-sm table-bordered" style="width: 100%; margin-top: 20px;"> + <tr> + <td> + <div class="d-flex align-items-center justify-content-center"> + <img t-if="o.company_id.logo" t-att-src="image_data_uri(o.company_id.logo)" style="height: 70px;" /> + </div> + </td> + <td> + <div class="d-flex flex-column justify-content-center p-2"> + <span><strong>Purchase Order No.:</strong> <t t-esc="o.name"/></span> + <span class="mt-3"><strong>Date:</strong> <span t-field="o.date_order" t-options="{'date_only': 'true'}"/></span> + </div> + </td> + </tr> + <tr> + <td class="p-3"> + <strong>Billing/Shipping Address:</strong><br/> + <t t-esc="o.company_id.name"/><br/> + <t t-esc="o.company_id.street"/><br/> + <t t-esc="o.company_id.city"/>, + <t t-esc="o.company_id.state_id.name"/>, + <t t-esc="o.company_id.zip"/><br/> + <span class="d-block mt-2"><strong>Phone:</strong> <t t-esc="o.company_id.phone"/></span> + </td> + <td> + <div class="d-flex flex-column justify-content-center p-2"> + <span class="d-block"><strong>GSTIN No :</strong> <t t-esc="o.company_id.vat or '--'"/></span> + <span class="d-block mt-2"><strong>CIN :</strong> <t t-esc="o.company_id.company_registry or '--'"/></span> + <span class="d-block mt-2"><strong>PAN No. :</strong> <t t-esc="'--'"/></span> + </div> + </td> + </tr> + <tr> + <td colspan="2"> + <div class="d-flex p-3 gap-3 align-items-center"> + <strong>Name of Vendor:</strong> + <h5 class="mb-0"><t t-esc="o.partner_id.name"/></h5> + </div> + </td> + </tr> + <tr> + <td> + <div class="d-flex flex-column justify-content-center p-2"> + <span><strong>Vendor address :</strong><br/><t t-esc="o.partner_id.contact_address"/></span> + <span class="d-block mt-2"><strong>GSTIN No :</strong> <t t-esc="o.partner_id.vat or '--'"/></span> + <span class="d-block mt-2"><strong>PAN No. :</strong> <t t-esc="'--'"/></span> + </div> + </td> + <td> + <div class="d-flex flex-column justify-content-center p-2"> + <span class="d-block"><strong>Quotation Ref No. :</strong> <t t-esc="o.partner_ref or '--'"/></span> + <span class="d-block mt-2"><strong>Contact Person :</strong> <t t-esc="o.partner_id.child_ids and o.partner_id.child_ids[0].name"/></span> + <span class="d-block mt-2"><strong>Contact No. :</strong> <t t-esc="o.partner_id.phone"/></span> + <span class="d-block mt-2"><strong>Email Id :</strong> <t t-esc="o.partner_id.email"/></span> + </div> + </td> + </tr> + </table> + + <!-- Product Table --> + <table class="table table-sm table-bordered mt-4"> + <thead> + <tr> + <th class="text-center">Sr. No.</th> + <th>MPN & Description</th> + <th class="text-center">Quantity</th> + <th class="text-center">UOM</th> + <th class="text-end">Rate/Unit</th> + <th class="text-end">Amount in INR</th> + </tr> + </thead> + <tbody> + <t t-set="line_num" t-value="1"/> + <t t-set="basic_total" t-value="0"/> + <t t-foreach="o.order_line.filtered(lambda line: not line.display_type)" t-as="line"> + <t t-set="basic_total" t-value="basic_total + line.price_subtotal"/> + <tr> + <td class="text-center"><span t-esc="line_num"/><t t-set="line_num" t-value="line_num + 1"/></td> + <td> + <span t-field="line.product_id.default_code"/> - <span t-field="line.name"/> + </td> + <td class="text-center"><span t-field="line.product_qty"/></td> + <td class="text-center"><span t-field="line.product_uom.name"/></td> + <td class="text-end"><span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/></td> + <td class="text-end"><span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/></td> + </tr> + </t> + <!--next table row will display total and taxes details--> + <tr class="fw-bold"> + <td colspan="5" class="text-end">Basic Total</td> + <td class="text-end"> + <span t-esc="basic_total" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> + </td> + </tr> + <tr> + <td colspan="5" class="text-end">IGST</td> + <td class="text-end"> + <span t-esc="o.amount_tax or 0.0" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> + </td> + </tr> + <!-- Packing (static) --> + <tr> + <td colspan="5" class="text-end">Packing</td> + <td class="text-end"> + <span>--</span> + </td> + </tr> + <!-- Freight (static) --> + <tr> + <td colspan="5" class="text-end">Freight</td> + <td class="text-end"> + <span>--</span> + </td> + </tr> + <tr> + <td colspan="5" class="text-center align-middle" style="bold;"> + <strong>Total PO Value in INR</strong> + <br/> + <span style="display: inline-block; text-align: center; width: 100%;"> + <strong><span t-esc="o.currency_id.amount_to_text(o.amount_total)"/></strong> + </span> + </td> + <td class="text-end"> + <span t-field="o.amount_total" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> + </td> + </tr> + </tbody> + </table> + + <!-- Terms and Conditions --> + <div class="mt-4 no-page-break"> + <strong>Commercial terms & Conditions:</strong> + <br/> + <strong>Payment Terms:</strong> <span t-field="o.payment_term_id.name"/><br/> + <strong>Delivery Schedules:</strong> Expected arrival date <span t-field="o.date_planned" t-options='{"widget": "date"}'/><br/> + <strong>Other terms & conditions:</strong> + + <div class="text-left"> <strong>General Terms</strong></div> + <p t-field="o.notes"/> + + <div class="text-left mt-4"> + <img t-if="o.x_employee_id.x_signature_seal_image" + t-att-src="image_data_uri(o.x_employee_id.x_signature_seal_image)" + style="height: 50px;"/> + </div> + <div class="text-left mt-2"> + <strong><t t-esc="o.x_employee_id.name"/></strong><br/> + <strong> <t t-esc="o.x_employee_id.job_id.name"/></strong> + </div> + </div> + </div> + + + + <div class="page" style="font-size: 12px; page-break-before: always;"> + <h4 class="text-center"><u>Annexure I</u></h4> + + <!-- Company Information --> + <div class="row mt-4"> + <div class="col-12"> + <strong>Authorised Distributor / OEM Name and Address:</strong> + <div style="border-bottom: 0px dotted #000; height: 20px; margin-bottom: 10px;"></div> + <div style="border-bottom: 0px dotted #000; height: 20px; margin-bottom: 10px;"></div> + </div> + </div> + + <!-- Customer Information --> + <div class="row mt-4"> + <div class="col-12"> + <strong>Customer's Name and Address:</strong> + <div style="border-bottom: 0px dotted #000; height: 20px; margin-bottom: 10px;"></div> + <div style="border-bottom: 0px dotted #000; height: 20px; margin-bottom: 10px;"></div> + </div> + </div> + + <!-- Certificate of Conformance --> + <div class="mt-4"> + <div class="text-right"> + <strong>Date: ................................</strong> + </div> + <div class="text-center mt-2"> + <strong><u>CERTIFICATE OF CONFORMANCE</u></strong> + </div> + <div class="mt-4"> + <div style="border-bottom: 1px dotted #000; height: 20px; margin-bottom: 10px;"></div> + <div style="border-bottom: 1px dotted #000; height: 20px; margin-bottom: 10px;"></div> + <div style="border-bottom: 1px dotted #000; height: 20px; margin-bottom: 10px;"></div> + <div style="border-bottom: 1px dotted #000; height: 20px; margin-bottom: 10px;"></div> + <div style="border-bottom: 1px dotted #000; height: 20px; margin-bottom: 10px;"></div> + <div style="border-bottom: 1px dotted #000; height: 20px; margin-bottom: 10px;"></div> + </div> + <div class="row mt-4"> + <div class="col-6"> + <strong>PO No.</strong> .................................... <strong>Date:</strong> ................................. + </div> + </div> + <div class="row mt-2"> + <div class="col-6"> + <strong>INVOICE NO.</strong> ................................ <strong>Date:</strong> ................................ + </div> + </div> + <table class="table table-sm table-bordered mt-4"> + <thead> + <tr> + <th>Sr. No.</th> + <th>Make</th> + <th>MPN (Part Number)</th> + <th>Device type / Components / Description</th> + <th>Quantity</th> + <th>Lot No./Batch / Code *</th> + <th>Date Code *</th> + </tr> + </thead> + <tbody> + <t t-set="line_num" t-value="1"/> + <t t-foreach="o.order_line.filtered(lambda line: not line.display_type)" t-as="line"> + </t> + <tr t-foreach="range(5)" t-as="empty_row"> + <td style="height: 30px;"></td> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + </tr> + </tbody> + </table> + <!-- Add Annexure Terms here --> + <div class="mt-4"> + <p t-field="o.x_annexure_terms"/> + </div> + </div> + <div> + <div class="row"> + <div class="text-end"> + <strong>Authorised Signature (with date) and seal</strong> + </div> + </div> + </div> + </div> + </t> + </xpath> + </template> +</odoo> \ No newline at end of file diff --git a/purchase_order_print/views/hr_employee_model_fields.xml b/purchase_order_print/views/hr_employee_model_fields.xml new file mode 100644 index 00000000000..7156b717b24 --- /dev/null +++ b/purchase_order_print/views/hr_employee_model_fields.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <record id="field_hr_employee_signature_seal" model="ir.model.fields"> + <field name="name">x_signature_seal_image</field> + <field name="model">hr.employee</field> + <field name="model_id" ref="hr.model_hr_employee"/> + <field name="field_description">Signature/Seal Image</field> + <field name="ttype">binary</field> + <field name="store">True</field> + </record> +</odoo> diff --git a/purchase_order_print/views/hr_employee_views.xml b/purchase_order_print/views/hr_employee_views.xml new file mode 100644 index 00000000000..35c19beb9a0 --- /dev/null +++ b/purchase_order_print/views/hr_employee_views.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <record id="view_hr_employee_form_inherit_signature_seal" model="ir.ui.view"> + <field name="name">hr.employee.form.inherit.signature.seal</field> + <field name="model">hr.employee</field> + <field name="inherit_id" ref="hr.view_employee_form"/> + <field name="arch" type="xml"> + <xpath expr="//sheet/group" position="inside"> + <group string="Signature/Seal"> + <field name="x_signature_seal_image" widget="image" class="oe_avatar"/> + </group> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/purchase_order_print/views/product_model_fields.xml b/purchase_order_print/views/product_model_fields.xml new file mode 100644 index 00000000000..25c4c461e1a --- /dev/null +++ b/purchase_order_print/views/product_model_fields.xml @@ -0,0 +1,31 @@ +<odoo> + <data noupdate="0"> + <!-- Part No. --> + <record id="field_product_template_x_part_no" model="ir.model.fields"> + <field name="name">x_part_no</field> + <field name="model">product.template</field> + <field name="model_id" ref="product.model_product_template"/> + <field name="ttype">char</field> + <field name="field_description">Part No.</field> + <field name="state">manual</field> + </record> + <!-- Packing --> + <record id="field_product_template_x_packing" model="ir.model.fields"> + <field name="name">x_packing</field> + <field name="model">product.template</field> + <field name="model_id" ref="product.model_product_template"/> + <field name="ttype">float</field> + <field name="field_description">Packing</field> + <field name="state">manual</field> + </record> + <!-- Freight --> + <record id="field_product_template_x_freight" model="ir.model.fields"> + <field name="name">x_freight</field> + <field name="model">product.template</field> + <field name="model_id" ref="product.model_product_template"/> + <field name="ttype">float</field> + <field name="field_description">Freight</field> + <field name="state">manual</field> + </record> + </data> +</odoo> \ No newline at end of file diff --git a/purchase_order_print/views/product_template_views.xml b/purchase_order_print/views/product_template_views.xml new file mode 100644 index 00000000000..85f43cdba92 --- /dev/null +++ b/purchase_order_print/views/product_template_views.xml @@ -0,0 +1,16 @@ +<odoo> + <record id="view_product_template_form_inherit_partno_packing_freight" model="ir.ui.view"> + <field name="name">product.template.form.inherit.partno.packing.freight</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="product.product_template_only_form_view"/> + <field name="arch" type="xml"> + <xpath expr="//page[@name='purchase']//group[.//field[@name='description_purchase']]" position="after"> + <group string="Purchase Extra Info"> + <field name="x_part_no" string="Part No"/> + <field name="x_packing" string="Packing charges"/> + <field name="x_freight" string="Frieght charges"/> + </group> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/purchase_order_print/views/purchase_order_model_fields.xml b/purchase_order_print/views/purchase_order_model_fields.xml new file mode 100644 index 00000000000..a13915bfbaa --- /dev/null +++ b/purchase_order_print/views/purchase_order_model_fields.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="0"> + <record id="field_purchase_order_employee_id" model="ir.model.fields"> + <field name="name">x_employee_id</field> + <field name="model_id" ref="purchase.model_purchase_order"/> + <field name="ttype">many2one</field> + <field name="relation">hr.employee</field> + <field name="field_description">Authorized By</field> + <field name="state">manual</field> + </record> + <record id="field_purchase_order_annexure_terms" model="ir.model.fields"> + <field name="name">x_annexure_terms</field> + <field name="model_id" ref="purchase.model_purchase_order"/> + <field name="ttype">html</field> + <field name="field_description">Annexure terms</field> + <field name="state">manual</field> + </record> + </data> +</odoo> \ No newline at end of file diff --git a/purchase_order_print/views/purchase_order_views.xml b/purchase_order_print/views/purchase_order_views.xml new file mode 100644 index 00000000000..aa14a62acd9 --- /dev/null +++ b/purchase_order_print/views/purchase_order_views.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <record id="view_order_form_custom" model="ir.ui.view"> + <field name="name">purchase.order.form.inherit.employee</field> + <field name="model">purchase.order</field> + <field name="inherit_id" ref="purchase.purchase_order_form"/> + <field name="arch" type="xml"> + <xpath expr="//div[@name='date_planned_div']" position="after"> + <field name="x_employee_id"/> + </xpath> + + <xpath expr="//page[@name='products']//field[@name='notes']" position="replace"/> + + <xpath expr="//page[@name='purchase_delivery_invoice']/group/group[@name='other_info']" position="inside"> + <field colspan="2" name="notes" nolabel="1" placeholder="Define your terms and conditions ..."/> + </xpath> + + <xpath expr="//page[@name='purchase_delivery_invoice']" position="after"> + <page name="purchase_annexure_terms" string="Annexure Terms"> + <group> + <field colspan="2" name="x_annexure_terms" nolabel="1" placeholder="Define your annexure terms ..."/> + </group> + </page> + </xpath> + </field> + </record> + </data> +</odoo> \ No newline at end of file