[ADD] snailmail: new iap module to send letters by post

This commit is contained in:
Rémi Rahir
2018-07-19 15:54:00 +02:00
parent 01efffc472
commit 4cb9194275
18 changed files with 550 additions and 6 deletions
+1
View File
@@ -68,6 +68,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th
'views/res_users_views.xml',
'data/service_cron_reverse.xml',
'views/account_fiscal_year_view.xml',
'wizard/multi_compose_message_views.xml',
],
'demo': [
'demo/account_demo.xml',
+1
View File
@@ -16,3 +16,4 @@ from . import res_config_settings
from . import account_cash_rounding
from . import reconciliation_widget
from . import res_users
from . import res_company
+2 -2
View File
@@ -593,7 +593,7 @@ class AccountInvoice(models.Model):
"""
self.ensure_one()
template = self.env.ref('account.email_template_edi_invoice', False)
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
compose_form = self.env.ref('account.multi_compose_message_wizard_form', False)
ctx = dict(
default_model='account.invoice',
default_res_id=self.id,
@@ -609,7 +609,7 @@ class AccountInvoice(models.Model):
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'res_model': 'multi.compose.message',
'views': [(compose_form.id, 'form')],
'view_id': compose_form.id,
'target': 'new',
+11
View File
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
is_email = fields.Boolean('Email by default', default=True)
is_print = fields.Boolean('Print by default', default=True)
@@ -387,10 +387,8 @@
<field name="arch" type="xml">
<form string="Invoice" class="o_invoice_form">
<header>
<button name="action_invoice_sent" type="object" string="Send by Email" attrs="{'invisible':['|',('sent','=',True), ('state', 'not in', ('open','paid'))]}" class="oe_highlight o_invoice_send" groups="base.group_user"/>
<button name="invoice_print" string="Print Invoice" type="object" attrs="{'invisible':['|',('sent','=',True), ('state', 'not in', ('open','paid'))]}" class="oe_highlight" groups="base.group_user"/>
<button name="action_invoice_sent" type="object" string="Send by Email" attrs="{'invisible':['|',('sent','=',False), ('state', 'not in', ('open','paid'))]}" groups="base.group_user"/>
<button name="invoice_print" string="Print Invoice" type="object" attrs="{'invisible':['|',('sent','=',False), ('state', 'not in', ('open','paid'))]}" groups="base.group_user"/>
<button name="action_invoice_sent" string="Send &amp; Print" type="object" attrs="{'invisible':['|',('sent','=',True), ('state', 'not in', ('open','paid'))]}" class="oe_highlight" groups="base.group_user"/>
<button name="action_invoice_sent" string="Send &amp; Print" type="object" attrs="{'invisible':['|',('sent','=',False), ('state', 'not in', ('open','paid'))]}" groups="base.group_user"/>
<button name="%(action_account_invoice_payment)d" id="account_invoice_payment_btn" type="action"
attrs="{'invisible': [('state', '!=', 'open')]}"
string="Register Payment" groups="account.group_account_invoice" class="oe_highlight"/>
+1
View File
@@ -22,3 +22,4 @@ from . import account_report_aged_partner_balance
from . import setup_wizards
from . import wizard_tax_adjustments
from . import account_report_tax
from . import multi_compose_message
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MultiComposer(models.TransientModel):
_name = 'multi.compose.message'
_inherit = 'mail.compose.message'
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True,
default=lambda self: self.env.user.company_id.id)
is_email = fields.Boolean('Email', default=lambda self: self.env.user.company_id.is_email)
is_print = fields.Boolean('Print', default=lambda self: self.env.user.company_id.is_print)
printed = fields.Boolean('Is Printed', default=False)
partner_ids = fields.Many2many(
'res.partner', 'account_compose_message_res_partner_rel',
'wizard_id', 'partner_id', 'Additional Contacts')
attachment_ids = fields.Many2many(
'ir.attachment', 'account_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', 'Attachments')
print_attachment_id = fields.Many2one('ir.attachment', string='Print Attachment')
@api.multi
def _update_company(self):
for rec in self:
values = {}
for field in ['is_print', 'is_email']:
if rec.company_id[field] != rec[field]:
values.update({field: rec[field]})
if values:
rec.company_id.write(values)
@api.multi
def write(self, vals):
res = super(MultiComposer, self).write(vals)
self._update_company()
return res
@api.multi
@api.onchange('template_id')
def onchange_template_id_wrapper(self):
self.ensure_one()
super(MultiComposer,self).onchange_template_id_wrapper()
self.print_attachment_id = self.attachment_ids[:1]
def _send_email(self):
if self.is_email:
self.send_mail()
@api.multi
def _print_document(self):
""" to override for each type of models that will use this composer."""
self.ensure_one()
action = {
'type': 'ir.actions.act_url',
'url': '/web/content/%s?download=true' % (self.print_attachment_id.id),
'target': 'new',
}
self.printed = True
return action
@api.multi
def send_and_print_action(self):
self.ensure_one()
self._send_email()
return self._print_document()
@api.multi
def send_action(self):
for wizard in self:
wizard._send_email()
return {'type': 'ir.actions.act_window_close'}
@api.multi
def print_action(self):
self.ensure_one()
return self._print_document()
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="multi_compose_message_wizard_form">
<field name="name">multi.compose.message.form</field>
<field name="model">multi.compose.message</field>
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Compose Email &amp; Print">
<field name="print_attachment_id" invisible="1"/>
<div name="option_print" class="col-md-12 mb16">
<field name="is_print" widget="boolean_toggle"/>
<b><label for="is_print"/></b>
</div>
<div class="col-md-12 mb16" style="border-bottom: 1px solid #e5e5e5;"></div>
<div name="option_email" class="col-md-12 mb16">
<field name="is_email" widget="boolean_toggle"/>
<b><label for="is_email"/></b>
</div>
<div name="mail_form" attrs="{'invisible': [('is_email', '=', False)]}">
<group>
<!-- truly invisible fields for control and options -->
<field name="printed" invisible="1"/>
<field name="model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="is_log" invisible="1"/>
<field name="company_id" invisible="1"/>
<!-- visible wizard -->
<label for="partner_ids" string="Recipients" groups="base.group_user"/>
<div groups="base.group_user">
<span attrs="{'invisible':['|', ('model', '=', False)]}">Followers of the document and</span>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"/>
</div>
<field name="subject" placeholder="Subject..." required="True"/>
</group>
<field name="body" options="{'style-inline': true}"/>
<group col="4">
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" nolabel="1" colspan="2"/>
<field name="template_id" options="{'no_create': True}"
context="{'default_model': model, 'default_body_html': body, 'default_subject': subject}"/>
</group>
</div>
<footer>
<button string="Send &amp; Print"
attrs="{'invisible': ['|', '|', ('is_log', '=', True), ('printed', '=', True), '|', ('is_email', '=', False), ('is_print', '=', False)]}"
name="send_and_print_action" type="object" class="btn-primary o_mail_send"/>
<button string="Send"
attrs="{'invisible': ['|', ('is_print', '=', True), ('is_email', '=', False)]}"
name="send_action" type="object" class="btn-primary o_mail_send"/>
<button string="Print"
attrs="{'invisible': ['|', '|', '|', ('is_log', '=', True), ('printed', '=', True), ('is_print', '=', False), ('is_email', '=', True)]}"
name="print_action" type="object" class="btn-primary o_mail_send"/>
<button string="Done" attrs="{'invisible': [('printed', '=', False)]}" special="cancel" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel" />
<button icon="fa-lg fa-save" type="object" name="save_as_template" string="Save as new template"
attrs="{'invisible': ['|', '|', '|', ('is_log', '=', True), ('printed', '=', True), ('is_email', '=', False)]}"
class="pull-right btn-default" help="Save as a new template" />
</footer>
</form>
</field>
</record>
</data>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
{
'name': "Snail Mail",
'website': "https://www.odoo.com",
'description': """
Allows users to send invoices by post
=====================================================
""",
'category': 'Tools',
'version': '0.1',
'depends': ['account', 'iap'],
'data': [
'data/mail_activity_data.xml',
'wizard/multi_compose_message_views.xml',
'views/res_config_settings_views.xml',
],
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="mail_activity_data_snailmail" model="mail.activity.type">
<field name="name">Snailmail</field>
<field name="icon">fa-address-card</field>
<field name="sequence">15</field>
</record>
</data>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import res_company
from . import res_config_settings
+13
View File
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
class Company(models.Model):
_inherit = "res.company"
snailmail_color = fields.Boolean(string='Color', default=False)
snailmail_duplex = fields.Boolean(string='Both sides', default=False)
snailmail_send_by_letter = fields.Boolean(string='Send by Letter by default', default=True)
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
snailmail_color = fields.Boolean(string='color', related='company_id.snailmail_color')
snailmail_duplex = fields.Boolean(string='Both sides', related='company_id.snailmail_duplex')
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.snailmail</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="100"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='multi_company']" position="after">
<h2>Postal Printings</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="snailmail_duplex"/>
</div>
<div class="o_setting_right_pane">
<label for="snailmail_duplex"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="text-muted">
Choose the layout to print documents
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="snailmail_color"/>
</div>
<div class="o_setting_right_pane">
<label for="snailmail_color"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="text-muted">
Choose the ink used to print documents
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import multi_compose_message
@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from odoo import api, fields, models, _
from odoo.addons.iap import jsonrpc
DEFAULT_ENDPOINT = 'https://iap-services.odoo.com'
ESTIMATE_ENDPOINT = '/iap/snailmail/1/estimate'
PRINT_ENDPOINT = '/iap/snailmail/1/print'
class MultiComposer(models.TransientModel):
_name = 'multi.compose.message'
_inherit = 'multi.compose.message'
partner_id = fields.Many2one('res.partner', string='Partner')
snailmail_color = fields.Boolean(string='Color', default=lambda self: self.env.user.company_id.snailmail_color)
snailmail_duplex = fields.Boolean(string='Both side', default=lambda self: self.env.user.company_id.snailmail_duplex)
snailmail_send_by_letter = fields.Boolean('Post', default=lambda self: self.env.user.company_id.snailmail_send_by_letter)
snailmail_cost = fields.Float(string='Credits', compute='_snailmail_estimate', store=True, readonly=True)
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.user.company_id.currency_id.id, string="Currency")
@api.multi
def _update_company(self):
super(MultiComposer,self)._update_company()
for rec in self:
if rec.company_id.snailmail_send_by_letter != rec.snailmail_send_by_letter:
rec.company_id.write({
'snailmail_send_by_letter': rec.snailmail_send_by_letter,
})
@api.multi
def _snailmail_create(self, route, batch=False):
"""
Create a dictionnary object to send to snailmail server.
:return: Dict in the form:
{
account_token: string, //IAP Account token of the user
documents: [{
pages: int,
pdf_bin: pdf file
res_id: int (client-side res_id),
res_model: char (client-side res_model),
address: {
name: char,
street: char,
street2: char (OPTIONAL),
zip: int,
city: char,
state: char (state code (OPTIONAL)),
country_code: char (country code)
}
return_address: {
name: char,
street: char,
street2: char (OPTIONAL),
zip: int,
city: char,
state: char (state code (OPTIONAL)),
country_code: char (country code)
}
}],
options: {
color: boolean (true if color, false if black-white),
duplex: boolean (true if duplex, false otherwise),
}
}
"""
account_token = self.env['iap.account'].get('snailmail').account_token
documents = []
pdf = self.print_attachment_id.datas
if pdf:
documents.append({
'pdf_bin': route == 'print' and pdf,
'pages': route == 'estimate' and self._count_pages_pdf(pdf),
'address': {
'name': self.partner_id.name,
'street': self.partner_id.street,
'street2': self.partner_id.street2,
'zip': self.partner_id.zip,
'state': self.partner_id.state_id.code if self.partner_id.state_id else False,
'city': self.partner_id.city,
'country_code': self.partner_id.country_id.code
},
'return_address': {
'name': self.company_id.partner_id.name,
'street': self.company_id.partner_id.street,
'street2': self.company_id.partner_id.street2,
'zip': self.company_id.partner_id.zip,
'state': self.company_id.partner_id.state_id.code if self.company_id.partner_id.state_id else False,
'city': self.company_id.partner_id.city,
'country_code': self.company_id.partner_id.country_id.code,
}
})
return {
'account_token': account_token,
'documents': documents,
'options': {
'color': self.snailmail_color,
'duplex': self.snailmail_duplex,
},
'batch': batch,
}
@api.multi
def action_snailmail_print(self):
"""
get response
{
'request_code': RESPONSE_OK, # 200 if good or fail
'total_cost': total_cost,
'credit_error': credit_error,
'request': {
'documents': documents,
'options': options
}
}
}
"""
self.ensure_one()
endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
params = self._snailmail_create('print', batch=True)
response = jsonrpc(endpoint + PRINT_ENDPOINT, params=params)
error = False
doc = response['request']['documents'][0]
record = self.env[self.model].browse(self.res_id)
if doc.get('sent') and response['request_code'] == 200:
if hasattr(record, '_message_log'):
message = _('The document was correctly sent by post.<br>The tracking id is %s' % doc['send_id'])
record._message_log(body=message)
else:
activity_data = {
'res_id': self.res_id,
'res_model_id': self.env['ir.model']._get(self.model).id,
'activity_type_id': self.env.ref('snailmail.mail_activity_data_snailmail').id,
'summary': _('Post letter: an error occured.'),
'note': _('An error occured when sending the document by post.<br>Error: %s' % \
self._get_error_message(doc['error'] if response['request_code'] == 200 else response['reason'])),
'user_id': self.env.user.id,
}
self.env['mail.activity'].create(activity_data)
error = True
if not error:
notification = _('Post letter: document sent.')
else:
notification = _('Post letter: an error occured. Please check the document for more information.')
# as we interact with the exterior world, we commit
self.env.cr.commit()
self._update_company()
return {
'error': error,
'message': notification,
}
@api.multi
@api.depends('print_attachment_id')
@api.onchange('snailmail_send_by_letter')
def _snailmail_estimate(self):
"""
Send a request to estimate the cost of sending all the documents with
the differents options.
The JSON object sent is the one generated from self._snailmail_create()
arguments sent:
{
"documents":[{
pages: int,
res_id: int (client_side, optional),
res_model: int (client_side, optional),
address: {
country_code: char (country name)
}
}],
'color': # color on the letter,
'duplex': # one/two side printing,
}
The answer of the server is the same JSON object with some additionnal fields:
{
"total_cost": integer, //The cost of sending ALL the documents
body: JSON object (same as body param except new param 'cost' in each documents)
}
"""
for composer in self:
if not composer.partner_id:
composer.partner_id = self.env[self.model].browse(self.res_id).partner_id
if composer.snailmail_send_by_letter and composer.print_attachment_id :# and not composer.is_estimated:
endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
params = composer._snailmail_create('estimate')
req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params)
composer.is_estimated = True
# The cost is sent in eurocents, we change it to euros and then convert it to the company currency
cost = int(req['total_cost'])/100.0
currency_eur = self.env.ref('base.EUR')
composer.snailmail_cost = currency_eur._convert(cost, self.currency_id, self.company_id, fields.Datetime.now())
# all the print stuff will go to the cron onsnailail print orders
def _count_pages_pdf(self, bin_pdf):
""" Count the number of pages of the given pdf file.
:param bin_pdf : binary content of the pdf file
"""
pages = 0
for match in re.compile(b"/Count\s+(\d+)").finditer(bin_pdf):
pages = int(match.group(1))
return pages
def _get_error_message(self, error):
if error == 'CREDIT_ERROR':
link = self.env['iap.account'].get_credits_url(service_name='snailmail')
return _('You don\'t have enough credits to perform this operation.<br>Please go to your <a href=%s target="new">iap account</a>.' % link)
if error == 'NO_PRICE_AVAILABLE':
return _('The country of the partner is not covered by Snailmail.')
if error == 'MISSING_REQUIRED_FIELDS':
return _('One or more required fields are empty.')
if error == 'SOMETHING_IS_WRONG':
return _('An unknown error happened. Please contact the support.')
return error
@api.multi
def send_and_print_action(self):
res = super(MultiComposer,self).send_and_print_action()
if self.snailmail_send_by_letter:
self.action_snailmail_print()
return res
@api.multi
def send_action(self):
res = super(MultiComposer,self).send_action()
for wizard in self:
if wizard.snailmail_send_by_letter:
wizard.action_snailmail_print()
return res
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="multi_compose_message_inherit_account_wizard_form">
<field name="name">multi.compose.message.form.inherited.snailmail</field>
<field name="model">multi.compose.message</field>
<field name="inherit_id" ref="account.multi_compose_message_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='option_email']" position='before'>
<div name="option_letter" class="col-md-12 mb16">
<div name="option" class="pull-left">
<field name="snailmail_send_by_letter" widget="boolean_toggle"/>
<b><label for="snailmail_send_by_letter"/></b>
</div>
<div class="pull-right" name='cost' attrs="{'invisible': [('snailmail_send_by_letter','=',False)]}">
<field name="currency_id" invisible="1"/>
<b><field name="snailmail_cost" widget='monetary' options="{'currency_field': 'currency_id'}"/></b>
</div>
</div>
<div name="address_form" attrs="{'invisible': [('snailmail_send_by_letter', '=', False)]}">
<group>
<field string="Delivery Address" name="partner_id" readonly="1" force_save="1" context="{'show_address': 1}" options="{'always_reload': True, 'no_quick_create': True}" />
</group>
</div>
<div class="col-md-12 mb16" style="border-bottom: 1px solid #e5e5e5;"></div>
</xpath>
<xpath expr="//footer/button[@name='send_and_print_action']" position='attributes'>
<attribute name="attrs">{'invisible': ['|', '|', ('is_log', '=', True), ('printed', '=', True), '|', ('is_print', '=', False), '&amp;', '&amp;', ('is_print', '=', True), ('snailmail_send_by_letter', '=', False), ('is_email', '=', False)]}</attribute>
</xpath>
<xpath expr="//footer/button[@name='send_action']" position='attributes'>
<attribute name="attrs">{'invisible': ['|' , ('is_log', '=', True), '|', ('is_print', '=', True), '&amp;', '&amp;', ('is_print', '=', False), ('snailmail_send_by_letter', '=', False), ('is_email', '=', False)]}</attribute>
</xpath>
<xpath expr="//footer/button[@name='print_action']" position='attributes'>
<attribute name="attrs">{'invisible': ['|', '|', '|', '|', ('printed', '=', True), ('is_log', '=', True), ('is_print', '=', False), ('snailmail_send_by_letter', '=', True), ('is_email', '=', True)]}</attribute>
</xpath>
</field>
</record>
</data>
</odoo>