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