| @ -0,0 +1,47 @@ | |||
| .. image:: https://img.shields.io/badge/license-LGPL--3-green.svg | |||
|     :target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html | |||
|     :alt: License: LGPL-3 | |||
| 
 | |||
| Fountain Widget | |||
| =============== | |||
| * This module introduce a new widget that is used to select the many2one field in fountain manner. | |||
| 
 | |||
| Configuration | |||
| ============= | |||
| No additional configurations needed. | |||
| 
 | |||
| License | |||
| ------- | |||
| Lesser General Public License, Version 3 (LGPL v3). | |||
| 
 | |||
| (https://www.gnu.org/licenses/lgpl-3.0-standalone.html) | |||
| 
 | |||
| Company | |||
| ------- | |||
| * `Cybrosys Techno Solutions <https://cybrosys.com/>`__ | |||
| 
 | |||
| Credits | |||
| ------- | |||
| Developer: (V18) Manasa T P ,Contact : odoo@cybrosys.com | |||
| 
 | |||
| Contacts | |||
| -------- | |||
| * Mail Contact : odoo@cybrosys.com | |||
| * Website : https://cybrosys.com | |||
| 
 | |||
| Bug Tracker | |||
| ----------- | |||
| Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. | |||
| 
 | |||
| Maintainer | |||
| ========== | |||
| .. image:: https://cybrosys.com/images/logo.png | |||
|    :target: https://cybrosys.com | |||
| 
 | |||
| This module is maintained by Cybrosys Technologies. | |||
| 
 | |||
| For support and more information, please visit https://www.cybrosys.com | |||
| 
 | |||
| Further information | |||
| =================== | |||
| HTML Description: `<static/description/index.html>`__ | |||
| @ -0,0 +1,21 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ############################################################################### | |||
| # | |||
| #  Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #  Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) | |||
| #  Author: Manasa T P (odoo@cybrosys.com) | |||
| # | |||
| #  You can modify it under the terms of the GNU LESSER | |||
| #  GENERAL PUBLIC LICENSE (LGPL v3), Version 3. | |||
| # | |||
| #  This program is distributed in the hope that it will be useful, | |||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| #  GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. | |||
| # | |||
| #  You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE | |||
| #  (LGPL v3) along with this program. | |||
| #  If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ############################################################################### | |||
| @ -0,0 +1,53 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ############################################################################### | |||
| # | |||
| #  Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #  Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) | |||
| #  Author: Manasa T P (odoo@cybrosys.com) | |||
| # | |||
| #  You can modify it under the terms of the GNU LESSER | |||
| #  GENERAL PUBLIC LICENSE (LGPL v3), Version 3. | |||
| # | |||
| #  This program is distributed in the hope that it will be useful, | |||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| #  GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. | |||
| # | |||
| #  You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE | |||
| #  (LGPL v3) along with this program. | |||
| #  If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ############################################################################### | |||
| { | |||
|     "name": "Fountain Widget", | |||
|     "version": "18.0.1.0.0", | |||
|     "category": 'Extra Tools', | |||
|     "summary": "This widget is used to select the many2one field in fountain " | |||
|                "manner.", | |||
|     "description": "This is a widget for many2one fields. By using this " | |||
|                    "widget we can select record in a fountain manner. It " | |||
|                    "generates dynamic dropdown menus with hierarchical options " | |||
|                    "and thus Supports parent-child relationships between " | |||
|                    "dropdown options.", | |||
|     "author": "Cybrosys Techno Solutions", | |||
|     'company': 'Cybrosys Techno Solutions', | |||
|     'maintainer': 'Cybrosys Techno Solutions', | |||
|     'website': 'https://www.cybrosys.com', | |||
|     "depends": ['base'], | |||
|     'assets': { | |||
|         'web.assets_backend': [ | |||
|             'fountain_widget_many2one/static/src/js/fountain_widget_many2one.js', | |||
|             'fountain_widget_many2one/static/src/xml/fountain_widget_many2one.xml', | |||
|             'fountain_widget_many2one/static/src/css/fountain_widget_many2one.css', | |||
|             'fountain_widget_many2one/static/src/css/fountain_widget_many2one.scss', | |||
|             'fountain_widget_many2one/static/src/xml/fountain_widget_component.xml', | |||
|             'fountain_widget_many2one/static/src/js/fountain_widget_component.js', | |||
|         ], | |||
|     }, | |||
|     'images': ['static/description/banner.jpg'], | |||
|     'license': 'LGPL-3', | |||
|     'installable': True, | |||
|     'auto_install': False, | |||
|     'application': False, | |||
| } | |||
| @ -0,0 +1,6 @@ | |||
| ## Module <fountain_widget_many2one> | |||
| #### 17.04.2025 | |||
| #### Version 18.0.1.0.0 | |||
| ##### ADD | |||
| 
 | |||
| - Initial Commit for Fountain Widget | |||
| After Width: | Height: | Size: 2.2 KiB | 
| After Width: | Height: | Size: 28 KiB | 
| After Width: | Height: | Size: 628 KiB | 
| After Width: | Height: | Size: 1.1 KiB | 
| After Width: | Height: | Size: 210 KiB | 
| After Width: | Height: | Size: 209 KiB | 
| After Width: | Height: | Size: 109 KiB | 
| After Width: | Height: | Size: 495 B | 
| After Width: | Height: | Size: 1.0 KiB | 
| After Width: | Height: | Size: 624 B | 
| After Width: | Height: | Size: 136 KiB | 
| After Width: | Height: | Size: 214 KiB | 
| After Width: | Height: | Size: 36 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 310 B | 
| After Width: | Height: | Size: 929 B | 
| After Width: | Height: | Size: 1.3 KiB | 
| After Width: | Height: | Size: 3.3 KiB | 
| After Width: | Height: | Size: 1.4 KiB | 
| After Width: | Height: | Size: 17 KiB | 
| After Width: | Height: | Size: 542 B | 
| After Width: | Height: | Size: 576 B | 
| After Width: | Height: | Size: 733 B | 
| After Width: | Height: | Size: 4.3 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 4.0 KiB | 
| After Width: | Height: | Size: 1.7 KiB | 
| After Width: | Height: | Size: 738 KiB | 
| After Width: | Height: | Size: 2.2 KiB | 
| After Width: | Height: | Size: 911 B | 
| After Width: | Height: | Size: 1.1 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 600 B | 
| After Width: | Height: | Size: 673 B | 
| After Width: | Height: | Size: 2.0 KiB | 
| After Width: | Height: | Size: 462 B | 
| After Width: | Height: | Size: 2.1 KiB | 
| After Width: | Height: | Size: 926 B | 
| After Width: | Height: | Size: 9.0 KiB | 
| After Width: | Height: | Size: 23 KiB | 
| After Width: | Height: | Size: 7.0 KiB | 
| After Width: | Height: | Size: 878 B | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 653 B | 
| After Width: | Height: | Size: 800 B | 
| After Width: | Height: | Size: 905 B | 
| After Width: | Height: | Size: 189 KiB | 
| After Width: | Height: | Size: 4.3 KiB | 
| After Width: | Height: | Size: 839 B | 
| After Width: | Height: | Size: 1.7 KiB | 
| After Width: | Height: | Size: 5.9 KiB | 
| After Width: | Height: | Size: 1.6 KiB | 
| After Width: | Height: | Size: 34 KiB | 
| After Width: | Height: | Size: 26 KiB | 
| After Width: | Height: | Size: 3.8 KiB | 
| After Width: | Height: | Size: 23 KiB | 
| After Width: | Height: | Size: 1.9 KiB | 
| After Width: | Height: | Size: 2.3 KiB | 
| After Width: | Height: | Size: 427 B | 
| After Width: | Height: | Size: 627 B | 
| After Width: | Height: | Size: 1.1 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 988 B | 
| After Width: | Height: | Size: 3.7 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 875 B | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 767 KiB | 
| After Width: | Height: | Size: 116 KiB | 
| After Width: | Height: | Size: 100 KiB | 
| After Width: | Height: | Size: 89 KiB | 
| After Width: | Height: | Size: 126 KiB | 
| After Width: | Height: | Size: 76 KiB | 
| After Width: | Height: | Size: 95 KiB | 
| After Width: | Height: | Size: 154 KiB | 
| After Width: | Height: | Size: 81 KiB | 
| After Width: | Height: | Size: 78 KiB | 
| After Width: | Height: | Size: 84 KiB | 
| After Width: | Height: | Size: 86 KiB | 
| After Width: | Height: | Size: 80 KiB | 
| After Width: | Height: | Size: 388 KiB | 
| After Width: | Height: | Size: 880 KiB | 
| After Width: | Height: | Size: 702 KiB | 
| After Width: | Height: | Size: 40 KiB | 
| @ -0,0 +1,11 @@ | |||
| .divMargin{ | |||
| margin-right: 11px; | |||
| border-right: 1px solid black; | |||
| padding-right: 10px; | |||
| } | |||
| .dropdown-item-text { | |||
|     display: block; | |||
|     padding: 3px 20px; | |||
|     color: #495057; | |||
|     position: relative; | |||
| } | |||
| @ -0,0 +1,8 @@ | |||
| .icon_style{ | |||
|     &::after{ | |||
|     content: "\f061"; | |||
|     font-family: 'FontAwesome'; | |||
|     position: absolute; | |||
|     right: 0px; | |||
| } | |||
| } | |||
| @ -0,0 +1,46 @@ | |||
| /** @odoo-module **/ | |||
| import { Dropdown } from "@web/core/dropdown/dropdown"; | |||
| import { onMounted, onWillUnmount, useRef } from "@odoo/owl"; | |||
| 
 | |||
| // Extending the Dropdown and adding the inputData into the props
 | |||
| export class FountainDropdown extends Dropdown { | |||
|     setup() { | |||
|         super.setup(); | |||
|         this.state.open = false; // Default state is closed
 | |||
| 
 | |||
|         // Use OWL's useRef to reference the root element
 | |||
|         this.rootRef = useRef("root"); | |||
| 
 | |||
|         // Add click outside detection
 | |||
|         this.handleClickOutside = (event) => { | |||
|             if ( | |||
|                 this.rootRef.el && | |||
|                 !this.rootRef.el.contains(event.target) && | |||
|                 this.state.open | |||
|             ) { | |||
|                 this.state.open = false; // Close dropdown if clicked outside
 | |||
|             } | |||
|         }; | |||
| 
 | |||
|         onMounted(() => { | |||
|             // Add event listener when component is mounted
 | |||
|             document.addEventListener('click', this.handleClickOutside); | |||
|         }); | |||
| 
 | |||
|         onWillUnmount(() => { | |||
|             // Remove event listener when component is unmounted
 | |||
|             document.removeEventListener('click', this.handleClickOutside); | |||
|         }); | |||
|     } | |||
| 
 | |||
|     onClick(event) { | |||
|         event.preventDefault(); // Prevent default behavior
 | |||
|         this.state.open = !this.state.open; // Toggle dropdown visibility
 | |||
|     } | |||
| } | |||
| 
 | |||
| FountainDropdown.template = "fountain.Dropdown"; | |||
| FountainDropdown.props = { | |||
|     ...Dropdown.props, | |||
|     inputData: { type: Object, optional: true } | |||
| }; | |||
| @ -0,0 +1,87 @@ | |||
| /** @odoo-module **/ | |||
| import { Component, useEffect, useState, onWillStart } from "@odoo/owl"; | |||
| import { registry } from "@web/core/registry"; | |||
| import { standardFieldProps } from "@web/views/fields/standard_field_props"; | |||
| import { FountainDropdown } from "./fountain_widget_component"; | |||
| import { useService } from "@web/core/utils/hooks"; | |||
| 
 | |||
| export class FountainWidget extends Component { | |||
|     static template = "FountainWidgetField"; | |||
|     static components = { | |||
|         FountainDropdown, | |||
|     }; | |||
|     setup() { | |||
|         this.orm = useService('orm'); | |||
|         useEffect(() => { this.state.inputData = this.props.record.data[this.props.name] }, () => [this.props]); | |||
|         this.state = useState({ | |||
|             childMenu: {}, | |||
|             inputData: this.props.record.data[this.props.name], | |||
|             open: false // Control dropdown visibility
 | |||
|         }); | |||
|         onWillStart(async () => { | |||
|             this.state.category = await this.orm.searchRead(this.props.record.fields.categ_id.relation, []); | |||
|         }); | |||
|         this.index = 0; // Track the current depth of the dropdown
 | |||
| 
 | |||
|         // Bind the onClickDropDown method to ensure `this` refers to the component instance
 | |||
|         this.onClickDropDown = this.onClickDropDown.bind(this); | |||
|     } | |||
| 
 | |||
|     get inputData() { | |||
|         return { | |||
|             input: this.state.inputData ? this.state.inputData[1] : '' | |||
|         }; | |||
|     } | |||
| 
 | |||
|     get parentMenu() { | |||
|         return this.state.category.filter(item => !item.parent_id); | |||
|     } | |||
| 
 | |||
|     get childKeys() { | |||
|         return Object.keys(this.state.childMenu); | |||
|     } | |||
| 
 | |||
|     onClickDropDown(parent_id, index = 0) { | |||
|         // Ensure index is a number, default to 0 if undefined
 | |||
|         index = Number(index) || 0; | |||
| 
 | |||
|         if (this.index >= index) { | |||
|             let childLength = this.childKeys.length; | |||
|             while (index < childLength) { | |||
|                 const keyIndex = this.childKeys[index]; | |||
|                 delete this.state.childMenu[keyIndex]; | |||
|                 childLength--; | |||
|             } | |||
|         } | |||
|         let selectedCategory = this.state.category.find((item) => item.id === parent_id); | |||
|         let obj = this.state.category.filter(item => item.parent_id && item.parent_id[0] === parent_id); | |||
| 
 | |||
|         if (!this.state.childMenu.hasOwnProperty(parent_id) && obj.length > 0) { | |||
|             this.state.childMenu[parent_id] = obj; | |||
|             this.index = index + 1; // Increment index for nested levels
 | |||
|         } | |||
| 
 | |||
|         if (this.props.record && this.props.record.update) { | |||
|             this.state.inputData = [parent_id, selectedCategory.complete_name]; | |||
|             this.props.record.update({ [this.props.name]: [parent_id, selectedCategory.complete_name] }); | |||
|         } else { | |||
|             console.error('Update function not found in record props:', this.props); | |||
|         } | |||
|         this.state.open = false; // Close dropdown after selection
 | |||
|         return obj; | |||
|     } | |||
| 
 | |||
|     static props = { | |||
|         ...standardFieldProps, | |||
|         inputData: { type: Object, optional: true, default: {} }, | |||
|     }; | |||
| } | |||
| 
 | |||
| // Define the widget field with the correct structure
 | |||
| export const FountainWidgetField = { | |||
|     component: FountainWidget, | |||
|     supportedTypes: ["many2one"], | |||
| }; | |||
| 
 | |||
| // Register the widget in the field registry
 | |||
| registry.category("fields").add("fountain_widget", FountainWidgetField); | |||
| @ -0,0 +1,37 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <templates id="template" xml:space="preserve"> | |||
|     <!-- Changing the button into an input --> | |||
|     <t t-name="fountain.Dropdown" owl="1"> | |||
|         <div class="o-dropdown dropdown" | |||
|              t-att-class="props.class" | |||
|              t-attf-class=" | |||
|              {{ directionCaretClass || ''}} | |||
|              {{ state.open ? 'show' : ''}} | |||
|              {{ !showCaret ? 'o-dropdown--no-caret' : '' }}" | |||
|              t-ref="root"> | |||
|             <input style="width:100%;" | |||
|                    class="dropdown-toggle" | |||
|                    t-att-class="{ | |||
|                    [props.togglerClass]: props.togglerClass, | |||
|                    'dropdown-item': parentDropdown }" | |||
|                    t-on-click.stop="onClick" | |||
|                    t-att-title="props.title" | |||
|                    t-att-value="props.inputData.input || ''" | |||
|                    t-att-data-tooltip="props.tooltip" | |||
|                    t-att-data-hotkey="props.hotkey" | |||
|                    t-att-aria-expanded="state.open ? 'true' : 'false'" | |||
|                    t-att-tabindex="props.skipTogglerTabbing ? -1 : 0" | |||
|                    t-ref="togglerRef"> | |||
|                 <t t-slot="default"/> | |||
|             </input> | |||
|             <div t-if="state.open" | |||
|                  class="o-dropdown--menu dropdown-menu d-block" | |||
|                  t-att-class="props.menuClass" | |||
|                  role="menu" | |||
|                  t-ref="menuRef" | |||
|                  style="min-width: unset;"> | |||
|                 <t t-slot="default"/> | |||
|             </div> | |||
|         </div> | |||
|     </t> | |||
| </templates> | |||
| @ -0,0 +1,37 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <templates> | |||
|     <t t-name="FountainWidgetField" owl="1"> | |||
|         <!-- Template containing the dropdown component --> | |||
|         <FountainDropdown inputData="inputData"> | |||
|             <div style="display: flex;"> | |||
|                 <div class="divMargin"> | |||
|                     <t t-foreach="parentMenu" t-as="category" t-key="category.id"> | |||
|                         <t t-if="category.child_id.length > 0"> | |||
|                             <t t-set="parent_class" t-value="'icon_style'"/> | |||
|                         </t> | |||
|                         <option class="dropdown-item-text" | |||
|                                 t-on-click.stop.prevent="() => onClickDropDown(category.id, 0)" | |||
|                                 t-attf-class="#{parent_class}"> | |||
|                             <t t-esc="category.name"/> | |||
|                         </option> | |||
|                         <li class="dropdown-divider" role="separator"/> | |||
|                     </t> | |||
|                 </div> | |||
|                 <div t-foreach="childKeys" t-as="cKeys" t-key="cKeys"> | |||
|                     <t t-foreach="state.childMenu[cKeys]" t-as="child" t-key="child.id"> | |||
|                         <t t-if="child.child_id.length > 0"> | |||
|                             <t t-set="child_class" t-value="'icon_style'"/> | |||
|                         </t> | |||
|                         <option class="dropdown-item-text" | |||
|                                 t-on-click.stop.prevent="() => onClickDropDown(child.id, cKeys_index + 1)" | |||
|                                 t-attf-class="#{child_class}"> | |||
|                             <t t-esc="child.name"/> | |||
|                         </option> | |||
|                         <t t-set="child_class" t-value=""/> | |||
|                         <li class="dropdown-divider" role="separator"/> | |||
|                     </t> | |||
|                 </div> | |||
|             </div> | |||
|         </FountainDropdown> | |||
|     </t> | |||
| </templates> | |||