diff --git a/document_url_google_drive/README.rst b/document_url_google_drive/README.rst new file mode 100644 index 00000000..38929e87 --- /dev/null +++ b/document_url_google_drive/README.rst @@ -0,0 +1,35 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. + + +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`HISTORY.rst` can be auto generated using `towncrier `_. + +Just put towncrier compatible changelog fragments into `readme/newsfragments` +and the changelog file will be automatically generated and updated when a new fragment is added. + +Please refer to `towncrier` documentation to know more. + +NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. +If you need to run it manually, refer to `OCA/maintainer-tools README `_. diff --git a/document_url_google_drive/__init__.py b/document_url_google_drive/__init__.py new file mode 100644 index 00000000..9fd0534a --- /dev/null +++ b/document_url_google_drive/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models, wizard diff --git a/document_url_google_drive/__manifest__.py b/document_url_google_drive/__manifest__.py new file mode 100644 index 00000000..72c079b0 --- /dev/null +++ b/document_url_google_drive/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +{ + "name": "Google Drive URL attachment", + "version": "16.0.1.0.0", + "category": "Tools", + "website": "https://github.com/OCA/knowledge", + "author": "Cetmix, Odoo Community Association (OCA)", + "maintainers": ["CetmixGitDrone"], + "license": "AGPL-3", + "installable": True, + "depends": [ + "document_url", + ], + "data": [ + "views/res_users_view.xml", + "views/res_config_settings_view.xml", + ], + "assets": { + "web.assets_backend": [ + "document_url_google_drive/static/src/js/attachment_google_picker.esm.js", + "document_url_google_drive/static/src/xml/google_picker_url.xml", + "document_url_google_drive/static/src/xml/attachment_google_picker.xml", + ], + }, +} diff --git a/document_url_google_drive/models/__init__.py b/document_url_google_drive/models/__init__.py new file mode 100644 index 00000000..bb64651b --- /dev/null +++ b/document_url_google_drive/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import res_users, res_config_settings diff --git a/document_url_google_drive/models/res_config_settings.py b/document_url_google_drive/models/res_config_settings.py new file mode 100644 index 00000000..883f35e7 --- /dev/null +++ b/document_url_google_drive/models/res_config_settings.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + is_active_google_api = fields.Boolean( + string="Google APIs", + config_parameter="document_url_google_drive.is_active_google_api", + ) diff --git a/document_url_google_drive/models/res_users.py b/document_url_google_drive/models/res_users.py new file mode 100644 index 00000000..1be9bb90 --- /dev/null +++ b/document_url_google_drive/models/res_users.py @@ -0,0 +1,51 @@ +# Copyright (C) 2023 Cetmix OÜ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + google_picker_client_id = fields.Char(string="Google Client ID") + google_picker_api_key = fields.Char(string="Google API Key") + google_picker_scope = fields.Char( + string="Google Scope", + default="https://www.googleapis.com/auth/drive.readonly", + ) + google_picker_app_id = fields.Char( + string="Google App ID", + default="odoo", + ) + google_picker_access_token = fields.Char(string="Google Access Token") + google_picker_mime_types = fields.Char(string="Google Mime Types") + + def get_google_picker_params(self): + """ + Get Google Picker params + :return: dict + """ + self.ensure_one() + is_active_google_api = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("document_url_google_drive.is_active_google_api") + ) + if not is_active_google_api: + return {} + return { + "client_id": self.google_picker_client_id, + "api_key": self.google_picker_api_key, + "scope": self.google_picker_scope, + "app_id": self.google_picker_app_id, + "access_token": self.google_picker_access_token, + "mime_types": self.google_picker_mime_types, + } + + def save_google_picker_access_token(self, access_token): + """ + Save Google Picker access token + :param access_token: str + :return: None + """ + self.ensure_one() + self.google_picker_access_token = access_token diff --git a/document_url_google_drive/readme/CONFIGURE.md b/document_url_google_drive/readme/CONFIGURE.md new file mode 100644 index 00000000..e04e247e --- /dev/null +++ b/document_url_google_drive/readme/CONFIGURE.md @@ -0,0 +1,22 @@ +To configure this module, you need to: + +- Go to Settings -> General Settings and scroll down to the Integrations section. + +- Enable "Google API", save. + +- Next, open your user profile and set up personal access credentials on the "Google + API" tab. + + - field "Google Client ID" - enter the client ID from the Google API console. + - field "Google API key" - enter the API key from the Google API console. + - field "Google Scope" - enter the scope for the Google API. The default value is + `https://www.googleapis.com/auth/drive.readonly`. + - field "Google App ID" - enter the ID of the Google application. The default value is + `odoo`. + - field "Google Access Token" - your token will be displayed here. It is necessary to + edit it. + - field "Google Mime Types" - enter the file formats to be filtered when selecting. + Example: `application/pdf, image/jpeg, image/png`. By default, all files are + selected + +- You will be asked to authenticate when you add a link for the first time. diff --git a/document_url_google_drive/readme/DESCRIPTION.md b/document_url_google_drive/readme/DESCRIPTION.md new file mode 100644 index 00000000..750f6b24 --- /dev/null +++ b/document_url_google_drive/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module extends the functionality of the document_url module and allows you to +attach a link to a file from your Google Drive diff --git a/document_url_google_drive/readme/USAGE.md b/document_url_google_drive/readme/USAGE.md new file mode 100644 index 00000000..29dfb7f4 --- /dev/null +++ b/document_url_google_drive/readme/USAGE.md @@ -0,0 +1,8 @@ +To use this module, you need to: + +#. Open the form view of an object (Example: Customer Invoice INV/2019/0007). #. Go to +the chatter and click on the attached icon. #. Click **Add GDrive link**. #. Fill the +wizard fields and click on Add button. #. In the open window, select the files you need +and press the select button. #. You will see a new **URL attachment** in the set of +attachments related to the object. #. In order to log in under another google user, +click on the logout icon located after **Add GDrive link** diff --git a/document_url_google_drive/readme/newsfragments/.gitkeep b/document_url_google_drive/readme/newsfragments/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/document_url_google_drive/static/description/icon.png b/document_url_google_drive/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/document_url_google_drive/static/description/icon.png differ diff --git a/document_url_google_drive/static/description/icon.svg b/document_url_google_drive/static/description/icon.svg new file mode 100644 index 00000000..a7a26d09 --- /dev/null +++ b/document_url_google_drive/static/description/icon.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/document_url_google_drive/static/description/index.html b/document_url_google_drive/static/description/index.html new file mode 100644 index 00000000..f92d18ab --- /dev/null +++ b/document_url_google_drive/static/description/index.html @@ -0,0 +1,124 @@ +
+
+
+

