diff --git a/pingen/README.rst b/pingen/README.rst new file mode 100644 index 0000000..55781eb --- /dev/null +++ b/pingen/README.rst @@ -0,0 +1,108 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +=========================== +Integration with pingen.com +=========================== + +What is pingen.com +================== + +Pingen.com is a paid online service. +It sends uploaded documents by letter post. + +Scope of the integration +======================== + +One can decide, per document / attachment, if it should be pushed +to pingen.com. The documents are pushed asynchronously. + +A second cron updates the informations of the documents from pingen.com, so we +know which of them have been sent. + +Configuration +============= + +The authentication token 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. + +The setup of the 2 crons can be changed as well: + + * Run Pingen Document Push + * Run Pingen Document Update + +Usage +===== + +On the attachment view, a new pingen.com tab has been added. +You can tick a box to push the document to pingen.com. + +There is 3 additional options: + + * Send: the document will not be only uploaded, but will be also be sent + * Speed: priority or economy + * Type of print: color or black and white + +Once the configuration is done and the attachment saved, a Pingen Document +is created. You can directly access to the latter on the Link on the right on +the attachment view. + +You can find them in `Settings > Customization > Low Level Objets > Pingen +Documents` or in the more convenient `Documents` menu if you have installed the +`document` module. + +Errors +====== + +Sometimes, pingen.com will refuse to send a document because it does not meet +its requirements. In such case, the document's state becomes "Pingen Error" +and you will need to manually handle the case, either from the pingen.com +backend, or by changing the document on OpenERP and resolving the error on the +Pingen Document. + +When a connection error occurs, the action will be retried on the next +scheduler run. + +Dependencies +============ + + * Require the Python library `requests `_ + * The PDF files sent to pingen.com have to respect some `formatting rules + `_. + * The address must be in a format accepted by pingen.com: the last line + is the country in English or German. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +============ + +* Guewen Baconnier +* Anar Baghirli +* Akim Juillerat + +Maintainer +========== + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/pingen/__init__.py b/pingen/__init__.py index 4ff2fea..d93cc0f 100644 --- a/pingen/__init__.py +++ b/pingen/__init__.py @@ -1,25 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Author: Guewen Baconnier -# Copyright 2012 Camptocamp SA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# Author: Guewen Baconnier +# Copyright 2012-2017 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -import ir_attachment -import pingen -import pingen_document -import res_company +from . import models \ No newline at end of file diff --git a/pingen/__manifest__.py b/pingen/__manifest__.py index 4ae5dc8..d292266 100644 --- a/pingen/__manifest__.py +++ b/pingen/__manifest__.py @@ -1,119 +1,30 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Author: Guewen Baconnier -# Copyright 2012 Camptocamp SA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# Author: Guewen Baconnier +# Copyright 2012-2017 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { 'name': 'pingen.com integration', - 'version': '1.0', + 'version': '10.0.1.0.0', 'author': "Camptocamp,Odoo Community Association (OCA)", 'maintainer': 'Camptocamp', 'license': 'AGPL-3', 'category': 'Reporting', 'complexity': 'easy', - 'depends': [], + 'depends': ['base_setup'], 'external_dependencies': { 'python': ['requests'], }, - 'description': """ -Integration with pingen.com -=========================== - -What is pingen.com ------------------- - -Pingen.com is a paid online service. -It sends uploaded documents by letter post. - -Scope of the integration ------------------------- - -One can decide, per document / attachment, if it should be pushed -to pingen.com. The documents are pushed asynchronously. - -A second cron updates the informations of the documents from pingen.com, so we -know which of them have been sent. - -Configuration -------------- - -The authentication token 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. - -The setup of the 2 crons can be changed as well: - - * Run Pingen Document Push - * Run Pingen Document Update - -Usage ------ - -On the attachment view, a new pingen.com tab has been added. -You can tick a box to push the document to pingen.com. - -There is 3 additional options: - - * Send: the document will not be only uploaded, but will be also be sent - * Speed: priority or economy - * Type of print: color or black and white - -Once the configuration is done and the attachment saved, a Pingen Document -is created. You can directly access to the latter on the Link on the right on -the attachment view. - -You can find them in `Settings > Customization > Low Level Objets > Pingen -Documents` or in the more convenient `Documents` menu if you have installed the -`document` module. - -Errors ------- - -Sometimes, pingen.com will refuse to send a document because it does not meet -its requirements. In such case, the document's state becomes "Pingen Error" and -you will need to manually handle the case, either from the pingen.com backend, -or by changing the document on OpenERP and resolving the error on the Pingen -Document. - -When a connection error occurs, the action will be retried on the next scheduler -run. - -Dependencies ------------- - - * Require the Python library `requests `_ - * The PDF files sent to pingen.com have to respect some `formatting rules - `_. - * The address must be in a format accepted by pingen.com: the last line - is the country in English or German. - -""", 'website': 'http://www.camptocamp.com', 'data': [ - 'ir_attachment_view.xml', - 'pingen_document_view.xml', - 'pingen_data.xml', - 'res_company_view.xml', + 'views/ir_attachment_view.xml', + 'views/pingen_document_view.xml', + 'data/pingen_data.xml', + 'views/base_config_settings.xml', 'security/ir.model.access.csv', ], 'tests': [], - 'installable': False, + 'installable': True, 'auto_install': False, 'application': True, } diff --git a/pingen/data/pingen_data.xml b/pingen/data/pingen_data.xml new file mode 100644 index 0000000..5c0572d --- /dev/null +++ b/pingen/data/pingen_data.xml @@ -0,0 +1,32 @@ + + + + + + Run Pingen Document Push + + + 1 + hours + -1 + + pingen.document + _push_and_send_to_pingen_cron + () + + + + Run Pingen Document Update + + + 1 + days + -1 + + pingen.document + _update_post_infos_cron + () + + + + diff --git a/pingen/ir_attachment.py b/pingen/ir_attachment.py deleted file mode 100644 index ba00c47..0000000 --- a/pingen/ir_attachment.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Author: Guewen Baconnier -# Copyright 2012 Camptocamp SA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import requests -import base64 - -from openerp.osv import osv, orm, fields -from openerp.tools.translate import _ - - -class ir_attachment(orm.Model): - - _inherit = 'ir.attachment' - - _columns = { - 'send_to_pingen': fields.boolean('Send to Pingen.com'), - 'pingen_document_ids': fields.one2many( - 'pingen.document', 'attachment_id', - string='Pingen Document', readonly=True), - 'pingen_send': fields.boolean( - 'Send', - help="Defines if a document is merely uploaded or also sent"), - 'pingen_speed': fields.selection( - [('1', 'Priority'), ('2', 'Economy')], - 'Speed', - help="Defines the sending speed if the document is automatically sent"), - 'pingen_color': fields.selection([('0', 'B/W'), ('1', 'Color')], 'Type of print'), - } - - _defaults = { - 'pingen_send': True, - 'pingen_color': '0', - 'pingen_speed': '2', - } - - def _prepare_pingen_document_vals(self, cr, uid, attachment, context=None): - return {'attachment_id': attachment.id, - 'config': 'created from attachment'} - - def _handle_pingen_document(self, cr, uid, attachment_id, context=None): - """ 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.pool.get('pingen.document') - attachment = self.browse(cr, uid, attachment_id, context=context) - document = attachment.pingen_document_ids[0] if attachment.pingen_document_ids else None - if attachment.send_to_pingen: - if document: - document.write({'state': 'pending'}, context=context) - else: - pingen_document_obj.create( - cr, uid, - self._prepare_pingen_document_vals( - cr, uid, attachment, context=context), - context=context) - else: - if document: - if document.state == 'pushed': - raise osv.except_osv( - _('Error'), - _('The attachment %s is already pushed to pingen.com.') % - attachment.name) - document.write({'state': 'canceled'}, context=context) - return - - def create(self, cr, uid, vals, context=None): - attachment_id = super(ir_attachment, self).create(cr, uid, vals, context=context) - if 'send_to_pingen' in vals: - self._handle_pingen_document(cr, uid, attachment_id, context=context) - return attachment_id - - def write(self, cr, uid, ids, vals, context=None): - res = super(ir_attachment, self).write(cr, uid, ids, vals, context=context) - if 'send_to_pingen' in vals: - for attachment_id in ids: - self._handle_pingen_document(cr, uid, attachment_id, context=context) - return res - - def _decoded_content(self, cr, uid, attachment, context=None): - """ 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 attachment.type == 'binary': - decoded_document = base64.decodestring(attachment.datas) - elif attachment.type == 'url': - response = requests.get(attachment.url) - if response.ok: - decoded_document = requests.content - else: - raise Exception( - 'The type of attachment %s is not handled' % attachment.type) - return decoded_document diff --git a/pingen/ir_attachment_view.xml b/pingen/ir_attachment_view.xml deleted file mode 100644 index d51a906..0000000 --- a/pingen/ir_attachment_view.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - ir.attachment.pingen.view - ir.attachment - form - - - - - - - - - - - - - - - - - diff --git a/pingen/models/__init__.py b/pingen/models/__init__.py new file mode 100644 index 0000000..5cce2da --- /dev/null +++ b/pingen/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import ir_attachment +from . import pingen +from . import pingen_document +from . import res_company +from . import base_config_settings diff --git a/pingen/models/base_config_settings.py b/pingen/models/base_config_settings.py new file mode 100644 index 0000000..c1d43c1 --- /dev/null +++ b/pingen/models/base_config_settings.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class BaseConfigSettings(models.TransientModel): + _inherit = 'base.config.settings' + + pingen_token = fields.Char(related='company_id.pingen_token') + pingen_staging = fields.Boolean(related='company_id.pingen_staging') diff --git a/pingen/models/ir_attachment.py b/pingen/models/ir_attachment.py new file mode 100644 index 0000000..c8791ee --- /dev/null +++ b/pingen/models/ir_attachment.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Author: Guewen Baconnier +# Copyright 2012-2017 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import requests +import base64 + +from odoo import models, fields, _, api +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) + pingen_send = fields.Boolean( + 'Send', help="Defines if a document is merely uploaded or also sent", + default=True) + pingen_speed = fields.Selection( + [('1', 'Priority'), ('2', 'Economy')], + 'Speed', default='2', + help='Defines the sending speed if the document is automatically sent') + pingen_color = fields.Selection([('0', 'B/W'), ('1', 'Color')], + 'Type of print', + default='0') + + def _prepare_pingen_document_vals(self): + return {'attachment_id': self.id, + 'config': 'created from attachment'} + + 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 + + @api.multi + 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) + if response.ok: + decoded_document = requests.content + else: + raise UserError( + _('The type of attachment %s is not handled') + % self.type) + return decoded_document diff --git a/pingen/pingen.py b/pingen/models/pingen.py similarity index 72% rename from pingen/pingen.py rename to pingen/models/pingen.py index 475407b..0cb5964 100644 --- a/pingen/pingen.py +++ b/pingen/models/pingen.py @@ -1,23 +1,7 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Author: Guewen Baconnier -# Copyright 2012 Camptocamp SA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# Author: Guewen Baconnier +# Copyright 2012-2017 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import requests import logging @@ -34,7 +18,7 @@ POST_SENDING_STATUS = { 100: 'Ready/Pending', 101: 'Processing', 102: 'Waiting for confirmation', - 200: 'Sent', + 1: 'Sent', 300: 'Some error occured and object wasn\'t sent', 400: 'Sending cancelled', } @@ -62,10 +46,6 @@ class PingenException(RuntimeError): request.""" -class ConnectionError(PingenException): - """An Error occured with the pingen API""" - - class APIError(PingenException): """An Error occured with the pingen API""" @@ -90,14 +70,9 @@ class Pingen(object): """ Build a requests session """ if self._session is not None: return self._session - self._session = requests.Session( - params={'token': self._token}, - # with safe_mode, requests catch errors and - # returns a blank response with an error - config={'safe_mode': True}, - # verify = False required for staging environment - # because the SSL certificate is wrong - verify=not self.staging) + self._session = requests.Session() + self._session.params = {'token': self._token} + self._session.verify = not self.staging return self._session def __enter__(self): @@ -121,22 +96,31 @@ class Pingen(object): :param str endpoint: endpoint to call :param kwargs: additional arguments forwarded to the requests method """ - complete_url = urlparse.urljoin(self.url, endpoint) + + p_url = urlparse.urljoin(self.url, endpoint) + + if endpoint == 'document/get': + complete_url = '{}{}{}{}{}'.format(p_url, + '/id/', + kwargs['params']['id'], + '/token/', + self._token) + else: + complete_url = '{}{}{}'.format(p_url, + '/token/', + self._token) response = method(complete_url, **kwargs) - if not response.ok: - raise ConnectionError( - "%s: %s" % (response.json['errorcode'], - response.json['errormessage'])) - - if response.json['error']: + if response.json()['error']: raise APIError( - "%s: %s" % (response.json['errorcode'], response.json['errormessage'])) + "%s: %s" % (response.json()['errorcode'], + response.json()['errormessage'])) return response - def push_document(self, filename, filestream, send=None, speed=None, color=None): + def push_document(self, filename, filestream, + send=None, speed=None, color=None): """ Upload a document to pingen.com and eventually ask to send it :param str filename: name of the file to push @@ -174,7 +158,7 @@ class Pingen(object): headers={'Content-Type': content_type}, data=multipart) - rjson = response.json + rjson = response.json() document_id = rjson['id'] if rjson.get('send'): @@ -203,7 +187,7 @@ class Pingen(object): params={'id': document_id}, data={'data': json.dumps(data)}) - return response.json['id'] + return response.json()['id'] def post_infos(self, post_id): """ Return the information of a post @@ -213,10 +197,10 @@ class Pingen(object): """ response = self._send( self.session.get, - 'post/get', + 'document/get', params={'id': post_id}) - return response.json['item'] + return response.json()['item'] @staticmethod def is_posted(post_infos): @@ -224,4 +208,4 @@ class Pingen(object): :param dict post_infos: post infos returned by `post_infos` """ - return post_infos['status'] == 200 + return post_infos['status'] == 1 diff --git a/pingen/models/pingen_document.py b/pingen/models/pingen_document.py new file mode 100644 index 0000000..48cef4f --- /dev/null +++ b/pingen/models/pingen_document.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- +# Author: Guewen Baconnier +# Copyright 2012-2017 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +from requests.exceptions import ConnectionError +from cStringIO import StringIO + +import odoo +from odoo import models, fields, _ +from odoo.exceptions import UserError +from .pingen import APIError, pingen_datetime_to_utc, POST_SENDING_STATUS + + +_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' + _inherits = {'ir.attachment': 'attachment_id'} + + 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', 'Connection Error'), + ('pingen_error', 'Pingen Error'), + ('canceled', 'Canceled')], + string='State', readonly=True, + required=True, default='pending') + push_date = fields.Datetime('Push Date', readonly=True) + # for `error` and `pingen_error` states when we push + last_error_message = fields.Text('Error Message', readonly=True) + # pingen IDs + pingen_id = fields.Integer( + 'Pingen ID', readonly=True, + help="ID of the document in the Pingen Documents") + post_id = fields.Integer( + 'Pingen Post ID', readonly=True, + help="ID of the document in the Pingen Sendcenter") + # sendcenter infos + post_status = fields.Char('Post Status', size=128, readonly=True) + parsed_address = fields.Text('Parsed Address', readonly=True) + cost = fields.Float('Cost', 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('Pages', readonly=True) + + _sql_constraints = [ + ('pingen_document_attachment_uniq', + 'unique (attachment_id)', + 'Only one Pingen document is allowed per attachment.'), + ] + + def _get_pingen_session(self): + """ Returns a pingen session for a user """ + return self.company_id._pingen() + + 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._get_pingen_session() + try: + doc_id, post_id, infos = pingen.push_document( + self.datas_fname, + StringIO(decoded_document), + self.pingen_send, + self.pingen_speed, + self.pingen_color) + except ConnectionError: + _logger.exception( + 'Connection Error when pushing Pingen Document %s to %s.' % + (self.id, pingen.url)) + raise + except APIError: + _logger.error( + 'API Error when pushing Pingen Document %s to %s.' % + (self.id, pingen.url)) + raise + error = False + 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['date']) + self.write( + {'last_error_message': error, + 'state': state, + 'push_date': fields.Datetime.to_string(push_date), + 'pingen_id': doc_id, + 'post_id': post_id},) + _logger.info( + 'Pingen Document %s: pushed to %s' % (self.id, pingen.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._get_pingen_session() + self._push_to_pingen(pingen=session) + except ConnectionError as e: + state = 'error' + error_msg = _('Connection Error when asking for ' + 'sending the document %s to Pingen') % self.name + except APIError as e: + state = 'pingen_error' + error_msg = _('Error when asking Pingen to send the document %s: ' + '\n%s') % (self.name, e) + except Exception as e: + _logger.exception( + 'Unexpected Error when updating the status of pingen.document ' + '%s: ' % self.id) + error_msg = _('Unexpected Error when updating the ' + 'status of Document %s') % self.name + 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')]) + for document in not_sent_docs: + session = document._get_pingen_session() + if document.state == 'error': + document._resolve_error() + document.refresh() + try: + if document.state == 'pending': + document._push_to_pingen(pingen=session) + elif document.state == 'pushed': + document._ask_pingen_send(pingen=session) + except ConnectionError 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 as e: + _logger.error('Unexpected error in pingen cron') + return True + + def _resolve_error(self): + """ A document as resolved, put in the correct state """ + if self.post_id: + state = 'sendcenter' + elif self.pingen_id: + 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 + """ + # sending has been explicitely asked so we change the option + # for consistency + + if not self.pingen_send: + self.write({'pingen_send': True}) + try: + post_id = pingen.send_document( + self.pingen_id, + self.pingen_speed, + self.pingen_color) + except ConnectionError: + _logger.exception( + 'Connection Error when asking for sending Pingen Document %s ' + 'to %s.' % (self.id, pingen.url)) + raise + except APIError: + _logger.exception( + 'API Error when asking for sending Pingen Document %s to %s.' % + (self.id, pingen.url)) + raise + self.write( + {'last_error_message': False, + 'state': 'sendcenter', + 'post_id': post_id}) + _logger.info( + 'Pingen Document %s: asked for sending to %s' % ( + self.id, pingen.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._get_pingen_session() + self._ask_pingen_send(pingen=session) + except ConnectionError as e: + raise UserError( + _('Connection Error when asking for ' + 'sending the document %s to Pingen') % self.name) + + except APIError as e: + raise UserError( + _('Error when asking Pingen to send the document %s: ' + '\n%s') % (self.name, 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) + return True + + def _update_post_infos(self, pingen): + """ Update the informations from + pingen of a document in the Sendcenter + :param Pingen pingen: pingen object to reuse + """ + if not self.pingen_id: + return + try: + post_infos = pingen.post_infos(self.pingen_id) + except ConnectionError: + _logger.exception( + 'Connection Error when asking for ' + 'sending Pingen Document %s to %s.' % + (self.id, pingen.url)) + raise + except APIError: + _logger.exception( + 'API Error when asking for sending Pingen Document %s to %s.' % + (self.id, pingen.url)) + raise + country = self.env['res.country'].search( + [('code', '=', post_infos['country'])]) + send_date = pingen_datetime_to_utc(post_infos['date']) + vals = { + 'post_status': POST_SENDING_STATUS[post_infos['status']], + 'parsed_address': post_infos['address'], + 'country_id': country.id, + 'send_date': fields.Datetime.to_string(send_date), + 'pages': post_infos['pages'], + 'last_error_message': False, + } + if pingen.is_posted(post_infos): + vals['state'] = 'sent' + self.write(vals) + _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._get_pingen_session() + try: + document._update_post_infos(pingen=session) + except (ConnectionError, 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._get_pingen_session() + self._update_post_infos(pingen=session) + except ConnectionError as e: + raise UserError( + _('Connection Error when updating the status ' + 'of Document %s from Pingen') % self.name) + except APIError as e: + raise UserError( + _('Error when updating the status of Document %s from ' + 'Pingen: \n%s') % (self.name, 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) + return True diff --git a/pingen/models/res_company.py b/pingen/models/res_company.py new file mode 100644 index 0000000..1da79a1 --- /dev/null +++ b/pingen/models/res_company.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Author: Guewen Baconnier +# Copyright 2012-2017 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models, fields +from .pingen import Pingen + + +class ResCompany(models.Model): + + _inherit = 'res.company' + + pingen_token = fields.Char('Pingen Token', size=32) + pingen_staging = fields.Boolean('Pingen Staging') + + def _pingen(self): + """ Return a Pingen instance to work on """ + self.ensure_one() + return Pingen(self.pingen_token, staging=self.pingen_staging) diff --git a/pingen/pingen_data.xml b/pingen/pingen_data.xml deleted file mode 100644 index 5227210..0000000 --- a/pingen/pingen_data.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - Run Pingen Document Push - - - 1 - hours - -1 - - pingen.document - _push_and_send_to_pingen_cron - (None,) - - - - Run Pingen Document Update - - - 1 - days - -1 - - pingen.document - _update_post_infos_cron - (None,) - - - - diff --git a/pingen/pingen_document.py b/pingen/pingen_document.py deleted file mode 100644 index ac59a02..0000000 --- a/pingen/pingen_document.py +++ /dev/null @@ -1,425 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Author: Guewen Baconnier -# Copyright 2012 Camptocamp SA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import logging - -from cStringIO import StringIO - -from contextlib import closing -from openerp.osv import osv, orm, fields -from openerp.tools.translate import _ -from openerp import pooler, tools -from .pingen import APIError, ConnectionError, POST_SENDING_STATUS, \ - pingen_datetime_to_utc - -_logger = logging.getLogger(__name__) - - -class pingen_document(orm.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' - _inherits = {'ir.attachment': 'attachment_id'} - - _columns = { - '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', 'Connection Error'), - ('pingen_error', 'Pingen Error'), - ('canceled', 'Canceled')], - string='State', readonly=True, required=True), - 'push_date': fields.datetime('Push Date', readonly=True), - - # for `error` and `pingen_error` states when we push - 'last_error_message': fields.text('Error Message', readonly=True), - - # pingen IDs - 'pingen_id': fields.integer( - 'Pingen ID', readonly=True, - help="ID of the document in the Pingen Documents"), - 'post_id': fields.integer( - 'Pingen Post ID', readonly=True, - help="ID of the document in the Pingen Sendcenter"), - - # sendcenter infos - 'post_status': fields.char('Post Status', size=128, readonly=True), - 'parsed_address': fields.text('Parsed Address', readonly=True), - 'cost': fields.float('Cost', 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('Pages', readonly=True), - } - - _defaults = { - 'state': 'pending', - } - - _sql_constraints = [ - ('pingen_document_attachment_uniq', - 'unique (attachment_id)', - 'Only one Pingen document is allowed per attachment.'), - ] - - def _get_pingen_session(self, cr, uid, context=None): - """ Returns a pingen session for a user """ - company = self.pool.get('res.users').browse( - cr, uid, uid, context=context).company_id - return self.pool.get('res.company')._pingen(cr, uid, company, context=context) - - def _push_to_pingen(self, cr, uid, document, pingen=None, context=None): - """ Push a document to pingen.com - - :param Pingen pingen: optional pingen object to reuse session - """ - attachment_obj = self.pool.get('ir.attachment') - - decoded_document = attachment_obj._decoded_content( - cr, uid, document.attachment_id, context=context) - - if pingen is None: - pingen = self._get_pingen_session(cr, uid, context=context) - try: - doc_id, post_id, infos = pingen.push_document( - document.datas_fname, - StringIO(decoded_document), - document.pingen_send, - document.pingen_speed, - document.pingen_color) - except ConnectionError: - _logger.exception( - 'Connection Error when pushing Pingen Document %s to %s.' % - (document.id, pingen.url)) - raise - - except APIError: - _logger.error( - 'API Error when pushing Pingen Document %s to %s.' % - (document.id, pingen.url)) - raise - - error = False - 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['date']) - - document.write( - {'last_error_message': error, - 'state': state, - 'push_date': push_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), - 'pingen_id': doc_id, - 'post_id': post_id}, - context=context) - _logger.info('Pingen Document %s: pushed to %s' % (document.id, pingen.url)) - - def push_to_pingen(self, cr, uid, ids, context=None): - """ 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. - """ - assert len(ids) == 1, "Only 1 id is allowed" - with self._get_pingen_session(cr, uid, context=context) as session: - for document in self.browse(cr, uid, ids, context=context): - try: - self._push_to_pingen( - cr, uid, document, pingen=session, context=context) - except ConnectionError as e: - raise osv.except_osv( - _('Pingen Connection Error'), - _('Connection Error when asking for sending the document %s to Pingen') % document.name) - - except APIError as e: - raise osv.except_osv( - _('Pingen Error'), - _('Error when asking Pingen to send the document %s: ' - '\n%s') % (document.name, e)) - - except: - _logger.exception( - 'Unexcepted Error when updating the status of pingen.document %s: ' % - document.id) - raise osv.except_osv( - _('Error'), - _('Unexcepted Error when updating the status of Document %s') % document.name) - return True - - def _push_and_send_to_pingen_cron(self, cr, uid, ids, context=None): - """ 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 - """ - if not ids: - ids = self.search( - cr, uid, - # do not retry pingen_error, they should be treated manually - [('state', 'in', ['pending', 'pushed', 'error'])], - limit=100, - context=context) - - with closing(pooler.get_db(cr.dbname).cursor()) as loc_cr, \ - self._get_pingen_session(cr, uid, context=context) as session: - for document in self.browse(loc_cr, uid, ids, context=context): - - if document.state == 'error': - self._resolve_error(loc_cr, uid, document, context=context) - document.refresh() - - try: - if document.state == 'pending': - self._push_to_pingen( - loc_cr, uid, document, pingen=session, context=context) - - elif document.state == 'pushed': - self._ask_pingen_send( - loc_cr, uid, document, pingen=session, context=context) - except ConnectionError as e: - document.write({'last_error_message': e, - 'state': 'error'}, - context=context) - except APIError as e: - document.write({'last_error_message': e, - 'state': 'pingen_error'}, - context=context) - except: - _logger.error('Unexcepted error in pingen cron') - loc_cr.rollback() - raise - - else: - loc_cr.commit() - - return True - - def _resolve_error(self, cr, uid, document, context=None): - """ A document as resolved, put in the correct state """ - if document.post_id: - state = 'sendcenter' - elif document.pingen_id: - state = 'pushed' - else: - state = 'pending' - document.write({'state': state}, context=context) - - def resolve_error(self, cr, uid, ids, context=None): - """ A document as resolved, put in the correct state """ - for document in self.browse(cr, uid, ids, context=context): - self._resolve_error(cr, uid, document, context=context) - return True - - def _ask_pingen_send(self, cr, uid, document, pingen, context=None): - """ For a document already pushed to pingen, ask to send it. - - :param Pingen pingen: pingen object to reuse - """ - # sending has been explicitely asked so we change the option - # for consistency - if not document.pingen_send: - document.write({'pingen_send': True}, context=context) - - try: - post_id = pingen.send_document( - document.pingen_id, - document.pingen_speed, - document.pingen_color) - except ConnectionError: - _logger.exception('Connection Error when asking for sending Pingen Document %s to %s.' % - (document.id, pingen.url)) - raise - except APIError: - _logger.exception('API Error when asking for sending Pingen Document %s to %s.' % - (document.id, pingen.url)) - raise - - document.write( - {'last_error_message': False, - 'state': 'sendcenter', - 'post_id': post_id}, - context=context) - _logger.info('Pingen Document %s: asked for sending to %s' % (document.id, pingen.url)) - - return True - - def ask_pingen_send(self, cr, uid, ids, context=None): - """ 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. - """ - assert len(ids) == 1, "Only 1 id is allowed" - with self._get_pingen_session(cr, uid, context=context) as session: - for document in self.browse(cr, uid, ids, context=context): - try: - self._ask_pingen_send(cr, uid, document, pingen=session, context=context) - except ConnectionError as e: - raise osv.except_osv( - _('Pingen Connection Error'), - _('Connection Error when asking for ' - 'sending the document %s to Pingen') % document.name) - - except APIError as e: - raise osv.except_osv( - _('Pingen Error'), - _('Error when asking Pingen to send the document %s: ' - '\n%s') % (document.name, e)) - - except: - _logger.exception( - 'Unexcepted Error when updating the status of pingen.document %s: ' % - document.id) - raise osv.except_osv( - _('Error'), - _('Unexcepted Error when updating the status of Document %s') % document.name) - return True - - def _update_post_infos(self, cr, uid, document, pingen, context=None): - """ Update the informations from pingen of a document in the Sendcenter - - :param Pingen pingen: pingen object to reuse - """ - if not document.post_id: - return - - try: - post_infos = pingen.post_infos(document.post_id) - except ConnectionError: - _logger.exception( - 'Connection Error when asking for ' - 'sending Pingen Document %s to %s.' % - (document.id, pingen.url)) - raise - except APIError: - _logger.exception( - 'API Error when asking for sending Pingen Document %s to %s.' % - (document.id, pingen.url)) - raise - - currency_ids = self.pool.get('res.currency').search( - cr, uid, [('name', '=', post_infos['currency'])], context=context) - country_ids = self.pool.get('res.country').search( - cr, uid, [('code', '=', post_infos['country'])], context=context) - - send_date = pingen_datetime_to_utc(post_infos['date']) - - vals = { - 'post_status': POST_SENDING_STATUS[post_infos['status']], - 'cost': post_infos['cost'], - 'currency_id': currency_ids[0] if currency_ids else False, - 'parsed_address': post_infos['address'], - 'country_id': country_ids[0] if country_ids else False, - 'send_date': send_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), - 'pages': post_infos['pages'], - 'last_error_message': False, - } - if pingen.is_posted(post_infos): - vals['state'] = 'sent' - - document.write(vals, context=context) - _logger.info('Pingen Document %s: status updated' % document.id) - - def _update_post_infos_cron(self, cr, uid, ids, context=None): - """ 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. - """ - if not ids: - ids = self.search( - cr, uid, - [('state', '=', 'sendcenter')], - context=context) - - with closing(pooler.get_db(cr.dbname).cursor()) as loc_cr, \ - self._get_pingen_session(cr, uid, context=context) as session: - for document in self.browse(loc_cr, uid, ids, context=context): - try: - self._update_post_infos( - loc_cr, uid, document, pingen=session, context=context) - except (ConnectionError, APIError): - # will be retried the next time - # In any case, the error has been logged by _update_post_infos - loc_cr.rollback() - except: - _logger.error('Unexcepted error in pingen cron') - loc_cr.rollback() - raise - else: - loc_cr.commit() - return True - - def update_post_infos(self, cr, uid, ids, context=None): - """ 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. - """ - assert len(ids) == 1, "Only 1 id is allowed" - with self._get_pingen_session(cr, uid, context=context) as session: - for document in self.browse(cr, uid, ids, context=context): - try: - self._update_post_infos( - cr, uid, document, pingen=session, context=context) - except ConnectionError as e: - raise osv.except_osv( - _('Pingen Connection Error'), - _('Connection Error when updating the status of Document %s' - ' from Pingen') % document.name) - - except APIError as e: - raise osv.except_osv( - _('Pingen Error'), - _('Error when updating the status of Document %s from Pingen: ' - '\n%s') % (document.name, e)) - - except: - _logger.exception( - 'Unexcepted Error when updating the status of pingen.document %s: ' % - document.id) - raise osv.except_osv( - _('Error'), - _('Unexcepted Error when updating the status of Document %s') % document.name) - return True diff --git a/pingen/pingen_document_view.xml b/pingen/pingen_document_view.xml deleted file mode 100644 index 1686167..0000000 --- a/pingen/pingen_document_view.xml +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - pingen.document.tree - pingen.document - tree - - - - - - - - - - - - - - pingen.document.form - pingen.document - form - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -