[IMP] pingen: tie up everything with crons

This commit is contained in:
@
2012-11-26 10:15:12 +01:00
committed by Anna Janiszewska
parent 0fdf47e7ae
commit ceab2938a8
8 changed files with 284 additions and 50 deletions

View File

@@ -22,4 +22,5 @@
import ir_attachment import ir_attachment
import pingen import pingen
import pingen_document import pingen_document
import res_company

View File

@@ -43,6 +43,53 @@ Scope of the integration
One can decide, per document / attachment, if it should be pushed One can decide, per document / attachment, if it should be pushed
to pingen.com. The documents are pushed asynchronously. to pingen.com. The documents are pushed asynchronously.
A second cron updates the informations of the documents from pingen.com, so we
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 send
* 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 Dependencies
------------ ------------
@@ -50,16 +97,17 @@ Dependencies
* The PDF files sent to pingen.com have to respect some `formatting rules * The PDF files sent to pingen.com have to respect some `formatting rules
<https://stage-app.pingen.com/resources/pingen_requirements_v1_en.pdf>`_. <https://stage-app.pingen.com/resources/pingen_requirements_v1_en.pdf>`_.
* The address must be in a format accepted by pingen.com: the last line * The address must be in a format accepted by pingen.com: the last line
is the country. is the country in English or German.
""", """,
'website': 'http://www.camptocamp.com', 'website': 'http://www.camptocamp.com',
'init_xml': [], 'data': [
'update_xml': [
'ir_attachment_view.xml', 'ir_attachment_view.xml',
'pingen_document_view.xml', 'pingen_document_view.xml',
'pingen_data.xml',
'res_company_view.xml',
'security/ir.model.access.csv',
], ],
'demo_xml': [],
'tests': [], 'tests': [],
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,

32
pingen/pingen_data.xml Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_push_pingen" model="ir.cron">
<field name="name">Run Pingen Document Push</field>
<field eval="True" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field name="model">pingen.document</field>
<field name="function">_push_and_send_to_pingen_cron</field>
<field name="args">(None,)</field>
</record>
<record forcecreate="True" id="ir_cron_update_pingen" model="ir.cron">
<field name="name">Run Pingen Document Update</field>
<field eval="True" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field name="model">pingen.document</field>
<field name="function">_update_post_infos_cron</field>
<field name="args">(None,)</field>
</record>
</data>
</openerp>

View File

