diff --git a/addons/account/wizard/__init__.py b/addons/account/wizard/__init__.py
index b7374520988..d8440f9e57c 100644
--- a/addons/account/wizard/__init__.py
+++ b/addons/account/wizard/__init__.py
@@ -21,4 +21,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
+from . import account_invoice_send
diff --git a/addons/account/wizard/account_invoice_send.py b/addons/account/wizard/account_invoice_send.py
new file mode 100644
index 00000000000..2bb3c6b8750
--- /dev/null
+++ b/addons/account/wizard/account_invoice_send.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+
+class AccountInvoiceSend(models.TransientModel):
+ _name = 'account.invoice.send'
+ _inherits = {'mail.compose.message':'composer_id'}
+
+ is_email = fields.Boolean('Email', default=lambda self: self.env.user.company_id.invoice_is_email)
+ is_print = fields.Boolean('Print', default=lambda self: self.env.user.company_id.invoice_is_print)
+ printed = fields.Boolean('Is Printed', default=False)
+ invoice_ids = fields.Many2many('account.invoice', 'account_invoice_account_invoice_send_rel', string='Invoices')
+ composer_id = fields.Many2one('mail.compose.message', string='Composer', required=True, ondelete='cascade')
+ template_id = fields.Many2one(
+ 'mail.template', 'Use template', index=True,
+ domain="[('model', '=', 'account.invoice')]"
+ )
+
+ @api.model
+ def default_get(self, fields):
+ res = super(AccountInvoiceSend, self).default_get(fields)
+ res_ids = self._context.get('active_ids')
+ composer = self.env['mail.compose.message'].create({
+ 'composition_mode': 'comment' if len(res_ids) == 1 else 'mass_mail',
+ })
+ res.update({
+ 'invoice_ids': res_ids,
+ 'composer_id': composer.id,
+ })
+ return res
+
+ @api.multi
+ @api.onchange('invoice_ids')
+ def _compute_composition_mode(self):
+ for wizard in self:
+ wizard.composition_mode = 'comment' if len(wizard.invoice_ids) == 1 else 'mass_mail'
+
+ @api.onchange('template_id')
+ def onchange_template_id(self):
+ if self.composer_id:
+ self.composer_id.template_id = self.template_id.id
+ self.composer_id.onchange_template_id_wrapper()
+
+ @api.multi
+ def _send_email(self):
+ if self.is_email:
+ self.composer_id.send_mail()
+
+ @api.multi
+ def _print_document(self):
+ """ to override for each type of models that will use this composer."""
+ self.ensure_one()
+ return self.invoice_ids.invoice_print()
+
+ @api.multi
+ def send_and_print_action(self):
+ self.ensure_one()
+ self._send_email()
+ if self.is_print:
+ return self._print_document()
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/account/wizard/account_invoice_send_views.xml b/addons/account/wizard/account_invoice_send_views.xml
new file mode 100644
index 00000000000..c48e7b4dc02
--- /dev/null
+++ b/addons/account/wizard/account_invoice_send_views.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+ account.invoice.send.form
+ account.invoice.send
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/account/wizard/multi_compose_message.py b/addons/account/wizard/multi_compose_message.py
deleted file mode 100644
index cdbae7610f2..00000000000
--- a/addons/account/wizard/multi_compose_message.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# -*- 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()
diff --git a/addons/account/wizard/multi_compose_message_views.xml b/addons/account/wizard/multi_compose_message_views.xml
deleted file mode 100644
index bed5b6939e8..00000000000
--- a/addons/account/wizard/multi_compose_message_views.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
- multi.compose.message.form
- multi.compose.message
-
-
-
-
-
-
-
-
-
diff --git a/addons/snailmail/__init__.py b/addons/snailmail/__init__.py
index 35e7c9600c5..cde864bae21 100644
--- a/addons/snailmail/__init__.py
+++ b/addons/snailmail/__init__.py
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
-from . import wizard
diff --git a/addons/snailmail/__manifest__.py b/addons/snailmail/__manifest__.py
index 2a83ff96e5e..eb40e985b9c 100644
--- a/addons/snailmail/__manifest__.py
+++ b/addons/snailmail/__manifest__.py
@@ -2,15 +2,17 @@
{
'name': "Snail Mail",
'description': """
-Allows users to send invoices by post
+Allows users to send documents by post
=====================================================
""",
'category': 'Tools',
'version': '0.1',
- 'depends': ['account', 'iap'],
+ 'depends': ['iap', 'base_setup'],
'data': [
- 'data/mail_activity_data.xml',
- 'wizard/multi_compose_message_views.xml',
+ 'data/snailmail_data.xml',
'views/res_config_settings_views.xml',
+ 'views/snailmail_views.xml',
+ 'security/ir.model.access.csv',
],
+ 'auto_install': True,
}
diff --git a/addons/snailmail/data/mail_activity_data.xml b/addons/snailmail/data/mail_activity_data.xml
deleted file mode 100644
index 191fb9bc03a..00000000000
--- a/addons/snailmail/data/mail_activity_data.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- Snailmail
- fa-address-card
- 15
-
-
-
diff --git a/addons/snailmail/data/snailmail_data.xml b/addons/snailmail/data/snailmail_data.xml
new file mode 100644
index 00000000000..354c30aaf53
--- /dev/null
+++ b/addons/snailmail/data/snailmail_data.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ Snaimail: process letters queue
+
+ code
+ model._snailmail_cron()
+ 1
+ hours
+ -1
+
+
+
+
+ Snailmail
+ fa-address-card
+ 15
+
+
+
diff --git a/addons/snailmail/models/__init__.py b/addons/snailmail/models/__init__.py
index 883205a7200..ee0d4f4704c 100644
--- a/addons/snailmail/models/__init__.py
+++ b/addons/snailmail/models/__init__.py
@@ -2,3 +2,4 @@
from . import res_company
from . import res_config_settings
+from . import snailmail_letter
diff --git a/addons/snailmail/models/res_company.py b/addons/snailmail/models/res_company.py
index c4875d754ac..b51147b2ef1 100644
--- a/addons/snailmail/models/res_company.py
+++ b/addons/snailmail/models/res_company.py
@@ -1,13 +1,10 @@
# -*- 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
-
+from odoo import fields, models
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)
diff --git a/addons/snailmail/models/res_config_settings.py b/addons/snailmail/models/res_config_settings.py
index 52468b7a0f5..7b916ce0e1f 100644
--- a/addons/snailmail/models/res_config_settings.py
+++ b/addons/snailmail/models/res_config_settings.py
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import fields, models, api
-
+
+from odoo import fields, models
+
+
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')
+
+ snailmail_color = fields.Boolean(string='Print In Color', related='company_id.snailmail_color')
+ snailmail_duplex = fields.Boolean(string='Print Both sides', related='company_id.snailmail_duplex')
diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py
new file mode 100644
index 00000000000..5b09681bead
--- /dev/null
+++ b/addons/snailmail/models/snailmail_letter.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import re
+import base64
+import datetime
+
+from odoo import fields, models, api, _, tools
+from odoo.addons.iap import jsonrpc
+from odoo.exceptions import UserError
+from odoo.tools.safe_eval import safe_eval
+
+DEFAULT_ENDPOINT = 'https://iap-snailmail.odoo.com'
+ESTIMATE_ENDPOINT = '/iap/snailmail/1/estimate'
+PRINT_ENDPOINT = '/iap/snailmail/1/print'
+
+
+class SnailmailLetter(models.Model):
+ _name = 'snailmail.letter'
+
+ user_id = fields.Many2one('res.users', 'User sending the letter')
+ model = fields.Char('Model', required=True)
+ res_id = fields.Integer('Document ID', required=True)
+ partner_id = fields.Many2one('res.partner', string='Recipient', required=True)
+ company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True,
+ default=lambda self: self.env.user.company_id.id)
+ report_template = fields.Many2one('ir.actions.report', 'Optional report to print and attach')
+
+ attachment_id = fields.Many2one('ir.attachment', string='Attachment', ondelete='cascade')
+ color = fields.Boolean(string='Color', default=lambda self: self.env.user.company_id.snailmail_color)
+ duplex = fields.Boolean(string='Both side', default=lambda self: self.env.user.company_id.snailmail_duplex)
+ state = fields.Selection([
+ ('draft', 'Draft'),
+ ('pending', 'In Queue'),
+ ('sent', 'Sent'),
+ ('error', 'Error'),
+ ('canceled', 'Canceled')
+ ], 'Status', readonly=True, copy=False, default='draft',
+ help="When a letter is created, the status is 'Draft'.\n"
+ "If the letter is correctly sent, the status goes in 'Sent',\n"
+ "If not, it will got in state 'Error' and the error message will be displayed in the field 'Error Message'.")
+ info_msg = fields.Char('Information')
+
+ @api.multi
+ def _fetch_attachment(self):
+ """
+ This method will check if we have any existent attachement matching the model
+ and res_ids and create them if not found.
+ """
+ self.ensure_one()
+ obj = self.env[self.model].browse(self.res_id)
+ if not self.attachment_id:
+ report = self.report_template
+ if not report:
+ report_name = self.env.context.get('report_name')
+ report = self.env['ir.actions.report']._get_report_from_name(report_name)
+ if not report:
+ return False
+ else:
+ self.write({'report_template': report.id})
+ # report = self.env.ref('account.account_invoices')
+ if report.print_report_name:
+ report_name = safe_eval(report.print_report_name, {'object': obj})
+ elif report.attachment:
+ report_name = safe_eval(report.attachment, {'object': obj})
+ else:
+ report_name = 'Document'
+ filename = "%s.%s" % (report_name, "pdf")
+ pdf_bin, _ = report.render_qweb_pdf(self.res_id)
+ attachment = self.env['ir.attachment'].create({
+ 'name': filename,
+ 'datas': base64.b64encode(pdf_bin),
+ 'datas_fname': filename,
+ 'res_model': 'snailmail.letter',
+ 'res_id': self.id,
+ 'type': 'binary', # override default_type from context, possibly meant for another model!
+ })
+ self.write({'attachment_id': attachment.id})
+
+ return self.attachment_id
+
+ 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
+
+ @api.multi
+ def _snailmail_create(self, route):
+ """
+ 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,at
+ 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 = []
+
+ batch = len(self) > 1
+ for letter in self:
+ document = {
+ # generic informations to send
+ 'letter_id': letter.id,
+ 'res_model': letter.model,
+ 'res_id': letter.res_id,
+ 'address': {
+ 'name': letter.partner_id.name,
+ 'street': letter.partner_id.street,
+ 'street2': letter.partner_id.street2,
+ 'zip': letter.partner_id.zip,
+ 'state': letter.partner_id.state_id.code if letter.partner_id.state_id else False,
+ 'city': letter.partner_id.city,
+ 'country_code': letter.partner_id.country_id.code
+ },
+ 'return_address': {
+ 'name': letter.company_id.partner_id.name,
+ 'street': letter.company_id.partner_id.street,
+ 'street2': letter.company_id.partner_id.street2,
+ 'zip': letter.company_id.partner_id.zip,
+ 'state': letter.company_id.partner_id.state_id.code if letter.company_id.partner_id.state_id else False,
+ 'city': letter.company_id.partner_id.city,
+ 'country_code': letter.company_id.partner_id.country_id.code,
+ }
+ }
+ # Specific to each case:
+ # If we are estimating the price: batch: 1 obj = 1 page else: we generate the attachment to have the exact price.
+ # If we are printing -> attach the pdf
+ if route == 'estimate' and batch:
+ document.update(pages=1)
+ else:
+ attachment = letter._fetch_attachment()
+ if attachment:
+ document.update({
+ 'pdf_bin': route == 'print' and attachment.datas,
+ 'pages': route == 'estimate' and self._count_pages_pdf(base64.b64decode(attachment.datas)),
+ })
+ else:
+ letter.write({
+ 'info_msg': 'The attachment could not be generated.',
+ 'state': 'error',
+ })
+ continue
+ documents.append(document)
+
+ return {
+ 'account_token': account_token,
+ 'documents': documents,
+ 'options': {
+ 'color': self and self[0].color,
+ 'duplex': self and self[0].duplex,
+ },
+ # this will not raise the InsufficientCreditError which is the behaviour we want for now
+ 'batch': True,
+ }
+
+ 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.
Please go to your
iap account.' % 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 _snailmail_print(self):
+ """
+ get response
+ {
+ 'request_code': RESPONSE_OK, # because we receive 200 if good or fail
+ 'total_cost': total_cost,
+ 'credit_error': credit_error,
+ 'request': {
+ 'documents': documents,
+ 'options': options
+ }
+ }
+ }
+ """
+ self.write({'state': 'pending'})
+ endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
+ params = self._snailmail_create('print')
+ response = jsonrpc(endpoint + PRINT_ENDPOINT, params=params)
+ for doc in response['request']['documents']:
+ letter = self.browse(doc['letter_id'])
+ record = self.env[doc['res_model']].browse(doc['res_id'])
+ if doc.get('sent') and response['request_code'] == 200:
+ if hasattr(record, '_message_log'):
+ message = _('The document was correctly sent by post.
The tracking id is %s' % doc['send_id'])
+ record._message_log(body=message)
+ letter.write({'info_msg': message, 'state': 'sent'})
+ else:
+ note = _('An error occured when sending the document by post.
Error: %s' % \
+ self._get_error_message(doc['error'] if response['request_code'] == 200 else response['reason']))
+ activity_data = {
+ 'res_id': letter.res_id,
+ 'res_model_id': self.env['ir.model']._get(letter.model).id,
+ 'activity_type_id': self.env.ref('snailmail.mail_activity_data_snailmail').id,
+ 'summary': _('Post letter: an error occured.'),
+ 'note': note,
+ 'user_id': letter.user_id.id,
+ }
+ self.env['mail.activity'].create(activity_data)
+ letter.write({'info_msg': note})
+
+ self.env.cr.commit()
+
+ @api.multi
+ def snailmail_print(self):
+ self._snailmail_print()
+
+ @api.multi
+ def cancel(self):
+ self.write({'state': 'canceled'})
+
+ @api.multi
+ 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)
+ }
+ """
+ endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
+ params = self._snailmail_create('estimate')
+ req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params)
+ # The cost is sent in hunderth of eurocents, we change it to euros and then convert it to the company currency
+ cost = int(req['total_cost'])/10000.0
+ currency_eur = self.env.ref('base.EUR')
+
+ return currency_eur._convert(cost, self[0].company_id.currency_id, self[0].company_id, fields.Datetime.now())
+
+ @api.model
+ def _snailmail_cron(self):
+ letters_send = self.search([('state', 'in', ['pending', 'error'])])
+ if letters_send:
+ letters_send._snailmail_print()
+ limit_date = datetime.datetime.utcnow() - datetime.timedelta(days=1)
+ limit_date_str = datetime.datetime.strftime(limit_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ letters_canceled = self.search([
+ '|',
+ ('state', '=', 'canceled'),
+ '&',
+ ('state' ,'=', 'draft'),
+ ('write_date', '<', limit_date_str),
+ ])
+ letters_canceled.unlink()
diff --git a/addons/snailmail/security/ir.model.access.csv b/addons/snailmail/security/ir.model.access.csv
new file mode 100644
index 00000000000..3c111854063
--- /dev/null
+++ b/addons/snailmail/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_snailmail_letter_all,snailmail.letter.all,model_snailmail_letter,,1,0,0,0
+access_snailmail_letter_portal,snailmail.letter.portal,model_snailmail_letter,base.group_portal,1,0,1,0
+access_snailmail_letter_user,snailmail.letter.user,model_snailmail_letter,base.group_user,1,1,1,0
+access_snailmail_letter_system,snailmail.letter.system,model_snailmail_letter,base.group_system,1,1,1,1
\ No newline at end of file
diff --git a/addons/snailmail/views/res_config_settings_views.xml b/addons/snailmail/views/res_config_settings_views.xml
index cd5577c8cde..1e8b864d928 100644
--- a/addons/snailmail/views/res_config_settings_views.xml
+++ b/addons/snailmail/views/res_config_settings_views.xml
@@ -1,41 +1,41 @@
-
-
+
+
res.config.settings.view.form.inherit.snailmail
- res.config.settings
-
-
-
-
- Postal Printings
-
+
res.config.settings
+
+
+
+
+ Postal Printings
+
-
-
-
-
-
-
-
- Choose the layout to print documents
-
-
-
+
+
+
+
+
+
+
+ Choose the layout to print documents
+
+
+
-
-
-
-
-
-
-
- Choose the ink used to print documents
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ Choose the ink used to print documents
+
+
+