diff --git a/pingen/README.rst b/pingen/README.rst new file mode 100644 index 00000000000..48de33a2ff0 --- /dev/null +++ b/pingen/README.rst @@ -0,0 +1,131 @@ +====================== +pingen.com integration +====================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Freport--print--send-lightgray.png?logo=github + :target: https://github.com/OCA/report-print-send/tree/16.0-mig-pingen/pingen + :alt: OCA/report-print-send +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/report-print-send-16-0-mig-pingen/report-print-send-16-0-mig-pingen-pingen + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/144/16.0-mig-pingen + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Pingen.com is a paid online service. +It sends uploaded documents by letter post. + +One can decide, per document / attachment, if it should be pushed +to pingen.com. The documents are pushed asynchronously. + +The informations of the documents from pingen.com are updated through webhook calls. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +The authentication token, client ID, organization ID and webhook secret is configured +on the company's view. You can also tick a checkbox if the staging environment +(https://stage-api.pingen.com) should be used. + +Webhooks should be configured on pingen account. Organization ID and webhook secret must match. + +Usage +===== + +On the attachment view, a new pingen.com section has been added. +You can tick a box to push the document to pingen.com. + +There is 3 additional options: + + * Send: the document will not be only uploaded, but will be also be sent + * Speed: priority or economy + * Type of print: color or black and white + +Once the configuration is done and the attachment saved, a Pingen Document +is created. You can directly access to the latter on the Link on the right on +the attachment view. + +You can find them in `Pingen Documents` App or in the more convenient `Documents` menu if you have installed the +`document` module. + +Errors +====== + +Sometimes, pingen.com will refuse to send a document because it does not meet +its requirements. In such case, the document's state becomes "Pingen Error" +and you will need to manually handle the case, either from the pingen.com +backend, or by changing the document on OpenERP and resolving the error on the +Pingen Document. + +When a connection error occurs, the action will be retried on the next +scheduler run. + + +Dependencies +============ + + * Require the Python library `requests_oauthlib `_ + * The address must be in a format accepted by pingen.com: the last line + is the country in English or German. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Guewen Baconnier +* Anar Baghirli +* Akim Juillerat +* Anna Janiszewska + + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/report-print-send `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pingen/__init__.py b/pingen/__init__.py new file mode 100644 index 00000000000..91c5580fed3 --- /dev/null +++ b/pingen/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/pingen/__manifest__.py b/pingen/__manifest__.py new file mode 100644 index 00000000000..314737bf3c9 --- /dev/null +++ b/pingen/__manifest__.py @@ -0,0 +1,27 @@ +# Author: Guewen Baconnier +# Copyright 2012-2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "pingen.com integration", + "version": "16.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "maintainers": ["ajaniszewska-dev", "grindtildeath"], + "license": "AGPL-3", + "category": "Reporting", + "maturity": "Production/Stable", + "depends": ["base_setup"], + "external_dependencies": { + "python": ["requests_oauthlib", "oauthlib"], + }, + "website": "https://github.com/OCA/report-print-send", + "data": [ + "views/ir_attachment_view.xml", + "views/pingen_document_view.xml", + "data/pingen_data.xml", + "views/base_config_settings.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": True, +} diff --git a/pingen/controllers/__init__.py b/pingen/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/pingen/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/pingen/controllers/main.py b/pingen/controllers/main.py new file mode 100644 index 00000000000..d2b9eb6097f --- /dev/null +++ b/pingen/controllers/main.py @@ -0,0 +1,141 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import hashlib +import hmac +import json +import logging + +import werkzeug + +from odoo import http + +from ..models.pingen import pingen_datetime_to_utc + +_logger = logging.getLogger(__name__) + + +class PingenController(http.Controller): + def _verify_signature(self, request_content): + webhook_signature = http.request.httprequest.headers.get("Signature") + companies = http.request.env["res.company"].sudo().search([]) + for company in companies: + # We could not search on `pingen_webhook_secret + # if this field is computed (e.g. env field) + if not company.pingen_webhook_secret: + continue + secret_signature = hmac.new( + company.pingen_webhook_secret.encode("utf-8"), + request_content, + hashlib.sha256, + ).hexdigest() + if webhook_signature == secret_signature: + return company + msg = "Webhook signature does not match with any company secret" + _logger.warning(msg) + raise werkzeug.exceptions.Forbidden() + + def _get_request_content(self): + return http.request.httprequest.stream.read() + + def _get_json_content(self, request_content): + return json.loads(request_content) + + def _get_document_uuid(self, json_content): + return ( + json_content.get("data", {}) + .get("relationships", {}) + .get("letter", {}) + .get("data", {}) + .get("id", "") + ) + + def _find_pingen_document(self, document_uuid): + if document_uuid: + return http.request.env["pingen.document"].search( + [("pingen_uuid", "=", document_uuid)] + ) + return http.request.env["pingen.document"].browse() + + def _get_error_reason(self, json_content): + return json_content.get("data", {}).get("attributes", {}).get("reason", "") + + def _get_letter_infos(self, json_content, document_uuid): + for node in json_content.get("included", {}): + if node.get("type") == "letters" and node.get("id") == document_uuid: + return node.get("attributes", {}) + return {} + + def _get_emitted_date(self, json_content): + emitted_at = "" + for node in json_content.get("included", {}): + if node.get("type") == "letters_events": + attributes = node.get("attributes", {}) + if attributes.get("code") == "transferred_to_distributor": + emitted_at = attributes.get("emitted_at", "") + break + return pingen_datetime_to_utc(emitted_at.encode()) + + def _update_pingen_document(self, request_content, values): + json_content = self._get_json_content(request_content) + document_uuid = self._get_document_uuid(json_content) + pingen_doc = self._find_pingen_document(document_uuid) + if pingen_doc: + info_values = pingen_doc._prepare_values_from_post_infos( + self._get_letter_infos(json_content, document_uuid) + ) + info_values.update(values) + pingen_doc.sudo().write(info_values) + msg = "Pingen document with UUID %s updated successfully" % document_uuid + _logger.info(msg) + return msg + msg = "Could not find related Pingen document for UUID %s" % document_uuid + _logger.warning(msg) + return msg + + @http.route( + "/pingen/letter_issues", type="http", auth="none", methods=["POST"], csrf=False + ) + def letter_issues(self, **post): + _logger.info("Webhook call received on /pingen/letter_issues") + request_content = self._get_request_content() + json_content = self._get_json_content(request_content) + self._verify_signature(request_content) + values = { + "state": "pingen_error", + "last_error_message": self._get_error_reason(json_content), + } + self._update_pingen_document(request_content, values) + + @http.route( + "/pingen/sent_letters", type="http", auth="none", methods=["POST"], csrf=False + ) + def sent_letters(self, **post): + _logger.info("Webhook call received on /pingen/sent_letters") + request_content = self._get_request_content() + json_content = self._get_json_content(request_content) + self._verify_signature(request_content) + emitted_date = self._get_emitted_date(json_content) + values = { + "state": "sent", + } + if emitted_date: + values["send_date"] = emitted_date + self._update_pingen_document(request_content, values) + + @http.route( + "/pingen/undeliverable_letters", + type="http", + auth="none", + methods=["POST"], + csrf=False, + ) + def undeliverable_letters(self, **post): + _logger.info("Webhook call received on /pingen/undeliverable_letters") + request_content = self._get_request_content() + json_content = self._get_json_content(request_content) + self._verify_signature(request_content) + values = { + "state": "error_undeliverable", + "last_error_message": self._get_error_reason(json_content), + } + self._update_pingen_document(request_content, values) diff --git a/pingen/data/pingen_data.xml b/pingen/data/pingen_data.xml new file mode 100644 index 00000000000..424b0ce526b --- /dev/null +++ b/pingen/data/pingen_data.xml @@ -0,0 +1,30 @@ + + + + + + Run Pingen Document Push + + + 1 + hours + -1 + + + model._push_and_send_to_pingen_cron + + + + Run Pingen Document Update + + + 1 + days + -1 + + + model._update_post_infos_cron + + + + diff --git a/pingen/i18n/fr.po b/pingen/i18n/fr.po new file mode 100644 index 00000000000..9ab65831782 --- /dev/null +++ b/pingen/i18n/fr.po @@ -0,0 +1,556 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * pingen +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 6.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-11-26 10:55+0000\n" +"PO-Revision-Date: 2014-02-25 15:09+0000\n" +"Last-Translator: Yannick Vaucher @ Camptocamp \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2014-05-06 07:28+0000\n" +"X-Generator: Launchpad (build 16996)\n" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Actions" +msgstr "Actions" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Ask pingen.com to send the document" +msgstr "Demander à pingen.com d'envoyer le document" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Attached To" +msgstr "Attaché à" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Attachment" +msgstr "Attachement" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_name +#, fuzzy +msgid "Attachment Name" +msgstr "Attachement" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_local_url +#, fuzzy +msgid "Attachment URL" +msgstr "Attachement" + +#. module: pingen +#: selection:ir.attachment,pingen_color:0 +msgid "B/W" +msgstr "N/B" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Canceled" +msgstr "Annulé" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_checksum +msgid "Checksum/SHA1" +msgstr "" + +#. module: pingen +#: selection:ir.attachment,pingen_color:0 +msgid "Color" +msgstr "Color" + +#. module: pingen +#: model:ir.model,name:pingen.model_res_company +msgid "Companies" +msgstr "Compagnies" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_company_id +#, fuzzy +msgid "Company" +msgstr "Compagnies" + +#. module: pingen +#: selection:pingen.document,state:0 +msgid "Connection Error" +msgstr "Erreur de connexion" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:128 +#: code:addons/pingen/models/pingen_document.py:246 +#, python-format +msgid "Connection Error when asking for sending the document %s to Pingen" +msgstr "Erreur de connexion avec Pingen lors de l'envoi de %s" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:337 +#, python-format +msgid "Connection Error when updating the status of Document %s from Pingen" +msgstr "" +"Erreur de connexion lors de la mise à jour de l'état du document %s depuis " +"Pingen" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_cost +msgid "Cost" +msgstr "Coût" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_country_id +msgid "Country" +msgstr "Pays" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_create_uid +msgid "Created by" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_create_date +msgid "Created on" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_currency_id +msgid "Currency" +msgstr "Devise" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Data" +msgstr "Données" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_db_datas +msgid "Database Data" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_send_date +msgid "Date of sending" +msgstr "Date d'envoi" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Dates" +msgstr "Dates" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_ir_attachment_pingen_send +#: model:ir.model.fields,help:pingen.field_pingen_document_pingen_send +msgid "Defines if a document is merely uploaded or also sent" +msgstr "Définit si un fichier est juste ajouté ou également envoyé" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_ir_attachment_pingen_speed +#: model:ir.model.fields,help:pingen.field_pingen_document_pingen_speed +msgid "Defines the sending speed if the document is automatically sent" +msgstr "Définit la vitesse d'envoi si le document est automatiquement envoyé" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_description +msgid "Description" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_display_name +msgid "Display Name" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_attachment_id +msgid "Document" +msgstr "Document" + +#. module: pingen +#: selection:ir.attachment,pingen_speed:0 +msgid "Economy" +msgstr "Économique" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +msgid "Error" +msgstr "Erreur" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_last_error_message +msgid "Error Message" +msgstr "Message d'erreur" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:132 +#: code:addons/pingen/models/pingen_document.py:251 +#, python-format +msgid "" +"Error when asking Pingen to send the document %s: \n" +"%s" +msgstr "" +"Erreurs lors de l'envoi du document par Pingen %s: \n" +"%s" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:341 +#, python-format +msgid "" +"Error when updating the status of Document %s from Pingen: \n" +"%s" +msgstr "" +"Erreur lors de la mise à jour de l'état du document %s depuis Pingen: \n" +"%s" + +#. module: pingen +#: code:addons/pingen/models/ir_attachment.py:64 +#, fuzzy, python-format +msgid "Error. The attachment %s is already pushed to pingen.com." +msgstr "L'attachement %s est déjà envoyé sur pingen.com." + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Errors" +msgstr "Erreurs" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Errors resolved" +msgstr "Erreurs résolues" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_datas +msgid "File Content" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_datas_fname +msgid "File Name" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_file_size +msgid "File Size" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_id +msgid "ID" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_pingen_id +msgid "ID of the document in the Pingen Documents" +msgstr "ID du document sur Pingen" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_post_id +msgid "ID of the document in the Pingen Sendcenter" +msgstr "ID du document dans le Sendcenter Pingen" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "In Sendcenter" +msgstr "Dans le Sendcenter" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_index_content +msgid "Indexed Content" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_public +#, fuzzy +msgid "Is public document" +msgstr "pingen.document" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document___last_update +msgid "Last Modified on" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_write_date +msgid "Last Updated on" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_mimetype +msgid "Mime Type" +msgstr "" + +#. module: pingen +#: sql_constraint:pingen.document:0 +msgid "Only one Pingen document is allowed per attachment." +msgstr "Uniquement un document Pingen est autorisé par attachement." + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Options" +msgstr "Options" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pages +msgid "Pages" +msgstr "Pages" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_parsed_address +msgid "Parsed Address" +msgstr "Adresse analysée" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Pending" +msgstr "En attente" + +#. module: pingen +#: model:ir.actions.act_window,name:pingen.act_attachment_to_pingen_document +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_document_ids +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_document_ids +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_tree +msgid "Pingen Document" +msgstr "Document Pingen" + +#. module: pingen +#: model:ir.actions.act_window,name:pingen.action_pingen_document +#: model:ir.ui.menu,name:pingen.menu_pingen_document +msgid "Pingen Documents" +msgstr "Documents Pingen" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Pingen Error" +msgstr "Erreur Pingen" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_id +msgid "Pingen ID" +msgstr "ID Pingen" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.base_config_settings_inherit +#, fuzzy +msgid "Pingen Integration" +msgstr "Staging Pingen" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_post_id +msgid "Pingen Post ID" +msgstr "ID de lettre Pingen" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_base_config_settings_pingen_staging +#: model:ir.model.fields,field_description:pingen.field_res_company_pingen_staging +msgid "Pingen Staging" +msgstr "Staging Pingen" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_base_config_settings_pingen_token +#: model:ir.model.fields,field_description:pingen.field_res_company_pingen_token +msgid "Pingen Token" +msgstr "Token Pingen" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_attachment_form +#, fuzzy +msgid "Pingen info" +msgstr "Staging Pingen" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Pingen.com" +msgstr "Pingen.com" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_post_status +msgid "Post Status" +msgstr "État de la lettre" + +#. module: pingen +#: selection:ir.attachment,pingen_speed:0 +msgid "Priority" +msgstr "Priorité" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_push_date +msgid "Push Date" +msgstr "Date d'ajout" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Push to pingen.com" +msgstr "Ajouter sur pingen.com" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Pushed" +msgstr "Ajouté" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_field +msgid "Resource Field" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_id +msgid "Resource ID" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_model +msgid "Resource Model" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_name +msgid "Resource Name" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_send +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_send +msgid "Send" +msgstr "Envoyer" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_send_to_pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_send_to_pingen +msgid "Send to Pingen.com" +msgstr "Ajouter sur Pingen.com" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Sendcenter" +msgstr "Sendcenter" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Sent" +msgstr "Envoyé" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_speed +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_speed +msgid "Speed" +msgstr "Vitesse" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_state +msgid "State" +msgstr "État" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_store_fname +msgid "Stored Filename" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_res_model +msgid "The database object this attachment will be attached to." +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:103 +#, python-format +msgid "The document does not meet the Pingen requirements." +msgstr "Le document ne remplit pas les exigences de Pingen" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_res_id +msgid "The record id this is attached to." +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/ir_attachment.py:97 +#, python-format +msgid "The type of attachment %s is not handled" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_type +msgid "Type" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_color +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_color +msgid "Type of print" +msgstr "Type d'impression" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:138 +#: code:addons/pingen/models/pingen_document.py:259 +#: code:addons/pingen/models/pingen_document.py:348 +#, fuzzy, python-format +msgid "Unexpected Error when updating the status of Document %s" +msgstr "Erreur inattendue lors de la mise à jour du document %s" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Update the letter's informations" +msgstr "Mettre à jour les informations de la lettre" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_url +msgid "Url" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_type +msgid "" +"You can either upload a file from your computer or copy/paste an internet " +"link to your file." +msgstr "" + +#. module: pingen +#: model:ir.model,name:pingen.model_base_config_settings +msgid "base.config.settings" +msgstr "" + +#. module: pingen +#: model:ir.model,name:pingen.model_ir_attachment +msgid "ir.attachment" +msgstr "ir.attachment" + +#. module: pingen +#: model:ir.model,name:pingen.model_pingen_document +msgid "pingen.document" +msgstr "pingen.document" + +#~ msgid "Error! You can not create recursive companies." +#~ msgstr "Error! You can not create recursive companies." + +#~ msgid "Configuration" +#~ msgstr "Configuration" + +#~ msgid "The company name must be unique !" +#~ msgstr "The company name must be unique !" + +#~ msgid "Notes" +#~ msgstr "Notes" + +#~ msgid "Pingen Connection Error" +#~ msgstr "Erreur de connexion avec Pingen" + +#~ msgid "pingen.task" +#~ msgstr "pingen.task" diff --git a/pingen/i18n/pingen.pot b/pingen/i18n/pingen.pot new file mode 100644 index 00000000000..b3bda2a6032 --- /dev/null +++ b/pingen/i18n/pingen.pot @@ -0,0 +1,519 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pingen +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Actions" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Ask pingen.com to send the document" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Attached To" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Attachment" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_name +msgid "Attachment Name" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_local_url +msgid "Attachment URL" +msgstr "" + +#. module: pingen +#: selection:ir.attachment,pingen_color:0 +msgid "B/W" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Canceled" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_checksum +msgid "Checksum/SHA1" +msgstr "" + +#. module: pingen +#: selection:ir.attachment,pingen_color:0 +msgid "Color" +msgstr "" + +#. module: pingen +#: model:ir.model,name:pingen.model_res_company +msgid "Companies" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_company_id +msgid "Company" +msgstr "" + +#. module: pingen +#: selection:pingen.document,state:0 +msgid "Connection Error" +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:128 +#: code:addons/pingen/models/pingen_document.py:246 +#, python-format +msgid "Connection Error when asking for sending the document %s to Pingen" +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:337 +#, python-format +msgid "Connection Error when updating the status of Document %s from Pingen" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_cost +msgid "Cost" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_country_id +msgid "Country" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_create_uid +msgid "Created by" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_create_date +msgid "Created on" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_currency_id +msgid "Currency" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Data" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_db_datas +msgid "Database Data" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_send_date +msgid "Date of sending" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Dates" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_ir_attachment_pingen_send +#: model:ir.model.fields,help:pingen.field_pingen_document_pingen_send +msgid "Defines if a document is merely uploaded or also sent" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_ir_attachment_pingen_speed +#: model:ir.model.fields,help:pingen.field_pingen_document_pingen_speed +msgid "Defines the sending speed if the document is automatically sent" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_description +msgid "Description" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_display_name +msgid "Display Name" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_attachment_id +msgid "Document" +msgstr "" + +#. module: pingen +#: selection:ir.attachment,pingen_speed:0 +msgid "Economy" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +msgid "Error" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_last_error_message +msgid "Error Message" +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:132 +#: code:addons/pingen/models/pingen_document.py:251 +#, python-format +msgid "Error when asking Pingen to send the document %s: \n" +"%s" +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:341 +#, python-format +msgid "Error when updating the status of Document %s from Pingen: \n" +"%s" +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/ir_attachment.py:64 +#, python-format +msgid "Error. The attachment %s is already pushed to pingen.com." +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Errors" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Errors resolved" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_datas +msgid "File Content" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_datas_fname +msgid "File Name" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_file_size +msgid "File Size" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_id +msgid "ID" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_pingen_id +msgid "ID of the document in the Pingen Documents" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_post_id +msgid "ID of the document in the Pingen Sendcenter" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "In Sendcenter" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_index_content +msgid "Indexed Content" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_public +msgid "Is public document" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document___last_update +msgid "Last Modified on" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_write_date +msgid "Last Updated on" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_mimetype +msgid "Mime Type" +msgstr "" + +#. module: pingen +#: sql_constraint:pingen.document:0 +msgid "Only one Pingen document is allowed per attachment." +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Options" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pages +msgid "Pages" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_parsed_address +msgid "Parsed Address" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Pending" +msgstr "" + +#. module: pingen +#: model:ir.actions.act_window,name:pingen.act_attachment_to_pingen_document +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_document_ids +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_document_ids +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_tree +msgid "Pingen Document" +msgstr "" + +#. module: pingen +#: model:ir.actions.act_window,name:pingen.action_pingen_document +#: model:ir.ui.menu,name:pingen.menu_pingen_document +msgid "Pingen Documents" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Pingen Error" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_id +msgid "Pingen ID" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.base_config_settings_inherit +msgid "Pingen Integration" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_post_id +msgid "Pingen Post ID" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_base_config_settings_pingen_staging +#: model:ir.model.fields,field_description:pingen.field_res_company_pingen_staging +msgid "Pingen Staging" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_base_config_settings_pingen_token +#: model:ir.model.fields,field_description:pingen.field_res_company_pingen_token +msgid "Pingen Token" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_attachment_form +msgid "Pingen info" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Pingen.com" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_post_status +msgid "Post Status" +msgstr "" + +#. module: pingen +#: selection:ir.attachment,pingen_speed:0 +msgid "Priority" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_push_date +msgid "Push Date" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Push to pingen.com" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Pushed" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_field +msgid "Resource Field" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_id +msgid "Resource ID" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_model +msgid "Resource Model" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_res_name +msgid "Resource Name" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_send +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_send +msgid "Send" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_send_to_pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_send_to_pingen +msgid "Send to Pingen.com" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Sendcenter" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_search +#: selection:pingen.document,state:0 +msgid "Sent" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_speed +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_speed +msgid "Speed" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_state +msgid "State" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_store_fname +msgid "Stored Filename" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_res_model +msgid "The database object this attachment will be attached to." +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:103 +#, python-format +msgid "The document does not meet the Pingen requirements." +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_res_id +msgid "The record id this is attached to." +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/ir_attachment.py:97 +#, python-format +msgid "The type of attachment %s is not handled" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_type +msgid "Type" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_ir_attachment_pingen_color +#: model:ir.model.fields,field_description:pingen.field_pingen_document_pingen_color +msgid "Type of print" +msgstr "" + +#. module: pingen +#: code:addons/pingen/models/pingen_document.py:138 +#: code:addons/pingen/models/pingen_document.py:259 +#: code:addons/pingen/models/pingen_document.py:348 +#, python-format +msgid "Unexpected Error when updating the status of Document %s" +msgstr "" + +#. module: pingen +#: model:ir.ui.view,arch_db:pingen.view_pingen_document_form +msgid "Update the letter's informations" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,field_description:pingen.field_pingen_document_url +msgid "Url" +msgstr "" + +#. module: pingen +#: model:ir.model.fields,help:pingen.field_pingen_document_type +msgid "You can either upload a file from your computer or copy/paste an internet link to your file." +msgstr "" + +#. module: pingen +#: model:ir.model,name:pingen.model_base_config_settings +msgid "base.config.settings" +msgstr "" + +#. module: pingen +#: model:ir.model,name:pingen.model_ir_attachment +msgid "ir.attachment" +msgstr "" + +#. module: pingen +#: model:ir.model,name:pingen.model_pingen_document +msgid "pingen.document" +msgstr "" + diff --git a/pingen/models/__init__.py b/pingen/models/__init__.py new file mode 100644 index 00000000000..2e32eed7e44 --- /dev/null +++ b/pingen/models/__init__.py @@ -0,0 +1,5 @@ +from . import ir_attachment +from . import pingen +from . import pingen_document +from . import res_company +from . import base_config_settings diff --git a/pingen/models/base_config_settings.py b/pingen/models/base_config_settings.py new file mode 100644 index 00000000000..ed9fae663d8 --- /dev/null +++ b/pingen/models/base_config_settings.py @@ -0,0 +1,29 @@ +# Copyright 2012-2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + pingen_clientid = fields.Char( + string="Pingen Client ID", related="company_id.pingen_clientid", readonly=False + ) + pingen_client_secretid = fields.Char( + string="Pingen Client Secret ID", + related="company_id.pingen_client_secretid", + readonly=False, + ) + pingen_organization = fields.Char( + string="Pingen organization", + related="company_id.pingen_organization", + readonly=False, + ) + pingen_webhook_secret = fields.Char( + string="Pingen webhook secret", + related="company_id.pingen_webhook_secret", + readonly=False, + ) + pingen_staging = fields.Boolean( + string="Pingen Staging", related="company_id.pingen_staging", readonly=False + ) diff --git a/pingen/models/ir_attachment.py b/pingen/models/ir_attachment.py new file mode 100644 index 00000000000..349d8591ebe --- /dev/null +++ b/pingen/models/ir_attachment.py @@ -0,0 +1,89 @@ +# Author: Guewen Baconnier +# Copyright 2012-2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class IrAttachment(models.Model): + + _inherit = "ir.attachment" + + send_to_pingen = fields.Boolean("Send to Pingen.com") + pingen_document_ids = fields.One2many( + "pingen.document", "attachment_id", string="Pingen Document", readonly=True + ) + + def _prepare_pingen_document_vals(self): + return { + "attachment_id": self.id, + } + + def _handle_pingen_document(self): + """Reponsible of the related ``pingen.document`` + when the ``send_to_pingen`` + field is modified. + Only one pingen document can be created per attachment. + When ``send_to_pingen`` is activated: + * Create a ``pingen.document`` if it does not already exist + * Put the related ``pingen.document`` to ``pending`` + if it already exist + When it is deactivated: + * Do nothing if no related ``pingen.document`` exists + * Or cancel it + * If it has already been pushed to pingen.com, raises + an `osv.except_osv` exception + """ + pingen_document_obj = self.env["pingen.document"] + document = self.pingen_document_ids[0] if self.pingen_document_ids else None + if self.send_to_pingen: + if document: + document.write({"state": "pending"}) + else: + pingen_document_obj.create(self._prepare_pingen_document_vals()) + else: + if document: + if document.state == "pushed": + raise UserError( + _( + "Error. The attachment %s is " + "already pushed to pingen.com." + ) + % self.name + ) + document.write({"state": "canceled"}) + return + + @api.model + def create(self, vals): + attachment = super(IrAttachment, self).create(vals) + if "send_to_pingen" in vals: + attachment._handle_pingen_document() + return attachment + + def write(self, vals): + res = super(IrAttachment, self).write(vals) + if "send_to_pingen" in vals: + for attachment in self: + attachment._handle_pingen_document() + return res + + def _decoded_content(self): + """Returns the decoded content of an attachment (stored or url) + Returns None if the type is 'url' and the url is not reachable. + """ + decoded_document = None + if self.type == "binary": + decoded_document = base64.b64decode(self.datas) + elif self.type == "url": + response = requests.get(self.url, timeout=30) + if response.ok: + decoded_document = requests.content + else: + raise UserError(_("The type of attachment %s is not handled") % self.type) + return decoded_document diff --git a/pingen/models/pingen.py b/pingen/models/pingen.py new file mode 100644 index 00000000000..e7829c59005 --- /dev/null +++ b/pingen/models/pingen.py @@ -0,0 +1,298 @@ +# Author: Guewen Baconnier +# Copyright 2012-2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +import logging +from datetime import datetime +from urllib.parse import urljoin + +import pytz +import requests +from dateutil import parser +from oauthlib.oauth2 import BackendApplicationClient +from requests_oauthlib import OAuth2Session + +_logger = logging.getLogger(__name__) + + +def pingen_datetime_to_utc(dt): + """Convert a date/time used by pingen.com to UTC timezone + + :param dt: pingen date/time iso string (as received from the API) + to convert to UTC + :return: TZ naive datetime in the UTC timezone + """ + utc = pytz.utc + localized_dt = parser.parse(dt) + return localized_dt.astimezone(utc).replace(tzinfo=None) + + +class PingenException(RuntimeError): + """There was an ambiguous exception that occurred while handling your + request.""" + + +class APIError(PingenException): + """An Error occured with the pingen API""" + + +class Pingen(object): + """Interface to the pingen.com API""" + + def __init__(self, clientid, secretid, organization, staging=True): + self.clientid = clientid + self.secretid = secretid + self.organization = organization + self.staging = staging + self._session = None + self._init_token_registry() + super(Pingen, self).__init__() + + @property + def api_url(self): + if self.staging: + return "https://api-staging.v2.pingen.com" + return "https://api.v2.pingen.com" + + @property + def identity_url(self): + if self.staging: + return "https://identity-staging.pingen.com" + return "https://identity.pingen.com" + + @property + def token_url(self): + return "auth/access-tokens" + + @property + def file_upload_url(self): + return "file-upload" + + @property + def session(self): + """Build a requests session""" + if self._session is not None: + return self._session + client = BackendApplicationClient(client_id=self.clientid) + self._session = OAuth2Session(client=client) + self._set_session_header_token() + return self._session + + @classmethod + def _init_token_registry(cls): + if hasattr(cls, "token_registry"): + return + cls.token_registry = { + "staging": {"token": "", "expiry": datetime.now()}, + "prod": {"token": "", "expiry": datetime.now()}, + } + + @classmethod + def _get_token_infos(cls, staging): + if staging: + return cls.token_registry.get("staging") + else: + return cls.token_registry.get("prod") + + @classmethod + def _set_token_data(cls, token_data, staging): + token_string = " ".join( + [token_data.get("token_type"), token_data.get("access_token")] + ) + token_expiry = datetime.fromtimestamp(token_data.get("expires_at")) + if staging: + cls.token_registry["staging"] = { + "token": token_string, + "expiry": token_expiry, + } + else: + cls.token_registry["prod"] = {"token": token_string, "expiry": token_expiry} + + def _fetch_token(self): + # TODO: Handle scope 'letter' only? + token_url = urljoin(self.identity_url, self.token_url) + # FIXME: requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] + # certificate verify failed (_ssl.c:581) + # without verify=False parameter on prod/staging + _logger.debug("Fetching new token from %s" % token_url) + return self._session.fetch_token( + token_url=token_url, + client_id=self.clientid, + client_secret=self.secretid, + verify=False, + ) + + def _set_session_header_token(self): + if self._is_token_expired(): + token_data = self._fetch_token() + self._set_token_data(token_data, self.staging) + token_infos = self._get_token_infos(self.staging) + self._session.headers["Authorization"] = token_infos.get("token") + + def _is_token_expired(self): + token_infos = self._get_token_infos(self.staging) + expired = token_infos.get("expiry") <= datetime.now() + if expired: + _logger.debug("Pingen token is expired") + return expired + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def close(self): + """Dispose of any internal state.""" + if self._session: + self._session.close() + + def _send(self, method, endpoint, letter_id="", **kwargs): + """Send a request to the pingen API using requests + + Add necessary boilerplate to call pingen.com API + (authentication, configuration, ...) + + :param boundmethod method: requests method to call + :param str endpoint: endpoint to call + :param kwargs: additional arguments forwarded to the requests method + """ + + if self._is_token_expired(): + self._set_session_header_token() + + p_url = urljoin(self.api_url, endpoint) + + if endpoint == "document/get": + complete_url = "{}{}{}{}{}".format( + p_url, "/id/", kwargs["params"]["id"], "/token/", self._token + ) + else: + complete_url = p_url.format( + organisationId=self.organization, letterId=letter_id + ) + response = method(complete_url, verify=False, **kwargs) + errors = response.json().get("errors") + if errors: + raise APIError( + "\n".join( + [ + "%s (%s): %s" + % (err.get("code"), err.get("title"), err.get("detail")) + for err in errors + ] + ) + ) + return response + + def _get_file_upload(self): + _logger.debug("Getting new URL for file upload") + response = self._send(self.session.get, self.file_upload_url) + json_response_attributes = response.json().get("data", {}).get("attributes") + url = json_response_attributes.get("url") + url_signature = json_response_attributes.get("url_signature") + return url, url_signature + + def upload_file(self, url, multipart, content_type): + _logger.debug("Uploading new file") + response = requests.put( + url, data=multipart, headers={"Content-Type": content_type}, timeout=30 + ) + return response + + def push_document( + self, + filename, + filestream, + content_type, + send=None, + delivery_product=None, + print_spectrum=None, + print_mode=None, + ): + """Upload a document to pingen.com and eventually ask to send it + + :param str filename: name of the file to push + :param StringIO filestream: file to push + :param boolean send: if True, the document will be sent by pingen.com + :param str delivery_product: sending product of the document if it is send + :param str print_spectrum: type of print, grayscale or color + :return: tuple with 3 items: + 1. document_id on pingen.com + 2. post_id on pingen.com if it has been sent or None + 3. dict of the created item on pingen (details) + """ + + url, url_signature = self._get_file_upload() + self.upload_file(url, filestream.read(), content_type) + + data_attributes = { + "file_original_name": filename, + "file_url": url, + "file_url_signature": url_signature, + "address_position": "left", + "auto_send": send, + "delivery_product": delivery_product, + "print_spectrum": print_spectrum, + "print_mode": print_mode, + } + + data = {"data": {"type": "letters", "attributes": data_attributes}} + + response = self._send( + self.session.post, + "organisations/{organisationId}/letters", + headers={"Content-Type": "application/vnd.api+json"}, + data=json.dumps(data), + ) + rjson_data = response.json().get("data", {}) + + document_id = rjson_data.get("id") + item = rjson_data.get("attributes") + + return document_id, False, item + + def send_document( + self, document_uuid, delivery_product=None, print_spectrum=None, print_mode=None + ): + """Send a uploaded document to pingen.com + + :param str document_uuid: id of the document to send + :param str delivery_product: sending product of the document + :param str print_spectrum: type of print, grayscale or color + :return: id of the post on pingen.com + """ + data_attributes = { + "delivery_product": delivery_product, + "print_mode": print_mode, + "print_spectrum": print_spectrum, + } + data = { + "data": { + "id": document_uuid, + "type": "letters", + "attributes": data_attributes, + } + } + response = self._send( + self.session.patch, + "organisations/{organisationId}/letters/{letterId}/send", + letter_id=document_uuid, + headers={"Content-Type": "application/vnd.api+json"}, + data=json.dumps(data), + ) + return response.json().get("data", {}).get("attributes") + + def post_infos(self, document_uuid): + """Return the information of a post + + :param str document_uuid: id of the document to send + :return: dict of infos of the post + """ + response = self._send( + self.session.get, + "organisations/{organisationId}/letters/{letterId}", + letter_id=document_uuid, + ) + return response.json().get("data", {}).get("attributes") diff --git a/pingen/models/pingen_document.py b/pingen/models/pingen_document.py new file mode 100644 index 00000000000..0adebbf01b4 --- /dev/null +++ b/pingen/models/pingen_document.py @@ -0,0 +1,442 @@ +# Author: Guewen Baconnier +# Copyright 2012-2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +from io import BytesIO +from itertools import groupby + +from oauthlib.oauth2.rfc6749.errors import OAuth2Error + +import odoo +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +from .pingen import APIError, pingen_datetime_to_utc + +_logger = logging.getLogger(__name__) + + +class PingenDocument(models.Model): + """A pingen document is the state of the synchronization of + an attachment with pingen.com + + It stores the configuration and the current state of the synchronization. + It also serves as a queue of documents to push to pingen.com + """ + + _name = "pingen.document" + _description = "pingen.document" + _inherits = {"ir.attachment": "attachment_id"} + _order = "push_date desc, id desc" + + attachment_id = fields.Many2one( + "ir.attachment", "Document", required=True, readonly=True, ondelete="cascade" + ) + state = fields.Selection( + [ + ("pending", "Pending"), + ("pushed", "Pushed"), + ("sendcenter", "In Sendcenter"), + ("sent", "Sent"), + ("error_undeliverable", "Undeliverable"), + ("error", "Connection Error"), + ("pingen_error", "Pingen Error"), + ("canceled", "Canceled"), + ], + readonly=True, + required=True, + default="pending", + ) + auto_send = fields.Boolean( + help="Defines if a document is merely uploaded or also sent", + default=True, + ) + delivery_product = fields.Selection( + [ + ("fast", "fast"), + ("cheap", "cheap"), + ("bulk", "bulk"), + ("track", "track"), + ("sign", "sign"), + ("atpost_economy", "atpost_economy"), + ("atpost_priority", "atpost_priority"), + ("postag_a", "postag_a"), + ("postag_b", "postag_b"), + ("postag_b2", "postag_b2"), + ("postag_registered", "postag_registered"), + ("postag_aplus", "postag_aplus"), + ("dpag_standard", "dpag_standard"), + ("dpag_economy", "dpag_economy"), + ("indpost_mail", "indpost_mail"), + ("indpost_speedmail", "indpost_speedmail"), + ("nlpost_priority", "nlpost_priority"), + ("dhl_priority", "dhl_priority"), + ], + "Delivery product", + default="cheap", + ) + print_spectrum = fields.Selection( + [("grayscale", "Grayscale"), ("color", "Color")], + default="grayscale", + ) + print_mode = fields.Selection( + [("simplex", "Simplex"), ("duplex", "Duplex")], "Print mode", default="simplex" + ) + + push_date = fields.Datetime(readonly=True) + # for `error` and `pingen_error` states when we push + last_error_message = fields.Text("Error Message", readonly=True) + # pingen API v2 fields + pingen_uuid = fields.Char(readonly=True) + pingen_status = fields.Char(readonly=True) + # sendcenter infos + parsed_address = fields.Text(readonly=True) + cost = fields.Float(readonly=True) + currency_id = fields.Many2one("res.currency", "Currency", readonly=True) + country_id = fields.Many2one("res.country", "Country", readonly=True) + send_date = fields.Datetime("Date of sending", readonly=True) + pages = fields.Integer(readonly=True) + company_id = fields.Many2one(related="attachment_id.company_id") + + _sql_constraints = [ + ( + "pingen_document_attachment_uniq", + "unique (attachment_id)", + "Only one Pingen document is allowed per attachment.", + ), + ] + + def _push_to_pingen(self, pingen=None): + """Push a document to pingen.com + :param Pingen pingen: optional pingen object to reuse session + """ + decoded_document = self.attachment_id._decoded_content() + if pingen is None: + pingen = self.company_id._get_pingen_client() + try: + doc_id, post_id, infos = pingen.push_document( + self.name, + BytesIO(decoded_document), + self.attachment_id.mimetype, + self.auto_send, + self.delivery_product, + self.print_spectrum, + self.print_mode, + ) + except OAuth2Error as e: + _logger.exception( + "Connection Error when pushing Pingen Document with ID %s to %s: %s" + % (self.id, pingen.api_url, e.description) + ) + raise + except APIError: + _logger.error( + "API Error when pushing Pingen Document %s to %s." + % (self.id, pingen.api_url) + ) + raise + error = False + state = "pushed" + push_date = pingen_datetime_to_utc(infos.get("created_at")) + self.write( + { + "last_error_message": error, + "state": state, + "push_date": fields.Datetime.to_string(push_date), + "pingen_uuid": doc_id, + "pingen_status": infos.get("status"), + } + ) + _logger.info("Pingen Document %s: pushed to %s" % (self.id, pingen.api_url)) + + def push_to_pingen(self): + """Push a document to pingen.com + Convert errors to osv.except_osv to be handled by the client. + Wrapper method for multiple ids (when triggered from button for + instance) for public interface. + """ + self.ensure_one() + state = False + error_msg = False + try: + session = self.company_id._get_pingen_client() + self._push_to_pingen(pingen=session) + except OAuth2Error: + state = "error" + error_msg = ( + _("Connection Error when pushing document %s to Pingen") % self.name + ) + except APIError as e: + state = "pingen_error" + error_msg = _( + "Error when pushing the document %(name) to Pingen:\n%(exc)" + ) % { + "name": self.name, + "exc": e, + } + except Exception as e: + error_msg = _( + "Unexpected Error when pushing the document %(name) to Pingen:\n%(exc)" + ) % {"name": self.name, "exc": e} + _logger.exception(error_msg) + finally: + if error_msg: + vals = {"last_error_message": error_msg} + if state: + vals.update({"state": state}) + with odoo.registry(self.env.cr.dbname).cursor() as new_cr: + new_env = odoo.api.Environment( + new_cr, self.env.uid, self.env.context + ) + self.with_env(new_env).write(vals) + + raise UserError(error_msg) + return True + + def _push_and_send_to_pingen_cron(self): + """Push a document to pingen.com + Intended to be used in a cron. + Commit after each record + Instead of raising, store the error in the pingen.document + """ + with odoo.api.Environment.manage(): + with odoo.registry(self.env.cr.dbname).cursor() as new_cr: + new_env = odoo.api.Environment(new_cr, self.env.uid, self.env.context) + # Instead of raising, store the error in the pingen.document + self = self.with_env(new_env) + not_sent_docs = self.search( + [("state", "!=", "sent")], order="company_id" + ) + for company, documents in groupby( + not_sent_docs, lambda d: d.company_id + ): + session = company._get_pingen_client() + for document in documents: + if document.state == "error": + document._resolve_error() + document.refresh() + try: + if document.state == "pending": + document._push_to_pingen(pingen=session) + elif document.state == "pushed" and not document.auto_send: + document._ask_pingen_send(pingen=session) + except OAuth2Error as e: + document.write({"last_error_message": e, "state": "error"}) + except APIError as e: + document.write( + {"last_error_message": e, "state": "pingen_error"} + ) + except BaseException: + _logger.error("Unexpected error in pingen cron") + return True + + def _resolve_error(self): + """A document as resolved, put in the correct state""" + if self.send_date: + state = "sent" + elif self.pingen_uuid: + state = "pushed" + else: + state = "pending" + self.write({"state": state}) + + def resolve_error(self): + """A document as resolved, put in the correct state""" + for document in self: + document._resolve_error() + return True + + def _ask_pingen_send(self, pingen): + """For a document already pushed to pingen, ask to send it. + :param Pingen pingen: pingen object to reuse + """ + try: + infos = pingen.send_document( + self.pingen_uuid, + self.delivery_product, + self.print_spectrum, + self.print_mode, + ) + except OAuth2Error: + _logger.exception( + "Connection Error when asking for sending Pingen Document %s " + "to %s." % (self.id, pingen.api_url) + ) + raise + except APIError: + _logger.exception( + "API Error when asking for sending Pingen Document %s to %s." + % (self.id, pingen.api_url) + ) + raise + self.write( + { + "last_error_message": False, + "state": "sendcenter", + "pingen_status": infos.get("status"), + } + ) + _logger.info( + "Pingen Document %s: asked for sending to %s" % (self.id, pingen.api_url) + ) + return True + + def ask_pingen_send(self): + """For a document already pushed to pingen, ask to send it. + Wrapper method for multiple ids (when triggered from button for + instance) for public interface. + """ + self.ensure_one() + try: + session = self.company_id._get_pingen_client() + self._ask_pingen_send(pingen=session) + except OAuth2Error as e: + raise UserError( + _( + "Connection Error when asking for " + "sending the document %s to Pingen" + ) + % self.name + ) from e + + except APIError as e: + raise UserError( + _("Error when asking Pingen to send the document %(name): " "\n%(exc)") + % {"name": self.name, "exc": e} + ) from e + + except BaseException as e: + _logger.exception( + "Unexpected Error when updating the status " + "of pingen.document %s: " % self.id + ) + raise UserError( + _("Unexpected Error when updating the status " "of Document %s") + % self.name + ) from e + return True + + def _get_and_update_post_infos(self, pingen): + """Update the informations from + pingen of a document in the Sendcenter + :param Pingen pingen: pingen object to reuse + """ + post_infos = self._get_post_infos(pingen) + self._update_post_infos(post_infos) + + def _get_post_infos(self, pingen): + if not self.pingen_uuid: + return + try: + post_infos = pingen.post_infos(self.pingen_uuid) + except OAuth2Error: + _logger.exception( + "Connection Error when asking for " + "sending Pingen Document %s to %s." % (self.id, pingen.api_url) + ) + raise + except APIError: + _logger.exception( + "API Error when asking for sending Pingen Document %s to %s." + % (self.id, pingen.api_url) + ) + raise + return post_infos + + @api.model + def _prepare_values_from_post_infos(self, post_infos): + country = self.env["res.country"].search( + [("code", "=", post_infos.get("country"))] + ) + currency = self.env["res.currency"].search( + [("name", "=", post_infos.get("price_currency"))] + ) + vals = { + "pingen_status": post_infos.get("status"), + "parsed_address": post_infos.get("address"), + "country_id": country.id, + "pages": post_infos.get("file_pages"), + "last_error_message": False, + "cost": post_infos.get("price_value"), + "currency_id": currency.id, + } + is_posted = post_infos.get("status") == "sent" + if is_posted: + post_date = post_infos.get("submitted_at") + send_date = fields.Datetime.to_string(pingen_datetime_to_utc(post_date)) + vals["state"] = "sent" + else: + send_date = False + vals["send_date"] = send_date + return vals + + def _update_post_infos(self, post_infos): + self.ensure_one() + values = self._prepare_values_from_post_infos(post_infos) + self.write(values) + _logger.info("Pingen Document %s: status updated" % self.id) + + def _update_post_infos_cron(self): + """Update the informations from pingen of a + document in the Sendcenter + Intended to be used in a cron. + Commit after each record + Do not raise errors, only skip the update of the record.""" + with odoo.api.Environment.manage(): + with odoo.registry(self.env.cr.dbname).cursor() as new_cr: + new_env = odoo.api.Environment(new_cr, self.env.uid, self.env.context) + # Instead of raising, store the error in the pingen.document + self = self.with_env(new_env) + pushed_docs = self.search([("state", "!=", "sent")]) + for document in pushed_docs: + session = document.company_id._get_pingen_client() + try: + document._get_and_update_post_infos(pingen=session) + # pylint: disable=W7938 + # pylint: disable=W8138 + except (OAuth2Error, APIError): + # will be retried the next time + # In any case, the error has been + # logged by _update_post_infos + pass + except BaseException as e: + _logger.error("Unexcepted error in pingen cron: %", e) + raise + return True + + def update_post_infos(self): + """Update the informations from pingen of a document in the Sendcenter + Wrapper method for multiple ids (when triggered from button for + instance) for public interface. + """ + self.ensure_one() + try: + session = self.company_id._get_pingen_client() + self._get_and_update_post_infos(pingen=session) + except OAuth2Error as e: + raise UserError( + _( + "Connection Error when updating the status " + "of Document %s from Pingen" + ) + % self.name + ) from e + except APIError as e: + raise UserError( + _( + "Error when updating the status of Document %(name) from " + "Pingen: \n%(exc)" + ) + % {"name": self.name, "exc": e} + ) from e + except BaseException as e: + _logger.exception( + "Unexpected Error when updating the status " + "of pingen.document %s: " % self.id + ) + raise UserError( + _("Unexpected Error when updating the status " "of Document %s") + % self.name + ) from e + return True diff --git a/pingen/models/res_company.py b/pingen/models/res_company.py new file mode 100644 index 00000000000..cd30e5058e2 --- /dev/null +++ b/pingen/models/res_company.py @@ -0,0 +1,35 @@ +# Author: Guewen Baconnier +# Copyright 2012-2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from .pingen import Pingen + + +class ResCompany(models.Model): + + _inherit = "res.company" + + pingen_clientid = fields.Char(size=20) + pingen_client_secretid = fields.Char(size=80) + pingen_organization = fields.Char("Pingen organization ID") + pingen_webhook_secret = fields.Char() + pingen_staging = fields.Boolean() + + def _pingen(self): + """Return a Pingen instance to work on""" + self.ensure_one() + + clientid = self.pingen_clientid + secretid = self.pingen_client_secretid + return Pingen( + clientid, + secretid, + organization=self.pingen_organization, + staging=self.pingen_staging, + ) + + def _get_pingen_client(self): + """Returns a pingen session for a user""" + return self._pingen() diff --git a/pingen/readme/CONFIGURE.rst b/pingen/readme/CONFIGURE.rst new file mode 100644 index 00000000000..c65a9f24202 --- /dev/null +++ b/pingen/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +The authentication token, client ID, organization ID and webhook secret is configured +on the company's view. You can also tick a checkbox if the staging environment +(https://stage-api.pingen.com) should be used. + +Webhooks should be configured on pingen account. Organization ID and webhook secret must match. diff --git a/pingen/readme/CONTRIBUTORS.rst b/pingen/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..9bcc28af0b4 --- /dev/null +++ b/pingen/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* Guewen Baconnier +* Anar Baghirli +* Akim Juillerat +* Anna Janiszewska diff --git a/pingen/readme/DESCRIPTION.rst b/pingen/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..c851c4a3c1c --- /dev/null +++ b/pingen/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +Pingen.com is a paid online service. +It sends uploaded documents by letter post. + +One can decide, per document / attachment, if it should be pushed +to pingen.com. The documents are pushed asynchronously. + +The informations of the documents from pingen.com are updated through webhook calls. diff --git a/pingen/readme/USAGE.rst b/pingen/readme/USAGE.rst new file mode 100644 index 00000000000..2ba48d402b4 --- /dev/null +++ b/pingen/readme/USAGE.rst @@ -0,0 +1,35 @@ +On the attachment view, a new pingen.com section has been added. +You can tick a box to push the document to pingen.com. + +There is 3 additional options: + + * Send: the document will not be only uploaded, but will be also be sent + * Speed: priority or economy + * Type of print: color or black and white + +Once the configuration is done and the attachment saved, a Pingen Document +is created. You can directly access to the latter on the Link on the right on +the attachment view. + +You can find them in `Pingen Documents` App or in the more convenient `Documents` menu if you have installed the +`document` module. + +Errors +====== + +Sometimes, pingen.com will refuse to send a document because it does not meet +its requirements. In such case, the document's state becomes "Pingen Error" +and you will need to manually handle the case, either from the pingen.com +backend, or by changing the document on Odoo and resolving the error on the +Pingen Document. + +When a connection error occurs, the action will be retried on the next +scheduler run. + + +Dependencies +============ + + * Require the Python library `requests_oauthlib `_ + * The address must be in a format accepted by pingen.com: the last line + is the country in English or German. diff --git a/pingen/security/ir.model.access.csv b/pingen/security/ir.model.access.csv new file mode 100644 index 00000000000..4b6e683a3fc --- /dev/null +++ b/pingen/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_pingen_document_all","pingen_document all","model_pingen_document",,1,0,0,0 +"access_pingen_document_group_user","pingen_document group_user","model_pingen_document","base.group_user",1,1,1,1 diff --git a/pingen/static/description/icon.png b/pingen/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/pingen/static/description/icon.png differ diff --git a/pingen/static/description/index.html b/pingen/static/description/index.html new file mode 100644 index 00000000000..26dd5a7abf1 --- /dev/null +++ b/pingen/static/description/index.html @@ -0,0 +1,473 @@ + + + + + + +pingen.com integration + + + +
+

