Merge PR #315 into 16.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2023-12-28 10:01:13 +00:00
31 changed files with 4781 additions and 0 deletions

131
pingen/README.rst Normal file
View File

@@ -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 <https://github.com/requests/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 <https://github.com/OCA/report-print-send/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 <https://github.com/OCA/report-print-send/issues/new?body=module:%20pingen%0Aversion:%2016.0-mig-pingen%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
Contributors
~~~~~~~~~~~~
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Anar Baghirli <a.baghirli@mobilunity.com>
* Akim Juillerat <akim.juillerat@camptocamp.com>
* Anna Janiszewska <anna.janiszewska@camptocamp.com>
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 <https://github.com/OCA/report-print-send/tree/16.0-mig-pingen/pingen>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

2
pingen/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import controllers
from . import models

27
pingen/__manifest__.py Normal file
View File

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

View File

@@ -0,0 +1 @@
from . import main

141
pingen/controllers/main.py Normal file
View File

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

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record forcecreate="True" id="ir_cron_push_pingen" model="ir.cron">
<field name="name">Run Pingen Document Push</field>
<field eval="True" name="active" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
<field name="model_id" ref="model_pingen_document" />
<field name="code">model._push_and_send_to_pingen_cron</field>
</record>
<record forcecreate="True" id="ir_cron_update_pingen" model="ir.cron">
<field name="name">Run Pingen Document Update</field>
<field eval="False" name="active" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
<field name="model_id" ref="model_pingen_document" />
<field name="code">model._update_post_infos_cron</field>
</record>
</odoo>

556
pingen/i18n/fr.po Normal file
View File

@@ -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 <Unknown>\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"

519
pingen/i18n/pingen.pot Normal file
View File

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

View File

@@ -0,0 +1,5 @@
from . import ir_attachment
from . import pingen
from . import pingen_document
from . import res_company
from . import base_config_settings

View File

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

View File

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

298
pingen/models/pingen.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Anar Baghirli <a.baghirli@mobilunity.com>
* Akim Juillerat <akim.juillerat@camptocamp.com>
* Anna Janiszewska <anna.janiszewska@camptocamp.com>

View File

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

35
pingen/readme/USAGE.rst Normal file
View File

