# 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 requests.exceptions import ConnectionError import odoo from odoo import _, fields, models from odoo.exceptions import UserError from .pingen import POST_SENDING_STATUS, APIError, pingen_datetime_to_utc _logger = logging.getLogger(__name__) class PingenDocument(models.Model): """A pingen document is the state of the synchronization of an attachment with pingen.com It stores the configuration and the current state of the synchronization. It also serves as a queue of documents to push to pingen.com """ _name = "pingen.document" _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: 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: _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: _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: 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: _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: 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: _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