diff --git a/addons_extensions/menu_control_center/__init__.py b/addons_extensions/menu_control_center/__init__.py index 9a7e03ede..a66486a18 100644 --- a/addons_extensions/menu_control_center/__init__.py +++ b/addons_extensions/menu_control_center/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import controllers, models \ No newline at end of file diff --git a/addons_extensions/menu_control_center/__manifest__.py b/addons_extensions/menu_control_center/__manifest__.py index 385729848..25a53fdeb 100644 --- a/addons_extensions/menu_control_center/__manifest__.py +++ b/addons_extensions/menu_control_center/__manifest__.py @@ -10,10 +10,12 @@ 'category': 'Tools', 'author': 'PRANAY', 'website': 'https://ftprotech.in', - 'depends': ['base','hr'], + 'depends': ['base','hr','web','one2many_search_widget'], 'data': [ 'security/ir.model.access.csv', 'data/data.xml', + 'views/masters.xml', + 'views/login.xml', 'views/menu_access_control_views.xml', ], # 'assets': { diff --git a/addons_extensions/menu_control_center/controllers/__init__.py b/addons_extensions/menu_control_center/controllers/__init__.py new file mode 100644 index 000000000..deec4a8b8 --- /dev/null +++ b/addons_extensions/menu_control_center/controllers/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/addons_extensions/menu_control_center/controllers/main.py b/addons_extensions/menu_control_center/controllers/main.py new file mode 100644 index 000000000..cb59c6a73 --- /dev/null +++ b/addons_extensions/menu_control_center/controllers/main.py @@ -0,0 +1,26 @@ +from odoo import http, _ +from odoo.http import request +from odoo.addons.web.controllers.home import Home +import werkzeug + + +class CustomMasterLogin(Home): + + @http.route() + def web_login(self, *args, **kw): + # Call the original Odoo login + master_selected = kw.get('master_select') + + response = super(CustomMasterLogin, self).web_login(*args, **kw) + + # We only modify the QWeb response (GET request) + if response.is_qweb: + # load your masters + masters = request.env['master.control'].sudo().search([]) + response.qcontext['masters'] = masters + + # After successful login + if request.session.uid and master_selected: + request.session['active_master'] = master_selected + + return response diff --git a/addons_extensions/menu_control_center/models/__init__.py b/addons_extensions/menu_control_center/models/__init__.py index 52e35f001..3e41a1d7b 100644 --- a/addons_extensions/menu_control_center/models/__init__.py +++ b/addons_extensions/menu_control_center/models/__init__.py @@ -1,2 +1,3 @@ +from . import masters from . import models from . import menu \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/masters.py b/addons_extensions/menu_control_center/models/masters.py new file mode 100644 index 000000000..93b4f4f5d --- /dev/null +++ b/addons_extensions/menu_control_center/models/masters.py @@ -0,0 +1,95 @@ +from odoo import models, fields, _ + +class MasterControl(models.Model): + _name = 'master.control' + _description = 'Master Control' + + sequence = fields.Integer() + name = fields.Char(string='Master Name', required=True) + code = fields.Char(string='Code', required=True) + default_show = fields.Boolean(default=True) + access_group_ids = fields.One2many('group.access.line','master_control_id',string='Roles') + + + def action_generate_groups(self): + """Generate category → groups list""" + for rec in self: + + # Clear old groups + rec.access_group_ids.unlink() + + groups = self.env['res.groups'].sudo().search([],order='category_id') + show_group = True if rec.default_show else False + print(show_group) + for grp in groups: + self.env['group.access.line'].create({ + 'master_control_id': rec.id, + 'group_id': grp.id, + 'show_group': show_group, + }) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Groups generated successfully!'), + 'type': 'success', + } + } + + # ----------------------------------------- + # UPDATE GROUPS (Detect new groups) + # ----------------------------------------- + def action_update_groups(self): + for rec in self: + created_count = 0 + + existing_ids = set(rec.access_group_ids.mapped('group_id.id')) + + categories = self.env['ir.module.category'].search([]) + + for category in categories: + groups = self.env['res.groups'].search([ + ('category_id', '=', category.id) + ]) + + for grp in groups: + # create only missing group + if grp.id not in existing_ids: + rec.access_group_ids.create({ + 'master_control_id': rec.id, + 'category_id': category.id, + 'group_id': grp.id, + 'show_group': True if rec.default_show else False, + }) + created_count += 1 + existing_ids.add(grp.id) + + if created_count: + message = f"Added {created_count} new groups." + msg_type = "success" + else: + message = "No new groups found." + msg_type = "info" + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Group Update'), + 'message': _(message), + 'type': msg_type, + } + } + + +class GroupsAccessLine(models.Model): + _name = 'group.access.line' + _description = 'Group Access Line' + _rec_name = 'group_id' + + category_id = fields.Many2one('ir.module.category', related='group_id.category_id') + group_id = fields.Many2one('res.groups', string="Role") + show_group = fields.Boolean(string="Show", default=True) + master_control_id = fields.Many2one('master.control') \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/menu.py b/addons_extensions/menu_control_center/models/menu.py index c82f101c0..8a0e1f736 100644 --- a/addons_extensions/menu_control_center/models/menu.py +++ b/addons_extensions/menu_control_center/models/menu.py @@ -17,7 +17,10 @@ class IrUiMenu(models.Model): group_ids = set(self.env.user._get_group_ids()) if not debug: - hide_menus_list = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids + parent_menus = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids + sub_menus = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_sub_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids + + hide_menus_list = list(set(parent_menus + sub_menus)) menus = menus.filtered(lambda menu: (menu.id not in hide_menus_list)) group_ids = group_ids - { diff --git a/addons_extensions/menu_control_center/models/models.py b/addons_extensions/menu_control_center/models/models.py index fbd4813e9..4d21b244b 100644 --- a/addons_extensions/menu_control_center/models/models.py +++ b/addons_extensions/menu_control_center/models/models.py @@ -37,61 +37,124 @@ class MenuAccessControl(models.Model): access_menu_line_ids = fields.One2many( 'menu.access.line', 'access_control_id', - string="Accessible Menus" + string="Accessible Menus", domain=[('menu_id.parent_id','=',False)] ) + access_sub_menu_line_ids = fields.One2many('menu.access.line', 'access_control_id', + string="Accessible Menus", domain=[('menu_id.parent_id','!=',False)] + ) + + def _get_all_submenus(self, menu): + """Returns all submenus recursively for a given menu.""" + submenus = self.env['ir.ui.menu'].search([('parent_id', '=', menu.id), ('active', '=', True)]) + all_subs = submenus + for sm in submenus: + all_subs |= self._get_all_submenus(sm) + return all_subs + def action_generate_menus(self): - """Button to fetch active top-level menus and populate access lines.""" - menu_lines = [] - active_menus = self.env['ir.ui.menu'].search([ - ('parent_id', '=', False), # top-level menus - ('active', '=', True) - ]) - for menu in active_menus: - menu_lines.append((0, 0, { - 'menu_id': menu.id, - 'is_main_menu': True - })) - self.access_menu_line_ids = menu_lines + """ + Generate main menus and all submenus (recursive), + and set access_line_id for every submenu. + """ + for rec in self: - def action_update_menus(self): - """Button to add new top-level menus that are not already in the list.""" - for record in self: - # Get existing menu IDs in the current record - existing_menu_ids = record.access_menu_line_ids.mapped('menu_id').ids + # clear old menus + rec.access_menu_line_ids.unlink() + rec.access_sub_menu_line_ids.unlink() - # Find new top-level menus that are not in existing_menu_ids - new_menus = self.env['ir.ui.menu'].search([ + active_menus = self.env['ir.ui.menu'].search([ ('parent_id', '=', False), - ('active', '=', True), - ('id', 'not in', existing_menu_ids) + ('active', '=', True) ]) - # Create new lines for the new menus - new_lines = [] - for menu in new_menus: - new_lines.append((0, 0, { - 'menu_id': menu.id, - 'is_main_menu': True # Default to True as requested - })) + for menu in active_menus: - if new_lines: - record.write({ - 'access_menu_line_ids': new_lines + # 1️⃣ Create main menu line + main_line = self.env['menu.access.line'].create({ + 'access_control_id': rec.id, + 'menu_id': menu.id, + 'is_main_menu': True, }) - # Show success message + + # 2️⃣ Fetch all recursive submenus + submenus = self._get_all_submenus(menu) + + # 3️⃣ Create submenu lines with correct parent + for sm in submenus: + self.env['menu.access.line'].create({ + 'access_control_id': rec.id, + 'menu_id': sm.id, + 'is_main_menu': True, + 'access_line_id': main_line.id, # important + }) + + def action_update_menus(self): + line = self.env['menu.access.line'] + menu = self.env['ir.ui.menu'] + + for rec in self: + created_count = 0 + + # All existing menu IDs across BOTH One2manys + existing_menu_ids = set( + rec.access_menu_line_ids.mapped('menu_id.id') + + rec.access_sub_menu_line_ids.mapped('menu_id.id') + ) + + # ---------- Step 1: Ensure all top-level menus exist ---------- + top_menus = menu.search([ + ('parent_id', '=', False), + ('active', '=', True), + ]) + + for menu in top_menus: + + # Create missing MAIN MENU + main_line = line.search([ + ('access_control_id', '=', rec.id), + ('menu_id', '=', menu.id), + ('access_line_id', '=', False) + ], limit=1) + + if not main_line: + main_line = line.create({ + 'access_control_id': rec.id, + 'menu_id': menu.id, + 'is_main_menu': True, + }) + created_count += 1 + existing_menu_ids.add(menu.id) + + # ---------- Step 2: Ensure all SUBMENUS exist ---------- + submenus = rec._get_all_submenus(menu) + + for sm in submenus: + + # If submenu is missing → create it + if sm.id not in existing_menu_ids: + line.create({ + 'access_control_id': rec.id, + 'menu_id': sm.id, + 'is_main_menu': True, + 'access_line_id': main_line.id, + }) + created_count += 1 + existing_menu_ids.add(sm.id) + + # ---------- Notification ---------- + if created_count: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Success'), - 'message': _('Added %s new menu(s)') % len(new_menus), + 'message': _('Added %s new menu(s) (including submenus)') % created_count, 'type': 'success', 'sticky': False, } } else: - # Show info message if no new menus found return { 'type': 'ir.actions.client', 'tag': 'display_notification', @@ -103,6 +166,7 @@ class MenuAccessControl(models.Model): } } + class MenuAccessLine(models.Model): _name = 'menu.access.line' _description = 'Menu Access Line' @@ -111,3 +175,29 @@ class MenuAccessLine(models.Model): access_control_id = fields.Many2one('menu.access.control', ondelete='cascade') menu_id = fields.Many2one('ir.ui.menu', string="Menu") is_main_menu = fields.Boolean(string="Is Main Menu", default=True) + parent_menu = fields.Many2one('ir.ui.menu',related='menu_id.parent_id') + access_line_id = fields.Many2one('menu.access.line') + control_unit = fields.Many2one( + 'menu.control.units', + related='access_control_id.control_unit', + store=True + ) + + def open_submenus_popup_view(self): + self.ensure_one() + + return { + "name": _("Sub Menus"), + "type": "ir.actions.act_window", + "res_model": "menu.access.line", + "view_mode": "list,form", + "views": [ + (self.env.ref("menu_control_center.view_submenu_line_list").id, "list"), + ], + "target": "new", + "domain": [("access_line_id", "=", self.id)], + "context": {"default_access_line_id": self.id}, + } + + + diff --git a/addons_extensions/menu_control_center/security/ir.model.access.csv b/addons_extensions/menu_control_center/security/ir.model.access.csv index d7edab9d1..f6e0f1e02 100644 --- a/addons_extensions/menu_control_center/security/ir.model.access.csv +++ b/addons_extensions/menu_control_center/security/ir.model.access.csv @@ -1,4 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_menu_access_control,access.menu.access.control,model_menu_access_control,hr.group_hr_manager,1,1,1,1 access_menu_access_line,access.menu.access.line,model_menu_access_line,hr.group_hr_manager,1,1,1,1 -access_menu_control_units,access.menu.control.units,model_menu_control_units,hr.group_hr_manager,1,1,1,1 \ No newline at end of file +access_menu_control_units,access.menu.control.units,model_menu_control_units,hr.group_hr_manager,1,1,1,1 +access_master_control_public,master.control.public,model_master_control,base.group_public,1,0,0,0 +access_master_control_hr,master.control.hr,model_master_control,hr.group_hr_manager,1,1,1,1 +group_access_line_access,group_access_line_access,model_group_access_line,,1,1,1,1 diff --git a/addons_extensions/menu_control_center/views/login.xml b/addons_extensions/menu_control_center/views/login.xml new file mode 100644 index 000000000..dc5e0d5a5 --- /dev/null +++ b/addons_extensions/menu_control_center/views/login.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/addons_extensions/menu_control_center/views/masters.xml b/addons_extensions/menu_control_center/views/masters.xml new file mode 100644 index 000000000..b15db1e26 --- /dev/null +++ b/addons_extensions/menu_control_center/views/masters.xml @@ -0,0 +1,76 @@ + + + + + master.control.list + master.control + + + + + + + + + + + master.control.form + master.control + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + Master Control + master.control + list,form + + + + + + + + +
\ No newline at end of file diff --git a/addons_extensions/menu_control_center/views/menu_access_control_views.xml b/addons_extensions/menu_control_center/views/menu_access_control_views.xml index d4ea37745..63bc7381c 100644 --- a/addons_extensions/menu_control_center/views/menu_access_control_views.xml +++ b/addons_extensions/menu_control_center/views/menu_access_control_views.xml @@ -1,5 +1,26 @@ + + menu.access.line.submenu.list + menu.access.line + + + + + + + + + + + + + Sub Menus + menu.access.line + list + new + + menu.control.units.list menu.control.units @@ -71,12 +92,29 @@ - - - - - - + + + + + + + + + + + diff --git a/addons_extensions/onlyoffice_odoo/static/src/css/form_gallery.scss b/addons_extensions/onlyoffice_odoo/static/src/css/form_gallery.scss new file mode 100644 index 000000000..67b5f5cd7 --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/css/form_gallery.scss @@ -0,0 +1,57 @@ +.o_onlyoffice_kanban_renderer { + row-gap: 30px; + column-gap: 30px; + background: none; + + .o_onlyoffice_kanban_record { + width: 200px; + border: 1px solid; + border-color: var(--black-25); + + .o_onlyoffice_record_selector { + color: var(--black-25); + position: absolute; + top: 8px; + right: 8px; + font-size: 16px; + z-index: 9; + cursor: pointer; + + background: rgba(255, 255, 255, 0.66); + border-radius: 50%; + width: 13px; + height: 13px; + display: flex; + align-items: center; + justify-content: center; + } + + .o_onlyoffice_record_selected { + opacity: 1; + border: 1px solid var(--primary); + box-shadow: 0 0 0 1px var(--primary); + background-color: var(--body-bg); + + .o_onlyoffice_record_selector::before { + color: var(--primary); + content: "\f058"; + } + } + } + + .o_onlyoffice_kanban_previewer { + cursor: zoom-in; + + .o_onlyoffice_kanban_image { + width: 100%; + height: 100%; + } + } + + .o_onlyoffice_kanban_details_wrapper { + flex-direction: row; + min-height: auto; + column-gap: 10px; + align-items: baseline; + } +} diff --git a/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_link_container.png b/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_link_container.png new file mode 100644 index 000000000..372532c98 Binary files /dev/null and b/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_link_container.png differ diff --git a/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_link_container_mobile.png b/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_link_container_mobile.png new file mode 100644 index 000000000..889098137 Binary files /dev/null and b/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_link_container_mobile.png differ diff --git a/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_preview.scss b/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_preview.scss new file mode 100644 index 000000000..96824b90c --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/css/onlyoffice_preview.scss @@ -0,0 +1,47 @@ +.o-overlay-item { + &:has(.o-onlyoffice-preview) { + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + } +} + +.o-onlyoffice-overlay-item { + top: 0; + width: 100%; + height: 100%; +} + +.o-onlyoffice-preview { + position: absolute; + z-index: 9998; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + background-color: rgba(0, 0, 0, 0.7); + + .o-onlyoffice-preview-header { + display: flex; + min-height: 40px; + align-items: center; + justify-content: space-between; + + .o-onlyoffice-header-title { + column-gap: 10px; + } + } + + .o-onlyoffice-preview-body { + .o-onlyoffice-body-iframe { + width: 100%; + height: 100%; + border: none; + } + } +} diff --git a/addons_extensions/onlyoffice_odoo/static/src/css/res_config_settings_views.scss b/addons_extensions/onlyoffice_odoo/static/src/css/res_config_settings_views.scss new file mode 100644 index 000000000..4373719b5 --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/css/res_config_settings_views.scss @@ -0,0 +1,44 @@ +.onlyoffice_link_container { + width: 549px; + height: 116px; + position: relative; + border-left: 0; + background-image: url("/onlyoffice_odoo/static/src/css/onlyoffice_link_container.png"); + background-size: contain; + + .onlyoffice_link_button { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + min-width: 93px; + max-width: 120px; + max-height: 40px; + padding: 5px 0; + background-color: #71639e; + color: white; + font-family: "SF UI Display", sans-serif; + text-align: center; + font-weight: 500; + border-radius: 3px; + top: 40.5px; + right: 14px; + } +} + +@media (max-width: 767px) { + .onlyoffice_link_container { + width: 345px; + height: 169px; + background-image: url("/onlyoffice_odoo/static/src/css/onlyoffice_link_container_mobile.png"); + + .onlyoffice_link_button { + width: 77px; + height: 34px; + bottom: 16px; + left: 20px; + top: unset; + right: unset; + } + } +} diff --git a/addons_extensions/onlyoffice_odoo/static/src/models/attachment_card_onlyoffice.js b/addons_extensions/onlyoffice_odoo/static/src/models/attachment_card_onlyoffice.js new file mode 100644 index 000000000..3dfd5edc3 --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/models/attachment_card_onlyoffice.js @@ -0,0 +1,71 @@ +/** @odoo-module **/ + +/* + * + * (c) Copyright Ascensio System SIA 2024 + * + */ + +import { AttachmentList } from "@mail/core/common/attachment_list" +import { _t } from "@web/core/l10n/translation" +import { useService } from "@web/core/utils/hooks" +import { patch } from "@web/core/utils/patch" + +let formats = [] +const loadFormats = async () => { + try { + const data = await fetch("/onlyoffice_odoo/static/assets/document_formats/onlyoffice-docs-formats.json") + formats = await data.json() + } catch (error) { + console.error("Error loading formats data:", error) + } +} + +loadFormats() + +patch(AttachmentList.prototype, { + setup() { + super.setup(...arguments) + this.orm = useService("orm") + this.notification = useService("notification") + this.actionService = useService("action") + }, + // eslint-disable-next-line sort-keys + onlyofficeCanOpen(attachment) { + const format = formats.find((f) => f.name === attachment.extension.toLowerCase()) + return format && format.actions && (format.actions.includes("view") || format.actions.includes("edit")) + }, + async openOnlyoffice(attachment) { + const demo = JSON.parse(await this.orm.call("onlyoffice.odoo", "get_demo")) + if (demo && demo.mode && demo.date) { + const isValidDate = (d) => d instanceof Date && !isNaN(d) + demo.date = new Date(Date.parse(demo.date)) + if (isValidDate(demo.date)) { + const today = new Date() + const difference = Math.floor((today - demo.date) / (1000 * 60 * 60 * 24)) + if (difference > 30) { + this.notification.add( + _t("The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server"), + { + title: _t("ONLYOFFICE Docs server"), + type: "warning", + }, + ) + return + } + } + } + const { same_tab } = JSON.parse(await this.orm.call("onlyoffice.odoo", "get_same_tab")) + if (same_tab) { + const action = { + params: { attachment_id: attachment.id }, + tag: "onlyoffice_editor", + target: "current", + type: "ir.actions.client", + } + return this.actionService.doAction(action) + } + const accessTokenQuery = attachment.accessToken ? `?access_token=${attachment.accessToken}` : "" + window.open(`/onlyoffice/editor/${attachment.id}${accessTokenQuery}`, "_blank") + }, +}) diff --git a/addons_extensions/onlyoffice_odoo/static/src/views/form_gallery/form_gallery.js b/addons_extensions/onlyoffice_odoo/static/src/views/form_gallery/form_gallery.js new file mode 100644 index 000000000..4a7fc5452 --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/views/form_gallery/form_gallery.js @@ -0,0 +1,274 @@ +/** @odoo-module **/ +import { OnlyofficePreview } from "@onlyoffice_odoo/views/preview/onlyoffice_preview" +import { Dialog } from "@web/core/dialog/dialog" +import { Dropdown } from "@web/core/dropdown/dropdown" +import { DropdownItem } from "@web/core/dropdown/dropdown_item" +import { _t } from "@web/core/l10n/translation" +import { rpc } from "@web/core/network/rpc" +import { Pager } from "@web/core/pager/pager" +import { useService } from "@web/core/utils/hooks" + +const { Component, useState, onWillStart, onWillUnmount } = owl + +export class FormGallery extends Component { + static template = "onlyoffice_odoo.FormGallery" + + static components = { + Dialog, + Dropdown, + DropdownItem, + Pager, + } + + setup() { + this.title = _t("Document templates") + this.action = useService("action") + this.notification = useService("notification") + this.rpc = rpc + this.orm = useService("orm") + + this.searchTimeout = null + + this.state = useState({ + categories: [], + error: null, + form: null, + forms: [], + ghost: 0, + limit: 12, + loading: false, + locale: { + code: "en", + name: "English", + }, + locales: [ + { + code: "en", + name: "English", + }, + ], + offset: 0, + search: "", + subcategories: {}, + subcategory: { + category_type: "category", + id: "all", + }, + total: 0, + type: "pdf", + }) + + onWillStart(async () => { + this.state.loading = true + await this.fetchLocales() + await this.fetchCategoryTypes() + await this.fetchOforms() + this.state.loading = false + }) + + onWillUnmount(() => { + if (this.searchTimeout) { + clearTimeout(this.searchTimeout) + } + }) + } + + async fetchLocales() { + try { + const url = "/onlyoffice/oforms/locales" + const response = await this.rpc(url) + + let localesData = [] + if (Array.isArray(response)) { + localesData = response.map((item) => ({ + code: item.code, + name: item.name || item.code, + })) + } else if (response && response.data) { + localesData = response.data + } + + this.state.locales = localesData + } catch (_error) { + this.state.locales = [ + { + code: "en", + name: "English", + }, + ] + } + } + + async fetchCategoryTypes() { + try { + const response = await this.rpc("/onlyoffice/oforms/category-types", { locale: this.state.locale.code }) + this.state.categories = response.data || [] + for (const categoryTypes of response.data) { + await this.fetchSubcategories(categoryTypes.categoryId) + } + } catch (_error) { + this.notification.add(_t("Failed to load categories"), { type: "danger" }) + } + } + + async fetchSubcategories(categoryId) { + try { + const category = this.state.categories.find((c) => c.categoryId === categoryId) + const response = await this.rpc("/onlyoffice/oforms/subcategories", { + category_type: category.type, + locale: this.state.locale.code, + }) + + this.state.subcategories[categoryId] = response.data || [] + } catch (_error) { + this.state.subcategories[categoryId] = [] + } + } + + async fetchOforms() { + this.state.loading = true + this.state.form = null + this.state.error = null + + try { + const params = { + ["filters[" + this.state.subcategory.category_type + "][$eq]"]: this.state.subcategory.id, + locale: this.state.locale.code, + "pagination[pageSize]": this.state.limit, + "pagination[page]": Math.floor(this.state.offset / this.state.limit) + 1, + type: this.state.type, + } + + if (this.state.search) { + params["filters[name_form][$containsi]"] = this.state.search + } + + const response = await this.rpc("/onlyoffice/oforms", { params: params }) + + this.state.forms = response.data || [] + + const oKanbanGhost = 4 - (this.state.forms.length % 4) + if (oKanbanGhost === 4) { + this.state.ghost = new Array(0).fill() + } else { + this.state.ghost = new Array(oKanbanGhost).fill() + } + + this.state.total = response.meta?.pagination?.total || 0 + } catch (_error) { + this.state.error = _t("Failed to load forms") + this.notification.add(_t("Error loading forms"), { type: "danger" }) + } + this.state.loading = false + } + + async onChangeType(type) { + this.state.type = type + this.state.subcategory = { + category_type: "category", + id: "all", + } + this.state.offset = 0 + await this.fetchOforms() + } + + async onSubcategorySelect(subcategory) { + this.state.subcategory = subcategory + this.state.type = "pdf" + this.state.offset = 0 + await this.fetchOforms() + } + + async onAllSubcategorySelect() { + this.state.subcategory = { + category_type: "category", + id: "all", + } + this.state.offset = 0 + await this.fetchOforms() + } + + async onSearch(search) { + if (this.searchTimeout) { + clearTimeout(this.searchTimeout) + } + + this.state.search = search + + this.searchTimeout = setTimeout(async () => { + this.state.offset = 0 + await this.fetchOforms() + }, 1000) + } + + async onLocaleChange(locale) { + this.state.loading = true + + this.state.locale = locale + this.state.subcategory = { + category_type: "category", + id: "all", + } + this.state.offset = 0 + await this.fetchCategoryTypes() + await this.fetchOforms() + + this.state.loading = false + } + + async onPageChange({ offset }) { + this.state.offset = offset + await this.fetchOforms() + } + + getImageUrl(form) { + const imageData = form.attributes?.template_image?.data + if (!imageData) { + return null + } + return ( + imageData.attributes.formats.medium?.url || + imageData.attributes.formats.small?.url || + imageData.attributes.formats.thumbnail?.url + ) + } + + getPreviewUrl(form) { + return form.attributes?.card_prewiew?.data?.attributes?.url + } + + previewForm(url, title, ext) { + this.env.services.dialog.add( + OnlyofficePreview, + { + close: () => { + this.env.services.dialog.close() + }, + title: title + "." + ext.split(".").pop(), + url: url, + }, + { + onClose: () => { + return + }, + }, + ) + } + + selectForm(form) { + if (this.state.form && this.state.form.id === form.id) { + this.state.form = null + } else { + this.state.form = form + } + } + + async download() { + if (this.props.onDownload && this.state.form) { + await this.props.onDownload(this.state.form) + if (this.props.close) { + this.props.close() + } + } + } +} diff --git a/addons_extensions/onlyoffice_odoo/static/src/views/form_gallery/form_gallery.xml b/addons_extensions/onlyoffice_odoo/static/src/views/form_gallery/form_gallery.xml new file mode 100644 index 000000000..74c9d4de1 --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/views/form_gallery/form_gallery.xml @@ -0,0 +1,174 @@ + + + + +
+
+
+ +
+
+
+
+ + + + + View all templates + + + +
+ + + + + Form + + + Document + + + Spreadsheet + + + Presentation + + + +
+
+
+ + + + + + + + + + +
+
+ +
+
+ + +
+ +

Loading forms...

+
+
+ + +
+ + +
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+
+ +
+ +
+ + +
+ +

No forms found

+

Try changing your search or filters

+
+
+
+ + + + + +
+
+
diff --git a/addons_extensions/onlyoffice_odoo/static/src/views/preview/onlyoffice_preview.js b/addons_extensions/onlyoffice_odoo/static/src/views/preview/onlyoffice_preview.js new file mode 100644 index 000000000..aa5602376 --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/views/preview/onlyoffice_preview.js @@ -0,0 +1,51 @@ +/** @odoo-module **/ + +import { Component, onMounted, onWillUnmount } from "@odoo/owl" + +export class OnlyofficePreview extends Component { + static template = "onlyoffice_odoo.OnlyofficePreview" + + static props = { + close: Function, + title: String, + url: String, + } + + setup() { + this.title = "Preview - " + this.props.title + this.url = + "/onlyoffice/preview" + + `?url=${encodeURIComponent(this.props.url)}&` + + `title=${encodeURIComponent(this.props.title)}` + + const handleKeyDown = (ev) => { + if (ev.key === "Escape") { + ev.stopPropagation() + ev.preventDefault() + this.props.close() + } + } + + onMounted(() => { + document.addEventListener("keydown", handleKeyDown, { capture: true }) + document.querySelectorAll(".o-overlay-item").forEach((item) => { + if (item.querySelector(".o-onlyoffice-preview")) { + item.classList.add("o-onlyoffice-overlay-item") + } + }) + }) + + onWillUnmount(() => { + document.removeEventListener("keydown", handleKeyDown, { capture: true }) + }) + } + + onClickOutside(ev) { + const isHeader = ev.target.closest(".o-onlyoffice-preview-header") + const isBody = ev.target.closest(".o-onlyoffice-preview-body") + + if (!isHeader && !isBody) { + this.props.close() + } + } +} diff --git a/addons_extensions/onlyoffice_odoo/static/src/views/preview/onlyoffice_preview.xml b/addons_extensions/onlyoffice_odoo/static/src/views/preview/onlyoffice_preview.xml new file mode 100644 index 000000000..a76374966 --- /dev/null +++ b/addons_extensions/onlyoffice_odoo/static/src/views/preview/onlyoffice_preview.xml @@ -0,0 +1,19 @@ + + + +
+
+
+ +
+
+
+ +
+
+
+