@@ -23,14 +23,12 @@ import logging
from cStringIO import StringIO from cStringIO import StringIO
from contextlib import closing
from openerp.osv import osv, orm, fields from openerp.osv import osv, orm, fields
from openerp import tools from openerp import tools
from openerp.tools.translate import _ from openerp.tools.translate import _
from .pingen import Pingen, APIError, ConnectionError, POST_SENDING_STATUS from openerp import pooler
from .pingen import APIError, ConnectionError, POST_SENDING_STATUS
# TODO should be configurable
TOKEN = '6bc041af6f02854461ef31c2121ef853'
STAGING = True
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -60,7 +58,6 @@ class pingen_document(orm.Model):
('pingen_error', 'Pingen Error'), ('pingen_error', 'Pingen Error'),
('canceled', 'Canceled')], ('canceled', 'Canceled')],
string='State', readonly=True, required=True), string='State', readonly=True, required=True),
'date': fields.datetime('Creation Date', readonly=True),
'push_date': fields.datetime('Push Date', readonly=True), 'push_date': fields.datetime('Push Date', readonly=True),
# for `error` and `pingen_error` states when we push # for `error` and `pingen_error` states when we push
@@ -94,12 +91,6 @@ class pingen_document(orm.Model):
'Only one Pingen document is allowed per attachment.'), 'Only one Pingen document is allowed per attachment.'),
] ]
def _pingen(self, cr, uid, ids, context=None):
""" Return a Pingen instance to work on """
assert not ids, "ids is there by convention, should not be used"
# TODO parameterize
return Pingen(TOKEN, staging=STAGING)
def _push_to_pingen(self, cr, uid, document, context=None): def _push_to_pingen(self, cr, uid, document, context=None):
""" Push a document to pingen.com """ """ Push a document to pingen.com """
attachment_obj = self.pool.get('ir.attachment') attachment_obj = self.pool.get('ir.attachment')
@@ -107,7 +98,9 @@ class pingen_document(orm.Model):
decoded_document = attachment_obj._decoded_content( decoded_document = attachment_obj._decoded_content(
cr, uid, document.attachment_id, context=context) cr, uid, document.attachment_id, context=context)
pingen = self._pingen(cr, uid, [], context=context) company = self.pool.get('res.users').browse(
cr, uid, uid, context=context).company_id
pingen = self.pool.get('res.company')._pingen(cr, uid, company, context=context)
try: try:
doc_id, post_id, infos = pingen.push_document( doc_id, post_id, infos = pingen.push_document(
document.datas_fname, document.datas_fname,
@@ -127,9 +120,17 @@ class pingen_document(orm.Model):
(document.id, pingen.url)) (document.id, pingen.url))
raise 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.')
document.write( document.write(
{'last_error_message': False, {'last_error_message': error,
'state': 'sendcenter' if post_id else 'pushed', 'state': state,
'push_date': infos['date'], 'push_date': infos['date'],
'pingen_id': doc_id, 'pingen_id': doc_id,
'post_id': post_id}, 'post_id': post_id},
@@ -157,11 +158,23 @@ class pingen_document(orm.Model):
_('Pingen Error'), _('Pingen Error'),
_('Error when asking Pingen to send the document %s: ' _('Error when asking Pingen to send the document %s: '
'\n%s') % (document.name, e)) '\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 return True
def _push_and_send_to_pingen_silent(self, cr, uid, ids, context=None): def _push_and_send_to_pingen_cron(self, cr, uid, ids, context=None):
""" Push a document to pingen.com """ 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 Instead of raising, store the error in the pingen.document
""" """
if not ids: if not ids:
@@ -169,18 +182,49 @@ class pingen_document(orm.Model):
cr, uid, cr, uid,
# do not retry pingen_error, they should be treated manually # do not retry pingen_error, they should be treated manually
[('state', 'in', ['pending', 'pushed', 'error'])], [('state', 'in', ['pending', 'pushed', 'error'])],
limit=100,
context=context) context=context)
for document in self.browse(cr, uid, ids, context=context):
try:
if not document.pingen_id:
self._push_to_pingen(cr, uid, document, context=context)
if not document.post_id and document.pingen_send:
self._ask_pingen_send(cr, uid, document, 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)
with closing(pooler.get_db(cr.dbname).cursor()) as loc_cr:
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, context=context)
elif document.state == 'pushed':
self._ask_pingen_send(loc_cr, uid, document, 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
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 return True
def _ask_pingen_send(self, cr, uid, document, context=None): def _ask_pingen_send(self, cr, uid, document, context=None):
@@ -191,7 +235,9 @@ class pingen_document(orm.Model):
if not document.pingen_send: if not document.pingen_send:
document.write({'pingen_send': True}, context=context) document.write({'pingen_send': True}, context=context)
pingen = self._pingen(cr, uid, [], context=context) company = self.pool.get('res.users').browse(
cr, uid, uid, context=context).company_id
pingen = self.pool.get('res.company')._pingen(cr, uid, company, context=context)
try: try:
post_id = pingen.send_document( post_id = pingen.send_document(
document.pingen_id, document.pingen_id,
@@ -235,6 +281,14 @@ class pingen_document(orm.Model):
_('Pingen Error'), _('Pingen Error'),
_('Error when asking Pingen to send the document %s: ' _('Error when asking Pingen to send the document %s: '
'\n%s') % (document.name, e)) '\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 return True
def _update_post_infos(self, cr, uid, document, context=None): def _update_post_infos(self, cr, uid, document, context=None):
@@ -243,7 +297,9 @@ class pingen_document(orm.Model):
if not document.post_id: if not document.post_id:
return return
pingen = self._pingen(cr, uid, [], context=context) company = self.pool.get('res.users').browse(
cr, uid, uid, context=context).company_id
pingen = self.pool.get('res.company')._pingen(cr, uid, company, context=context)
try: try:
post_infos = pingen.post_infos(document.post_id) post_infos = pingen.post_infos(document.post_id)
except ConnectionError as e: except ConnectionError as e:
@@ -270,6 +326,7 @@ class pingen_document(orm.Model):
'country_id': country_ids[0] if country_ids else False, 'country_id': country_ids[0] if country_ids else False,
'send_date': post_infos['date'], 'send_date': post_infos['date'],
'pages': post_infos['pages'], 'pages': post_infos['pages'],
'last_error_message': False,
} }
if pingen.is_posted(post_infos): if pingen.is_posted(post_infos):
vals['state'] = 'sent' vals['state'] = 'sent'
@@ -277,9 +334,13 @@ class pingen_document(orm.Model):
document.write(vals, context=context) document.write(vals, context=context)
_logger.info('Pingen Document %s: status updated' % document.id) _logger.info('Pingen Document %s: status updated' % document.id)
def _update_post_infos_silent(self, cr, uid, ids, context=None): def _update_post_infos_cron(self, cr, uid, ids, context=None):
""" Update the informations from pingen of a document in the Sendcenter """ 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. Do not raise errors, only skip the update of the record.
""" """
if not ids: if not ids:
@@ -288,15 +349,20 @@ class pingen_document(orm.Model):
[('state', '=', 'sendcenter')], [('state', '=', 'sendcenter')],
context=context) context=context)
for document in self.browse(cr, uid, ids, context=context): with closing(pooler.get_db(cr.dbname).cursor()) as loc_cr:
try: for document in self.browse(loc_cr, uid, ids, context=context):
self._update_post_infos(cr, uid, document, context=context) try:
except (ConnectionError, APIError): self._update_post_infos(loc_cr, uid, document, context=context)
# Intended silented exception, we can consider that it's not except (ConnectionError, APIError):
# important if the update not worked, that's # will be retried the next time
# only informative, and it will be retried the next time # In any case, the error has been logged by _update_post_infos
# In any case, the error has been by _update_post_infos loc_cr.rollback()
pass except:
_logger.error('Unexcepted error in pingen cron')
loc_cr.rollback()
raise
loc_cr.commit()
return True return True
def update_post_infos(self, cr, uid, ids, context=None): def update_post_infos(self, cr, uid, ids, context=None):
@@ -319,5 +385,13 @@ class pingen_document(orm.Model):
_('Pingen Error'), _('Pingen Error'),
_('Error when updating the status of Document %s from Pingen: ' _('Error when updating the status of Document %s from Pingen: '
'\n%s') % (document.name, e)) '\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 return True

