From 586f05d5676fe7953edb707602a01390a005baa5 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 4 May 2026 02:14:34 -0400 Subject: [PATCH] chnages --- fusion_helpdesk/__init__.py | 3 + fusion_helpdesk/__manifest__.py | 47 + fusion_helpdesk/controllers/__init__.py | 2 + fusion_helpdesk/controllers/main.py | 402 ++++++++ .../data/ir_config_parameter_data.xml | 32 + fusion_helpdesk/models/__init__.py | 2 + fusion_helpdesk/models/res_config_settings.py | 52 + fusion_helpdesk/security/ir.model.access.csv | 1 + fusion_helpdesk/static/description/icon.png | Bin 0 -> 36450 bytes .../static/src/js/fusion_helpdesk_dialog.js | 244 +++++ .../static/src/js/fusion_helpdesk_systray.js | 33 + .../static/src/scss/fusion_helpdesk.scss | 172 ++++ .../static/src/xml/fusion_helpdesk_dialog.xml | 110 +++ .../src/xml/fusion_helpdesk_systray.xml | 17 + .../views/res_config_settings_views.xml | 51 + fusion_helpdesk_central/__init__.py | 2 + fusion_helpdesk_central/__manifest__.py | 36 + .../data/ir_config_parameter_data.xml | 13 + fusion_helpdesk_central/models/__init__.py | 2 + .../models/fusion_helpdesk_client_key.py | 186 ++++ .../security/ir.model.access.csv | 2 + .../fusion_helpdesk_client_key_views.xml | 136 +++ .../specs/2026-05-04-fp-step-kind-model.md | 96 ++ fusion_plating/fusion_plating/__init__.py | 21 +- fusion_plating/fusion_plating/__manifest__.py | 12 +- .../controllers/simple_recipe_controller.py | 83 +- .../fusion_plating/data/fp_step_kind_data.xml | 928 ++++++++++++++++++ .../data/fp_step_template_data.xml | 16 +- .../migrations/19.0.18.13.0/post-migrate.py | 126 +++ .../migrations/19.0.18.13.0/pre-migrate.py | 57 ++ .../fusion_plating/models/__init__.py | 1 + .../fusion_plating/models/fp_process_node.py | 38 +- .../fusion_plating/models/fp_step_kind.py | 282 ++++++ .../fusion_plating/models/fp_step_template.py | 81 +- .../security/ir.model.access.csv | 6 + .../static/src/js/fp_icon_picker.js | 58 ++ .../static/src/js/simple_recipe_editor.js | 59 ++ .../static/src/scss/fp_icon_picker.scss | 150 +++ .../static/src/xml/fp_icon_picker.xml | 35 + .../static/src/xml/simple_recipe_editor.xml | 33 +- .../views/fp_process_node_views.xml | 3 +- .../views/fp_step_kind_views.xml | 127 +++ .../views/fp_step_template_views.xml | 11 +- 43 files changed, 3656 insertions(+), 112 deletions(-) create mode 100644 fusion_helpdesk/__init__.py create mode 100644 fusion_helpdesk/__manifest__.py create mode 100644 fusion_helpdesk/controllers/__init__.py create mode 100644 fusion_helpdesk/controllers/main.py create mode 100644 fusion_helpdesk/data/ir_config_parameter_data.xml create mode 100644 fusion_helpdesk/models/__init__.py create mode 100644 fusion_helpdesk/models/res_config_settings.py create mode 100644 fusion_helpdesk/security/ir.model.access.csv create mode 100644 fusion_helpdesk/static/description/icon.png create mode 100644 fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js create mode 100644 fusion_helpdesk/static/src/js/fusion_helpdesk_systray.js create mode 100644 fusion_helpdesk/static/src/scss/fusion_helpdesk.scss create mode 100644 fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml create mode 100644 fusion_helpdesk/static/src/xml/fusion_helpdesk_systray.xml create mode 100644 fusion_helpdesk/views/res_config_settings_views.xml create mode 100644 fusion_helpdesk_central/__init__.py create mode 100644 fusion_helpdesk_central/__manifest__.py create mode 100644 fusion_helpdesk_central/data/ir_config_parameter_data.xml create mode 100644 fusion_helpdesk_central/models/__init__.py create mode 100644 fusion_helpdesk_central/models/fusion_helpdesk_client_key.py create mode 100644 fusion_helpdesk_central/security/ir.model.access.csv create mode 100644 fusion_helpdesk_central/views/fusion_helpdesk_client_key_views.xml create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-04-fp-step-kind-model.md create mode 100644 fusion_plating/fusion_plating/data/fp_step_kind_data.xml create mode 100644 fusion_plating/fusion_plating/migrations/19.0.18.13.0/post-migrate.py create mode 100644 fusion_plating/fusion_plating/migrations/19.0.18.13.0/pre-migrate.py create mode 100644 fusion_plating/fusion_plating/models/fp_step_kind.py create mode 100644 fusion_plating/fusion_plating/static/src/js/fp_icon_picker.js create mode 100644 fusion_plating/fusion_plating/static/src/scss/fp_icon_picker.scss create mode 100644 fusion_plating/fusion_plating/static/src/xml/fp_icon_picker.xml create mode 100644 fusion_plating/fusion_plating/views/fp_step_kind_views.xml diff --git a/fusion_helpdesk/__init__.py b/fusion_helpdesk/__init__.py new file mode 100644 index 00000000..9e5827f9 --- /dev/null +++ b/fusion_helpdesk/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models diff --git a/fusion_helpdesk/__manifest__.py b/fusion_helpdesk/__manifest__.py new file mode 100644 index 00000000..8560f27c --- /dev/null +++ b/fusion_helpdesk/__manifest__.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +{ + 'name': 'Fusion Helpdesk Reporter', + 'version': '19.0.1.2.0', + 'category': 'Productivity', + 'summary': 'One-click in-app bug reporting & feature requesting — ' + 'auto-creates a helpdesk.ticket on a central Odoo Helpdesk.', + 'description': """ +Fusion Helpdesk Reporter +======================== +A standalone module that gives every backend user a quick "Report an +Issue / Request a Feature" button in the Odoo top systray. + +Submissions are forwarded over JSON-RPC / XML-RPC to a central Odoo +instance (typically the support shop's main Odoo) and become +`helpdesk.ticket` records with screenshots, file attachments, and +diagnostic context (URL, user agent, current user/company, error +code) automatically captured. + +Designed to ship alongside any other Fusion / Nexa Systems client +module bundle. No dependencies on the rest of Fusion Plating. + """, + 'author': 'Nexa Systems Inc.', + 'website': 'https://www.nexasystems.ca', + 'license': 'OPL-1', + 'depends': ['base', 'web', 'mail'], + 'data': [ + 'security/ir.model.access.csv', + 'data/ir_config_parameter_data.xml', + 'views/res_config_settings_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'fusion_helpdesk/static/src/scss/fusion_helpdesk.scss', + 'fusion_helpdesk/static/src/xml/fusion_helpdesk_systray.xml', + 'fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml', + 'fusion_helpdesk/static/src/js/fusion_helpdesk_systray.js', + 'fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js', + ], + }, + 'images': ['static/description/icon.png'], + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/fusion_helpdesk/controllers/__init__.py b/fusion_helpdesk/controllers/__init__.py new file mode 100644 index 00000000..757b12a1 --- /dev/null +++ b/fusion_helpdesk/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/fusion_helpdesk/controllers/main.py b/fusion_helpdesk/controllers/main.py new file mode 100644 index 00000000..b5ad514f --- /dev/null +++ b/fusion_helpdesk/controllers/main.py @@ -0,0 +1,402 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 +"""HTTP routes for the Fusion Helpdesk Reporter. + +`/fusion_helpdesk/submit` accepts the dialog payload, forwards it to a +remote Odoo Helpdesk over XML-RPC, attaches uploaded files + +screenshot, and returns the resulting ticket id/url to the OWL dialog. + +Why XML-RPC and not JSON-RPC? Helpdesk's external API surface is +exposed via Odoo's standard `/xmlrpc/2/object` endpoint which is +the most stable cross-version contract. JSON-RPC `/jsonrpc` works +too but historically had quirkier error handling on file payloads. +""" +import base64 +import logging +import socket +import ssl +import xmlrpc.client +from urllib.parse import urljoin, urlparse + +from odoo import _, http +from odoo.exceptions import UserError +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class FusionHelpdeskController(http.Controller): + + @http.route( + '/fusion_helpdesk/submit', + type='jsonrpc', auth='user', methods=['POST'], + ) + def submit(self, kind, subject, description, + error_code=None, attachments=None, + page_url=None, user_agent=None): + """Forward a bug report or feature request to the central Odoo + Helpdesk and return {ok, ticket_id, ticket_url, error}. + + Args: + kind: 'bug' or 'feature'. + subject: short title. + description: long-form. May contain HTML or plain text. + error_code: optional traceback / error string. + attachments: list of {name, mimetype, data_b64} dicts. + page_url: client-side window.location.href at submit time. + user_agent: client-side navigator.userAgent. + """ + cfg = self._read_config() + if not all([cfg['url'], cfg['db'], cfg['login'], cfg['password']]): + return { + 'ok': False, + 'error': 'config_missing', + 'message': _( + 'Fusion Helpdesk is not fully configured. Ask an ' + 'administrator to fill in the remote URL, database, ' + 'login and password under Settings → Fusion Helpdesk.' + ), + } + + # ---- Build the ticket payload --------------------------------- + prefix = ('[%s] ' % cfg['client_label']) if cfg['client_label'] else '' + kind_label = 'Bug Report' if kind == 'bug' else 'Feature Request' + full_subject = '%s%s: %s' % (prefix, kind_label, subject or '(untitled)') + + body_parts = [] + if description: + body_parts.append( + '

Description

%s
' % _html_escape(description) + ) + if error_code: + body_parts.append( + '

Error Code / Traceback

' + '
%s
' % _html_escape(error_code) + ) + body_parts.append(self._build_diag_block(page_url, user_agent)) + + ticket_vals = { + 'name': full_subject, + 'description': '\n'.join(body_parts), + } + if cfg['team_id']: + ticket_vals['team_id'] = cfg['team_id'] + + # ---- Talk to remote Odoo -------------------------------------- + try: + uid, models_proxy = self._authenticate(cfg) + except _RemoteError as e: + return e.to_response() + + try: + ticket_id = models_proxy.execute_kw( + cfg['db'], uid, cfg['password'], + 'helpdesk.ticket', 'create', [ticket_vals], + ) + except xmlrpc.client.Fault as e: + fault = (e.faultString or '').strip() + _logger.warning( + 'fusion_helpdesk: helpdesk.ticket.create failed: %s', + fault, + ) + # The Helpdesk app might not be installed on the remote. + if 'helpdesk.ticket' in fault and 'does not exist' in fault.lower(): + return { + 'ok': False, 'error': 'helpdesk_not_installed', + 'message': _( + 'The Helpdesk app is not installed on the central ' + 'Odoo at %s. Install the Helpdesk app there before ' + 'submitting tickets.' + ) % cfg['url'], + } + if 'access' in fault.lower() or 'rights' in fault.lower(): + return { + 'ok': False, 'error': 'permission_denied', + 'message': _( + 'The remote service account "%s" does not have ' + 'permission to create helpdesk tickets. Ask a ' + 'central Odoo admin to grant Helpdesk Officer ' + 'rights to that user.' + ) % cfg['login'], + } + return { + 'ok': False, 'error': 'create_failed', + 'message': _('The remote Helpdesk rejected the ticket: %s' + ) % fault, + } + except (socket.timeout, OSError, ssl.SSLError) as e: + _logger.warning( + 'fusion_helpdesk: ticket create network error: %s', e, + ) + return _network_error_response(cfg['url'], e) + + # ---- Push attachments ----------------------------------------- + attached = 0 + for att in attachments or []: + data_b64 = (att or {}).get('data_b64') + name = (att or {}).get('name') or 'attachment.bin' + mimetype = (att or {}).get('mimetype') or 'application/octet-stream' + if not data_b64: + continue + try: + models_proxy.execute_kw( + cfg['db'], uid, cfg['password'], + 'ir.attachment', 'create', [{ + 'name': name, + 'datas': data_b64, + 'res_model': 'helpdesk.ticket', + 'res_id': ticket_id, + 'mimetype': mimetype, + }], + ) + attached += 1 + except xmlrpc.client.Fault as e: + _logger.warning( + 'fusion_helpdesk: attachment "%s" upload failed: %s', + name, e.faultString, + ) + + ticket_url = urljoin( + cfg['url'].rstrip('/') + '/', + 'odoo/helpdesk/%s' % ticket_id, + ) + _logger.info( + 'fusion_helpdesk: created remote ticket #%s (%s attachments) ' + 'on %s for user %s', + ticket_id, attached, cfg['url'], + request.env.user.login, + ) + return { + 'ok': True, + 'ticket_id': ticket_id, + 'ticket_url': ticket_url, + 'attached': attached, + } + + # ------------------------------------------------------------------ + def _read_config(self): + """Return the active config as a plain dict. Run as sudo so + regular users can submit even without read-access on + ir.config_parameter (the password row is system-write but + readable by anyone with backend access).""" + ICP = request.env['ir.config_parameter'].sudo() + return { + 'url': (ICP.get_param('fusion_helpdesk.remote_url') or '').strip(), + 'db': (ICP.get_param('fusion_helpdesk.remote_db') or '').strip(), + 'login': (ICP.get_param('fusion_helpdesk.remote_login') or '').strip(), + 'password': ICP.get_param('fusion_helpdesk.remote_password') or '', + 'team_id': int( + ICP.get_param('fusion_helpdesk.remote_team_id') or 0 + ) or False, + 'client_label': ( + ICP.get_param('fusion_helpdesk.client_label') or '' + ).strip(), + } + + def _authenticate(self, cfg): + """Authenticate against the remote and return (uid, models_proxy). + + Raises _RemoteError with a granular `error` code and a friendly + end-user message so the dialog can show what actually broke + (network vs. credentials vs. server problem). + """ + url = cfg['url'].rstrip('/') + try: + common = xmlrpc.client.ServerProxy( + '%s/xmlrpc/2/common' % url, allow_none=True, + ) + try: + uid = common.authenticate( + cfg['db'], cfg['login'], cfg['password'], {}, + ) + except xmlrpc.client.ProtocolError as e: + # Server returned an HTTP error status. + _logger.warning( + 'fusion_helpdesk: HTTP %s from %s during authenticate: %s', + e.errcode, url, e.errmsg, + ) + if e.errcode in (401, 403): + raise _RemoteError( + 'auth_failed', + _('The central Odoo at %s rejected the login. ' + 'Check the Service Login and API Key in ' + 'Settings → Fusion Helpdesk.') % url, + ) + if e.errcode == 404: + raise _RemoteError( + 'endpoint_not_found', + _('The XML-RPC endpoint at %s/xmlrpc/2/common ' + 'returned 404. Verify the Remote URL points ' + 'at an Odoo server (no trailing path).') % url, + ) + raise _RemoteError( + 'remote_http_error', + _('The central Odoo returned HTTP %(code)s while ' + 'authenticating. Try again, or check that ' + '%(url)s is reachable from a browser.' + ) % {'code': e.errcode, 'url': url}, + ) + except xmlrpc.client.Fault as e: + # Server-side application error — usually wrong DB name. + fault = (e.faultString or '').strip() + _logger.warning( + 'fusion_helpdesk: XML-RPC fault during authenticate: %s', + fault, + ) + if 'database' in fault.lower() and ( + 'not exist' in fault.lower() or 'unknown' in fault.lower() + ): + raise _RemoteError( + 'wrong_database', + _('Database "%(db)s" does not exist on %(url)s. ' + 'Verify the Remote DB in Settings → Fusion ' + 'Helpdesk.') % {'db': cfg['db'], 'url': url}, + ) + raise _RemoteError( + 'auth_failed', + _('The central Odoo rejected authentication: %s' + ) % fault, + ) + except ssl.SSLError as e: + _logger.warning( + 'fusion_helpdesk: TLS error against %s: %s', url, e, + ) + raise _RemoteError( + 'tls_error', + _('TLS / SSL handshake with %(url)s failed: ' + '%(msg)s. Either the server cert is invalid or ' + 'the system cert store on this host is out of ' + 'date.') % {'url': url, 'msg': str(e)}, + ) + except socket.gaierror as e: + _logger.warning( + 'fusion_helpdesk: DNS lookup failed for %s: %s', url, e, + ) + raise _RemoteError( + 'dns_error', + _('Could not resolve "%(host)s". Check Settings → ' + 'Fusion Helpdesk → Remote URL, or your server\'s ' + 'DNS / outbound network.' + ) % {'host': urlparse(url).hostname or url}, + ) + except (ConnectionRefusedError, socket.timeout) as e: + _logger.warning( + 'fusion_helpdesk: connection problem to %s: %s', url, e, + ) + raise _RemoteError( + 'unreachable', + _('Could not reach %(url)s — connection refused or ' + 'timed out. Check that this server can make ' + 'outbound HTTPS to %(host)s (firewall, proxy).' + ) % {'url': url, 'host': urlparse(url).hostname or url}, + ) + except OSError as e: + # Catch-all for socket / network issues we didn't classify. + _logger.warning( + 'fusion_helpdesk: network error to %s: %s', url, e, + ) + raise _RemoteError( + 'unreachable', + _('Network error reaching %(url)s: %(msg)s. Verify ' + 'this server has outbound internet access.' + ) % {'url': url, 'msg': str(e)}, + ) + except _RemoteError: + raise + except Exception as e: + _logger.exception( + 'fusion_helpdesk: unexpected error during authenticate ' + 'against %s', url, + ) + raise _RemoteError( + 'unknown_error', + _('Unexpected error contacting the central Helpdesk: %s' + ) % str(e), + ) + + if not uid: + raise _RemoteError( + 'auth_failed', + _('The central Odoo at %(url)s did not accept the ' + 'login "%(login)s". Verify the Service Login and ' + 'API Key in Settings → Fusion Helpdesk.' + ) % {'url': url, 'login': cfg['login']}, + ) + + models_proxy = xmlrpc.client.ServerProxy( + '%s/xmlrpc/2/object' % url, allow_none=True, + ) + return uid, models_proxy + + def _build_diag_block(self, page_url, user_agent): + env = request.env + company = env.company + user = env.user + rows = [ + ('User', '%s (#%s, %s)' % (user.name, user.id, user.login)), + ('Company', '%s (#%s)' % (company.name, company.id)), + ('Source page', page_url or '—'), + ('User agent', user_agent or '—'), + ('Source DB', request.env.cr.dbname), + ('Source host', request.httprequest.host_url), + ] + body = '

Diagnostic context

' + for k, v in rows: + body += ( + '' + '' + ) % (_html_escape(k), _html_escape(str(v))) + body += '
%s%s
' + return body + + +def _html_escape(s): + return ( + (s or '') + .replace('&', '&') + .replace('<', '<') + .replace('>', '>') + ) + + +class _RemoteError(Exception): + """Typed wrapper that carries an `error` code + a friendly message + so the dialog can show the right thing.""" + def __init__(self, code, message): + super().__init__(message) + self.code = code + self.message = message + + def to_response(self): + return {'ok': False, 'error': self.code, 'message': self.message} + + +def _network_error_response(url, err): + """Map a raw network exception raised mid-RPC into the same + response shape the dialog already knows how to render.""" + host = urlparse(url).hostname or url + if isinstance(err, ssl.SSLError): + return { + 'ok': False, 'error': 'tls_error', + 'message': _('TLS / SSL error talking to %(url)s: %(msg)s' + ) % {'url': url, 'msg': str(err)}, + } + if isinstance(err, socket.gaierror): + return { + 'ok': False, 'error': 'dns_error', + 'message': _('Could not resolve "%s" — check DNS / outbound network.' + ) % host, + } + if isinstance(err, socket.timeout): + return { + 'ok': False, 'error': 'unreachable', + 'message': _('Timed out talking to %s — check the firewall ' + 'allows outbound HTTPS.') % url, + } + return { + 'ok': False, 'error': 'unreachable', + 'message': _('Network error talking to %(url)s: %(msg)s' + ) % {'url': url, 'msg': str(err)}, + } diff --git a/fusion_helpdesk/data/ir_config_parameter_data.xml b/fusion_helpdesk/data/ir_config_parameter_data.xml new file mode 100644 index 00000000..0ef084e8 --- /dev/null +++ b/fusion_helpdesk/data/ir_config_parameter_data.xml @@ -0,0 +1,32 @@ + + + + + + fusion_helpdesk.remote_url + https://erp.nexasystems.ca + + + + fusion_helpdesk.remote_db + nexamain + + + + fusion_helpdesk.remote_login + helpdesk_bot@nexasystems.ca + + + + fusion_helpdesk.client_label + + + + diff --git a/fusion_helpdesk/models/__init__.py b/fusion_helpdesk/models/__init__.py new file mode 100644 index 00000000..6084d2ca --- /dev/null +++ b/fusion_helpdesk/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import res_config_settings diff --git a/fusion_helpdesk/models/res_config_settings.py b/fusion_helpdesk/models/res_config_settings.py new file mode 100644 index 00000000..91aef7e6 --- /dev/null +++ b/fusion_helpdesk/models/res_config_settings.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 +"""Configuration for the Fusion Helpdesk Reporter. + +Stores the central Odoo Helpdesk endpoint that submissions are +forwarded to. Defaults point at erp.nexasystems.ca / nexamain; +each client deployment can override per system parameter. +""" +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + fhd_remote_url = fields.Char( + string='Helpdesk Remote URL', + config_parameter='fusion_helpdesk.remote_url', + help='Base URL of the central Odoo running the Helpdesk app, ' + 'e.g. https://erp.nexasystems.ca', + ) + fhd_remote_db = fields.Char( + string='Helpdesk Remote DB', + config_parameter='fusion_helpdesk.remote_db', + help='Database name on the remote Odoo (e.g. nexamain).', + ) + fhd_remote_login = fields.Char( + string='Helpdesk Remote Login', + config_parameter='fusion_helpdesk.remote_login', + help='Service-account login on the remote Odoo. Needs create ' + 'rights on helpdesk.ticket and ir.attachment.', + ) + fhd_remote_password = fields.Char( + string='Helpdesk Remote Password / API Key', + config_parameter='fusion_helpdesk.remote_password', + help='Service-account password or API key. Stored in ' + 'ir.config_parameter — restrict read access if needed.', + ) + fhd_remote_team_id = fields.Integer( + string='Helpdesk Team ID', + config_parameter='fusion_helpdesk.remote_team_id', + help='Optional. ID of the helpdesk.team on the remote that ' + 'should own all incoming tickets. Leave blank to use ' + 'the remote default routing.', + ) + fhd_client_label = fields.Char( + string='Client Label (auto-prepended to subject)', + config_parameter='fusion_helpdesk.client_label', + help='Short tag prefixed onto the ticket subject so support ' + 'can tell which client deployment a ticket came from. ' + 'e.g. "ENTECH" → "[ENTECH] My subject"', + ) diff --git a/fusion_helpdesk/security/ir.model.access.csv b/fusion_helpdesk/security/ir.model.access.csv new file mode 100644 index 00000000..97dd8b91 --- /dev/null +++ b/fusion_helpdesk/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/fusion_helpdesk/static/description/icon.png b/fusion_helpdesk/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..20f0f3f6ccf18393f30a8474236e7c6c8fe8f93e GIT binary patch literal 36450 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 literal 0 HcmV?d00001 diff --git a/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js b/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js new file mode 100644 index 00000000..486e5351 --- /dev/null +++ b/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js @@ -0,0 +1,244 @@ +/** @odoo-module **/ +// Fusion Helpdesk — submission dialog. Lets the user pick Bug or +// Feature, fill in subject + description, paste an error code, attach +// files, and capture a screenshot via the browser's getDisplayMedia +// API. On submit, the payload is POSTed to /fusion_helpdesk/submit +// which forwards it (XML-RPC) to a central Odoo Helpdesk. + +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { rpc } from "@web/core/network/rpc"; +import { useService } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; + +const MAX_BYTES_PER_FILE = 10 * 1024 * 1024; // 10 MB hard cap per file + +export class FusionHelpdeskDialog extends Component { + static template = "fusion_helpdesk.Dialog"; + static components = { Dialog }; + static props = { + close: Function, + }; + + setup() { + this.notification = useService("notification"); + this.state = useState({ + kind: "bug", // 'bug' | 'feature' + subject: "", + description: "", + errorCode: "", + attachments: [], // [{name, mimetype, sizeLabel, iconClass, data_b64}] + capturing: false, + submitting: false, + error: "", + success: false, + ticketId: null, + ticketUrl: "", + attached: 0, + }); + } + + get dialogTitle() { + return this.state.kind === "bug" + ? _t("Report a Bug") + : _t("Request a Feature"); + } + + setKind(kind) { + this.state.kind = kind; + } + + // ------------------------------------------------------------------ + // File input → b64 + async onFilesPicked(ev) { + const files = Array.from(ev.target.files || []); + for (const f of files) { + if (f.size > MAX_BYTES_PER_FILE) { + this.notification.add( + _t("File '%s' is over 10 MB and was skipped.").replace("%s", f.name), + { type: "warning" } + ); + continue; + } + try { + const b64 = await this._fileToB64(f); + this._addAttachment({ + name: f.name, + mimetype: f.type || "application/octet-stream", + data_b64: b64, + rawSize: f.size, + }); + } catch (err) { + this.notification.add( + _t("Could not read '%s'.").replace("%s", f.name), + { type: "danger" } + ); + } + } + // Reset the input so picking the same file again re-fires onchange. + ev.target.value = ""; + } + + _fileToB64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result || ""; + const idx = result.indexOf(","); + resolve(idx >= 0 ? result.slice(idx + 1) : result); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + // ------------------------------------------------------------------ + // Screenshot capture via getDisplayMedia + async onTakeScreenshot() { + if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) { + this.notification.add( + _t("Your browser doesn't support screen capture. Use Attach files instead."), + { type: "warning" } + ); + return; + } + this.state.capturing = true; + let stream; + try { + stream = await navigator.mediaDevices.getDisplayMedia({ + video: { displaySurface: "browser" }, + audio: false, + }); + const blob = await this._streamToBlob(stream); + const b64 = await this._blobToB64(blob); + const ts = new Date().toISOString().replace(/[:.]/g, "-"); + this._addAttachment({ + name: `screenshot-${ts}.png`, + mimetype: "image/png", + data_b64: b64, + rawSize: blob.size, + }); + } catch (err) { + // User cancelled the picker — silently swallow. Other errors → notify. + if (err && err.name !== "NotAllowedError" && err.name !== "AbortError") { + this.notification.add( + _t("Screenshot failed: %s").replace("%s", err.message || err), + { type: "danger" } + ); + } + } finally { + if (stream) { + stream.getTracks().forEach((t) => t.stop()); + } + this.state.capturing = false; + } + } + + async _streamToBlob(stream) { + const video = document.createElement("video"); + video.srcObject = stream; + await video.play(); + // Give the browser one frame to settle the picker chrome. + await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext("2d"); + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + return await new Promise((resolve) => + canvas.toBlob((b) => resolve(b), "image/png", 0.92) + ); + } + + _blobToB64(blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result || ""; + const idx = result.indexOf(","); + resolve(idx >= 0 ? result.slice(idx + 1) : result); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } + + _addAttachment({ name, mimetype, data_b64, rawSize }) { + this.state.attachments.push({ + name, + mimetype, + data_b64, + sizeLabel: this._formatBytes(rawSize), + iconClass: this._iconForMime(mimetype), + }); + } + + removeAttachment(idx) { + this.state.attachments.splice(idx, 1); + } + + _formatBytes(n) { + if (!n) return ""; + if (n < 1024) return n + " B"; + if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB"; + return (n / (1024 * 1024)).toFixed(1) + " MB"; + } + + _iconForMime(mt) { + mt = (mt || "").toLowerCase(); + if (mt.startsWith("image/")) return "fa fa-file-image-o"; + if (mt.startsWith("video/")) return "fa fa-file-video-o"; + if (mt.startsWith("audio/")) return "fa fa-file-audio-o"; + if (mt.includes("pdf")) return "fa fa-file-pdf-o"; + if (mt.includes("zip") || mt.includes("tar") || mt.includes("rar")) return "fa fa-file-archive-o"; + if (mt.includes("text")) return "fa fa-file-text-o"; + return "fa fa-file-o"; + } + + // ------------------------------------------------------------------ + // Submit + async onSubmit() { + if (this.state.submitting) return; + const subject = (this.state.subject || "").trim(); + if (!subject) { + this.state.error = _t("Subject is required."); + return; + } + this.state.error = ""; + this.state.success = false; + this.state.submitting = true; + try { + const payload = { + kind: this.state.kind, + subject, + description: this.state.description || "", + error_code: this.state.kind === "bug" ? this.state.errorCode || "" : "", + attachments: this.state.attachments.map((a) => ({ + name: a.name, + mimetype: a.mimetype, + data_b64: a.data_b64, + })), + page_url: window.location.href, + user_agent: navigator.userAgent, + }; + const res = await rpc("/fusion_helpdesk/submit", payload); + if (!res.ok) { + this.state.error = res.message || _t("Submission failed."); + } else { + this.state.success = true; + this.state.ticketId = res.ticket_id; + this.state.ticketUrl = res.ticket_url; + this.state.attached = res.attached || 0; + // Reset the editable fields so user can file another if they want. + this.state.subject = ""; + this.state.description = ""; + this.state.errorCode = ""; + this.state.attachments = []; + } + } catch (err) { + this.state.error = (err && err.message) || _t("Network error."); + } finally { + this.state.submitting = false; + } + } +} diff --git a/fusion_helpdesk/static/src/js/fusion_helpdesk_systray.js b/fusion_helpdesk/static/src/js/fusion_helpdesk_systray.js new file mode 100644 index 00000000..16761db7 --- /dev/null +++ b/fusion_helpdesk/static/src/js/fusion_helpdesk_systray.js @@ -0,0 +1,33 @@ +/** @odoo-module **/ +// Fusion Helpdesk — top systray icon. Sequence chosen so the icon +// appears to the LEFT of the attendance check-in button. Odoo +// systray ordering is by sequence ascending (lower = leftmost in the +// systray bar). hr_attendance ships at sequence 100, so we use 99. + +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { FusionHelpdeskDialog } from "./fusion_helpdesk_dialog"; + +class FusionHelpdeskSystray extends Component { + static template = "fusion_helpdesk.SystrayItem"; + static props = {}; + + setup() { + this.dialog = useService("dialog"); + } + + onClick() { + this.dialog.add(FusionHelpdeskDialog, {}); + } +} + +export const fusionHelpdeskSystrayItem = { + Component: FusionHelpdeskSystray, +}; + +registry + .category("systray") + .add("fusion_helpdesk.report_button", fusionHelpdeskSystrayItem, { + sequence: 99, + }); diff --git a/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss b/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss new file mode 100644 index 00000000..0fa58361 --- /dev/null +++ b/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss @@ -0,0 +1,172 @@ +// Fusion Helpdesk Reporter — systray button + dialog styling. +// Dark-mode aware via Odoo's $o-webclient-color-scheme branch. + +$o-webclient-color-scheme: bright !default; + +$_fhd-text-hex: #21252b; +$_fhd-muted-hex: #6c757d; +$_fhd-bg-hex: #ffffff; +$_fhd-border-hex: #d8dadd; +$_fhd-hover-hex: #f3f4f6; +$_fhd-accent-hex: #2c89e9; + +@if $o-webclient-color-scheme == dark { + $_fhd-text-hex: #e6e9ef !global; + $_fhd-muted-hex: #9aa3ad !global; + $_fhd-bg-hex: #22262d !global; + $_fhd-border-hex: #3a3f47 !global; + $_fhd-hover-hex: #2c313a !global; + $_fhd-accent-hex: #4ea3ff !global; +} + +$fhd-text: var(--fhd-text, $_fhd-text-hex); +$fhd-muted: var(--fhd-muted, $_fhd-muted-hex); +$fhd-bg: var(--fhd-bg, $_fhd-bg-hex); +$fhd-border: var(--fhd-border, $_fhd-border-hex); +$fhd-hover: var(--fhd-hover, $_fhd-hover-hex); +$fhd-accent: var(--fhd-accent, $_fhd-accent-hex); + +// Systray icon +.o_fhd_systray { + .o_fhd_systray_btn { + background: transparent; + border: none; + padding: 0 0.5rem; + line-height: 1; + cursor: pointer; + display: inline-flex; + align-items: center; + + &:hover .o_fhd_systray_img { + transform: scale(1.08); + filter: drop-shadow(0 0 2px rgba(78, 163, 255, 0.45)); + } + } + + .o_fhd_systray_img { + height: 22px; + width: 22px; + object-fit: contain; + transition: transform 120ms ease, filter 120ms ease; + } + + // Hide the dropdown caret Bootstrap injects. + .dropdown-toggle::after { display: none; } +} + +// Dialog +.o_fhd_dialog { + color: $fhd-text; + + .o_fhd_kind_row { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + } + + .o_fhd_kind_chip { + flex: 1; + padding: 0.6rem 0.75rem; + background-color: $fhd-bg; + border: 1px solid $fhd-border; + border-radius: 6px; + color: $fhd-text; + cursor: pointer; + font-weight: 500; + + &:hover { background-color: $fhd-hover; } + + &.o_fhd_kind_active { + border-color: $fhd-accent; + color: $fhd-accent; + background-color: rgba(44, 137, 233, 0.10); + } + } + + .o_fhd_field { + margin-bottom: 0.85rem; + + > label { + display: block; + font-weight: 500; + margin-bottom: 0.25rem; + color: $fhd-text; + } + } + + .o_fhd_hint { + font-weight: 400; + font-size: 0.8rem; + color: $fhd-muted; + margin-left: 0.4rem; + } + + .o_fhd_mono { + font-family: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace; + font-size: 0.85rem; + } + + .o_fhd_actions_row { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + + .o_fhd_btn { + display: inline-flex; + align-items: center; + padding: 0.4rem 0.8rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + border: 1px solid $fhd-border; + background-color: $fhd-bg; + color: $fhd-text; + + &:hover:not(:disabled) { background-color: $fhd-hover; } + + &:disabled { opacity: 0.6; cursor: default; } + } + + .o_fhd_attach_list { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 0.5rem; + } + + .o_fhd_attach_item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.6rem; + background-color: $fhd-hover; + border: 1px solid $fhd-border; + border-radius: 4px; + font-size: 0.85rem; + } + + .o_fhd_attach_name { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .o_fhd_attach_size { + color: $fhd-muted; + font-variant-numeric: tabular-nums; + } + + .o_fhd_attach_remove { + background: transparent; + border: none; + color: $fhd-muted; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; + padding: 0 0.25rem; + + &:hover { color: #d32f2f; } + } +} diff --git a/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml b/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml new file mode 100644 index 00000000..f53497f8 --- /dev/null +++ b/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml @@ -0,0 +1,110 @@ + + + + + +
+ +
+ + +
+ + +
+ + +
+ + +
+