[MIG] pingen: Migration to 16.0

Standard migration, incl. changes proposed in https://github.com/OCA/report-print-send/pull/290/
Add unit test.
This commit is contained in:
Anna Janiszewska
2023-01-30 14:46:41 +01:00
parent b72b28f004
commit 0d8676547a
27 changed files with 2401 additions and 372 deletions

View File

@@ -1,42 +1,56 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg ======================
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html pingen.com integration
:alt: License: AGPL-3 ======================
=========================== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Integration with pingen.com !! This file is generated by oca-gen-addon-readme !!
=========================== !! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
What is pingen.com .. |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. Pingen.com is a paid online service.
It sends uploaded documents by letter post. It sends uploaded documents by letter post.
Scope of the integration
========================
One can decide, per document / attachment, if it should be pushed One can decide, per document / attachment, if it should be pushed
to pingen.com. The documents are pushed asynchronously. to pingen.com. The documents are pushed asynchronously.
A second cron updates the informations of the documents from pingen.com, so we The informations of the documents from pingen.com are updated through webhook calls.
know which of them have been sent.
**Table of contents**
.. contents::
:local:
Configuration Configuration
============= =============
The authentication token is configured on the company's view. You can also The authentication token, client ID, organization ID and webhook secret is configured
tick a checkbox if the staging environment (https://stage-api.pingen.com) on the company's view. You can also tick a checkbox if the staging environment
should be used. (https://stage-api.pingen.com) should be used.
The setup of the 2 crons can be changed as well: Webhooks should be configured on pingen account. Organization ID and webhook secret must match.
* Run Pingen Document Push
* Run Pingen Document Update
Usage Usage
===== =====
On the attachment view, a new pingen.com tab has been added. On the attachment view, a new pingen.com section has been added.
You can tick a box to push the document to pingen.com. You can tick a box to push the document to pingen.com.
There is 3 additional options: There is 3 additional options:
@@ -49,8 +63,7 @@ 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 is created. You can directly access to the latter on the Link on the right on
the attachment view. the attachment view.
You can find them in `Settings > Customization > Low Level Objets > Pingen You can find them in `Pingen Documents` App or in the more convenient `Documents` menu if you have installed the
Documents` or in the more convenient `Documents` menu if you have installed the
`document` module. `document` module.
Errors Errors
@@ -65,44 +78,54 @@ Pingen Document.
When a connection error occurs, the action will be retried on the next When a connection error occurs, the action will be retried on the next
scheduler run. scheduler run.
Dependencies Dependencies
============ ============
* Require the Python library `requests <http://docs.python-requests.org/>`_ * Require the Python library `requests_oauthlib <https://github.com/requests/requests-oauthlib>`_
* The PDF files sent to pingen.com have to respect some `formatting rules
<https://stage-app.pingen.com/resources/pingen_requirements_v1_en.pdf>`_.
* The address must be in a format accepted by pingen.com: the last line * The address must be in a format accepted by pingen.com: the last line
is the country in English or German. is the country in English or German.
Bug Tracker Bug Tracker
=========== ===========
Bugs are tracked on `GitHub Issues Bugs are tracked on `GitHub Issues <https://github.com/OCA/report-print-send/issues>`_.
<https://github.com/OCA/report-print-send/issues>`_. In case of trouble, please In case of trouble, please check there if your issue has already been reported.
check there if your issue has already been reported. If you spotted it first, If you spotted it first, help us smashing it by providing a detailed and welcomed
help us smashing it by providing a detailed and welcomed feedback. `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 Credits
======= =======
Authors
~~~~~~~
* Camptocamp
Contributors Contributors
============ ~~~~~~~~~~~~
* Guewen Baconnier <guewen.baconnier@camptocamp.com> * Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Anar Baghirli <a.baghirli@mobilunity.com> * Anar Baghirli <a.baghirli@mobilunity.com>
* Akim Juillerat <akim.juillerat@camptocamp.com> * Akim Juillerat <akim.juillerat@camptocamp.com>
* Anna Janiszewska <anna.janiszewska@camptocamp.com>
Maintainer
========== Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png .. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association :alt: Odoo Community Association
:target: https://odoo-community.org :target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and mission is to support the collaborative development of Odoo features and
promote its widespread use. promote its widespread use.
To contribute to this module, please visit https://odoo-community.org. 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.

View File

@@ -1,5 +1,2 @@
# Author: Guewen Baconnier
# Copyright 2012-2017 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import controllers from . import controllers
from . import models from . import models

View File

@@ -1,18 +1,18 @@
# Author: Guewen Baconnier # Author: Guewen Baconnier
# Copyright 2012-2017 Camptocamp SA # Copyright 2012-2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{ {
"name": "pingen.com integration", "name": "pingen.com integration",
"version": "10.0.2.0.0", "version": "16.0.1.0.0",
"author": "Camptocamp,Odoo Community Association (OCA)", "author": "Camptocamp,Odoo Community Association (OCA)",
"maintainer": "Camptocamp", "maintainers": ["ajaniszewska-dev", "grindtildeath"],
"license": "AGPL-3", "license": "AGPL-3",
"category": "Reporting", "category": "Reporting",
"complexity": "easy", "maturity": "Production/Stable",
"depends": ["base_setup"], "depends": ["base_setup"],
"external_dependencies": { "external_dependencies": {
"python": ["requests_oauthlib"], "python": ["requests_oauthlib", "oauthlib"],
}, },
"website": "https://github.com/OCA/report-print-send", "website": "https://github.com/OCA/report-print-send",
"data": [ "data": [
@@ -22,8 +22,6 @@
"views/base_config_settings.xml", "views/base_config_settings.xml",
"security/ir.model.access.csv", "security/ir.model.access.csv",
], ],
"tests": [],
"installable": True, "installable": True,
"auto_install": False,
"application": True, "application": True,
} }

View File

@@ -17,12 +17,12 @@ _logger = logging.getLogger(__name__)
class PingenController(http.Controller): class PingenController(http.Controller):
def _verify_signature(self, request_content): def _verify_signature(self, request_content):
webhook_signature = http.request.httprequest.headers.get("Signature") webhook_signature = http.request.httprequest.headers.get("Signature")
companies = ( companies = http.request.env["res.company"].sudo().search([])
http.request.env["res.company"]
.sudo()
.search([("pingen_webhook_secret", "!=", False)])
)
for company in companies: 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( secret_signature = hmac.new(
company.pingen_webhook_secret.encode("utf-8"), company.pingen_webhook_secret.encode("utf-8"),
request_content, request_content,

View File

@@ -10,22 +10,20 @@
<field name="interval_type">hours</field> <field name="interval_type">hours</field>
<field name="numbercall">-1</field> <field name="numbercall">-1</field>
<field eval="False" name="doall" /> <field eval="False" name="doall" />
<field name="model">pingen.document</field> <field name="model_id" ref="model_pingen_document" />
<field name="function">_push_and_send_to_pingen_cron</field> <field name="code">model._push_and_send_to_pingen_cron</field>
<field name="args">()</field>
</record> </record>
<record forcecreate="True" id="ir_cron_update_pingen" model="ir.cron"> <record forcecreate="True" id="ir_cron_update_pingen" model="ir.cron">
<field name="name">Run Pingen Document Update</field> <field name="name">Run Pingen Document Update</field>
<field eval="True" name="active" /> <field eval="False" name="active" />
<field name="user_id" ref="base.user_root" /> <field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field> <field name="interval_number">1</field>
<field name="interval_type">days</field> <field name="interval_type">days</field>
<field name="numbercall">-1</field> <field name="numbercall">-1</field>
<field eval="False" name="doall" /> <field eval="False" name="doall" />
<field name="model">pingen.document</field> <field name="model_id" ref="model_pingen_document" />
<field name="function">_update_post_infos_cron</field> <field name="code">model._update_post_infos_cron</field>
<field name="args">()</field>
</record> </record>

View File

@@ -1,195 +0,0 @@
# Copyright 2022 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from collections import namedtuple
from openupgradelib.openupgrade import (
add_fields,
column_exists,
logged_query,
migrate as oumigrate,
table_exists,
)
from psycopg2 import sql
field_definition = namedtuple("FieldDefinition", ["name", "type", "db_type"])
@oumigrate(use_env=True)
def migrate(env, version):
move_and_migrate_to_api_v2_field(
env,
"ir_attachment",
field_definition("pingen_speed", "selection", "integer"),
"id",
"pingen_document",
field_definition("delivery_product", "selection", "varchar"),
"attachment_id",
field_default="cheap",
values_mapping={"1": "fast", "2": "cheap"},
)
move_and_migrate_to_api_v2_field(
env,
"ir_attachment",
field_definition("pingen_color", "selection", "integer"),
"id",
"pingen_document",
field_definition("print_spectrum", "selection", "varchar"),
"attachment_id",
field_default="grayscale",
values_mapping={"0": "grayscale", "1": "color"},
)
move_and_migrate_to_api_v2_field(
env,
"ir_attachment",
field_definition("pingen_send", "boolean", "bool"),
"id",
"pingen_document",
field_definition("auto_send", "boolean", "bool"),
"attachment_id",
field_default="True",
)
migrate_to_api_v2_field(
env,
"pingen_document",
field_definition("pingen_id", "integer", "int4"),
field_definition("pingen_uuid", "char", "varchar"),
)
migrate_to_api_v2_field(
env,
"pingen_document",
field_definition("post_status", "char", "varchar"),
field_definition("pingen_status", "char", "varchar"),
)
def migrate_to_api_v2_field(
env, table_name, old_field_definition, new_field_definition
):
if table_exists(env.cr, table_name) and not column_exists(
env.cr, table_name, new_field_definition.name
):
add_fields(
env,
[
(
new_field_definition.name,
table_name.replace("_", "."),
table_name,
new_field_definition.type,
new_field_definition.db_type,
"pingen",
)
],
)
migrate_data(env.cr, table_name, old_field_definition, new_field_definition)
def move_and_migrate_to_api_v2_field(
env,
old_model_table,
old_field_definition,
old_model_id,
new_model_table,
new_field_definition,
new_model_id,
field_default=None,
values_mapping={},
):
if table_exists(env.cr, new_model_table) and not column_exists(
env.cr, new_model_table, new_field_definition.name
):
add_fields(
env,
[
(
new_field_definition.name,
new_model_table.replace("_", "."),
new_model_table,
new_field_definition.type,
new_field_definition.db_type,
"pingen",
)
],
)
move_and_migrate_data(
env.cr,
old_model_table,
old_field_definition,
new_model_table,
new_field_definition,
old_model_id,
new_model_id,
values_mapping=values_mapping,
)
def migrate_data(cr, table_name, old_field_definition, new_field_definition):
query = sql.SQL(
"""
UPDATE {table_name}
SET {new_fname} = {old_fname};
"""
)
format_params = {
"table_name": sql.Identifier(table_name),
"new_fname": sql.Identifier(new_field_definition.name),
"old_fname": sql.Identifier(old_field_definition.name),
}
if old_field_definition.db_type != new_field_definition.db_type:
format_params.update(
{
"old_fname": sql.SQL(
"%s::%s" % (old_field_definition.name, new_field_definition.db_type)
)
}
)
query = query.format(**format_params)
logged_query(cr, query)
def move_and_migrate_data(
cr,
old_table,
old_field_definition,
new_table,
new_field_definition,
old_table_key,
new_table_key,
values_mapping={},
):
query = sql.SQL(
"""
UPDATE {table_to}
SET {table_to_col} = {table_from_col}
FROM {table_from}
WHERE {table_to}.{table_to_id} = {table_from}.{table_from_id};"""
)
format_params = {
"table_to": sql.Identifier(new_table),
"table_from": sql.Identifier(old_table),
"table_to_col": sql.Identifier(new_field_definition.name),
"table_from_col": sql.Identifier(old_field_definition.name),
"table_to_id": sql.Identifier(new_table_key),
"table_from_id": sql.Identifier(old_table_key),
}
query = query.format(**format_params)
logged_query(cr, query)
for old_val, new_val in values_mapping.items():
values_query = sql.SQL(
"""
UPDATE {table_to}
SET {table_to_col} = {new_val}
WHERE {table_to_col} = {old_val};
"""
)
if new_val.isdigit():
format_params["new_val"] = sql.SQL(new_val)
else:
format_params["new_val"] = sql.Literal(new_val)
format_params.update({"old_val": sql.SQL(old_val)})
if old_field_definition.db_type != new_field_definition.db_type:
format_params.update(
{"old_val": sql.SQL("%s::%s" % (old_val, new_field_definition.db_type))}
)
values_query = values_query.format(**format_params)
logged_query(cr, values_query)

View File

@@ -1,5 +1,3 @@
# Copyright 2018 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import ir_attachment from . import ir_attachment
from . import pingen from . import pingen
from . import pingen_document from . import pingen_document

View File

@@ -1,13 +1,29 @@
# Copyright 2018 Camptocamp SA # Copyright 2012-2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models from odoo import fields, models
class BaseConfigSettings(models.TransientModel): class ResConfigSettings(models.TransientModel):
_inherit = "base.config.settings" _inherit = "res.config.settings"
pingen_clientid = fields.Char(related="company_id.pingen_clientid") pingen_clientid = fields.Char(
pingen_client_secretid = fields.Char(related="company_id.pingen_client_secretid") string="Pingen Client ID", related="company_id.pingen_clientid", readonly=False
pingen_organization = fields.Char(related="company_id.pingen_organization") )
pingen_webhook_secret = fields.Char(related="company_id.pingen_webhook_secret") pingen_client_secretid = fields.Char(
pingen_staging = fields.Boolean(related="company_id.pingen_staging") 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

@@ -1,5 +1,5 @@
# Author: Guewen Baconnier # Author: Guewen Baconnier
# Copyright 2012-2017 Camptocamp SA # Copyright 2012-2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import base64 import base64
@@ -22,7 +22,6 @@ class IrAttachment(models.Model):
def _prepare_pingen_document_vals(self): def _prepare_pingen_document_vals(self):
return { return {
"attachment_id": self.id, "attachment_id": self.id,
# 'config': 'created from attachment'
} }
def _handle_pingen_document(self): def _handle_pingen_document(self):
@@ -67,7 +66,6 @@ class IrAttachment(models.Model):
attachment._handle_pingen_document() attachment._handle_pingen_document()
return attachment return attachment
@api.multi
def write(self, vals): def write(self, vals):
res = super(IrAttachment, self).write(vals) res = super(IrAttachment, self).write(vals)
if "send_to_pingen" in vals: if "send_to_pingen" in vals:
@@ -83,7 +81,7 @@ class IrAttachment(models.Model):
if self.type == "binary": if self.type == "binary":
decoded_document = base64.b64decode(self.datas) decoded_document = base64.b64decode(self.datas)
elif self.type == "url": elif self.type == "url":
response = requests.get(self.url) response = requests.get(self.url, timeout=30)
if response.ok: if response.ok:
decoded_document = requests.content decoded_document = requests.content
else: else:

View File

@@ -1,14 +1,14 @@
# Author: Guewen Baconnier # Author: Guewen Baconnier
# Copyright 2012-2017 Camptocamp SA # Copyright 2012-2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import json import json
import logging import logging
from datetime import datetime from datetime import datetime
from urllib.parse import urljoin
import pytz import pytz
import requests import requests
import urlparse
from dateutil import parser from dateutil import parser
from oauthlib.oauth2 import BackendApplicationClient from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
@@ -111,8 +111,9 @@ class Pingen(object):
def _fetch_token(self): def _fetch_token(self):
# TODO: Handle scope 'letter' only? # TODO: Handle scope 'letter' only?
token_url = urlparse.urljoin(self.identity_url, self.token_url) token_url = urljoin(self.identity_url, self.token_url)
# FIXME: requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:581) # FIXME: requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED]
# certificate verify failed (_ssl.c:581)
# without verify=False parameter on prod/staging # without verify=False parameter on prod/staging
_logger.debug("Fetching new token from %s" % token_url) _logger.debug("Fetching new token from %s" % token_url)
return self._session.fetch_token( return self._session.fetch_token(
@@ -161,7 +162,7 @@ class Pingen(object):
if self._is_token_expired(): if self._is_token_expired():
self._set_session_header_token() self._set_session_header_token()
p_url = urlparse.urljoin(self.api_url, endpoint) p_url = urljoin(self.api_url, endpoint)
if endpoint == "document/get": if endpoint == "document/get":
complete_url = "{}{}{}{}{}".format( complete_url = "{}{}{}{}{}".format(
@@ -196,7 +197,7 @@ class Pingen(object):
def upload_file(self, url, multipart, content_type): def upload_file(self, url, multipart, content_type):
_logger.debug("Uploading new file") _logger.debug("Uploading new file")
response = requests.put( response = requests.put(
url, data=multipart, headers={"Content-Type": content_type} url, data=multipart, headers={"Content-Type": content_type}, timeout=30
) )
return response return response
@@ -223,27 +224,13 @@ class Pingen(object):
3. dict of the created item on pingen (details) 3. dict of the created item on pingen (details)
""" """
# we cannot use the `files` param alongside
# with the `datas`param when data is a
# JSON-encoded data. We have to construct
# the entire body and send it to `data`
# https://github.com/kennethreitz/requests/issues/950
# formdata = {
# 'file': (filename, filestream.read()),
# }
url, url_signature = self._get_file_upload() url, url_signature = self._get_file_upload()
# file_upload = self._get_file_upload()
# multipart, content_type = encode_multipart_formdata(formdata)
self.upload_file(url, filestream.read(), content_type) self.upload_file(url, filestream.read(), content_type)
data_attributes = { data_attributes = {
"file_original_name": filename, "file_original_name": filename,
"file_url": url, "file_url": url,
"file_url_signature": url_signature, "file_url_signature": url_signature,
# TODO Use parameters and mapping
"address_position": "left", "address_position": "left",
"auto_send": send, "auto_send": send,
"delivery_product": delivery_product, "delivery_product": delivery_product,
@@ -262,10 +249,6 @@ class Pingen(object):
rjson_data = response.json().get("data", {}) rjson_data = response.json().get("data", {})
document_id = rjson_data.get("id") document_id = rjson_data.get("id")
# if rjson.get('send'):
# # confusing name but send_id is the posted id
# posted_id = rjson['send'][0]['send_id']
# item = rjson['item']
item = rjson_data.get("attributes") item = rjson_data.get("attributes")
return document_id, False, item return document_id, False, item
@@ -313,4 +296,3 @@ class Pingen(object):
letter_id=document_uuid, letter_id=document_uuid,
) )
return response.json().get("data", {}).get("attributes") return response.json().get("data", {}).get("attributes")
# return response.json()['item']

View File

@@ -1,11 +1,11 @@
# Author: Guewen Baconnier # Author: Guewen Baconnier
# Copyright 2012-2017 Camptocamp SA # Copyright 2012-2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging import logging
from io import BytesIO
from itertools import groupby from itertools import groupby
from cStringIO import StringIO
from oauthlib.oauth2.rfc6749.errors import OAuth2Error from oauthlib.oauth2.rfc6749.errors import OAuth2Error
import odoo import odoo
@@ -26,6 +26,7 @@ class PingenDocument(models.Model):
""" """
_name = "pingen.document" _name = "pingen.document"
_description = "pingen.document"
_inherits = {"ir.attachment": "attachment_id"} _inherits = {"ir.attachment": "attachment_id"}
_order = "push_date desc, id desc" _order = "push_date desc, id desc"
@@ -43,13 +44,11 @@ class PingenDocument(models.Model):
("pingen_error", "Pingen Error"), ("pingen_error", "Pingen Error"),
("canceled", "Canceled"), ("canceled", "Canceled"),
], ],
string="State",
readonly=True, readonly=True,
required=True, required=True,
default="pending", default="pending",
) )
auto_send = fields.Boolean( auto_send = fields.Boolean(
"Auto Send",
help="Defines if a document is merely uploaded or also sent", help="Defines if a document is merely uploaded or also sent",
default=True, default=True,
) )
@@ -79,26 +78,25 @@ class PingenDocument(models.Model):
) )
print_spectrum = fields.Selection( print_spectrum = fields.Selection(
[("grayscale", "Grayscale"), ("color", "Color")], [("grayscale", "Grayscale"), ("color", "Color")],
"Print Spectrum",
default="grayscale", default="grayscale",
) )
print_mode = fields.Selection( print_mode = fields.Selection(
[("simplex", "Simplex"), ("duplex", "Duplex")], "Print mode", default="simplex" [("simplex", "Simplex"), ("duplex", "Duplex")], "Print mode", default="simplex"
) )
push_date = fields.Datetime("Push Date", readonly=True) push_date = fields.Datetime(readonly=True)
# for `error` and `pingen_error` states when we push # for `error` and `pingen_error` states when we push
last_error_message = fields.Text("Error Message", readonly=True) last_error_message = fields.Text("Error Message", readonly=True)
# pingen API v2 fields # pingen API v2 fields
pingen_uuid = fields.Char(readonly=True) pingen_uuid = fields.Char(readonly=True)
pingen_status = fields.Char(readonly=True) pingen_status = fields.Char(readonly=True)
# sendcenter infos # sendcenter infos
parsed_address = fields.Text("Parsed Address", readonly=True) parsed_address = fields.Text(readonly=True)
cost = fields.Float("Cost", readonly=True) cost = fields.Float(readonly=True)
currency_id = fields.Many2one("res.currency", "Currency", readonly=True) currency_id = fields.Many2one("res.currency", "Currency", readonly=True)
country_id = fields.Many2one("res.country", "Country", readonly=True) country_id = fields.Many2one("res.country", "Country", readonly=True)
send_date = fields.Datetime("Date of sending", readonly=True) send_date = fields.Datetime("Date of sending", readonly=True)
pages = fields.Integer("Pages", readonly=True) pages = fields.Integer(readonly=True)
company_id = fields.Many2one(related="attachment_id.company_id") company_id = fields.Many2one(related="attachment_id.company_id")
_sql_constraints = [ _sql_constraints = [
@@ -119,7 +117,7 @@ class PingenDocument(models.Model):
try: try:
doc_id, post_id, infos = pingen.push_document( doc_id, post_id, infos = pingen.push_document(
self.name, self.name,
StringIO(decoded_document), BytesIO(decoded_document),
self.attachment_id.mimetype, self.attachment_id.mimetype,
self.auto_send, self.auto_send,
self.delivery_product, self.delivery_product,
@@ -140,11 +138,6 @@ class PingenDocument(models.Model):
raise raise
error = False error = False
state = "pushed" state = "pushed"
# if post_id:
# state = 'sendcenter'
# elif infos['requirement_failure']:
# state = 'pingen_error'
# error = _('The document does not meet the Pingen requirements.')
push_date = pingen_datetime_to_utc(infos.get("created_at")) push_date = pingen_datetime_to_utc(infos.get("created_at"))
self.write( self.write(
{ {
@@ -176,14 +169,16 @@ class PingenDocument(models.Model):
) )
except APIError as e: except APIError as e:
state = "pingen_error" state = "pingen_error"
error_msg = _("Error when pushing the document %s to Pingen:\n%s") % ( error_msg = _(
self.name, "Error when pushing the document %(name) to Pingen:\n%(exc)"
e, ) % {
) "name": self.name,
"exc": e,
}
except Exception as e: except Exception as e:
error_msg = _( error_msg = _(
"Unexpected Error when pushing the document %s to Pingen:\n%s" "Unexpected Error when pushing the document %(name) to Pingen:\n%(exc)"
) % (self.name, e) ) % {"name": self.name, "exc": e}
_logger.exception(error_msg) _logger.exception(error_msg)
finally: finally:
if error_msg: if error_msg:
@@ -296,22 +291,22 @@ class PingenDocument(models.Model):
try: try:
session = self.company_id._get_pingen_client() session = self.company_id._get_pingen_client()
self._ask_pingen_send(pingen=session) self._ask_pingen_send(pingen=session)
except OAuth2Error: except OAuth2Error as e:
raise UserError( raise UserError(
_( _(
"Connection Error when asking for " "Connection Error when asking for "
"sending the document %s to Pingen" "sending the document %s to Pingen"
) )
% self.name % self.name
) ) from e
except APIError as e: except APIError as e:
raise UserError( raise UserError(
_("Error when asking Pingen to send the document %s: " "\n%s") _("Error when asking Pingen to send the document %(name): " "\n%(exc)")
% (self.name, e) % {"name": self.name, "exc": e}
) ) from e
except BaseException: except BaseException as e:
_logger.exception( _logger.exception(
"Unexpected Error when updating the status " "Unexpected Error when updating the status "
"of pingen.document %s: " % self.id "of pingen.document %s: " % self.id
@@ -319,7 +314,7 @@ class PingenDocument(models.Model):
raise UserError( raise UserError(
_("Unexpected Error when updating the status " "of Document %s") _("Unexpected Error when updating the status " "of Document %s")
% self.name % self.name
) ) from e
return True return True
def _get_and_update_post_infos(self, pingen): def _get_and_update_post_infos(self, pingen):
@@ -398,6 +393,8 @@ class PingenDocument(models.Model):
session = document.company_id._get_pingen_client() session = document.company_id._get_pingen_client()
try: try:
document._get_and_update_post_infos(pingen=session) document._get_and_update_post_infos(pingen=session)
# pylint: disable=W7938
# pylint: disable=W8138
except (OAuth2Error, APIError): except (OAuth2Error, APIError):
# will be retried the next time # will be retried the next time
# In any case, the error has been # In any case, the error has been
@@ -417,20 +414,23 @@ class PingenDocument(models.Model):
try: try:
session = self.company_id._get_pingen_client() session = self.company_id._get_pingen_client()
self._get_and_update_post_infos(pingen=session) self._get_and_update_post_infos(pingen=session)
except OAuth2Error: except OAuth2Error as e:
raise UserError( raise UserError(
_( _(
"Connection Error when updating the status " "Connection Error when updating the status "
"of Document %s from Pingen" "of Document %s from Pingen"
) )
% self.name % self.name
) ) from e
except APIError as e: except APIError as e:
raise UserError( raise UserError(
_("Error when updating the status of Document %s from " "Pingen: \n%s") _(
% (self.name, e) "Error when updating the status of Document %(name) from "
) "Pingen: \n%(exc)"
except BaseException: )
% {"name": self.name, "exc": e}
) from e
except BaseException as e:
_logger.exception( _logger.exception(
"Unexpected Error when updating the status " "Unexpected Error when updating the status "
"of pingen.document %s: " % self.id "of pingen.document %s: " % self.id
@@ -438,5 +438,5 @@ class PingenDocument(models.Model):
raise UserError( raise UserError(
_("Unexpected Error when updating the status " "of Document %s") _("Unexpected Error when updating the status " "of Document %s")
% self.name % self.name
) ) from e
return True return True

View File

@@ -1,5 +1,5 @@
# Author: Guewen Baconnier # Author: Guewen Baconnier
# Copyright 2012-2017 Camptocamp SA # Copyright 2012-2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models from odoo import fields, models
@@ -11,11 +11,11 @@ class ResCompany(models.Model):
_inherit = "res.company" _inherit = "res.company"
pingen_clientid = fields.Char("Pingen Client ID", size=20) pingen_clientid = fields.Char(size=20)
pingen_client_secretid = fields.Char("Pingen Client Secret ID", size=80) pingen_client_secretid = fields.Char(size=80)
pingen_organization = fields.Char("Pingen organization ID") pingen_organization = fields.Char("Pingen organization ID")
pingen_webhook_secret = fields.Char("Pingen Webhooks secret") pingen_webhook_secret = fields.Char()
pingen_staging = fields.Boolean("Pingen Staging") pingen_staging = fields.Boolean()
def _pingen(self): def _pingen(self):
"""Return a Pingen instance to work on""" """Return a Pingen instance to work on"""

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

@@ -1,19 +1,49 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="base_config_settings_inherit" model="ir.ui.view"> <record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">base.config.settings.inherit</field> <field name="name">res.config.settings.inherit</field>
<field name="inherit_id" ref="base_setup.view_general_configuration" /> <field name="model">res.config.settings</field>
<field name="model">base.config.settings</field> <field name="inherit_id" ref="base_setup.res_config_settings_view_form" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<group name="report" position="before"> <xpath expr="//div[@name='integration']" position='after'>
<group string="Pingen Integration"> <h2>Pingen</h2>
<field name="pingen_clientid" groups="base.group_system" /> <div class="o_settings_container" id="pingen_integraton">
<field name="pingen_client_secretid" groups="base.group_system" /> <div
<field name="pingen_organization" groups="base.group_system" /> id="pingen_configuration_settings"
<field name="pingen_webhook_secret" groups="base.group_system" /> class="o_settings_box col-12 col-lg-6"
<field name="pingen_staging" groups="base.group_system" /> >
</group> <div class="mt16 row">
</group> <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> </field>
</record> </record>
</odoo> </odoo>

View File

@@ -14,12 +14,12 @@
</field> </field>
</record> </record>
<act_window <record model="ir.actions.act_window" id="act_attachment_to_pingen_document">
context="{'search_default_attachment_id': [active_id], 'default_attachment_id': active_id}" <field
id="act_attachment_to_pingen_document" name="context"
name="Pingen Document" >"{'search_default_attachment_id': [active_id], 'default_attachment_id': active_id}"</field>
groups="" <field name="name">Pingen Document</field>
res_model="pingen.document" <field name="res_model">pingen.document</field>
src_model="ir.attachment" <field name="binding_model_id" ref="base.model_ir_attachment" />
/> </record>
</odoo> </odoo>

View File

@@ -6,7 +6,7 @@
<field name="model">pingen.document</field> <field name="model">pingen.document</field>
<field name="type">tree</field> <field name="type">tree</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Pingen Document"> <tree>
<field name="push_date" /> <field name="push_date" />
<field name="name" /> <field name="name" />
<field name="auto_send" /> <field name="auto_send" />
@@ -53,7 +53,7 @@
string="Update the letter's informations" string="Update the letter's informations"
icon="fa-refresh" icon="fa-refresh"
/> />
<field <field
name="state" name="state"
widget="statusbar" widget="statusbar"
statusbar_visible="pending,pushed,sent" statusbar_visible="pending,pushed,sent"
@@ -139,8 +139,8 @@
<separator string="Data" colspan="4" /> <separator string="Data" colspan="4" />
<newline /> <newline />
<group col="2" colspan="4" attrs="{'invisible':[('type','=','url')]}"> <group col="2" colspan="4" attrs="{'invisible':[('type','=','url')]}">
<field name="datas" filename="datas_fname" readonly="True" /> <field name="datas" filename="name" readonly="True" />
<field name="datas_fname" select="1" readonly="True" /> <field name="name" select="1" readonly="True" />
</group> </group>
<group col="2" colspan="4" attrs="{'invisible':[('type','=','binary')]}"> <group col="2" colspan="4" attrs="{'invisible':[('type','=','binary')]}">
<field name="url" widget="url" readonly="True" /> <field name="url" widget="url" readonly="True" />
@@ -161,27 +161,23 @@
<field name="model">pingen.document</field> <field name="model">pingen.document</field>
<field name="type">search</field> <field name="type">search</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Pingen Document"> <search>
<filter name="pending" string="Pending" domain="[('state','=','pending')]" />
<filter name="pushed" string="Pushed" domain="[('state','=','pushed')]" />
<filter <filter
icon="terp-project" name="in_sendcenter"
string="Pending"
domain="[('state','=','pending')]"
/>
<filter icon="terp-stage" string="Pushed" domain="[('state','=','pushed')]" />
<filter
icon="gtk-print"
string="In Sendcenter" string="In Sendcenter"
domain="[('state','=','sendcenter')]" domain="[('state','=','sendcenter')]"
/> />
<filter icon="kanban-apply" string="Sent" domain="[('state','=','sent')]" /> <filter name="sent" string="Sent" domain="[('state','=','sent')]" />
<filter icon="kanban-stop" string="Error" domain="[('state','=','error')]" /> <filter name="error" string="Error" domain="[('state','=','error')]" />
<filter <filter
icon="STOCK_NO" name="error"
string="Pingen Error" string="Pingen Error"
domain="[('state','=','pingen_error')]" domain="[('state','=','pingen_error')]"
/> />
<filter <filter
icon="terp-dialog-close" name="cancelled"
string="Canceled" string="Canceled"
domain="[('state','=','canceled')]" domain="[('state','=','canceled')]"
/> />
@@ -195,7 +191,6 @@
<field name="name">Pingen Documents</field> <field name="name">Pingen Documents</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">pingen.document</field> <field name="res_model">pingen.document</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_pingen_document_search" /> <field name="search_view_id" ref="view_pingen_document_search" />
</record> </record>

View File

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

View File

@@ -1 +0,0 @@
__import__('pkg_resources').declare_namespace(__name__)

View File

@@ -1 +0,0 @@
__import__('pkg_resources').declare_namespace(__name__)

3
test-requirements.txt Normal file
View File

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