View File

@@ -42,7 +42,6 @@
<separator string="Dates" colspan="4"/> <separator string="Dates" colspan="4"/>
<newline /> <newline />
<group col="2" colspan="2"> <group col="2" colspan="2">
<field name="date"/>
<field name="push_date"/> <field name="push_date"/>
</group> </group>
@@ -75,14 +74,17 @@
<newline /> <newline />
<group col="2" colspan="2"> <group col="2" colspan="2">
<button name="push_to_pingen" type="object" <button name="push_to_pingen" type="object"
states="pending,error,pingen_error" states="pending"
string="Push to pingen.com" icon="terp-camera_test"/> string="Push to pingen.com" icon="terp-stage"/>
<button name="ask_pingen_send" type="object" <button name="ask_pingen_send" type="object"
states="pushed" states="pushed"
string="Ask pingen.com to send the document" icon="terp-camera_test"/> string="Ask pingen.com to send the document" icon="gtk-print"/>
<button name="resolve_error" type="object"
states="error,pingen_error"
string="Errors resolved" icon="gtk-redo"/>
<button name="update_post_infos" type="object" <button name="update_post_infos" type="object"
states="sendcenter" states="sendcenter"
string="Update the letter's informations" icon="terp-camera_test"/> string="Update the letter's informations" icon="gtk-refresh"/>
</group> </group>
</page> </page>
<page string="Attachment"> <page string="Attachment">
@@ -117,15 +119,27 @@
<field name="type">search</field> <field name="type">search</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Pingen Document"> <search string="Pingen Document">
<filter icon="terp-stage" <filter icon="terp-project"
string="Pending" string="Pending"
domain="[('state','=','pending')]"/> domain="[('state','=','pending')]"/>
<filter icon="terp-stock_align_left_24" <filter icon="terp-stage"
string="Pushed" string="Pushed"
domain="[('state','=','pushed')]"/> domain="[('state','=','pushed')]"/>
<filter icon="terp-stock_align_left_24" <filter icon="gtk-print"
string="In Sendcenter"
domain="[('state','=','sendcenter')]"/>
<filter icon="kanban-apply"
string="Sent"
domain="[('state','=','sent')]"/>
<filter icon="kanban-stop"
string="Error" string="Error"
domain="[('state','=','error')]"/> domain="[('state','=','error')]"/>
<filter icon="STOCK_NO"
string="Pingen Error"
domain="[('state','=','pingen_error')]"/>
<filter icon="terp-dialog-close"
string="Canceled"
domain="[('state','=','canceled')]"/>
<separator orientation="vertical"/> <separator orientation="vertical"/>
<field name="attachment_id" /> <field name="attachment_id" />
</search> </search>

42
pingen/res_company.py Normal file
View File

@@ -0,0 +1,42 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.osv.orm import browse_record
from .pingen import Pingen
class res_company(orm.Model):
_inherit = 'res.company'
_columns = {
'pingen_token': fields.char('Pingen Token', size=32),
'pingen_staging': fields.boolean('Pingen Staging')
}
def _pingen(self, cr, uid, company, context=None):
""" 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_id, context=context)
return Pingen(company.pingen_token, staging=company.pingen_staging)

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record model="ir.ui.view" id="view_company_inherit_form">
<field name="name">res.company.form.inherit</field>
<field name="model">res.company</field>
<field name="type">form</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<page string="Configuration" position="inside">
<separator string="Pingen.com" colspan="4"/>
<field name="pingen_token" groups="base.group_system"/>
<field name="pingen_staging" groups="base.group_system"/>
</page>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,3 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_pingen_document_all","pingen_document all","model_pingen_document",,1,0,0,0
"access_pingen_document_group_user","pingen_document group_user","model_pingen_document","base.group_user",1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_pingen_document_all pingen_document all model_pingen_document 1 0 0 0
3 access_pingen_document_group_user pingen_document group_user model_pingen_document base.group_user 1 1 1 1