@@ -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 <https://github.com/requests/requests-oauthlib>`_
* The address must be in a format accepted by pingen.com: the last line
is the country in English or German.

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_pingen_document_all pingen_document all model_pingen_document 1 0 0 0
3 access_pingen_document_group_user pingen_document group_user model_pingen_document base.group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,473 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" />
<title>pingen.com integration</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="pingen-com-integration">
<h1 class="title">pingen.com integration</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/report-print-send/tree/16.0-mig-pingen/pingen"><img alt="OCA/report-print-send" src="https://img.shields.io/badge/github-OCA%2Freport--print--send-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/report-print-send-16-0-mig-pingen/report-print-send-16-0-mig-pingen-pingen"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runbot.odoo-community.org/runbot/144/16.0-mig-pingen"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>Pingen.com is a paid online service.
It sends uploaded documents by letter post.</p>
<p>One can decide, per document / attachment, if it should be pushed
to pingen.com. The documents are pushed asynchronously.</p>
<p>The informations of the documents from pingen.com are updated through webhook calls.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#errors" id="toc-entry-3">Errors</a></li>
<li><a class="reference internal" href="#dependencies" id="toc-entry-4">Dependencies</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>The authentication token, client ID, organization ID and webhook secret is configured
on the companys view. You can also tick a checkbox if the staging environment
(<a class="reference external" href="https://stage-api.pingen.com">https://stage-api.pingen.com</a>) should be used.</p>
<p>Webhooks should be configured on pingen account. Organization ID and webhook secret must match.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>On the attachment view, a new pingen.com tab has been added.
You can tick a box to push the document to pingen.com.</p>
<p>There is 3 additional options:</p>
<blockquote>
<ul class="simple">
<li>Send: the document will not be only uploaded, but will be also be sent</li>
<li>Speed: priority or economy</li>
<li>Type of print: color or black and white</li>
</ul>
</blockquote>
<p>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.</p>
<p>You can find them in <cite>Settings &gt; Customization &gt; Low Level Objets &gt; Pingen
Documents</cite> or in the more convenient <cite>Documents</cite> menu if you have installed the
<cite>document</cite> module.</p>
</div>
<div class="section" id="errors">
<h1><a class="toc-backref" href="#toc-entry-3">Errors</a></h1>
<p>Sometimes, pingen.com will refuse to send a document because it does not meet
its requirements. In such case, the documents 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.</p>
<p>When a connection error occurs, the action will be retried on the next
scheduler run.</p>
</div>
<div class="section" id="dependencies">
<h1><a class="toc-backref" href="#toc-entry-4">Dependencies</a></h1>
<blockquote>
<ul class="simple">
<li>Require the Python library <a class="reference external" href="https://github.com/requests/requests-oauthlib">requests_oauthlib</a></li>
<li>The address must be in a format accepted by pingen.com: the last line
is the country in English or German.</li>
</ul>
</blockquote>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-5">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/report-print-send/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/report-print-send/issues/new?body=module:%20pingen%0Aversion:%2016.0-mig-pingen%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-7">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-8">Contributors</a></h2>
<ul class="simple">
<li>Anna Janiszewska &lt;<a class="reference external" href="mailto:anna.janiszewska&#64;camptocamp.com">anna.janiszewska&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/report-print-send/tree/16.0-mig-pingen/pingen">OCA/report-print-send</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

1
pingen/tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import test_pingen

File diff suppressed because it is too large Load Diff

208
pingen/tests/test_pingen.py Normal file
View File

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

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.inherit</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='integration']" position='after'>
<h2>Pingen</h2>
<div class="o_settings_container" id="pingen_integraton">
<div
id="pingen_configuration_settings"
class="o_settings_box col-12 col-lg-6"
>
<div class="mt16 row">
<label class="o_form_label col-3" for="pingen_clientid" />
<field name="pingen_clientid" />
</div>
<div class="mt16 row">
<label
class="o_form_label col-3"
for="pingen_client_secretid"
/>
<field name="pingen_client_secretid" />
</div>
<div class="mt16 row">
<label
class="o_form_label col-3"
for="pingen_organization"
/>
<field name="pingen_organization" />
</div>
<div class="mt16 row">
<label
class="o_form_label col-3"
for="pingen_webhook_secret"
/>
<field name="pingen_webhook_secret" />
</div>
<div class="mt16 row">
<label class="o_form_label col-3" for="pingen_staging" />
<field name="pingen_staging" />
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_attachment_form" model="ir.ui.view">
<field name="name">ir.attachment.pingen.view</field>
<field name="model">ir.attachment</field>
<field name="type">form</field>
<field name="inherit_id" ref="base.view_attachment_form" />
<field name="arch" type="xml">
<xpath expr="//group/group[2]" position="after">
<group string="Pingen info" groups="base.group_no_one">
<field name="send_to_pingen" />
</group>
</xpath>
</field>
</record>
<record model="ir.actions.act_window" id="act_attachment_to_pingen_document">
<field
name="context"
>"{'search_default_attachment_id': [active_id], 'default_attachment_id': active_id}"</field>
<field name="name">Pingen Document</field>
<field name="res_model">pingen.document</field>
<field name="binding_model_id" ref="base.model_ir_attachment" />
</record>
</odoo>

View File

@@ -0,0 +1,200 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_pingen_document_tree" model="ir.ui.view">
<field name="name">pingen.document.tree</field>
<field name="model">pingen.document</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree>
<field name="push_date" />
<field name="name" />
<field name="auto_send" />
<field name="delivery_product" />
<field name="print_spectrum" />
<field name="state" />
<field name="pingen_status" />
</tree>
</field>
</record>
<record id="view_pingen_document_form" model="ir.ui.view">
<field name="name">pingen.document.form</field>
<field name="model">pingen.document</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Pingen Document">
<header>
<button
name="push_to_pingen"
type="object"
states="pending"
string="Push to pingen.com"
icon="fa-upload"
/>
<button
name="ask_pingen_send"
type="object"
string="Ask pingen.com to send the document"
icon="fa-share"
attrs="{'invisible': ['|', ('state', '!=', 'pushed'),('auto_send', '=', True)]}"
/>
<button
name="resolve_error"
type="object"
states="error,pingen_error"
string="Errors resolved"
icon="fa-repeat"
/>
<button
name="update_post_infos"
type="object"
states="pushed,sendcenter,sent"
string="Update the letter's informations"
icon="fa-refresh"
/>
<field
name="state"
widget="statusbar"
statusbar_visible="pending,pushed,sent"
statusbar_colors='{"error":"red","pingen_error":"red","canceled":"grey","pushed":"blue","sent":"green"}'
/>
</header>
<group>
<field name="name" readonly="True" />
<field name="type" readonly="True" />
<field
name="company_id"
readonly="True"
groups="base.group_multi_company"
widget="selection"
/>
</group>
<notebook>
<page string="Pingen.com">
<group>
<group>
<field name="pingen_uuid" />
<field name="pingen_status" />
</group>
</group>
<separator string="Options" />
<newline />
<group>
<group>
<field
name="auto_send"
attrs="{'readonly': [('state', 'in', ['sendcenter', 'sent'])]}"
/>
<field
name="delivery_product"
attrs="{'readonly': [('state', 'in', ['sendcenter', 'sent'])], 'required': [('auto_send', '=', True)]}"
/>
<field
name="print_spectrum"
attrs="{'readonly': [('state', 'in', ['sendcenter', 'sent'])]}"
/>
<field
name="print_mode"
attrs="{'readonly': [('state', 'in', ['sendcenter', 'sent'])]}"
/>
</group>
</group>
<separator string="Dates" />
<newline />
<group>
<field name="push_date" />
</group>
<group attrs="{'invisible': [('last_error_message', '=', False)]}">
<separator string="Errors" />
<newline />
<group>
<field nolabel="1" name="last_error_message" />
</group>
</group>
<group attrs="{'invisible': [('state', 'not in', ['sendcenter', 'sent'])]}">
<separator string="Sendcenter" colspan="4" />
<newline />
<group>
<label for="cost" />
<div>
<field name="cost" class="oe_inline" />
<field
name="currency_id"
class="oe_inline"
groups="base.group_multi_currency"
/>
</div>
<newline />
<field name="parsed_address" />
<field name="country_id" />
<field name="send_date" />
<field name="pages" />
</group>
</group>
</page>
<page string="Attachment">
<group col="4" colspan="4">
<separator string="Data" colspan="4" />
<newline />
<group col="2" colspan="4" attrs="{'invisible':[('type','=','url')]}">
<field name="datas" filename="name" readonly="True" />
<field name="name" select="1" readonly="True" />
</group>
<group col="2" colspan="4" attrs="{'invisible':[('type','=','binary')]}">
<field name="url" widget="url" readonly="True" />
</group>
</group>
<group col="2" colspan="4">
<separator string="Attached To" colspan="2" />
<field name="attachment_id" />
</group>
</page>
</notebook>
</form>
</field>
</record>
<record id="view_pingen_document_search" model="ir.ui.view">
<field name="name">pingen.document.search</field>
<field name="model">pingen.document</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search>
<filter name="pending" string="Pending" domain="[('state','=','pending')]" />
<filter name="pushed" string="Pushed" domain="[('state','=','pushed')]" />
<filter
name="in_sendcenter"
string="In Sendcenter"
domain="[('state','=','sendcenter')]"
/>
<filter name="sent" string="Sent" domain="[('state','=','sent')]" />
<filter name="error" string="Error" domain="[('state','=','error')]" />
<filter
name="error"
string="Pingen Error"
domain="[('state','=','pingen_error')]"
/>
<filter
name="cancelled"
string="Canceled"
domain="[('state','=','canceled')]"
/>
<separator orientation="vertical" />
<field name="attachment_id" />
</search>
</field>
</record>
<record id="action_pingen_document" model="ir.actions.act_window">
<field name="name">Pingen Documents</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pingen.document</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_pingen_document_search" />
</record>
<menuitem action="action_pingen_document" id="menu_pingen_document" />
</odoo>

View File

@@ -1,2 +1,4 @@
# generated from manifests external_dependencies
oauthlib
pycups
requests_oauthlib

View File

@@ -0,0 +1 @@
../../../../pingen

6
setup/pingen/setup.py Normal file
View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

3
test-requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
vcrpy
requests_mock
freezegun