diff --git a/pingen/README.rst b/pingen/README.rst new file mode 100644 index 00000000000..14a55b8ffa5 --- /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 +============ + +* Yannick Vaucher +* Guewen Baconnier +* Anar Baghirli + +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 4ff2feaecd5..e26b57004e1 100644 --- a/pingen/__init__.py +++ b/pingen/__init__.py @@ -1,25 +1,9 @@ # -*- 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 ir_attachment +from . import pingen +from . import pingen_document +from . import res_company diff --git a/pingen/__manifest__.py b/pingen/__manifest__.py index 4ae5dc88634..98523bd5618 100644 --- a/pingen/__manifest__.py +++ b/pingen/__manifest__.py @@ -1,27 +1,11 @@ # -*- 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', @@ -31,79 +15,6 @@ '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', @@ -113,7 +24,7 @@ 'security/ir.model.access.csv', ], 'tests': [], - 'installable': False, + 'installable': True, 'auto_install': False, 'application': True, } diff --git a/pingen/ir_attachment.py b/pingen/ir_attachment.py index ba00c47b7b2..423230cae45 100644 --- a/pingen/ir_attachment.py +++ b/pingen/ir_attachment.py @@ -1,123 +1,99 @@ # -*- 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 base64 -from openerp.osv import osv, orm, fields -from openerp.tools.translate import _ +from odoo import models, fields, _ +from odoo.exceptions import UserError -class ir_attachment(orm.Model): +class IrAttachment(models.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'), - } + 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') - _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, + def _prepare_pingen_document_vals(self): + return {'attachment_id': self.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`` + 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 + * 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: + 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'}, context=context) + document.write({'state': 'pending'}) else: pingen_document_obj.create( - cr, uid, - self._prepare_pingen_document_vals( - cr, uid, attachment, context=context), - context=context) + self._prepare_pingen_document_vals()) 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) + raise UserError( + _('Error. The attachment ' + + '%s is already pushed to pingen.com.') % + self.name) + document.write({'state': 'canceled'}) return - def create(self, cr, uid, vals, context=None): - attachment_id = super(ir_attachment, self).create(cr, uid, vals, context=context) + def create(self, vals): + attachment_id = super(IrAttachment, self).create(vals) if 'send_to_pingen' in vals: - self._handle_pingen_document(cr, uid, attachment_id, context=context) + attachment_id._handle_pingen_document() return attachment_id - def write(self, cr, uid, ids, vals, context=None): - res = super(ir_attachment, self).write(cr, uid, ids, vals, context=context) + def write(self, vals): + res = super(IrAttachment, self).write(vals) if 'send_to_pingen' in vals: - for attachment_id in ids: - self._handle_pingen_document(cr, uid, attachment_id, context=context) + for attachment in self: + attachment._handle_pingen_document() return res - def _decoded_content(self, cr, uid, attachment, context=None): + 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 attachment.type == 'binary': - decoded_document = base64.decodestring(attachment.datas) - elif attachment.type == 'url': - response = requests.get(attachment.url) + 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 Exception( - 'The type of attachment %s is not handled' % attachment.type) + raise UserError( + _('The type of attachment %s is not handled') + % self.type) return decoded_document diff --git a/pingen/ir_attachment_view.xml b/pingen/ir_attachment_view.xml index d51a906f0f8..24e2335efcc 100644 --- a/pingen/ir_attachment_view.xml +++ b/pingen/ir_attachment_view.xml @@ -1,6 +1,6 @@ - - + + ir.attachment.pingen.view @@ -8,14 +8,14 @@ form - - + + - - + + @@ -28,4 +28,4 @@ src_model="ir.attachment"/> - + diff --git a/pingen/pingen.py b/pingen/pingen.py index 475407b39ee..c705e6a1970 100644 --- a/pingen/pingen.py +++ b/pingen/pingen.py @@ -1,29 +1,14 @@ # -*- 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 import urlparse import json import pytz +import base64 from datetime import datetime from requests.packages.urllib3.filepost import encode_multipart_formdata @@ -90,14 +75,9 @@ def session(self): """ 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,7 +101,8 @@ def _send(self, method, endpoint, **kwargs): :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) + complete_url = p_url + '/token/' + self._token response = method(complete_url, **kwargs) @@ -132,11 +113,13 @@ def _send(self, method, endpoint, **kwargs): 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 @@ -162,7 +145,7 @@ def push_document(self, filename, filestream, send=None, speed=None, color=None) # the entire body and send it to `data` # https://github.com/kennethreitz/requests/issues/950 formdata = { - 'file': (filename, filestream.read()), + 'file': (filename, base64.b64decode(filestream.read())), 'data': json.dumps(data), } diff --git a/pingen/pingen_document.py b/pingen/pingen_document.py index ac59a028f67..3e77752731c 100644 --- a/pingen/pingen_document.py +++ b/pingen/pingen_document.py @@ -1,39 +1,23 @@ # -*- 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 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 +# from contextlib import closing +from odoo import models, fields, _ +from odoo.exceptions import UserError +import odoo +from .pingen import APIError, ConnectionError, \ + POST_SENDING_STATUS, pingen_datetime_to_utc _logger = logging.getLogger(__name__) -class pingen_document(orm.Model): +class PingenDocument(models.Model): """ A pingen document is the state of the synchronization of an attachment with pingen.com @@ -44,46 +28,38 @@ class pingen_document(orm.Model): _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', - } + 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', @@ -91,43 +67,34 @@ class pingen_document(orm.Model): 'Only one Pingen document is allowed per attachment.'), ] - def _get_pingen_session(self, cr, uid, context=None): + def _get_pingen_session(self): """ 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) + return self.company_id._pingen() - def _push_to_pingen(self, cr, uid, document, pingen=None, context=None): + def _push_to_pingen(self, pingen=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) - + decoded_document = self.attachment_id._decoded_content() if pingen is None: - pingen = self._get_pingen_session(cr, uid, context=context) + pingen = self._get_pingen_session() try: doc_id, post_id, infos = pingen.push_document( - document.datas_fname, + self.datas_fname, StringIO(decoded_document), - document.pingen_send, - document.pingen_speed, - document.pingen_color) + self.pingen_send, + self.pingen_speed, + self.pingen_color) except ConnectionError: _logger.exception( 'Connection Error when pushing Pingen Document %s to %s.' % - (document.id, pingen.url)) + (self.id, pingen.url)) raise - except APIError: _logger.error( 'API Error when pushing Pingen Document %s to %s.' % - (document.id, pingen.url)) + (self.id, pingen.url)) raise - error = False state = 'pushed' if post_id: @@ -135,291 +102,254 @@ def _push_to_pingen(self, cr, uid, document, pingen=None, context=None): 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( + self.write( {'last_error_message': error, 'state': state, - 'push_date': push_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), + 'push_date': fields.Datetime.to_string(push_date), 'pingen_id': doc_id, - 'post_id': post_id}, - context=context) - _logger.info('Pingen Document %s: pushed to %s' % (document.id, pingen.url)) + 'post_id': post_id},) + _logger.info( + 'Pingen Document %s: pushed to %s' % (self.id, pingen.url)) - def push_to_pingen(self, cr, uid, ids, context=None): + 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. """ - 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) + self.ensure_one() + for document in self: + try: + session = self._get_pingen_session() + document._push_to_pingen(pingen=session) + except ConnectionError as e: + raise UserError( + _('Connection Error when asking for ' + + 'sending the document %s to Pingen') + % document.name) + except APIError as e: + raise UserError( + _('Error when asking Pingen to send the document %s: ' + '\n%s') % (document.name, e)) + except Exception as e: + _logger.exception( + 'Unexcepted Error when updating ' + + 'the status of pingen.document %s: ' % + document.id) + raise UserError( + _('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): + 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 """ - 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() - + 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) + for document in self: + session = document._get_pingen_session() + if document.state == 'error': + self._resolve_error() + document.refresh() + try: + if document.state == 'pending': + self._push_to_pingen(pingen=session) + + elif document.state == 'pushed': + self._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('Unexcepted error in pingen cron') return True - def _resolve_error(self, cr, uid, document, context=None): + def _resolve_error(self): """ A document as resolved, put in the correct state """ - if document.post_id: + if self.post_id: state = 'sendcenter' - elif document.pingen_id: + elif self.pingen_id: state = 'pushed' else: state = 'pending' - document.write({'state': state}, context=context) + self.write({'state': state}) - def resolve_error(self, cr, uid, ids, context=None): + def resolve_error(self): """ 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) + for document in self: + document._resolve_error() return True - def _ask_pingen_send(self, cr, uid, document, pingen, context=None): + 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 document.pingen_send: - document.write({'pingen_send': True}, context=context) + if not self.pingen_send: + self.write({'pingen_send': True}) try: post_id = pingen.send_document( - document.pingen_id, - document.pingen_speed, - document.pingen_color) + self.pingen_id, + self.pingen_speed, + self.pingen_color) except ConnectionError: - _logger.exception('Connection Error when asking for sending Pingen Document %s to %s.' % - (document.id, pingen.url)) + _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.' % - (document.id, pingen.url)) + _logger.exception( + 'API Error when asking for sending ' + + 'Pingen Document %s to %s.' % + (self.id, pingen.url)) raise - - document.write( + self.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)) - + '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, cr, uid, ids, context=None): + 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. """ - 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) + self.ensure_one() + for document in self: + try: + session = document._get_pingen_session() + document._ask_pingen_send(pingen=session) + except ConnectionError as e: + raise UserError( + _('Connection Error when asking for ' + 'sending the document %s to Pingen') % document.name) + + except APIError as e: + raise UserError( + _('Error when asking Pingen to send the document %s: ' + '\n%s') % (document.name, e)) + + except BaseException as e: + _logger.exception( + 'Unexcepted Error when updating the ' + + 'status of pingen.document %s: ' % + document.id) + raise UserError( + _('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 - + 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 document.post_id: + if not self.post_id: return - try: - post_infos = pingen.post_infos(document.post_id) + post_infos = pingen.post_infos(self.post_id) except ConnectionError: _logger.exception( 'Connection Error when asking for ' 'sending Pingen Document %s to %s.' % - (document.id, pingen.url)) + (self.id, pingen.url)) raise except APIError: _logger.exception( - 'API Error when asking for sending Pingen Document %s to %s.' % - (document.id, pingen.url)) + 'API Error when asking for sending ' + + 'Pingen Document %s to %s.' % + (self.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) - + currency_ids = self.env['res.currency'].search( + [('name', '=', post_infos['currency'])]) + country_ids = 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']], '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), + '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) - 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 - + 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. - """ - 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() + 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) + for document in self: + session = document._get_pingen_session() + try: + self._update_post_infos( + document, 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, cr, uid, ids, context=None): + 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. """ - 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) + self.ensure_one() + for document in self: + try: + session = document._get_pingen_session() + document._update_post_infos(pingen=session) + except ConnectionError as e: + raise UserError( + _('Connection Error when updating' + + 'the status of Document %s' + ' from Pingen') % document.name) + except APIError as e: + raise UserError( + _('Error when updating the status' + + 'of Document %s from Pingen: ' + '\n%s') % (document.name, e)) + except BaseException as e: + _logger.exception( + 'Unexcepted Error when updating ' + + 'the status of pingen.document %s: ' % + document.id) + raise UserError( + _('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 index 1686167654f..253e837d164 100644 --- a/pingen/pingen_document_view.xml +++ b/pingen/pingen_document_view.xml @@ -155,7 +155,7 @@ - + diff --git a/pingen/res_company.py b/pingen/res_company.py index ad0c2c88771..1da79a12293 100644 --- a/pingen/res_company.py +++ b/pingen/res_company.py @@ -1,42 +1,20 @@ # -*- 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). -from openerp.osv import orm, fields -from openerp.osv.orm import browse_record +from odoo import models, fields from .pingen import Pingen -class res_company(orm.Model): +class ResCompany(models.Model): _inherit = 'res.company' - _columns = { - 'pingen_token': fields.char('Pingen Token', size=32), - 'pingen_staging': fields.boolean('Pingen Staging') - } + pingen_token = fields.Char('Pingen Token', size=32) + pingen_staging = fields.Boolean('Pingen Staging') - def _pingen(self, cr, uid, company, context=None): + def _pingen(self): """ Return a Pingen instance to work on """ - assert isinstance(company, (int, long, browse_record)), \ - "one id or browse_record expected" - if not isinstance(company, browse_record): - company = self.browse(cr, uid, company, context=context) - return Pingen(company.pingen_token, staging=company.pingen_staging) + self.ensure_one() + return Pingen(self.pingen_token, staging=self.pingen_staging) diff --git a/pingen/res_company_view.xml b/pingen/res_company_view.xml index d915f74e126..763556706df 100644 --- a/pingen/res_company_view.xml +++ b/pingen/res_company_view.xml @@ -1,20 +1,19 @@ - - res.company.form.inherit res.company form - - - - - + + + + + + + + - -