pingen.com integration

+ + +

Beta License: AGPL-3 OCA/report-print-send Translate me on Weblate Try me on Runbot

+

Pingen.com is a paid online service. +It sends uploaded documents by letter post.

+

One can decide, per document / attachment, if it should be pushed +to pingen.com. The documents are pushed asynchronously.

+

The informations of the documents from pingen.com are updated through webhook calls.

+

Table of contents

+ +
+

Configuration

+

The authentication token, client ID, organization ID and webhook secret is configured +on the company’s view. You can also tick a checkbox if the staging environment +(https://stage-api.pingen.com) should be used.

+

Webhooks should be configured on pingen account. Organization ID and webhook secret must match.

+
+
+

Usage

+

On the attachment view, a new pingen.com tab has been added. +You can tick a box to push the document to pingen.com.

+

There is 3 additional options:

+
+
    +
  • Send: the document will not be only uploaded, but will be also be sent
  • +
  • Speed: priority or economy
  • +
  • Type of print: color or black and white
  • +
+
+

Once the configuration is done and the attachment saved, a Pingen Document +is created. You can directly access to the latter on the Link on the right on +the attachment view.

+

You can find them in Settings > Customization > Low Level Objets > Pingen +Documents or in the more convenient Documents menu if you have installed the +document module.

+
+
+

Errors

+

Sometimes, pingen.com will refuse to send a document because it does not meet +its requirements. In such case, the document’s state becomes “Pingen Error” +and you will need to manually handle the case, either from the pingen.com +backend, or by changing the document on OpenERP and resolving the error on the +Pingen Document.

+

When a connection error occurs, the action will be retried on the next +scheduler run.

+
+
+

Dependencies

+
+
    +
  • Require the Python library requests_oauthlib
  • +
  • The address must be in a format accepted by pingen.com: the last line +is the country in English or German.
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/report-print-send project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/pingen/tests/__init__.py b/pingen/tests/__init__.py new file mode 100644 index 00000000000..e87d2b8ffe6 --- /dev/null +++ b/pingen/tests/__init__.py @@ -0,0 +1 @@ +from . import test_pingen diff --git a/pingen/tests/fixtures/cassettes/test_pingen_push_document.yaml b/pingen/tests/fixtures/cassettes/test_pingen_push_document.yaml new file mode 100644 index 00000000000..3c8e52ac28c --- /dev/null +++ b/pingen/tests/fixtures/cassettes/test_pingen_push_document.yaml @@ -0,0 +1,1454 @@ +interactions: +- request: + body: grant_type=client_credentials + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic RkZDU0NPNFFPRzJPVk4zRlU2RUQ6MXoyTk0zVlV4bVdpZ3Y0N1RGYktrV2xtR3N4SFFQTlRxeGRzZlJTTHVPWFJEMmFpZGpaVFJxYVBIcEc4amZKeC9tK21JaVFiRk1BQTBEVXg= + Connection: + - keep-alive + Content-Length: + - '29' + Content-Type: + - application/x-www-form-urlencoded;charset=UTF-8 + User-Agent: + - python-requests/2.28.1 + method: POST + uri: https://identity-staging.pingen.com/auth/access-tokens + response: + body: + string: '{"token_type":"Bearer","expires_in":43200,"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6IjA5Y2M1MGJhNDA0ZjViY2Y1NjkyZDRkNjAzOTEwNzg5YWM1NjUxYjg0OWYwMzBjMTljYzVmNzc1YjdhYWY1MzJkMDAzN2E3MDRkMmUxZmQ0IiwiaWF0IjoxNjg0ODYxMDA1LjE2ODI4MywibmJmIjoxNjg0ODYxMDA1LjE2ODI4NSwiZXhwIjoxNjg0OTA0MjA1LCJzdWIiOiJleUpwZGlJNklrSkRlVFZWZVZvNWNYRndPRk0zWlZKUWFISjFkVkU5UFNJc0luWmhiSFZsSWpvaVNFTkNOMWhDV21nNVluSmplRmwzVGt0R05uWTNaejA5SWl3aWJXRmpJam9pWlRZNFlURmxOR0V6WVdRek1UazRZVGszTVRnMU5qQmlOVEEzWVdFNE1HUTRZMlU1TmpZNU9UWXhOV1UzTkdVNVl6bGxZakExTkRVeE16aG1ZamN4WVNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.BsjXSIFrNZys4ZkEUbAPGol14wovjXhZCTW9D0sO7bjahwwRCD_ahMgAsEBhDO9EPTJL1XGpMS5RaR_ggEewrPyCLudUGpH20cKhvwMz7oFN3tKmZei-zgF8YzsKRQ_6Jcx3kK59YmGyr0IiSb3KZlYURPwTzfQ7gemKHKmVy87oKuBXotafwy526OPk1-1gbWNCLbdO3hlopfSStFMOQsFO2OHhx1CUHg2kIEJn4EJGD9mgTpea03RDg97bwADfKma0J6a4Th2biLCQg_ExiS6DIgNM1H_Gb6HSU8YvIfR3gvPMkNeJELmBtzAXPNkriq3fzaTkGpeXHDw6wEjvDcpI4A2P74Q_vgu5UGuVqSpuewrINWO4aC58ZjoZGmCKY7aVjNb4KIHF3z626s02gJ6YkwIoTRg02z-mZVtKJqWbUGSDgoeb8_gOkgADKGsos7s5x2nlX7USquPclK_JGuJy8qhKiudm2qZsMVCB0RI2ULkyaB1CEEYJztFIbcKHufmRdI2facZ6Vjt0JWHRBKND7J8CO3Axlz0KMaMRTn3k7iOaXz11l0tvyn9EQ6NIGfc0iz7-VtJZU90bI1jJYLZaTFaMYlKoW5cj6mY2hoj8UyuzxOpb74XLe7KwzusVfaS_1ig5xtkZpQtdtCMrhe5vcZPDkwDxD37ivq0pdu4"}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-store, private + content-type: + - application/json + date: + - Tue, 23 May 2023 16:56:45 GMT + pragma: + - no-cache + server: + - nginx + set-cookie: + - fb7eb3c0fd7448a4f8905cb9504f3ebb=dbd6167e7abb8a3add8206709b5d4743; path=/; + HttpOnly; Secure; SameSite=None + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '898' + x-request-id: + - 857ca58d-8f94-4047-a8bf-18d1c87b7dc2 + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6IjA5Y2M1MGJhNDA0ZjViY2Y1NjkyZDRkNjAzOTEwNzg5YWM1NjUxYjg0OWYwMzBjMTljYzVmNzc1YjdhYWY1MzJkMDAzN2E3MDRkMmUxZmQ0IiwiaWF0IjoxNjg0ODYxMDA1LjE2ODI4MywibmJmIjoxNjg0ODYxMDA1LjE2ODI4NSwiZXhwIjoxNjg0OTA0MjA1LCJzdWIiOiJleUpwZGlJNklrSkRlVFZWZVZvNWNYRndPRk0zWlZKUWFISjFkVkU5UFNJc0luWmhiSFZsSWpvaVNFTkNOMWhDV21nNVluSmplRmwzVGt0R05uWTNaejA5SWl3aWJXRmpJam9pWlRZNFlURmxOR0V6WVdRek1UazRZVGszTVRnMU5qQmlOVEEzWVdFNE1HUTRZMlU1TmpZNU9UWXhOV1UzTkdVNVl6bGxZakExTkRVeE16aG1ZamN4WVNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.BsjXSIFrNZys4ZkEUbAPGol14wovjXhZCTW9D0sO7bjahwwRCD_ahMgAsEBhDO9EPTJL1XGpMS5RaR_ggEewrPyCLudUGpH20cKhvwMz7oFN3tKmZei-zgF8YzsKRQ_6Jcx3kK59YmGyr0IiSb3KZlYURPwTzfQ7gemKHKmVy87oKuBXotafwy526OPk1-1gbWNCLbdO3hlopfSStFMOQsFO2OHhx1CUHg2kIEJn4EJGD9mgTpea03RDg97bwADfKma0J6a4Th2biLCQg_ExiS6DIgNM1H_Gb6HSU8YvIfR3gvPMkNeJELmBtzAXPNkriq3fzaTkGpeXHDw6wEjvDcpI4A2P74Q_vgu5UGuVqSpuewrINWO4aC58ZjoZGmCKY7aVjNb4KIHF3z626s02gJ6YkwIoTRg02z-mZVtKJqWbUGSDgoeb8_gOkgADKGsos7s5x2nlX7USquPclK_JGuJy8qhKiudm2qZsMVCB0RI2ULkyaB1CEEYJztFIbcKHufmRdI2facZ6Vjt0JWHRBKND7J8CO3Axlz0KMaMRTn3k7iOaXz11l0tvyn9EQ6NIGfc0iz7-VtJZU90bI1jJYLZaTFaMYlKoW5cj6mY2hoj8UyuzxOpb74XLe7KwzusVfaS_1ig5xtkZpQtdtCMrhe5vcZPDkwDxD37ivq0pdu4 + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.1 + method: GET + uri: https://api-staging.v2.pingen.com/file-upload + response: + body: + string: '{"data":{"type":"file_uploads","id":"f14144b8-a211-4514-8333-f4e09d8a16df","attributes":{"url":"https:\/\/pingen2-staging-transfer.objects.rma.cloudscale.ch\/f14144b8-a211-4514-8333-f4e09d8a16df_2023-05-23_19%3A16%3A45?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20230523%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20230523T165645Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=ef959eb9ea0e53824005190a1b4f4f33e460c054e3f29865913945f29e4b1ec5","url_signature":"1fb811d0e1f010292d40bb672bfb1fde","expires_at":"2023-05-23T19:16:45+0200"}}}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-cache, private + content-type: + - application/vnd.api+json + date: + - Tue, 23 May 2023 16:56:45 GMT + server: + - nginx + set-cookie: + - 9e95c922595c91f6fd6e3f88ecd31058=dbd6167e7abb8a3add8206709b5d4743; path=/; + HttpOnly; Secure; SameSite=None + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '899' + x-request-id: + - 66d62b74-720f-4c4d-b12e-ad4891989b4e + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: !!binary | + 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 + jMtZN3hytjO605XEJD3grFBm9r3umLgCS9GoSTYVsEfaLzi4lgKXWGSxEZm6beILicxP0pTl5KLD + PHBuBl75uWmqohtbryNaW2IFc61XZH+VoP3S1zL7mznJpTXr1szKPQi8KZCJXUfmjS40vaHKq4vp + Lq5LurALcPmaCq2Owg9Yashtmtv2UArGDdZ6EwU14WrtOKmn1vIr18Dddi43VPB2ztxpju4pJMpS + mXspNX63iWm2LaaPgsiLXSPbx0HEB5L/nOJqYjbxxQr6tzmaBEfwnC7HiQWTvptmb1cBrk7tcX3H + hXa4WMu+GokeFGGmZJHIIHlU3wIJQy/aFhGx7BvmMnxaePOwgq4gghEJjQpICcUqJp7mypWGrQsT + CMcLl/mSjIIccK5SoHYipsdORYPqOmcilgiUmxZXNoeE8JEQrV8L/70QScOB7S5olb9hEC5062j5 + 3jQd3FHvZzgIzCoskQoLgaVkIo8IoIr5xIoS6TD/jKrCb+36Mu3eifab2xcnF/6bhic8m7hetPY8 + Czu23AzRVSsRh9rBPURt9yrpCGN0tEDn2kTRWAkAdA05ougRTNrkwpXNPqlCdp0Xd15sIZ0Ns7VG + 92BrDbrMs7zMhnm5p4rSQaD8KVuqsMa4cPeNwYZ9k7ptMVVP97D2Y+uOKQ1kpCbpnN7cklG/ikxK + 4GArYB+OjQF/EDg455mgQ/AUWxQHY5pXOy5u6y6VsT7Jhcif7UhW405NcWa9N2vbOmmQ30RIrLY7 + Q7qS0cwsotfq/HTBHOrnDBIY+GSj26OgXw5cKxw8Jwe+hMeMmGv44tzwhSS/KrhNUBZlR8WAbdvs + T89S0HhuNiYh3iE1atmSS5WIxEo3lAi9u1bjf04kwJ9uuboLe0YkIdywSMau342gudNelS0dyNBt + 85UKf8lSZvLNvHzEYXHQl0myGeGTln/OvElHVDhu7OvevG8UNc93isLgfGai+OHkQx/+BMHR3tCc + y8MbzC/rQyj+r/m1MixHI9k2PcmvC7JB/4Df/is2ymTJlyAbcOe9Ko73D/ylUynsyOlD7C+RKrLu + kb3OmnnCUWNbJfqH6T9wvJ3BCmVuZHN0cmVhbQplbmRvYmoKCjMgMCBvYmoKMjU2OQplbmRvYmoK + CjUgMCBvYmoKPDwvTGVuZ3RoIDYgMCBSL0ZpbHRlci9GbGF0ZURlY29kZS9MZW5ndGgxIDEwNTQw + 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 + b41fS371RNgS35cX7nObzoC2WHWXtvZo42LmOaw+3N7ADxbnrYeZDOeWvz8H7OFu6tSg/nIuL511 + fnYJl8jXSaNGCg1q8t9mWiTyBjWFnBBpv6WdoG/Jv1gPvkHN2pvabW2Q/hQi/ZkD2i0tRu9WzbQg + Aadw/e2jE2qmVeugt8DjJq6+/sxB+i59nW5XzbTh0hzB6+An5F97eaYdl+fqGXoDKm7SdXqBPl1m + HaJLSliwrWGfsEb4vXoXbO2Q/5U1CMP9/yIj+m9oWokTCmVuZHN0cmVhbQplbmRvYmoKCjYgMCBv + YmoKMzg0MgplbmRvYmoKCjcgMCBvYmoKPDwvVHlwZS9Gb250RGVzY3JpcHRvci9Gb250TmFtZS9F + QUFBQUErVGliZXRhbiM1Rk1hY2hpbmUjNUZVbmkKL0ZsYWdzIDQKL0ZvbnRCQm94Wy02MDEgLTE0 + NDAgMjE1OCAxMzYwXS9JdGFsaWNBbmdsZSAwCi9Bc2NlbnQgMTIxOAovRGVzY2VudCAtNzgxCi9D + YXBIZWlnaHQgMTM2MAovU3RlbVYgODAKL0ZvbnRGaWxlMiA1IDAgUgo+PgplbmRvYmoKCjggMCBv + YmoKPDwvTGVuZ3RoIDIyMS9GaWx0ZXIvRmxhdGVEZWNvZGU+PgpzdHJlYW0KeJxdkEGLwyAQhe/+ + ijm2h2JS9hgCpaWQw7al6f4Ao5NU2IwyMYf8+1Wb7sIeFJ7vffpGeWxODdkgb+x0iwF6S4ZxcjNr + hA4HS6Lcg7E6rCrvelReyMi2yxRwbKh3VSXkPXpT4AU2B+M63Ap5ZYNsaYDN17GNup29/8YRKUAh + 6hoM9vGeT+UvakSZqV1jom3DsovIX+CxeIR91uWrinYGJ680sqIBRVUUNVTncy2QzD9vJbpePxXH + ZJmSxUeZs+tpotJY7zagZ+bYJM+eK6THLeHv93jnE5XXD4TwbY4KZW5kc3RyZWFtCmVuZG9iagoK + OSAwIG9iago8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9CYXNlRm9udC9FQUFBQUErVGli + ZXRhbiM1Rk1hY2hpbmUjNUZVbmkKL0ZpcnN0Q2hhciAwCi9MYXN0Q2hhciAxCi9XaWR0aHNbNjQ0 + IDY3MiBdCi9Gb250RGVzY3JpcHRvciA3IDAgUgovVG9Vbmljb2RlIDggMCBSCj4+CmVuZG9iagoK + MTAgMCBvYmoKPDwvTGVuZ3RoIDExIDAgUi9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3RoMSA1Mzc2 + Pj4Kc3RyZWFtCnic5VhdbBxXFT6zu961Y8dOgn8SObR37eZ/sxs7XTsJP8qPYztNmrpxYryIkl7P + jncnmZ0ZzcyubSgoEhQhAwUJxFvhBXiBB8MDcishRQjxQIuUh9IHFAlUgegbL/QBlCZ898z1+oc0 + ChICic7qzv3Oueeec+75ueMkCuoWddAtStJpsyb9bsNIENGbRMYusxEJom6Qxh/xSsz7ldpLC9O9 + oP+EMVdxlubH//75o0TJSdA/qVqy/OZv38oSpf4G+ZEqGKl7g2milhzop6q1aPEf98+0gJ4C3eF4 + pnybYIJaPoNXa00u+n9I7ID9ljJo4cqadf3l8s9Af5mo61O+F0Zd1H4PPt1V635g+UeIVkGDRynl + KH7q6QBMKzpBH/KnmwOUu7hKbVOzPzWMV0qrxoOXV2nso69RGyWvf/boKhk5Ic7bYyvGiyASOTAO + Z4GSOTG+ktw3fmV2sCSWxfKF8rIYF1VZXknt4xkL1nKpIFZoetbG++psduV0qb8JrVLpFPSklJ4U + 61kuQcMNreEGa4CC9yHUkrsoVpL7p2afn125Nda/cnqs1J/NivMrt6dmV26P9WdLJUilm55i/oK9 + W/ucgc/pwwCtsZZp6ICK0vJyTA1mV24vL/cv4xyaXjVoK+O0ZuDgSkFy3/lV49YUL90azPYrxmB2 + MAu3SmMw1Za7OD17Ho5lS+gAStzP32998P37b1EL4tqFoLcWXueaTB45NpTMHhjN9mX6MgcW7v++ + 2GvsG3ltZKR4/6vFvmJupFiElPHgDSh5JfEDOkFvrNLJwiqdwkjcRfwwTu5YpY47GODl7p7ppzTl + qBdjP8YIxjjGDMY8RgPjKxjfxfgRxs8xfo2x/QU03+8A/oyReGGV9kDjHqUR2vcC7wVOAQ/fgf9J + +D/K/r9OBcrwfIAO8dwCurBz10mUFsR7sHUU8+47Md2v5+N3jg0ZgzszB4rpwYF8ovj0yGjewLs4 + eGD0eF9mcKAz0dP9ROJjRl9mIN3T3Xt8eKS4c1BFKrH4rd7i4dH9z5wc6P6IOGpcKva8U+zNtXek + jcS2vp4Txd6nR48XPnFuX7HHMIqJ0vu/6C1+rvfJ4tnBbLF798SRlraWYm+xt33HznRb367txZ5P + 5gdyJ4aMDnCJ7wjj+HHvmVe/cb3r4+9Rf4pb5Zfvlv661jYqm5wN3Eu0dolgX5pw1Qw0u8vY0m3J + 5Lvx7kRJ5ROPoLdZKkkntZ54zzZkmxI/1PsGjO+t6TION2UMyoCKcQJ4SOMkdRknNE6BP6FxC7Ub + lzVOg1/SuJU6jRc1bgN213wwfmO8pHE77UmON2/PPcnrGm+nd5KLGnfSU6lPa7yDMqkvarwT+GWN + dwF/HR4aqTY4fTX1bY0N6kz/ReME8HsaJ+mJTFLjFHVmshq3UG+moHEa/PMat9LezLTGbcCextuS + 38l8TeN2Gm5/UuMO4Oc13m78qv1LGnfS5PYJjXdQ5/ZvarwT+FWNdwH/+JznLwV2pRqJg+Yh8axd + lbYjzsolGSy5OTF87FgxL844jmCZUARWaAUNq5wX56rSLctQzHtuJGy14lgytMqi7patQExcviYm + LNcKpCOm6nOObYpLtmm5oZUX43UodGJKRNYi75cNWJZzjiVkJKpR5J8qFBYWFvIVt573gkrBhKOO + NR8VKr6Tr0Y1Jy8uych2BdwQ53AGR9nAR9uvhmLBCqCnXIY784FXE2PWDTlTj31V8jWvbM/bWJWm + 6QVl262IyBMTU5fyYqwqg5onAnlzGup5tiM52TzwFatSd2SwdvzN5IwVhLbniqH8sOZsjuhmak0H + Tj9mNaQrKzKwxTXXNr2yJZ7zLVdcXfKt2O2DUTR/SCzYUVXAXsVCACsyquOcoRXlxQUOYtkK7YqL + c1mhb5m2dJwl7A7EjFVGcDhSjgxD20RWpqUb3gzsSMzVI2FKBNIJPTFnibrKoto1abtlOycuW750 + bN7tRVXkFtGzAldcwCq2OdKt1GXFCvPialU7a7umU4cza4ZN5BrVw6GX7pJKjR0hUHBDORyKg35g + N2TExoUMLHmIhf3Aa9hKD/54smtKuu77XhCxextiFpqB7asouKLRTMHQxjJ4SK2YyLQ0I8gzH4WA + 6vY9l8thPfqRKsqQLTZDFgVAjo29Uh0jr+vVjBOaN+XNQNZU2X7gwv+i7/67/fI4PfzQnqJz5JFP + SxSQTRWqUoTPzEEy8XEW9Cx4VZJ4O6DOAi1hBHi7+OoLGqZj+BUpD3wGMkpqXU/IlIXZwtzAu8yS + 51inC0qyzDw8cNmu3dzjYEjeWQanztJKi6AJukzXeLbAVTzJdqcgNQdkw3dBl3hWEkqHsjqO9dhD + Z9OagGWLFjfYl/A1PrNkjRbz1Lo6VYRoncIfMQVa4F8ep3WhO49TBMAF6I4jqnbOQ74Arg8qz/tr + jJSHSqeNvUJHQ0UmzoPTPEcFaAm7q+zZAp839qfMESlz/AJYrAGNgXMDazPwZ2Nc1/TXwClD8zxG + vFfCismel9mXCsfD4/hOwcc8a61y1mvMV/G+SdPa+3XaBi1p8iEZvgJbFY6+0rI1+49aneHzhtDt + cZyGoH14i8yjavRRa1v9iHOvIthgvoRXkrMhUG8u58PjmAt6Dhmx2KOrnB1rU7QPcpXMcwctcFyq + XHWS68PSFVjhCNZ1PlUlRhy7CxsqsczdY3OFxflStLJngiu5Tpa0bdUbMywVV856TTl8wpD5ca9M + 82qIvAXsn0Cd13k2eUWwXMj5nmP/6s1eXLM1yfWirKmb4DJWfd5lb7Dt8dnjvq3p6AWs/4LeG1tz + eFZVIDlCIUfiKu/dfD+48NCBXByZrSc2dV9Huo/Xql6hpWbX2Fy5HufY4djHEQ45dz7HpMHZWT+5 + 4OypO+nQBs0+d14D8mv+KFuLoGtN3XVI+RyzaEP0Hl5nIfxXyG/WgopO4yFdMPSBt8Hj3Sum7mkV + s0jrX5ePb4RAV5uncxXfDg+r/ah5U4YbzvivVRaxRcVzmBPf3mvZyG+5X81NHZrn2rzJO2rN2/bf + 3/Fh+d79P39f/lPf4cf/TuFfcfzca6Wurf8tpp5VoiP/BFMbYlkKZW5kc3RyZWFtCmVuZG9iagoK + MTEgMCBvYmoKMjMwOAplbmRvYmoKCjEyIDAgb2JqCjw8L1R5cGUvRm9udERlc2NyaXB0b3IvRm9u + dE5hbWUvREFBQUFBK0NoYW5kYXMKL0ZsYWdzIDQKL0ZvbnRCQm94Wy01ODYgLTYwOCAxMzcxIDEz + OTRdL0l0YWxpY0FuZ2xlIDAKL0FzY2VudCAxNTAwCi9EZXNjZW50IC0xMDAwCi9DYXBIZWlnaHQg + MTM5NAovU3RlbVYgODAKL0ZvbnRGaWxlMiAxMCAwIFIKPj4KZW5kb2JqCgoxMyAwIG9iago8PC9M + ZW5ndGggMjI4L0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nF2Qz2rDMAzG734KHbtDcZJz + MHQthRz2h6Z7AMdWMsNiG8U55O0nu10LO9joQ99PfJI8dqfOuyQ/KZgeE4zOW8IlrGQQBpycF3UD + 1pl0V+U3s45CMttvS8K582NoWyEv3FsSbbA72DDgi5AfZJGcn2D3dexZ92uMPzijT1AJpcDiyHPe + dHzXM8pC7TvLbZe2PSNPw3WLCE3R9S2KCRaXqA2S9hOKtqoUtOezEujtv15zI4bRfGtiZ52d9eGk + uG5KXb0W7u7IE/KKf8nArEScqtyhxMlBnMfHqWKImSrvFzuqcBkKZW5kc3RyZWFtCmVuZG9iagoK + MTQgMCBvYmoKPDwvVHlwZS9Gb250L1N1YnR5cGUvVHJ1ZVR5cGUvQmFzZUZvbnQvREFBQUFBK0No + YW5kYXMKL0ZpcnN0Q2hhciAwCi9MYXN0Q2hhciAyCi9XaWR0aHNbOTc2IDAgNTg2IF0KL0ZvbnRE + ZXNjcmlwdG9yIDEyIDAgUgovVG9Vbmljb2RlIDEzIDAgUgo+PgplbmRvYmoKCjE1IDAgb2JqCjw8 + L0xlbmd0aCAxNiAwIFIvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aDEgMjY4NjA+PgpzdHJlYW0K + eJztfQt4FNXZ8DlzZvYye0+ym2yuk4QlREISw81wkQ0khEBIYhIwCGg22U2ymGTX7AZEQKC2XrBU + /ayIiOWiRUuRRuRTRERUbBW0YgVb77WF6tdGavtbqzEZ/vecmc1uIFiq1vb/nj/Dzp45c857v503 + 8THc1e1DRrQGEeRu7vAER4n5EkLoJYRwXPPSsKSfsO/vMP4tQlx9S7C1YyCA5iLEj4M1u1rbl7fc + va81Dp7hfdoP23we72efPnwJQllheD+hDSaq5E4tPO+C5xFtHeFrc5OLT8HzMXg+1B5o9mzfUboV + oezb4bmpw3Nt8G5SIsDzJ/AsdXo6fCkflS+ErQkITTUHA6HwFWjtGYQ8I+j7YJcvuHBLUx88lyKk + fRDmMFz0xwhDDX3mCC9otDq9aDCazBarLS4+we5ITHImp6SmpWdImVnZI1wjc0blXjQ6b0x+Afpf + 94Nb8Fjcgg6g/4HxVLQd9ZEMxMHVArP0exeuR73wvglWruW/h+vhu4N/AHHwfjX/MoDg8FjUhK6B + kYt/AB9A+9Ep2L0WrxdmCVfQ1QwRhfWp8Bz+WCjmilED38FP5ffwa/k9sKKbb+HXoh64F3Ov8pv5 + Ffwr/ArUQCnDlfRD6UCb8GycjTZxm3ApduJS7mX0DKN/Gt6EJwtHhaPoBDqBa2DlLrSME/Ev8F9x + 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 + NCAxMTc0XS9JdGFsaWNBbmdsZSAwCi9Bc2NlbnQgOTI4Ci9EZXNjZW50IC0yMzUKL0NhcEhlaWdo + dCAxMTc0Ci9TdGVtViA4MAovRm9udEZpbGUyIDE1IDAgUgo+PgplbmRvYmoKCjE4IDAgb2JqCjw8 + L0xlbmd0aCA0MjkvRmlsdGVyL0ZsYXRlRGVjb2RlPj4Kc3RyZWFtCnicXZPLbqswEIb3PIWXPYsK + fINGipDSpJGy6EVNzwMQmOQgnRjkkEXevp75aSt1AfpszwyfR0O+3m12oZ/ytzi0e5rUsQ9dpMtw + jS2pA536kGmjur6d5pW823MzZnnK3d8uE5134Tgsl1n+ns4uU7ypu1U3HOhPlr/GjmIfTuru73qf + 1vvrOP6nM4VJFVldq46Oqc5zM740Z8ol637XpeN+ut2nlJ+Aj9tIyshaQ6UdOrqMTUuxCSfKlkVR + q+V2W2cUul9nrkLK4dj+a2IK1Sm0KJypExvhcsFswWtmJ2wKZi/sHXOJmC1zJWxXzA+IKZkXqK+Z + V+An5kfUFF6jjsRswLL/BH5k3oI3iXUB9szwLy0z/CvZh3/Fnhr+ju+l4e/YTcPfSy78nTD8S8md + /Svm2Z/vq+HvuSca/p57qOHvJBf+Xnzg75gN/D3f18Dfs5uBv+OeG/i7B+bZn/tg4F9JPPwr/q6B + f8X+Bv6W3Qz8rcTD37C/gb+VePhbqQN/K27wt+Iw99/IUM3Tw+PF8/81tqq9xphGVn4SmVWe0j7Q + 9380DiNnyfMJYKDZQwplbmRzdHJlYW0KZW5kb2JqCgoxOSAwIG9iago8PC9UeXBlL0ZvbnQvU3Vi + dHlwZS9UcnVlVHlwZS9CYXNlRm9udC9DQUFBQUErRGVqYVZ1U2Fucy1Cb2xkCi9GaXJzdENoYXIg + MAovTGFzdENoYXIgNDcKL1dpZHRoc1s2MDAgNzYyIDM0MiAzNDIgMzQ4IDY4MiA2ODcgMzk5IDc3 + MyA3NzMgODM2IDM3OSA2NzQgNzExIDY2NSAxMDQxCjY3OCA1OTIgNzExIDQ3OCAzNzIgNjgzIDcy + MCA3MzMgNzE1IDgyMCA4NTAgNzMyIDc3MCA4MzAgODEyIDY4Mwo4NTAgNzI0IDYzNyA4MzYgNzc0 + IDY1MSA0OTMgNTk1IDY5NSA2OTUgMzY1IDY5NSA2OTUgNjk1IDY5NSA3MTUKXQovRm9udERlc2Ny + aXB0b3IgMTcgMCBSCi9Ub1VuaWNvZGUgMTggMCBSCj4+CmVuZG9iagoKMjAgMCBvYmoKPDwvTGVu + Z3RoIDIxIDAgUi9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3RoMSAyOTcwND4+CnN0cmVhbQp4nO18 + C1xUVf74Offce2fmzgAzwwzvxwXkpQgEPvJRDigqikCAprYWAzMICgwxoCm5+PgpmpZagWUqVGpq + ZmaugWlZUWbmbrvq7rrVr63stWvmb3+2tQrX//ecO8PDR9tW+/h9Pn/GmTn33HO+7+/3fL/fO1VX + W+9EBrQYEWQrrbLX+KdF+SKE3kYIm0vn1ckjg8pfhfEf4a2U1cyumj8/zYEQgWv09OzKBWUB55Lz + EeLXIDQIlTvtjoq/Tr8ZoRS4RsPKYcKuVGvguhOuB5RX1d1TGnxTLFx/DvAMla5Se8XM/x2HUOpF + uL+yyn5PTTTXyCN0071wLVfbq5z1r9TmwvUmhEZ/WONy181ES64gVL2Y3q+pddaUXeDPwHUbQkI5 + zGF40T8DDEV6zRFeEDVanaQ3+Pj6GU1mf4s1IDAoOCQ0LDwiUo6OGRAbF58wcFDS4OSU1JvS0ocM + HTb85hEjR42+5dYxtoxM9H/9Dw9F7eg4vF5Bu9AmvB2uymD6bphp4/ah5ageZl7Dx/EqbjDMbUcX + 0ElY2YSOk108wpNQOswidEbg0EVchPYDjBHYgkdoRB7xufx+voBv5z/jT6DhvJs/wRfzbpxOnhCm + CdvhPYK8zpnRMRSJ2vEHyI0Oki9IOjnEj+N90QfkBNmFPgEsoG/AsRZtRQ1AiwW7UCPXwBXAzFHh + BNoILxfcP4G34JNA3UG8DJ1GjxCem4i24NPA13H0V7SMFHGNYJfpXBnQfxRgnYD9G5GbR8JpLCGF + GwRzQD3gKmGf4WSwcJq9LqBGwFyEtortokUTA1ioxLbj1/A58SHUhk6Sn5G7ybt4OR/D7+AnorWq + BEgxWguwN9I9YhleALzTVwOFzs3ni/Eu9AVfrCkB2K9TjgDnfq4AOCpDh+A9XzQCT6PwcrIKKKV3 + w9EJzSQ+BfYDBM0i4BohFxmK5sCoAe1B+9Bg0oLWAiTGrzhc+Cvs3MR/CDyvxfdzf0UnyDiUiMr4 + 8yBrZEGoBaEXNKLAEw6jJNm4l4vNduy13TZdfnNG1OCkqy5lo0bei/L3+iyQ269cyZ/Ohwoz9gph + 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 + i9oKZW5kc3RyZWFtCmVuZG9iagoKMjEgMCBvYmoKMTQ0ODQKZW5kb2JqCgoyMiAwIG9iago8PC9U + eXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0JBQUFBQStEZWphVnVTYW5zCi9GbGFncyA0Ci9G + b250QkJveFstMTAyMCAtNDYyIDE3OTIgMTIzMl0vSXRhbGljQW5nbGUgMAovQXNjZW50IDkyOAov + RGVzY2VudCAtMjM1Ci9DYXBIZWlnaHQgMTIzMgovU3RlbVYgODAKL0ZvbnRGaWxlMiAyMCAwIFIK + Pj4KZW5kb2JqCgoyMyAwIG9iago8PC9MZW5ndGggNDk3L0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0 + cmVhbQp4nF2UzY6bMBSF9zwFy+liBNgGz0gRUiYBKYv+qJk+AAEngzQB5JBF3r6+57it1EWiz+b4 + 3g/CTbY77A/TuGY//Nwf3Zqex2nw7jbffe/Sk7uMU1KodBj7Na7w3V+7JcnC2ePjtrrrYTrPm02S + /QzXbqt/pE/bYT65L0n23Q/Oj9Mlffq1O4b18b4sn+7qpjXNk7pOB3cOdb52y7fu6jKcej4M4fK4 + Pp7DkX+B98fiUoV1QZV+Htxt6Xrnu+nikk2e1+mmbevETcN/1yrDI6dz/9H5EC1CNM+r1zqwIjfC + GmwrYcP9Vrgka+GKXApbsMqFX8io8wo2qL8lY/8NXKL+jnnU34O1Em7IqNmSi8BFTn4Rpr8Wh4L+ + 2grT3xhh+hucpX+JffobnKW/Fs+C/kZ8CvpXyNO/lHsv6G/A9K/gQ/8KvehvUZ/+VvKK/o30UvH5 + 74Xpb+XeFf219FXRX+5L0V8hT/8Sefo34qDor9GL/lqes4rPH3n6Vzvh6I/69DdvwtEfDvSvZF/T + 34iDpr+Rvpr+5VaY/pX01fH9kb6a/hqZ6C+/rxZ/lW/FR9PfIhPfH+zT3yJPf4X96I989EdG/Nu8 + AdNfib+hv7UYkDgJMioyy39GMO3v3ofxw8Bj7mTixsn9/U9Y5kVO4fMbT6YBpAplbmRzdHJlYW0K + ZW5kb2JqCgoyNCAwIG9iago8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9CYXNlRm9udC9C + QUFBQUErRGVqYVZ1U2FucwovRmlyc3RDaGFyIDAKL0xhc3RDaGFyIDY0Ci9XaWR0aHNbNjAwIDI3 + NyA2MzMgNTkxIDYxMSA1NDkgNjE1IDMxNyAzMTcgMjk0IDc0OCA2ODQgMzM2IDYzNiA2MzYgNjM2 + CjYzNiA2MzYgNjM2IDc3MCA2ODQgNjEwIDYzMSA2MzYgNzg3IDYzNCA2MzQgNjk4IDYzMyA2MTIg + NjMzIDUyMAo2MTUgOTc0IDQxMSA2MzYgNzc0IDM2MCA2OTQgNjE1IDYzNiA2MzYgNjg2IDI3NyA2 + MzQgNjU1IDM5MiA1NzkKODYyIDc1MSA2ODUgMzUyIDYzNCAzMzYgNjAzIDYzNiA1MjQgNTU3IDYz + NCAzMTcgMjc3IDEwMDAgNjAwIDgzNwo4MTcgXQovRm9udERlc2NyaXB0b3IgMjIgMCBSCi9Ub1Vu + aWNvZGUgMjMgMCBSCj4+CmVuZG9iagoKMjUgMCBvYmoKPDwvTGVuZ3RoIDI2IDAgUi9GaWx0ZXIv + RmxhdGVEZWNvZGUvTGVuZ3RoMSA4Njg0Pj4Kc3RyZWFtCnic5Vh7fFP3dT/nPizJDyQLTNyIRFcI + XKjfdqCY0PhiW8KunfqFEwkSrIskW0psSZUElCQtpl1b4pCaZIx2DW1ImyYhj3INSTFZE+iarc1a + GrYuS/PpmtBHurQLNc2SNC0g7/x+99oYl/LHtv925XvveZ/z+57zu7pyNr0lCoUwAiKo4WEttbio + wAkAPwRAZ3hrVhHbXy4j+jSAsGsgNTj85aO3vAMgxQAsTw8ObR+oW/FQEUDhIoC8jlhUi7ia2qoA + nKSHlTESbMh92kL8AeKXxIazn3hHTtcS/wLxFUPJsFaa/yOJ+LPElw5rn0h1i4sFgPlW4pWENhz9 + WlHPe8QvAyjYlEpmsvvgQ1MA1+5i+lQ6mtrddmSC+McAxGaSIX3YUUhkHuMFUZLzLFZbPvz/POQf + yj+ET8o7oQS28+slh7QaFsA2gKm3GHfxmrv5/7YKq3F7Gp6DQ3DgEtUu+BRdn7xEdhy+C09w6gG4 + 9wphj8HjJrUX/hY+/xftboPPUJyHKf/FI0TS7fAlyjwBj9KgLMZ6ynq7qf0pvHj5UPhzfBHuh8fI + 8n44StcHaGfcKbwN9ws9kBBeEXfCp+FuWuODGIcxsg/Bw7gRNpHUODZBFJJzgo7CHvgG3EG7cOaQ + d079FxSdP0KV301x9kEcPk6dtJ+/duptuE76NRTl/hWOi26q/ZvwDHfZOe1raRVvE74lCBf+mpj7 + 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 + YmoKPDwvVHlwZS9Gb250RGVzY3JpcHRvci9Gb250TmFtZS9GQUFBQUErTGliZXJhdGlvblNhbnMK + L0ZsYWdzIDQKL0ZvbnRCQm94Wy01NDMgLTMwMyAxMzAwIDk3OV0vSXRhbGljQW5nbGUgMAovQXNj + ZW50IDkwNQovRGVzY2VudCAtMjExCi9DYXBIZWlnaHQgOTc5Ci9TdGVtViA4MAovRm9udEZpbGUy + IDI1IDAgUgo+PgplbmRvYmoKCjI4IDAgb2JqCjw8L0xlbmd0aCAyNjgvRmlsdGVyL0ZsYXRlRGVj + b2RlPj4Kc3RyZWFtCnicXZHLasQgFIb3PoXL6WLQpJm2AyEQph3IoheazgMYPUmFRsWYRd6+Hp22 + 0IXyncsv/zmyU/fYGR3Ym7eyh0BHbZSHxa5eAh1g0oYUJVVahmuUbjkLR1jU9tsSYO7MaOuasPdY + W4Lf6K5VdoAbwl69Aq/NRHeXUx/jfnXuC2YwgXLSNFTBGN95Fu5FzMCSat+pWNZh20fJX8PH5oCW + KS6yFWkVLE5I8MJMQGrOG1qfzw0Bo/7VCp4lwyg/hY+tRWzlvCqayGXiQ4t8m/mAXGUukQ+5P+Xv + Epcc+T7nj8gPmZ+Qj1lbIbc5n41dHaBF3OHP6FSu3sex06LTvDipNvD7F846VKXzDZOigo4KZW5k + c3RyZWFtCmVuZG9iagoKMjkgMCBvYmoKPDwvVHlwZS9Gb250L1N1YnR5cGUvVHJ1ZVR5cGUvQmFz + ZUZvbnQvRkFBQUFBK0xpYmVyYXRpb25TYW5zCi9GaXJzdENoYXIgMAovTGFzdENoYXIgMTAKL1dp + ZHRoc1s3NTAgNjY2IDYxMCA3MjIgNzIyIDY2NiAyNzcgMjc3IDcyMiA2MTAgNzc3IF0KL0ZvbnRE + ZXNjcmlwdG9yIDI3IDAgUgovVG9Vbmljb2RlIDI4IDAgUgo+PgplbmRvYmoKCjMwIDAgb2JqCjw8 + L0YxIDI0IDAgUi9GMiAxOSAwIFIvRjMgMTQgMCBSL0Y0IDkgMCBSL0Y1IDI5IDAgUgo+PgplbmRv + YmoKCjMxIDAgb2JqCjw8L0ZvbnQgMzAgMCBSCi9Qcm9jU2V0Wy9QREYvVGV4dF0KPj4KZW5kb2Jq + CgoxIDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgNCAwIFIvUmVzb3VyY2VzIDMxIDAgUi9NZWRp + YUJveFswIDAgNTk0LjkzNTQzMzA3MDg2NiA4NDEuODg5NzYzNzc5NTI4XS9Hcm91cDw8L1MvVHJh + bnNwYXJlbmN5L0NTL0RldmljZVJHQi9JIHRydWU+Pi9Db250ZW50cyAyIDAgUj4+CmVuZG9iagoK + MzIgMCBvYmoKPDwvQ291bnQgMS9GaXJzdCAzMyAwIFIvTGFzdCAzMyAwIFIKPj4KZW5kb2JqCgoz + MyAwIG9iago8PC9Db3VudCAwL1RpdGxlPEZFRkYwMDUzMDA2QzAwNjkwMDY0MDA2NTAwMjAwMDMx + PgovRGVzdFsxIDAgUi9YWVogMCA4NDEuOCAwXS9QYXJlbnQgMzIgMCBSPj4KZW5kb2JqCgo0IDAg + b2JqCjw8L1R5cGUvUGFnZXMKL1Jlc291cmNlcyAzMSAwIFIKL01lZGlhQm94WyAwIDAgNTk0IDg0 + MSBdCi9LaWRzWyAxIDAgUiBdCi9Db3VudCAxPj4KZW5kb2JqCgozNCAwIG9iago8PC9UeXBlL0Nh + dGFsb2cvUGFnZXMgNCAwIFIKL09wZW5BY3Rpb25bMSAwIFIgL1hZWiBudWxsIG51bGwgMF0KL091 + dGxpbmVzIDMyIDAgUgo+PgplbmRvYmoKCjM1IDAgb2JqCjw8L0NyZWF0b3I8RkVGRjAwNDQwMDcy + MDA2MTAwNzc+Ci9Qcm9kdWNlcjxGRUZGMDA0QzAwNjkwMDYyMDA3MjAwNjUwMDRGMDA2NjAwNjYw + MDY5MDA2MzAwNjUwMDIwMDAzNjAwMkUwMDMwPgovQ3JlYXRpb25EYXRlKEQ6MjAxODEwMDIxNTE5 + MjYrMDInMDAnKT4+CmVuZG9iagoKeHJlZgowIDM2CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDA0 + NTU0NSAwMDAwMCBuIAowMDAwMDAwMDE5IDAwMDAwIG4gCjAwMDAwMDI2NTkgMDAwMDAgbiAKMDAw + MDA0NTg4MSAwMDAwMCBuIAowMDAwMDAyNjgwIDAwMDAwIG4gCjAwMDAwMDY2MDcgMDAwMDAgbiAK + MDAwMDAwNjYyOCAwMDAwMCBuIAowMDAwMDA2ODM1IDAwMDAwIG4gCjAwMDAwMDcxMjUgMDAwMDAg + biAKMDAwMDAwNzI5NiAwMDAwMCBuIAowMDAwMDA5NjkwIDAwMDAwIG4gCjAwMDAwMDk3MTIgMDAw + MDAgbiAKMDAwMDAwOTkwNSAwMDAwMCBuIAowMDAwMDEwMjAzIDAwMDAwIG4gCjAwMDAwMTAzNjMg + MDAwMDAgbiAKMDAwMDAyMjY4MCAwMDAwMCBuIAowMDAwMDIyNzAzIDAwMDAwIG4gCjAwMDAwMjI5 + MDMgMDAwMDAgbiAKMDAwMDAyMzQwMiAwMDAwMCBuIAowMDAwMDIzNzU0IDAwMDAwIG4gCjAwMDAw + MzgzMjUgMDAwMDAgbiAKMDAwMDAzODM0OCAwMDAwMCBuIAowMDAwMDM4NTQzIDAwMDAwIG4gCjAw + MDAwMzkxMTAgMDAwMDAgbiAKMDAwMDAzOTUyNSAwMDAwMCBuIAowMDAwMDQ0NjYwIDAwMDAwIG4g + CjAwMDAwNDQ2ODIgMDAwMDAgbiAKMDAwMDA0NDg3OCAwMDAwMCBuIAowMDAwMDQ1MjE2IDAwMDAw + IG4gCjAwMDAwNDU0MTggMDAwMDAgbiAKMDAwMDA0NTQ5MCAwMDAwMCBuIAowMDAwMDQ1NzE0IDAw + MDAwIG4gCjAwMDAwNDU3NzAgMDAwMDAgbiAKMDAwMDA0NTk4MCAwMDAwMCBuIAowMDAwMDQ2MDgx + IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSAzNi9Sb290IDM0IDAgUgovSW5mbyAzNSAwIFIKL0lE + IFsgPDNBNjc1RTc2RjhENTIzMjExQkE4RDU3MkMwMjQyQ0UwPgo8M0E2NzVFNzZGOEQ1MjMyMTFC + QThENTcyQzAyNDJDRTA+IF0KL0RvY0NoZWNrc3VtIC84OTY5MDdENzQ1QTdGNUNDNTg2RTY0MjdB + Mzg1NkE5MAo+PgpzdGFydHhyZWYKNDYyNDgKJSVFT0YK + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '47172' + Content-Type: + - application/pdf + User-Agent: + - python-requests/2.28.1 + method: PUT + uri: https://pingen2-staging-transfer.objects.rma.cloudscale.ch/f14144b8-a211-4514-8333-f4e09d8a16df_2023-05-23_19%3A16%3A45?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20230523%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20230523T165645Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=ef959eb9ea0e53824005190a1b4f4f33e460c054e3f29865913945f29e4b1ec5 + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Tue, 23 May 2023 16:56:45 GMT + ETag: + - '"287162a4ff07b702606deb97d1223d17"' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + x-amz-request-id: + - tx00000cb19ca4be7fcbec9-00646cf04d-606f5193f-rma1 + status: + code: 200 + message: OK +- request: + body: '{"data": {"type": "letters", "attributes": {"file_original_name": "in_invoice_yourcompany_demo.pdf", + "file_url": "https://pingen2-staging-transfer.objects.rma.cloudscale.ch/f14144b8-a211-4514-8333-f4e09d8a16df_2023-05-23_19%3A16%3A45?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20230523%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20230523T165645Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=ef959eb9ea0e53824005190a1b4f4f33e460c054e3f29865913945f29e4b1ec5", + "file_url_signature": "1fb811d0e1f010292d40bb672bfb1fde", "address_position": + "left", "auto_send": true, "delivery_product": "cheap", "print_spectrum": "grayscale", + "print_mode": "simplex"}}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6IjA5Y2M1MGJhNDA0ZjViY2Y1NjkyZDRkNjAzOTEwNzg5YWM1NjUxYjg0OWYwMzBjMTljYzVmNzc1YjdhYWY1MzJkMDAzN2E3MDRkMmUxZmQ0IiwiaWF0IjoxNjg0ODYxMDA1LjE2ODI4MywibmJmIjoxNjg0ODYxMDA1LjE2ODI4NSwiZXhwIjoxNjg0OTA0MjA1LCJzdWIiOiJleUpwZGlJNklrSkRlVFZWZVZvNWNYRndPRk0zWlZKUWFISjFkVkU5UFNJc0luWmhiSFZsSWpvaVNFTkNOMWhDV21nNVluSmplRmwzVGt0R05uWTNaejA5SWl3aWJXRmpJam9pWlRZNFlURmxOR0V6WVdRek1UazRZVGszTVRnMU5qQmlOVEEzWVdFNE1HUTRZMlU1TmpZNU9UWXhOV1UzTkdVNVl6bGxZakExTkRVeE16aG1ZamN4WVNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.BsjXSIFrNZys4ZkEUbAPGol14wovjXhZCTW9D0sO7bjahwwRCD_ahMgAsEBhDO9EPTJL1XGpMS5RaR_ggEewrPyCLudUGpH20cKhvwMz7oFN3tKmZei-zgF8YzsKRQ_6Jcx3kK59YmGyr0IiSb3KZlYURPwTzfQ7gemKHKmVy87oKuBXotafwy526OPk1-1gbWNCLbdO3hlopfSStFMOQsFO2OHhx1CUHg2kIEJn4EJGD9mgTpea03RDg97bwADfKma0J6a4Th2biLCQg_ExiS6DIgNM1H_Gb6HSU8YvIfR3gvPMkNeJELmBtzAXPNkriq3fzaTkGpeXHDw6wEjvDcpI4A2P74Q_vgu5UGuVqSpuewrINWO4aC58ZjoZGmCKY7aVjNb4KIHF3z626s02gJ6YkwIoTRg02z-mZVtKJqWbUGSDgoeb8_gOkgADKGsos7s5x2nlX7USquPclK_JGuJy8qhKiudm2qZsMVCB0RI2ULkyaB1CEEYJztFIbcKHufmRdI2facZ6Vjt0JWHRBKND7J8CO3Axlz0KMaMRTn3k7iOaXz11l0tvyn9EQ6NIGfc0iz7-VtJZU90bI1jJYLZaTFaMYlKoW5cj6mY2hoj8UyuzxOpb74XLe7KwzusVfaS_1ig5xtkZpQtdtCMrhe5vcZPDkwDxD37ivq0pdu4 + Connection: + - keep-alive + Content-Length: + - '730' + Content-Type: + - application/vnd.api+json + Cookie: + - 9e95c922595c91f6fd6e3f88ecd31058=dbd6167e7abb8a3add8206709b5d4743 + User-Agent: + - python-requests/2.28.1 + method: POST + uri: https://api-staging.v2.pingen.com/organisations/4c08c24e-65f8-47cf-b280-518ee76e3437/letters + response: + body: + string: '{"data":{"type":"letters","id":"bc095d48-996a-4995-bfba-1b8d3d9ef851","attributes":{"status":"validating","file_original_name":"in_invoice_yourcompany_demo.pdf","file_pages":null,"address":null,"address_position":"left","country":null,"delivery_product":"cheap","print_mode":"simplex","print_spectrum":"grayscale","price_currency":null,"price_value":null,"paper_types":null,"fonts":null,"source":"api","tracking_number":null,"submitted_at":null,"created_at":"2023-05-23T18:56:45+0200","updated_at":"2023-05-23T18:56:45+0200"},"relationships":{"organisation":{"links":{"related":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437"},"data":{"type":"organisations","id":"4c08c24e-65f8-47cf-b280-518ee76e3437"}},"events":{"links":{"related":{"href":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/bc095d48-996a-4995-bfba-1b8d3d9ef851\/events","meta":{"count":1}}}},"batch":{"data":null}},"links":{"self":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/bc095d48-996a-4995-bfba-1b8d3d9ef851"},"meta":{"abilities":{"self":{"cancel":"state","delete":"state","submit":"state","send-simplex":"ok","edit":"state","get-pdf-raw":"state","get-pdf-validation":"state","change-paper-type":"state","change-window-position":"state","create-coverpage":"state","fix-overwrite-restricted-areas":"state","fix-coverpage":"state","fix-country":"state","fix-regular-paper":"state"}}}}}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-cache, private + content-type: + - application/vnd.api+json + date: + - Tue, 23 May 2023 16:56:45 GMT + server: + - nginx + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '898' + x-request-id: + - 4c66e037-970c-4fcc-9daa-1a86623bf2c0 + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +- request: + body: grant_type=client_credentials + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic RkZDU0NPNFFPRzJPVk4zRlU2RUQ6MXoyTk0zVlV4bVdpZ3Y0N1RGYktrV2xtR3N4SFFQTlRxeGRzZlJTTHVPWFJEMmFpZGpaVFJxYVBIcEc4amZKeC9tK21JaVFiRk1BQTBEVXg= + Connection: + - keep-alive + Content-Length: + - '29' + Content-Type: + - application/x-www-form-urlencoded;charset=UTF-8 + User-Agent: + - python-requests/2.28.1 + method: POST + uri: https://identity-staging.pingen.com/auth/access-tokens + response: + body: + string: '{"token_type":"Bearer","expires_in":43200,"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6ImJkOTU3MTY0NDMyNDYyZTk0ZWM2NTUyZDkzNzhlMmJkOWZiNmVhNjBhMGE3Yzc2MWViMjhiZTI2OGZlNDVjNzFjNWRlOTZhZjA4MzBhODcwIiwiaWF0IjoxNjk2NDkyMTQxLjcwNTM2MiwibmJmIjoxNjk2NDkyMTQxLjcwNTM2NCwiZXhwIjoxNjk2NTM1MzQxLCJzdWIiOiJleUpwZGlJNklsVXhjVVVyWVdkQ09HZ3laVmRoV205R2FtazBSWGM5UFNJc0luWmhiSFZsSWpvaVNqWkNiemsyVWpSMlJEVTVMMkZEZDAwdlREZzVRVDA5SWl3aWJXRmpJam9pTURJMk56VmxZelZtTXpJd09USmhPVGN4T0RCaU9HSmlaV1l3T0dZeE5EQTVaVEpsTnpKaU5EWmpZVFV3WW1Kak1HRmxaREJtTVdZNVlUY3hNekUwTUNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.JwmdocLAu7eElyoRHkPOAnLWqv6THC_dxcgNgKsbxalIkcDPV53dSEaYJVnBe6tBEoC-blOhcbyKmI0zT67lra3PkZjMTVMBc1lieip340gs2ZthzDfMo5_y46pSS9QmBCzQAlzwACWoWvTcxEwd0Dha0aDR55QlYOUnY6WdrxbEf9tpJJDNFENSmrRRxaUfjVKIq1WR5IXFOCMCJhEaBOs8RCZBxz6GPwemGW2T7Qn5E2Q9kbcrGVixP1eShTCCUu6D9EduWcxWB9-kgcuMBDQwjktkYFhac7bYWVQpgOSilTDK5RXySNjFKA-SIMeDLJP0KkArqUGJRHuIVdhCVsKWAMHZzgUG0jbeMzjOoN0q5m4DWX-lzjReEdKmwFajUs44KXd_6FEd1e7Z2n3v2oDSf19svunLnWZw6lyQViWFuvpAw3jX05nryLcmXzcOYwsOw-oLWl19nek1sLpyJbCJqLOltBCsUjIbjDeDt9_8qYmSKZRA6FWateIuPLXJnzst4_-hkelcj_MKDeTEEPI5qEX9W4PQDRHfTPhsqaxrw8EtYuiRfxNOkM1TJ3-_96ZrfT9CeDH-tUAdwFyg93pvevV5zsWiWxgOMmJoZldGcazj0fN2BYCS7WZEOhYskYdXhTwLQCG1ycuIxxmI5lwGXFGBr13OTUP1qEOhA-Y"}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-store, private + content-type: + - application/json + date: + - Thu, 05 Oct 2023 07:49:01 GMT + pragma: + - no-cache + server: + - nginx + set-cookie: + - fb7eb3c0fd7448a4f8905cb9504f3ebb=ba0f79c6de12140eb5d2094b68953974; path=/; + HttpOnly; Secure; SameSite=None + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '899' + x-request-id: + - 7e763f85-f0e7-4aad-aeb6-27f8803d5231 + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6ImJkOTU3MTY0NDMyNDYyZTk0ZWM2NTUyZDkzNzhlMmJkOWZiNmVhNjBhMGE3Yzc2MWViMjhiZTI2OGZlNDVjNzFjNWRlOTZhZjA4MzBhODcwIiwiaWF0IjoxNjk2NDkyMTQxLjcwNTM2MiwibmJmIjoxNjk2NDkyMTQxLjcwNTM2NCwiZXhwIjoxNjk2NTM1MzQxLCJzdWIiOiJleUpwZGlJNklsVXhjVVVyWVdkQ09HZ3laVmRoV205R2FtazBSWGM5UFNJc0luWmhiSFZsSWpvaVNqWkNiemsyVWpSMlJEVTVMMkZEZDAwdlREZzVRVDA5SWl3aWJXRmpJam9pTURJMk56VmxZelZtTXpJd09USmhPVGN4T0RCaU9HSmlaV1l3T0dZeE5EQTVaVEpsTnpKaU5EWmpZVFV3WW1Kak1HRmxaREJtTVdZNVlUY3hNekUwTUNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.JwmdocLAu7eElyoRHkPOAnLWqv6THC_dxcgNgKsbxalIkcDPV53dSEaYJVnBe6tBEoC-blOhcbyKmI0zT67lra3PkZjMTVMBc1lieip340gs2ZthzDfMo5_y46pSS9QmBCzQAlzwACWoWvTcxEwd0Dha0aDR55QlYOUnY6WdrxbEf9tpJJDNFENSmrRRxaUfjVKIq1WR5IXFOCMCJhEaBOs8RCZBxz6GPwemGW2T7Qn5E2Q9kbcrGVixP1eShTCCUu6D9EduWcxWB9-kgcuMBDQwjktkYFhac7bYWVQpgOSilTDK5RXySNjFKA-SIMeDLJP0KkArqUGJRHuIVdhCVsKWAMHZzgUG0jbeMzjOoN0q5m4DWX-lzjReEdKmwFajUs44KXd_6FEd1e7Z2n3v2oDSf19svunLnWZw6lyQViWFuvpAw3jX05nryLcmXzcOYwsOw-oLWl19nek1sLpyJbCJqLOltBCsUjIbjDeDt9_8qYmSKZRA6FWateIuPLXJnzst4_-hkelcj_MKDeTEEPI5qEX9W4PQDRHfTPhsqaxrw8EtYuiRfxNOkM1TJ3-_96ZrfT9CeDH-tUAdwFyg93pvevV5zsWiWxgOMmJoZldGcazj0fN2BYCS7WZEOhYskYdXhTwLQCG1ycuIxxmI5lwGXFGBr13OTUP1qEOhA-Y + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.1 + method: GET + uri: https://api-staging.v2.pingen.com/file-upload + response: + body: + string: '{"data":{"type":"file_uploads","id":"640b2883-c576-4a35-87f9-1abf2801461f","attributes":{"url":"https:\/\/pingen2-staging-transfer.objects.rma.cloudscale.ch\/640b2883-c576-4a35-87f9-1abf2801461f_2023-10-05_10%3A09%3A01?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20231005%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20231005T074901Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=fe7fce59939b3bd73b03124ecc3fba6abb3312690c0c54674bc95e1fb8b2bd1f","url_signature":"a29e61a0f8f9a11fc76e90ede9a5d7e6","expires_at":"2023-10-05T10:09:01+0200"}}}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-cache, private + content-type: + - application/vnd.api+json + date: + - Thu, 05 Oct 2023 07:49:01 GMT + server: + - nginx + set-cookie: + - 9e95c922595c91f6fd6e3f88ecd31058=af9045857b546686a55bedf0fc71c236; path=/; + HttpOnly; Secure; SameSite=None + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '899' + x-request-id: + - fc3bcb68-9cbc-4615-8833-f2eedb391909 + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/pdf + User-Agent: + - python-requests/2.28.1 + method: PUT + uri: https://pingen2-staging-transfer.objects.rma.cloudscale.ch/640b2883-c576-4a35-87f9-1abf2801461f_2023-10-05_10%3A09%3A01?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20231005%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20231005T074901Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=fe7fce59939b3bd73b03124ecc3fba6abb3312690c0c54674bc95e1fb8b2bd1f + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Thu, 05 Oct 2023 07:49:01 GMT + ETag: + - '"d41d8cd98f00b204e9800998ecf8427e"' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + x-amz-request-id: + - tx000000fdc64c3dd45be93-00651e6a6d-62438a6ca-rma1 + status: + code: 200 + message: OK +- request: + body: '{"data": {"type": "letters", "attributes": {"file_original_name": "in_invoice_yourcompany_demo.pdf", + "file_url": "https://pingen2-staging-transfer.objects.rma.cloudscale.ch/640b2883-c576-4a35-87f9-1abf2801461f_2023-10-05_10%3A09%3A01?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20231005%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20231005T074901Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=fe7fce59939b3bd73b03124ecc3fba6abb3312690c0c54674bc95e1fb8b2bd1f", + "file_url_signature": "a29e61a0f8f9a11fc76e90ede9a5d7e6", "address_position": + "left", "auto_send": true, "delivery_product": "cheap", "print_spectrum": "grayscale", + "print_mode": "simplex"}}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6ImJkOTU3MTY0NDMyNDYyZTk0ZWM2NTUyZDkzNzhlMmJkOWZiNmVhNjBhMGE3Yzc2MWViMjhiZTI2OGZlNDVjNzFjNWRlOTZhZjA4MzBhODcwIiwiaWF0IjoxNjk2NDkyMTQxLjcwNTM2MiwibmJmIjoxNjk2NDkyMTQxLjcwNTM2NCwiZXhwIjoxNjk2NTM1MzQxLCJzdWIiOiJleUpwZGlJNklsVXhjVVVyWVdkQ09HZ3laVmRoV205R2FtazBSWGM5UFNJc0luWmhiSFZsSWpvaVNqWkNiemsyVWpSMlJEVTVMMkZEZDAwdlREZzVRVDA5SWl3aWJXRmpJam9pTURJMk56VmxZelZtTXpJd09USmhPVGN4T0RCaU9HSmlaV1l3T0dZeE5EQTVaVEpsTnpKaU5EWmpZVFV3WW1Kak1HRmxaREJtTVdZNVlUY3hNekUwTUNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.JwmdocLAu7eElyoRHkPOAnLWqv6THC_dxcgNgKsbxalIkcDPV53dSEaYJVnBe6tBEoC-blOhcbyKmI0zT67lra3PkZjMTVMBc1lieip340gs2ZthzDfMo5_y46pSS9QmBCzQAlzwACWoWvTcxEwd0Dha0aDR55QlYOUnY6WdrxbEf9tpJJDNFENSmrRRxaUfjVKIq1WR5IXFOCMCJhEaBOs8RCZBxz6GPwemGW2T7Qn5E2Q9kbcrGVixP1eShTCCUu6D9EduWcxWB9-kgcuMBDQwjktkYFhac7bYWVQpgOSilTDK5RXySNjFKA-SIMeDLJP0KkArqUGJRHuIVdhCVsKWAMHZzgUG0jbeMzjOoN0q5m4DWX-lzjReEdKmwFajUs44KXd_6FEd1e7Z2n3v2oDSf19svunLnWZw6lyQViWFuvpAw3jX05nryLcmXzcOYwsOw-oLWl19nek1sLpyJbCJqLOltBCsUjIbjDeDt9_8qYmSKZRA6FWateIuPLXJnzst4_-hkelcj_MKDeTEEPI5qEX9W4PQDRHfTPhsqaxrw8EtYuiRfxNOkM1TJ3-_96ZrfT9CeDH-tUAdwFyg93pvevV5zsWiWxgOMmJoZldGcazj0fN2BYCS7WZEOhYskYdXhTwLQCG1ycuIxxmI5lwGXFGBr13OTUP1qEOhA-Y + Connection: + - keep-alive + Content-Length: + - '730' + Content-Type: + - application/vnd.api+json + Cookie: + - 9e95c922595c91f6fd6e3f88ecd31058=af9045857b546686a55bedf0fc71c236 + User-Agent: + - python-requests/2.28.1 + method: POST + uri: https://api-staging.v2.pingen.com/organisations/4c08c24e-65f8-47cf-b280-518ee76e3437/letters + response: + body: + string: '{"data":{"type":"letters","id":"aa99abf2-8fb5-4f8d-9f06-f84fe6b752cf","attributes":{"status":"validating","file_original_name":"in_invoice_yourcompany_demo.pdf","file_pages":null,"address":null,"address_position":"left","country":null,"delivery_product":"cheap","print_mode":"simplex","print_spectrum":"grayscale","price_currency":null,"price_value":null,"paper_types":null,"fonts":null,"source":"api","tracking_number":null,"submitted_at":null,"created_at":"2023-10-05T09:49:02+0200","updated_at":"2023-10-05T09:49:02+0200"},"relationships":{"organisation":{"links":{"related":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437"},"data":{"type":"organisations","id":"4c08c24e-65f8-47cf-b280-518ee76e3437"}},"events":{"links":{"related":{"href":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/aa99abf2-8fb5-4f8d-9f06-f84fe6b752cf\/events","meta":{"count":1}}}},"batch":{"data":null}},"links":{"self":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/aa99abf2-8fb5-4f8d-9f06-f84fe6b752cf"},"meta":{"abilities":{"self":{"cancel":"state","delete":"state","submit":"state","send-simplex":"ok","edit":"state","get-pdf-raw":"state","get-pdf-validation":"state","change-paper-type":"state","change-window-position":"state","create-coverpage":"state","add-attachment":"state","fix-overwrite-restricted-areas":"state","fix-coverpage":"state","fix-country":"state","fix-regular-paper":"state","fix-address":"state"}}}}}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-cache, private + content-type: + - application/vnd.api+json + date: + - Thu, 05 Oct 2023 07:49:02 GMT + server: + - nginx + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '898' + x-request-id: + - 41992d11-0b17-47b3-b1d1-c8695d22a2ef + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +- request: + body: grant_type=client_credentials + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic RkZDU0NPNFFPRzJPVk4zRlU2RUQ6MXoyTk0zVlV4bVdpZ3Y0N1RGYktrV2xtR3N4SFFQTlRxeGRzZlJTTHVPWFJEMmFpZGpaVFJxYVBIcEc4amZKeC9tK21JaVFiRk1BQTBEVXg= + Connection: + - keep-alive + Content-Length: + - '29' + Content-Type: + - application/x-www-form-urlencoded;charset=UTF-8 + User-Agent: + - python-requests/2.28.1 + method: POST + uri: https://identity-staging.pingen.com/auth/access-tokens + response: + body: + string: '{"token_type":"Bearer","expires_in":43200,"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6IjA4MWVlMTZmY2U1YzlhZjcyNzc1MWJmMjA4M2JkM2Y5MWY5MmQ0NDVlOWMzNjUwM2U2YWUxODYxOWU0Yzg3Zjc3YTJhOTY5NDMwYWFiMTA2IiwiaWF0IjoxNjk2NDkyMzI2LjM3ODY0MywibmJmIjoxNjk2NDkyMzI2LjM3ODY0NCwiZXhwIjoxNjk2NTM1NTI2LCJzdWIiOiJleUpwZGlJNklsVnNiSFZuWTFOVFVVTkljSEZOV0hwck1TdFdjR2M5UFNJc0luWmhiSFZsSWpvaVMyeEtUMUZPT1VKTWVEbDBTR1UyWm1RM2VEaHNRVDA5SWl3aWJXRmpJam9pTm1FelpUbGhNRFU0WXpJd016VmlZelEwWXpFNE9HVXlNelkwTm1Zek5HRTBNekZoWXpCaFpqTTBOelk0WkdZNU4yVXlaR1ZsTldRM1lUVTVOVFF6WVNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.YF-MNUMiobEim76PDSxTYrLyTbjjaTKDP13010NpwrpM7UWf8jD7_tZALiR7WFSagPf_BqbhsZYJc8FRlH3XnDHmmdBc6QzwEbiV5oT7vvzBiv5vilzVKY-FuGSxJ3lHJfkcbCXNNmanllvkBwP4gTD6SQ1JJcLhZHDV-8fRBJojiHmS1cbHGaJ21LwT1o3nYXzAmWH2gGbLq_X3a0x6tk1IKR1FSPIb6BAKW5A43H9pw4v5-ZIOzjy5KTwqwF8-03eLcQkUEOoUN3FSmQI2bB_25DzXZLT-sjByTqBntquZ1Q08sbd8RQCjI6_CxxA5E-zwe2NYj25vUzwCI-_KQGwSq-_VUD26m1w61mVtAQBl74p6ey5w8EfN8TVfAT4roDU2PpT7FyRxIBQ25uG1shQZkWKqoOdRc7gbYHvnLj__LFeRWgupH6bZjJ3zbuTMygsmepjzGvKDaFUGMTKxiPSPqs6F7ksxsIw2iAL3Epi7I0bk5VEvoN9L628GQnMVRri859k9AVpfTjB8DNpxLP6PRhA2Fm9MdAHoEfjGOHjBHf53d-dWl9ZpKFUpRxXlyqaiRpsLosRgHyGwwfwfY3VQNHtaVew2k7cPf-Zi0wgk_ilvaudND6r_oMPzWiTVL4of5a_jLvBiiFDs5ZFNYvFbmQpWmGo5swXg7Aur5M8"}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-store, private + content-type: + - application/json + date: + - Thu, 05 Oct 2023 07:52:06 GMT + pragma: + - no-cache + server: + - nginx + set-cookie: + - fb7eb3c0fd7448a4f8905cb9504f3ebb=af9045857b546686a55bedf0fc71c236; path=/; + HttpOnly; Secure; SameSite=None + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '899' + x-request-id: + - e87ea252-7a6d-43e8-9540-9372c769265b + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6IjA4MWVlMTZmY2U1YzlhZjcyNzc1MWJmMjA4M2JkM2Y5MWY5MmQ0NDVlOWMzNjUwM2U2YWUxODYxOWU0Yzg3Zjc3YTJhOTY5NDMwYWFiMTA2IiwiaWF0IjoxNjk2NDkyMzI2LjM3ODY0MywibmJmIjoxNjk2NDkyMzI2LjM3ODY0NCwiZXhwIjoxNjk2NTM1NTI2LCJzdWIiOiJleUpwZGlJNklsVnNiSFZuWTFOVFVVTkljSEZOV0hwck1TdFdjR2M5UFNJc0luWmhiSFZsSWpvaVMyeEtUMUZPT1VKTWVEbDBTR1UyWm1RM2VEaHNRVDA5SWl3aWJXRmpJam9pTm1FelpUbGhNRFU0WXpJd016VmlZelEwWXpFNE9HVXlNelkwTm1Zek5HRTBNekZoWXpCaFpqTTBOelk0WkdZNU4yVXlaR1ZsTldRM1lUVTVOVFF6WVNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.YF-MNUMiobEim76PDSxTYrLyTbjjaTKDP13010NpwrpM7UWf8jD7_tZALiR7WFSagPf_BqbhsZYJc8FRlH3XnDHmmdBc6QzwEbiV5oT7vvzBiv5vilzVKY-FuGSxJ3lHJfkcbCXNNmanllvkBwP4gTD6SQ1JJcLhZHDV-8fRBJojiHmS1cbHGaJ21LwT1o3nYXzAmWH2gGbLq_X3a0x6tk1IKR1FSPIb6BAKW5A43H9pw4v5-ZIOzjy5KTwqwF8-03eLcQkUEOoUN3FSmQI2bB_25DzXZLT-sjByTqBntquZ1Q08sbd8RQCjI6_CxxA5E-zwe2NYj25vUzwCI-_KQGwSq-_VUD26m1w61mVtAQBl74p6ey5w8EfN8TVfAT4roDU2PpT7FyRxIBQ25uG1shQZkWKqoOdRc7gbYHvnLj__LFeRWgupH6bZjJ3zbuTMygsmepjzGvKDaFUGMTKxiPSPqs6F7ksxsIw2iAL3Epi7I0bk5VEvoN9L628GQnMVRri859k9AVpfTjB8DNpxLP6PRhA2Fm9MdAHoEfjGOHjBHf53d-dWl9ZpKFUpRxXlyqaiRpsLosRgHyGwwfwfY3VQNHtaVew2k7cPf-Zi0wgk_ilvaudND6r_oMPzWiTVL4of5a_jLvBiiFDs5ZFNYvFbmQpWmGo5swXg7Aur5M8 + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.1 + method: GET + uri: https://api-staging.v2.pingen.com/file-upload + response: + body: + string: '{"data":{"type":"file_uploads","id":"a52b11df-21cd-471c-9c4d-9e4022a78458","attributes":{"url":"https:\/\/pingen2-staging-transfer.objects.rma.cloudscale.ch\/a52b11df-21cd-471c-9c4d-9e4022a78458_2023-10-05_10%3A12%3A07?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20231005%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20231005T075207Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=b6194ba3b382f4de472123c9a7a16aa96be42df54e504a7137d26117bf9332d9","url_signature":"99ab75a425865c7f563dbd6880bdc453","expires_at":"2023-10-05T10:12:07+0200"}}}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-cache, private + content-type: + - application/vnd.api+json + date: + - Thu, 05 Oct 2023 07:52:07 GMT + server: + - nginx + set-cookie: + - 9e95c922595c91f6fd6e3f88ecd31058=ba0f79c6de12140eb5d2094b68953974; path=/; + HttpOnly; Secure; SameSite=None + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '899' + x-request-id: + - e928916a-d0f3-45ff-9320-3a297eef97cf + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/pdf + User-Agent: + - python-requests/2.28.1 + method: PUT + uri: https://pingen2-staging-transfer.objects.rma.cloudscale.ch/a52b11df-21cd-471c-9c4d-9e4022a78458_2023-10-05_10%3A12%3A07?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20231005%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20231005T075207Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=b6194ba3b382f4de472123c9a7a16aa96be42df54e504a7137d26117bf9332d9 + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '0' + Date: + - Thu, 05 Oct 2023 07:52:08 GMT + ETag: + - '"d41d8cd98f00b204e9800998ecf8427e"' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + x-amz-request-id: + - tx00000ea3646c679ebcb41-00651e6b28-65a723d5d-rma1 + status: + code: 200 + message: OK +- request: + body: '{"data": {"type": "letters", "attributes": {"file_original_name": "in_invoice_yourcompany_demo.pdf", + "file_url": "https://pingen2-staging-transfer.objects.rma.cloudscale.ch/a52b11df-21cd-471c-9c4d-9e4022a78458_2023-10-05_10%3A12%3A07?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Z3YMWIXX6Y1G0KHUQDZ7%2F20231005%2Fregion1%2Fs3%2Faws4_request&X-Amz-Date=20231005T075207Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=b6194ba3b382f4de472123c9a7a16aa96be42df54e504a7137d26117bf9332d9", + "file_url_signature": "99ab75a425865c7f563dbd6880bdc453", "address_position": + "left", "auto_send": true, "delivery_product": "cheap", "print_spectrum": "grayscale", + "print_mode": "simplex"}}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJGRkNTQ080UU9HMk9WTjNGVTZFRCIsImp0aSI6IjA4MWVlMTZmY2U1YzlhZjcyNzc1MWJmMjA4M2JkM2Y5MWY5MmQ0NDVlOWMzNjUwM2U2YWUxODYxOWU0Yzg3Zjc3YTJhOTY5NDMwYWFiMTA2IiwiaWF0IjoxNjk2NDkyMzI2LjM3ODY0MywibmJmIjoxNjk2NDkyMzI2LjM3ODY0NCwiZXhwIjoxNjk2NTM1NTI2LCJzdWIiOiJleUpwZGlJNklsVnNiSFZuWTFOVFVVTkljSEZOV0hwck1TdFdjR2M5UFNJc0luWmhiSFZsSWpvaVMyeEtUMUZPT1VKTWVEbDBTR1UyWm1RM2VEaHNRVDA5SWl3aWJXRmpJam9pTm1FelpUbGhNRFU0WXpJd016VmlZelEwWXpFNE9HVXlNelkwTm1Zek5HRTBNekZoWXpCaFpqTTBOelk0WkdZNU4yVXlaR1ZsTldRM1lUVTVOVFF6WVNJc0luUmhaeUk2SWlKOSIsInNjb3BlcyI6WyJsZXR0ZXIiLCJvcmdhbmlzYXRpb25fcmVhZCIsIndlYmhvb2siLCJiYXRjaCJdfQ.YF-MNUMiobEim76PDSxTYrLyTbjjaTKDP13010NpwrpM7UWf8jD7_tZALiR7WFSagPf_BqbhsZYJc8FRlH3XnDHmmdBc6QzwEbiV5oT7vvzBiv5vilzVKY-FuGSxJ3lHJfkcbCXNNmanllvkBwP4gTD6SQ1JJcLhZHDV-8fRBJojiHmS1cbHGaJ21LwT1o3nYXzAmWH2gGbLq_X3a0x6tk1IKR1FSPIb6BAKW5A43H9pw4v5-ZIOzjy5KTwqwF8-03eLcQkUEOoUN3FSmQI2bB_25DzXZLT-sjByTqBntquZ1Q08sbd8RQCjI6_CxxA5E-zwe2NYj25vUzwCI-_KQGwSq-_VUD26m1w61mVtAQBl74p6ey5w8EfN8TVfAT4roDU2PpT7FyRxIBQ25uG1shQZkWKqoOdRc7gbYHvnLj__LFeRWgupH6bZjJ3zbuTMygsmepjzGvKDaFUGMTKxiPSPqs6F7ksxsIw2iAL3Epi7I0bk5VEvoN9L628GQnMVRri859k9AVpfTjB8DNpxLP6PRhA2Fm9MdAHoEfjGOHjBHf53d-dWl9ZpKFUpRxXlyqaiRpsLosRgHyGwwfwfY3VQNHtaVew2k7cPf-Zi0wgk_ilvaudND6r_oMPzWiTVL4of5a_jLvBiiFDs5ZFNYvFbmQpWmGo5swXg7Aur5M8 + Connection: + - keep-alive + Content-Length: + - '730' + Content-Type: + - application/vnd.api+json + Cookie: + - 9e95c922595c91f6fd6e3f88ecd31058=ba0f79c6de12140eb5d2094b68953974 + User-Agent: + - python-requests/2.28.1 + method: POST + uri: https://api-staging.v2.pingen.com/organisations/4c08c24e-65f8-47cf-b280-518ee76e3437/letters + response: + body: + string: '{"data":{"type":"letters","id":"2f826b9d-7325-4494-887a-77652817a2bd","attributes":{"status":"validating","file_original_name":"in_invoice_yourcompany_demo.pdf","file_pages":null,"address":null,"address_position":"left","country":null,"delivery_product":"cheap","print_mode":"simplex","print_spectrum":"grayscale","price_currency":null,"price_value":null,"paper_types":null,"fonts":null,"source":"api","tracking_number":null,"submitted_at":null,"created_at":"2023-10-05T09:52:09+0200","updated_at":"2023-10-05T09:52:09+0200"},"relationships":{"organisation":{"links":{"related":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437"},"data":{"type":"organisations","id":"4c08c24e-65f8-47cf-b280-518ee76e3437"}},"events":{"links":{"related":{"href":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/2f826b9d-7325-4494-887a-77652817a2bd\/events","meta":{"count":1}}}},"batch":{"data":null}},"links":{"self":"https:\/\/api-staging.v2.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/2f826b9d-7325-4494-887a-77652817a2bd"},"meta":{"abilities":{"self":{"cancel":"state","delete":"state","submit":"state","send-simplex":"ok","edit":"state","get-pdf-raw":"state","get-pdf-validation":"state","change-paper-type":"state","change-window-position":"state","create-coverpage":"state","add-attachment":"state","fix-overwrite-restricted-areas":"state","fix-coverpage":"state","fix-country":"state","fix-regular-paper":"state","fix-address":"state"}}}}}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-Id, X-Redirect-Location + cache-control: + - no-cache, private + content-type: + - application/vnd.api+json + date: + - Thu, 05 Oct 2023 07:52:09 GMT + server: + - nginx + strict-transport-security: + - max-age=86400; includeSubDomains + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '900' + x-ratelimit-remaining: + - '898' + x-request-id: + - 8600aeff-7209-4cce-a480-0a29c8b628a2 + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +version: 1 diff --git a/pingen/tests/test_pingen.py b/pingen/tests/test_pingen.py new file mode 100644 index 00000000000..ac4f7a004ad --- /dev/null +++ b/pingen/tests/test_pingen.py @@ -0,0 +1,208 @@ +# Copyright 2012-2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import base64 +import json +import logging +from os.path import dirname, join +from unittest.mock import patch + +from freezegun import freeze_time +from vcr import VCR + +from odoo.tests import tagged +from odoo.tests.common import HttpCase + +from odoo.addons.website.tools import MockRequest + +from ..controllers.main import PingenController + +vcr_pingen = VCR( + record_mode="once", + cassette_library_dir=join(dirname(__file__), "fixtures/cassettes"), + path_transformer=VCR.ensure_suffix(".yaml"), + match_on=("method", "uri"), + decode_compressed_response=True, +) + +logging.basicConfig() +vcr_log = logging.getLogger("vcr") +vcr_log.setLevel(logging.INFO) + + +@tagged("post_install", "-at_install") +class TestPingen(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.user.company_id + cls.company.pingen_clientid = "1234" + cls.company.pingen_client_secretid = "1234567893" + cls.company.pingen_organization = "4c08c24e-65f8-47cf-b280-518ee76e3437" + cls.company.pingen_staging = True + + cls.pc = PingenController() + + # flake8: noqa: B950 + def get_request_json(self, uuid): + return json.dumps( + { + "data": { + "type": "webhook_issues", + "id": "735582d1-ccdf-423a-b493-1b3a6935df29", + "attributes": { + "reason": "Validation failed", + "url": r"https:\/\/36f4-2a02-1210-3497-c400-296e-3b7-6940-e38b.ngrok-free.app\/\/pingen\/letter_issues", + "created_at": "2023-05-19T09:35:01+0200", + }, + "relationships": { + "organisation": { + "links": { + "related": r"https:\/\/identity-staging.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437" + }, + "data": { + "type": "organisations", + "id": "4c08c24e-65f8-47cf-b280-518ee76e3437", + }, + }, + "letter": { + "links": { + "related": r"https:\/\/identity-staging.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/ddd105d8-42f6-4357-a103-9b2449bbd8e2" + }, + "data": { + "type": "letters", + "id": uuid, + }, + }, + "event": { + "data": { + "type": "letters_events", + "id": "a9612d08-81c0-4259-8b0a-648c85377f71", + } + }, + }, + }, + "included": [ + { + "type": "organisations", + "id": "4c08c24e-65f8-47cf-b280-518ee76e3437", + "attributes": { + "name": "c2c", + "status": "active", + "plan": "free", + "billing_mode": "prepaid", + "billing_currency": "CHF", + "billing_balance": 199995.56, + "default_country": "CH", + "default_address_position": "right", + "data_retention_addresses": 6, + "data_retention_pdf": 1, + "color": "#0758FF", + "created_at": "2022-10-19T16:08:17+0200", + "updated_at": "2023-02-24T14:06:17+0100", + }, + "links": { + "self": r"https:\/\/identity-staging.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437" + }, + }, + { + "type": "letters", + "id": "ddd105d8-42f6-4357-a103-9b2449bbd8e2", + "attributes": { + "status": "action_required", + "file_original_name": "in_invoice_yourcompany_demo.pdf", + "file_pages": 1, + "address": "405 Pushp Business Campus\n,\nhmedabad, Gujarat, 382418\nnfo@azureinterior.com\n+92 7405987125", + "address_position": "left", + "country": "CH", + "delivery_product": "cheap", + "print_mode": "simplex", + "print_spectrum": "grayscale", + "price_currency": False, + "price_value": False, + "paper_types": ["normal"], + "fonts": [ + { + "name": "LiberationSans", + "is_embedded": True, + }, + { + "name": "Tibetan_Machine_Uni", + "is_embedded": True, + }, + { + "name": "Chandas", + "is_embedded": True, + }, + { + "name": "DejaVuSans-Bold", + "is_embedded": True, + }, + { + "name": "DejaVuSans", + "is_embedded": True, + }, + ], + "source": "api", + "tracking_number": False, + "submitted_at": False, + "created_at": "2023-05-19T09:34:31+0200", + "updated_at": "2023-05-19T09:34:33+0200", + }, + "links": { + "self": r"https:\/\/identity-staging.pingen.com\/organisations\/4c08c24e-65f8-47cf-b280-518ee76e3437\/letters\/ddd105d8-42f6-4357-a103-9b2449bbd8e2" + }, + }, + { + "type": "letters_events", + "id": "a9612d08-81c0-4259-8b0a-648c85377f71", + "attributes": { + "code": "content_failed_inspection", + "name": "Validation failed", + "producer": "Pingen", + "location": "", + "has_image": False, + "data": [], + "emitted_at": "2023-05-19T09:34:33+0200", + "created_at": "2023-05-19T09:34:33+0200", + "updated_at": "2023-05-19T09:34:33+0200", + }, + }, + ], + } + ) + + @vcr_pingen.use_cassette + @freeze_time("2023-05-19") + def test_pingen_push_document(self): + attachment = self.env["ir.attachment"].create( + { + "name": "in_invoice_yourcompany_demo.pdf", + "datas": base64.b64encode(bytes("", "utf8")), + "type": "binary", + } + ) + attachment.write({"send_to_pingen": True}) + pingen_document = attachment.pingen_document_ids + pingen_document.push_to_pingen() + self.assertEqual(pingen_document.state, "pushed") + + # as the demo invoice report does not meet pingen requirements, + # pingen will notify us about it on letter_issues webhook + + with patch( + "odoo.addons.pingen.controllers.main.PingenController._get_request_content" + ) as mocked_function, MockRequest(self.env) as mock_request: + mocked_function.return_value = self.get_request_json( + pingen_document.pingen_uuid + ) + mock_request.httprequest.method = "POST" + # avoid checking signature when calling webhooks + with patch( + "odoo.addons.pingen.controllers.main.PingenController._verify_signature" + ) as mocked_verify_sign: + mocked_verify_sign.return_value = True + self.pc.letter_issues() + + self.assertEqual(pingen_document.state, "pingen_error") + self.assertEqual(pingen_document.last_error_message, "Validation failed") diff --git a/pingen/views/base_config_settings.xml b/pingen/views/base_config_settings.xml new file mode 100644 index 00000000000..538d389920b --- /dev/null +++ b/pingen/views/base_config_settings.xml @@ -0,0 +1,49 @@ + + + + res.config.settings.inherit + res.config.settings + + + +

Pingen

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pingen/views/ir_attachment_view.xml b/pingen/views/ir_attachment_view.xml new file mode 100644 index 00000000000..17bf9e3636c --- /dev/null +++ b/pingen/views/ir_attachment_view.xml @@ -0,0 +1,25 @@ + + + + ir.attachment.pingen.view + ir.attachment + form + + + + + + + + + + + + "{'search_default_attachment_id': [active_id], 'default_attachment_id': active_id}" + Pingen Document + pingen.document + + + diff --git a/pingen/views/pingen_document_view.xml b/pingen/views/pingen_document_view.xml new file mode 100644 index 00000000000..9494ca1f916 --- /dev/null +++ b/pingen/views/pingen_document_view.xml @@ -0,0 +1,200 @@ + + + + + pingen.document.tree + pingen.document + tree + + + + + + + + + + + + + + + pingen.document.form + pingen.document + form + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + pingen.document.search + pingen.document + search + + + + + + + + + + + + + + + + + Pingen Documents + ir.actions.act_window + pingen.document + tree,form + + + + + +
diff --git a/requirements.txt b/requirements.txt index 342693141bb..c9e7b42441c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ # generated from manifests external_dependencies +oauthlib pycups +requests_oauthlib diff --git a/setup/pingen/odoo/addons/pingen b/setup/pingen/odoo/addons/pingen new file mode 120000 index 00000000000..229d8931178 --- /dev/null +++ b/setup/pingen/odoo/addons/pingen @@ -0,0 +1 @@ +../../../../pingen \ No newline at end of file diff --git a/setup/pingen/setup.py b/setup/pingen/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/pingen/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000000..2c9c818c27c --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +vcrpy +requests_mock +freezegun