Module name

+

This module was written to extend the functionality of ... to support ... and allow you to ...

+
+
+
+ +
+
+
+

Installation

+
+
+

To install this module, you need to: +

    +
  • ...
  • +
+

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

Configuration

+
+
+

To configure this module, you need to: +

    +
  • ...
  • +
+

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

Usage

+
+
+

To use this module, you need to: +

    +
  • ...
  • +
+

+

For further information, please visit: +

+

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

Known issues / Roadmap

+
+
+

+

    +
  • ...
  • +
+

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

Credits

+
+
+

Contributors

+ +
+
+

Maintainer

+

+ This module is maintained by the OCA.
+ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.
+ To contribute to this module, please visit http://odoo-community.org.
+ +

+
+
+
diff --git a/document_url_google_drive/static/src/js/attachment_google_picker.esm.js b/document_url_google_drive/static/src/js/attachment_google_picker.esm.js new file mode 100644 index 00000000..976f0a1a --- /dev/null +++ b/document_url_google_drive/static/src/js/attachment_google_picker.esm.js @@ -0,0 +1,179 @@ +/** @odoo-module **/ + +import {loadJS} from "@web/core/assets"; +import {registerMessagingComponent} from "@mail/utils/messaging_component"; +import {useComponentToModel} from "@mail/component_hooks/use_component_to_model"; +import {useService} from "@web/core/utils/hooks"; + +const {Component, onWillStart, useState} = owl; + +export class AttachmentGooglePicker extends Component { + /** + * @override + */ + setup() { + super.setup(); + useComponentToModel({fieldName: "component"}); + this.orm = useService("orm"); + this.user = useService("user"); + this.action = useService("action"); + this.state = useState({ + pickerInited: false, + gisInited: false, + api_key: "", + scopes: "", + client_id: "", + app_id: "", + accessToken: null, + }); + this.tokenClient = null; + + onWillStart(async () => { + await this.getUserAuthParams(); + if (!this.checkActive()) { + return; + } + await loadJS("https://apis.google.com/js/api.js", { + attrs: {async: true, defer: true}, + }); + await this.gapiLoaded(); + await loadJS("https://accounts.google.com/gsi/client", { + attrs: {async: true, defer: true}, + }); + await this.gisLoaded(); + }); + } + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + _onAddGooglePickerUrl() { + console.log("Add Google Picker URL"); + this.handleAuthClick(); + } + + checkActive() { + return ( + Boolean(this.state.api_key) && + Boolean(this.state.scopes) && + Boolean(this.state.client_id) && + Boolean(this.state.app_id) + ); + } + + async _onSignOut() { + this.state.accessToken = null; + await this.saveUserAuthAccessToken(); + } + + // -------------------------------------------------------------------------- + // Private + // -------------------------------------------------------------------------- + + async getUserAuthParams() { + const res = await this.orm.call("res.users", "get_google_picker_params", [ + this.user.userId, + ]); + this.state.client_id = res.client_id; + this.state.api_key = res.api_key; + this.state.app_id = res.app_id; + this.state.scopes = res.scope; + this.state.accessToken = res.access_token; + this.state.mime_types = res.mime_types; + } + + async saveUserAuthAccessToken() { + this.orm.call("res.users", "save_google_picker_access_token", [ + this.user.userId, + this.state.accessToken, + ]); + } + + async gapiLoaded() { + window.gapi.load("client:picker", this.initializePicker.bind(this)); + } + + async initializePicker() { + await window.gapi.client.load( + "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest" + ); + this.state.pickerInited = true; + } + + handleAuthClick() { + this.tokenClient.callback = async (response) => { + if (response.error !== undefined) { + throw response; + } + this.state.accessToken = response.access_token; + await this.createPicker(); + await this.saveUserAuthAccessToken(); + }; + + if (this.state.accessToken === null) { + // Prompt the user to select a Google Account and ask for consent to share their data + // when establishing a new session. + this.tokenClient.requestAccessToken({prompt: "consent"}); + } else { + // Skip display of account chooser and consent dialog for an existing session. + this.tokenClient.requestAccessToken({prompt: ""}); + } + } + + async gisLoaded() { + this.tokenClient = window.google.accounts.oauth2.initTokenClient({ + client_id: this.state.client_id, + scope: this.state.scopes, + callback: "", + }); + this.state.gisInited = true; + } + + createPicker() { + const view = new window.google.picker.View(window.google.picker.ViewId.DOCS); + if (this.state.mime_types) { + view.setMimeTypes(this.state.mime_types); + } + const picker = new window.google.picker.PickerBuilder() + .enableFeature(window.google.picker.Feature.NAV_HIDDEN) + .enableFeature(window.google.picker.Feature.MULTISELECT_ENABLED) + .setDeveloperKey(this.state.api_key) + .setAppId(this.state.app_id) + .setOAuthToken(this.state.accessToken) + .addView(view) + .addView(new window.google.picker.DocsUploadView()) + .setCallback(this.pickerCallback.bind(this)) + .build(); + picker.setVisible(true); + } + + async pickerCallback(data) { + if (data.action === window.google.picker.Action.PICKED) { + data[window.google.picker.Response.DOCUMENTS].forEach((document) => { + this.createAttachment(document); + }); + await this._onAddedUrl(); + } + } + + async createAttachment(document) { + this.orm.call("ir.attachment.add_url", "add_attachment_google_drive", [ + document.url, + document.name, + this.props.record.chatter.thread.model, + [this.props.record.chatter.thread.id], + ]); + } + + async _onAddedUrl() { + this.props.record.chatter.refresh(); + } +} + +Object.assign(AttachmentGooglePicker, { + props: {record: Object}, + template: "document_url_google_drive.GooglePickerUrl", +}); + +registerMessagingComponent(AttachmentGooglePicker); diff --git a/document_url_google_drive/static/src/xml/attachment_google_picker.xml b/document_url_google_drive/static/src/xml/attachment_google_picker.xml new file mode 100644 index 00000000..e0af0d7d --- /dev/null +++ b/document_url_google_drive/static/src/xml/attachment_google_picker.xml @@ -0,0 +1,25 @@ + + + + + + +