diff options
32 files changed, 1627 insertions, 164 deletions
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 77469f2fe94..076dffd61b7 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -487,14 +487,7 @@ class Message(models.Model): 'moderation_status': 'pending_moderation' } """ - message_values = self.read([ - 'id', 'body', 'date', 'author_id', 'email_from', # base message fields - 'message_type', 'subtype_id', 'subject', # message specific - 'model', 'res_id', 'record_name', # document related - 'channel_ids', 'partner_ids', # recipients - 'starred_partner_ids', # list of partner ids for whom the message is starred - 'moderation_status', - ]) + message_values = self.read(self._get_message_format_fields()) message_tree = dict((m.id, m) for m in self.sudo()) self._message_read_dict_postprocess(message_values, message_tree) @@ -527,6 +520,17 @@ class Message(models.Model): return message_values @api.multi + def _get_message_format_fields(self): + return [ + 'id', 'body', 'date', 'author_id', 'email_from', # base message fields + 'message_type', 'subtype_id', 'subject', # message specific + 'model', 'res_id', 'record_name', # document related + 'channel_ids', 'partner_ids', # recipients + 'starred_partner_ids', # list of partner ids for whom the message is starred + 'moderation_status', + ] + + @api.multi def _format_mail_failures(self): """ A shorter message to notify a failure update diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index 4342b1e244a..dc98bb7195b 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -46,22 +46,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin Mixins.EventDispatcherMixin.init.call(this, arguments); this.setParent(parent); - this._customerEmailData = data.customer_email_data || []; - this._customerEmailStatus = data.customer_email_status; - this._documentModel = data.model; - this._documentName = data.record_name; - this._documentID = data.res_id; - this._emailFrom = data.email_from; - this._info = data.info; - this._moduleIcon = data.module_icon; - this._needactionPartnerIDs = data.needaction_partner_ids || []; - this._starredPartnerIDs = data.starred_partner_ids || []; - this._subject = data.subject; - this._subtypeDescription = data.subtype_description; - this._threadIDs = data.channel_ids || []; - this._trackingValueIDs = data.tracking_value_ids; - - this._moderationStatus = data.moderation_status || 'accepted'; + this._setInitialData(data); this._processBody(emojis); this._processMailboxes(); @@ -730,6 +715,44 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin } }, /** + * @private + * @param {Object} data + * @param {string} [data.body = ""] + * @param {(string|integer)[]} [data.channel_ids] + * @param {Object[]} [data.customer_email_data] + * @param {string} [data.customer_email_status] + * @param {string} [data.email_from] + * @param {string} [data.info] + * @param {string} [data.model] + * @param {string} [data.moderation_status='accepted'] + * @param {string} [data.module_icon] + * @param {Array} [data.needaction_partner_ids = []] + * @param {string} [data.record_name] + * @param {integer} [data.res_id] + * @param {Array} [data.starred_partner_ids = []] + * @param {string} [data.subject] + * @param {string} [data.subtype_description] + * @param {Object[]} [data.tracking_value_ids] + */ + _setInitialData: function (data){ + this._customerEmailData = data.customer_email_data || []; + this._customerEmailStatus = data.customer_email_status; + this._documentModel = data.model; + this._documentName = data.record_name; + this._documentID = data.res_id; + this._emailFrom = data.email_from; + this._info = data.info; + this._moduleIcon = data.module_icon; + this._needactionPartnerIDs = data.needaction_partner_ids || []; + this._starredPartnerIDs = data.starred_partner_ids || []; + this._subject = data.subject; + this._subtypeDescription = data.subtype_description; + this._threadIDs = data.channel_ids || []; + this._trackingValueIDs = data.tracking_value_ids; + + this._moderationStatus = data.moderation_status || 'accepted'; + }, + /** * Set whether the message is moderated by current user or not. * If it is moderated by the current user, the message is moved to the * "Moderation" mailbox. Note that this function only applies it locally, diff --git a/addons/snailmail/__init__.py b/addons/snailmail/__init__.py index cfa5456e4ac..362dae520e1 100644 --- a/addons/snailmail/__init__.py +++ b/addons/snailmail/__init__.py @@ -2,3 +2,4 @@ from . import models from . import country_utils +from . import wizard diff --git a/addons/snailmail/__manifest__.py b/addons/snailmail/__manifest__.py index 4555917d3ee..4043d079cba 100644 --- a/addons/snailmail/__manifest__.py +++ b/addons/snailmail/__manifest__.py @@ -12,7 +12,14 @@ Allows users to send documents by post 'data/snailmail_data.xml', 'views/report_assets.xml', 'views/snailmail_views.xml', + 'views/assets.xml', + 'wizard/snailmail_letter_cancel_views.xml', + 'wizard/snailmail_letter_format_error_views.xml', + 'wizard/snailmail_letter_missing_required_fields_views.xml', 'security/ir.model.access.csv', ], + 'qweb': [ + 'static/src/xml/thread.xml', + ], 'auto_install': True, } diff --git a/addons/snailmail/models/__init__.py b/addons/snailmail/models/__init__.py index 3ef34136bd1..a05bee36be9 100644 --- a/addons/snailmail/models/__init__.py +++ b/addons/snailmail/models/__init__.py @@ -6,3 +6,4 @@ from . import res_config_settings from . import snailmail_letter from . import ir_actions_report from . import ir_qweb_fields +from . import mail_message diff --git a/addons/snailmail/models/mail_message.py b/addons/snailmail/models/mail_message.py new file mode 100644 index 00000000000..f649455ca70 --- /dev/null +++ b/addons/snailmail/models/mail_message.py @@ -0,0 +1,48 @@ + +from odoo import api, fields, models + +class Message(models.Model): + _inherit = 'mail.message' + + snailmail_error = fields.Boolean("Snailmail message in error", compute="_compute_snailmail_error", search="_search_snailmail_error") + snailmail_status = fields.Char("Snailmail Status", compute="_compute_snailmail_error") + letter_ids = fields.One2many(comodel_name='snailmail.letter', inverse_name='message_id') + message_type = fields.Selection(selection_add=[('snailmail', 'Snailmail')]) + + @api.multi + def _get_message_format_fields(self): + res = super(Message, self)._get_message_format_fields() + res.append('snailmail_error') + res.append('snailmail_status') + return res + + @api.multi + @api.depends('letter_ids', 'letter_ids.state') + def _compute_snailmail_error(self): + for message in self: + if message.message_type == 'snailmail' and message.letter_ids: + message.snailmail_error = message.letter_ids[0].state == 'error' + message.snailmail_status = message.letter_ids[0].error_code if message.letter_ids[0].state == 'error' else message.letter_ids[0].state + else: + message.snailmail_error = False + message.snailmail_status = '' + + @api.multi + def _search_snailmail_error(self, operator, operand): + if operator == '=' and operand: + return ['&', ('letter_ids.state', '=', 'error'), ('letter_ids.user_id', '=', self.env.user.id)] + return ['!', '&', ('letter_ids.state', '=', 'error'), ('letter_ids.user_id', '=', self.env.user.id)] + + @api.multi + def cancel_letter(self): + self.mapped('letter_ids').cancel() + + @api.multi + def send_letter(self): + self.mapped('letter_ids')._snailmail_print() + + @api.multi + def message_fetch_failed(self): + res = super(Message, self).message_fetch_failed() + failed_letters = self.letter_ids.fetch_failed_letters() + return res + failed_letters diff --git a/addons/snailmail/models/res_company.py b/addons/snailmail/models/res_company.py index 25e694968da..2aa46f3ca43 100644 --- a/addons/snailmail/models/res_company.py +++ b/addons/snailmail/models/res_company.py @@ -7,4 +7,5 @@ class Company(models.Model): _inherit = "res.company" snailmail_color = fields.Boolean(string='Color', default=True) + snailmail_cover = fields.Boolean(string='Add a Cover Page', default=False) snailmail_duplex = fields.Boolean(string='Both sides', default=False) diff --git a/addons/snailmail/models/res_config_settings.py b/addons/snailmail/models/res_config_settings.py index bf79ec83c3d..84365f21dc8 100644 --- a/addons/snailmail/models/res_config_settings.py +++ b/addons/snailmail/models/res_config_settings.py @@ -8,4 +8,5 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' snailmail_color = fields.Boolean(string='Print In Color', related='company_id.snailmail_color', readonly=False) + snailmail_cover = fields.Boolean(string='Add a Cover Page', related='company_id.snailmail_cover', readonly=False) snailmail_duplex = fields.Boolean(string='Print Both sides', related='company_id.snailmail_duplex', readonly=False) diff --git a/addons/snailmail/models/res_partner.py b/addons/snailmail/models/res_partner.py index 449559d2e9d..c4b3b576bf0 100644 --- a/addons/snailmail/models/res_partner.py +++ b/addons/snailmail/models/res_partner.py @@ -10,6 +10,20 @@ class ResPartner(models.Model): _inherit = "res.partner" @api.multi + def write(self, vals): + letter_address_vals = {} + address_fields = ['street', 'street2', 'city', 'zip', 'state_id', 'country_id'] + for field in address_fields: + if field in vals: + letter_address_vals[field] = vals[field] + + if len(letter_address_vals): + letter_ids = self.env['snailmail.letter'].search([('state', 'not in', ['sent', 'canceled']), ('partner_id', '=', self.id)]) + letter_ids.write(letter_address_vals) + + return super(ResPartner, self).write(vals) + + @api.multi def _get_country_name(self): # when sending a letter, thus rendering the report with the snailmail_layout, # we need to override the country name to its english version following the @@ -23,7 +37,7 @@ class ResPartner(models.Model): @api.model def _get_address_format(self): # When sending a letter, the fields 'street' and 'street2' should be on a single line to fit in the address area - if self.env.context.get('snailmail_layout'): + if self.env.context.get('snailmail_layout') and self.street2: return "%(street)s, %(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s" return super(ResPartner, self)._get_address_format()
\ No newline at end of file diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py index c2e3a241b1f..bddadf07290 100644 --- a/addons/snailmail/models/snailmail_letter.py +++ b/addons/snailmail/models/snailmail_letter.py @@ -13,6 +13,15 @@ DEFAULT_ENDPOINT = 'https://iap-snailmail.odoo.com' ESTIMATE_ENDPOINT = '/iap/snailmail/1/estimate' PRINT_ENDPOINT = '/iap/snailmail/1/print' +ERROR_CODES = [ + 'MISSING_REQUIRED_FIELDS', + 'CREDIT_ERROR', + 'TRIAL_ERROR', + 'NO_PRICE_AVAILABLE', + 'FORMAT_ERROR', + 'UNKNOWN_ERROR', +] + class SnailmailLetter(models.Model): _name = 'snailmail.letter' @@ -29,24 +38,33 @@ class SnailmailLetter(models.Model): attachment_id = fields.Many2one('ir.attachment', string='Attachment', ondelete='cascade') attachment_datas = fields.Binary('Document', related='attachment_id.datas') attachment_fname = fields.Char('Attachment Filename', related='attachment_id.datas_fname') - activity_id = fields.Many2one('mail.activity', string='Activity') color = fields.Boolean(string='Color', default=lambda self: self.env.user.company_id.snailmail_color) + cover = fields.Boolean(string='Cover Page', default=lambda self: self.env.user.company_id.snailmail_cover) 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" + ], 'Status', readonly=True, copy=False, default='pending', required=True, + help="When a letter is created, the status is 'Pending'.\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'.") + error_code = fields.Selection([(err_code, err_code) for err_code in ERROR_CODES], string="Error") info_msg = fields.Char('Information') display_name = fields.Char('Display Name', compute="_compute_display_name") reference = fields.Char(string='Related Record', compute='_compute_reference', readonly=True, store=False) + message_id = fields.Many2one('mail.message', string="Snailmail Status Message") + + street = fields.Char('Street') + street2 = fields.Char('Street2') + zip = fields.Char('Zip') + city = fields.Char('City') + state_id = fields.Many2one("res.country.state", string='State') + country_id = fields.Many2one('res.country', string='Country') + @api.depends('reference', 'partner_id') def _compute_display_name(self): for letter in self: @@ -59,7 +77,25 @@ class SnailmailLetter(models.Model): def _compute_reference(self): for res in self: res.reference = "%s,%s" % (res.model, res.res_id) - + + @api.model + def create(self, vals): + msg_id = self.env[vals['model']].browse(vals['res_id']).message_post( + body=_("Letter sent by post with Snailmail"), + message_type='snailmail' + ) + partner_id = self.env['res.partner'].browse(vals['partner_id']) + vals.update({ + 'message_id': msg_id.id, + 'street': partner_id.street, + 'street2': partner_id.street2, + 'zip': partner_id.zip, + 'city': partner_id.city, + 'state_id': partner_id.state_id.id, + 'country_id': partner_id.country_id.id, + }) + return super(SnailmailLetter, self).create(vals) + @api.multi def _fetch_attachment(self): """ @@ -85,7 +121,7 @@ class SnailmailLetter(models.Model): else: report_name = 'Document' filename = "%s.%s" % (report_name, "pdf") - pdf_bin, _ = report.with_context(snailmail_layout=True).render_qweb_pdf(self.res_id) + pdf_bin, _ = report.with_context(snailmail_layout=not self.cover).render_qweb_pdf(self.res_id) attachment = self.env['ir.attachment'].create({ 'name': filename, 'datas': base64.b64encode(pdf_bin), @@ -157,6 +193,7 @@ class SnailmailLetter(models.Model): 'letter_id': letter.id, 'res_model': letter.model, 'res_id': letter.res_id, + 'contact_address': letter.partner_id.with_context(snailmail_layout=True, show_address=True).name_get()[0][1], 'address': { 'name': letter.partner_id.name, 'street': letter.partner_id.street, @@ -196,6 +233,7 @@ class SnailmailLetter(models.Model): letter.write({ 'info_msg': 'The attachment could not be generated.', 'state': 'error', + 'error_code': 'ATTACHMENT_ERROR' }) continue if letter.company_id.external_report_layout_id == self.env.ref('l10n_de.external_layout_din5008', False): @@ -210,6 +248,7 @@ class SnailmailLetter(models.Model): 'documents': documents, 'options': { 'color': self and self[0].color, + 'cover': self and self[0].cover, 'duplex': self and self[0].duplex, 'currency_name': 'EUR', }, @@ -235,7 +274,26 @@ class SnailmailLetter(models.Model): return error @api.multi - def _snailmail_print(self): + def _snailmail_print(self, immediate=True): + valid_address_letters = self.filtered(lambda l: l._is_valid_address(l)) + invalid_address_letters = self - valid_address_letters + invalid_address_letters._snailmail_print_invalid_address() + if valid_address_letters and immediate: + valid_address_letters._snailmail_print_valid_address() + self.env.cr.commit() + + @api.multi + def _snailmail_print_invalid_address(self): + for letter in self: + letter.write({ + 'state': 'error', + 'error_code': 'MISSING_REQUIRED_FIELDS', + 'info_msg': _('The address of the recipient is not complete') + }) + self.send_snailmail_update() + + @api.multi + def _snailmail_print_valid_address(self): """ get response { @@ -249,52 +307,36 @@ class SnailmailLetter(models.Model): } } """ - 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.<br>The tracking id is %s' % doc['send_id']) - record._message_log(body=message) - letter.write({'info_msg': message, 'state': 'sent'}) + note = _('The document was correctly sent by post.<br>The tracking id is %s' % doc['send_id']) + letter_data = {'info_msg': note, 'state': 'sent', 'error_code': False} else: - # look for existing activities related to snailmail to update or create a new one. - # TODO: in following versions, Add a link to a specifc activity on the letter - note = _('An error occured when sending the document by post.<br>Error: %s <br>Go to \'Configuration > Management > Snailmail Letters\' to see all letters awaiting dispatch to Snailmail.' % \ - self._get_error_message(doc['error'] if response['request_code'] == 200 else response['reason'])) - - domain = [ - ('summary', 'ilike', '[SNAILMAIL]'), - ('res_id', '=', letter.res_id), - ('res_model_id', '=', self.env['ir.model']._get(letter.model).id), - ('activity_type_id', '=', self.env.ref('mail.mail_activity_data_warning').id), - ] - activity = letter.activity_id - - activity_data = { - 'activity_type_id': self.env.ref('mail.mail_activity_data_warning').id, - 'summary': '[SNAILMAIL] ' + _('Post letter: an error occured.'), - 'note': note, - 'date_deadline': fields.Date.today() + error = doc['error'] if response['request_code'] == 200 else response['reason'] + + note = _('An error occured when sending the document by post.<br>Error: %s' % self._get_error_message(error)) + letter_data = { + 'info_msg': note, + 'state': 'error', + 'error_code': error if error in ERROR_CODES else 'UNKNOWN_ERROR' } - if activity: - activity.update(activity_data) - else: - activity_data.update({ - 'user_id': letter.user_id.id, - 'res_id': letter.res_id, - 'res_model_id': self.env['ir.model']._get(letter.model).id, - }) - activity = self.env['mail.activity'].create(activity_data) - letter.write({'activity_id': activity.id}) - letter.write({'info_msg': note, 'state': 'error'}) + letter = self.browse(doc['letter_id']) + letter.write(letter_data) + self.send_snailmail_update() - self.env.cr.commit() + @api.multi + def send_snailmail_update(self): + notifications = [] + for letter in self: + notifications.append([ + (self._cr.dbname, 'res.partner', letter.user_id.partner_id.id), + {'type': 'snailmail_update', 'elements': letter._format_snailmail_failures()} + ]) + self.env['bus.bus'].sendmany(notifications) @api.multi def snailmail_print(self): @@ -302,12 +344,8 @@ class SnailmailLetter(models.Model): @api.multi def cancel(self): - self.write({'state': 'canceled'}) - - @api.multi - def unlink(self): - self.mapped('activity_id').unlink() - return super(SnailmailLetter, self).unlink() + self.write({'state': 'canceled', 'error_code': False}) + self.send_snailmail_update() @api.multi def _snailmail_estimate(self): @@ -344,17 +382,75 @@ class SnailmailLetter(models.Model): return req['total_cost'] @api.model + def _snailmail_estimate_from_documents(self, res_model, res_ids, partner_field=None): + endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT) + docs = self.env[res_model].browse(res_ids) + doc_list = [] + for doc in docs: + if partner_field: + country_code = doc[partner_field].country_id.code + else: + country_code = doc.country_id.code + val = { + 'pages': 1, + 'address': { + 'country_code': country_code, + }, + } + doc_list.append(val) + params = { + 'account_token': "", + 'documents': doc_list, + 'options': { + 'color': self.env.user.company_id.snailmail_color, + 'duplex': self.env.user.company_id.snailmail_duplex, + 'currency_name': 'EUR', + }, + } + req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params) + return req['total_cost'] + + @api.model def _snailmail_cron(self): - letters_send = self.search([('state', '=', 'pending')]) - 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([ + letters_send = self.search([ '|', - ('state', '=', 'canceled'), - '&', - ('state' ,'=', 'draft'), - ('write_date', '<', limit_date_str), + ('state', '=', 'pending'), + '&', + ('state', '=', 'error'), + ('error_code', 'in', ['TRIAL_ERROR', 'CREDIT_ERROR', 'ATTACHMENT_ERROR', 'MISSING_REQUIRED_FIELDS']) ]) - letters_canceled.unlink() + letters_send._snailmail_print() + + @api.model + def fetch_failed_letters(self): + failed_letters = self.search([('state', '=', 'error'), ('user_id.id', '=', self.env.user.id), ('res_id', '!=', 0), ('model', '!=', False)]) + return failed_letters._format_snailmail_failures() + + @api.model + def _is_valid_address(self, record): + record.ensure_one() + required_keys = ['street', 'city', 'zip', 'country_id'] + return all(record[key] for key in required_keys) + + @api.multi + def _format_snailmail_failures(self): + """ + A shorter message to notify a failure update + """ + failures_infos = [] + for letter in self: + info = { + 'message_id': letter.message_id.id, + 'record_name': letter.message_id.record_name, + 'model_name': self.env['ir.model']._get(letter.model).display_name, + 'uuid': letter.message_id.message_id, + 'res_id': letter.res_id, + 'model': letter.model, + 'last_message_date': letter.message_id.date, + 'module_icon': '/snailmail/static/img/snailmail_failure.png', + 'snailmail_status': letter.error_code if letter.state == 'error' else '', + 'snailmail_error': letter.state == 'error', + 'failure_type': 'snailmail', + } + failures_infos.append(info) + return failures_infos diff --git a/addons/snailmail/static/img/snailmail_failure.png b/addons/snailmail/static/img/snailmail_failure.png Binary files differnew file mode 100644 index 00000000000..df591cd75c3 --- /dev/null +++ b/addons/snailmail/static/img/snailmail_failure.png diff --git a/addons/snailmail/static/src/js/mail_failure.js b/addons/snailmail/static/src/js/mail_failure.js new file mode 100644 index 00000000000..cb363051392 --- /dev/null +++ b/addons/snailmail/static/src/js/mail_failure.js @@ -0,0 +1,29 @@ +odoo.define('snailmail.model.MailFailure', function (require) { +"use strict"; + +var MailFailure = require('mail.model.MailFailure'); +var core = require('web.core'); +var _t = core._t; + +MailFailure.include({ + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + getPreview: function () { + var preview = this._super.apply(this, arguments); + if (this._failureType === 'snailmail') { + _.extend(preview, { + body: _t("An error occured when sending a letter with Snailmail"), + id: 'snailmail_failure', + }); + } + return preview; + }, +}); + +}); diff --git a/addons/snailmail/static/src/js/message.js b/addons/snailmail/static/src/js/message.js new file mode 100644 index 00000000000..b7b74e9242f --- /dev/null +++ b/addons/snailmail/static/src/js/message.js @@ -0,0 +1,85 @@ +odoo.define('snailmail.model.Message', function (require) { +"use strict"; + +var Message = require('mail.model.Message'); + +Message.include({ + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Cancels the 'snailmail.letter' which has a message_id corresponding to + * the ID of this message, then update the status of the message + * + * @returns {Deferred} + */ + cancelLetter: function () { + var self = this; + var mailBus = this.call('mail_service', 'getMailBus'); + return this._rpc({ + model: 'mail.message', + method: 'cancel_letter', + args: [[this.getID()], []], + }).then(function () { + self.setSnailmailStatus('canceled'); + self.setSnailmailError(false); + mailBus.trigger('update_message', self); + }); + }, + /** + * @return {string} + */ + getSnailmailStatus: function () { + return this._snailmailStatus.toLowerCase(); + }, + /** + * Is the sent snailmail letter in error + * + * @return {Boolean} + */ + getSnailmailError: function () { + return this._snailmailError; + }, + /** + * Retries to send the 'snailmail.letter' corresponding to this message + * + * @returns {Deferred} + */ + resendLetter: function () { + return this._rpc({ + model: 'mail.message', + method: 'send_letter', + args: [[this.getID()], []], + }); + }, + /** + * @param {string} snailmailStatus + */ + setSnailmailStatus: function (snailmailStatus) { + this._snailmailStatus = snailmailStatus; + }, + /** + * @param {Boolean} snailmailError + */ + setSnailmailError: function (snailmailError) { + this._snailmailError = snailmailError; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @override + */ + _setInitialData: function (data){ + this._super.apply(this, arguments); + this._snailmailStatus = data.snailmail_status; + this._snailmailError = data.snailmail_error; + }, +}); + +}); diff --git a/addons/snailmail/static/src/js/snailmail_notification_manager.js b/addons/snailmail/static/src/js/snailmail_notification_manager.js new file mode 100644 index 00000000000..4d33ba89127 --- /dev/null +++ b/addons/snailmail/static/src/js/snailmail_notification_manager.js @@ -0,0 +1,67 @@ +odoo.define('snailmail.NotificationManager', function (require) { +"use strict"; + +var MailManager = require('mail.Manager'); +var MailFailure = require('mail.model.MailFailure'); + +MailManager.include({ + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @param {Object} data structure depending on the type + * @param {integer} data.id + */ + _handlePartnerNotification: function (data) { + if (data.type === 'snailmail_update') { + this._handleSnailmailUpdateNotification(data); + } else { + this._super.apply(this, arguments); + } + }, + /** + * Updates message in thread and systray when there's an update in a snailmail letter + * + * @private + * @param {Object} datas + * @param {Object[]} datas.elements list of snailmail failure data + * @param {string} datas.elements[].message_id ID of related message that + * has a snailmail failure. + * @param {boolean} datas.elements[].snailmail_error letter is in error + * @param {string} datas.elements[].snailmail_status status of the letter + */ + _handleSnailmailUpdateNotification: function (datas) { + var self = this; + _.each(datas.elements, function (data) { + var isNewFailure = data.snailmail_error; + var matchedFailure = _.find(self._mailFailures, function (failure) { + return failure.getMessageID() === data.message_id; + }); + + if (matchedFailure) { + var index = _.findIndex(self._mailFailures, matchedFailure); + if (isNewFailure) { + self._mailFailures[index] = new MailFailure(self, data); + } else { + self._mailFailures.splice(index, 1); + } + } else if (isNewFailure) { + self._mailFailures.push(new MailFailure(self, data)); + } + var message = _.find(self._messages, function (msg) { + return msg.getID() === data.message_id; + }); + if (message) { + message.setSnailmailStatus(data.snailmail_status); + message.setSnailmailError(data.snailmail_error); + self._mailBus.trigger('update_message', message); + } + }); + this._mailBus.trigger('update_needaction', this.needactionCounter); + }, +}); + +}); diff --git a/addons/snailmail/static/src/js/systray_messaging_menu.js b/addons/snailmail/static/src/js/systray_messaging_menu.js new file mode 100644 index 00000000000..2265df23ca0 --- /dev/null +++ b/addons/snailmail/static/src/js/systray_messaging_menu.js @@ -0,0 +1,78 @@ +odoo.define('snailmail.systray.MessagingMenu', function (require) { +"use strict"; + +var MessagingMenu = require('mail.systray.MessagingMenu'); + +MessagingMenu.include({ + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Called when clicking on a preview related to a snailmail failure + * + * @private + * @param {$.Element} $target DOM of preview element clicked + */ + _clickSnailmailFailurePreview: function ($target) { + var documentID = $target.data('document-id'); + var documentModel = $target.data('document-model'); + if (documentModel && documentID) { + this._openDocument(documentModel, documentID); + } else if (documentModel !== 'mail.channel') { + // preview of mail failures grouped to different document of same model + this.do_action({ + name: "Snailmail failures", + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: documentModel, + domain: [['message_ids.snailmail_error', '=', true]], + }); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @override + */ + _onClickPreview: function (ev) { + var $target = $(ev.currentTarget); + var previewID = $target.data('preview-id'); + + if (previewID === 'snailmail_failure') { + this._clickSnailmailFailurePreview($target); + } else { + this._super.apply(this, arguments); + } + }, + /** + * @private + * @override + */ + _onClickPreviewMarkAsRead: function (ev) { + ev.stopPropagation(); + var $preview = $(ev.currentTarget).closest('.o_mail_preview'); + var previewID = $preview.data('preview-id'); + if (previewID === 'snailmail_failure') { + var documentModel = $preview.data('document-model'); + var unreadCounter = $preview.data('unread-counter'); + this.do_action('snailmail.snailmail_letter_cancel_action', { + additional_context: { + default_model: documentModel, + unread_counter: unreadCounter + } + }); + } else { + this._super.apply(this, arguments); + } + }, +}); + +}); diff --git a/addons/snailmail/static/src/js/thread_widget.js b/addons/snailmail/static/src/js/thread_widget.js new file mode 100644 index 00000000000..4fd2521b4c7 --- /dev/null +++ b/addons/snailmail/static/src/js/thread_widget.js @@ -0,0 +1,227 @@ +odoo.define('snailmail.widget.Thread', function (require) { +"use strict"; + +var ThreadWidget = require('mail.widget.Thread'); +var Dialog = require('web.Dialog'); + +var core = require('web.core'); +var QWeb = core.qweb; +var _t = core._t; + +ThreadWidget.include({ + dependencies: ['bus_service'], + events: _.extend({}, ThreadWidget.prototype.events, { + 'click .o_thread_message_snailmail_missing_required_fields': '_onClickMissingRequiredFields', + 'click .o_thread_message_snailmail_credit_error': '_onClickCreditError', + 'click .o_thread_message_snailmail_trial_error': '_onClickTrialError', + 'click .o_thread_message_snailmail_no_price_available': '_onClickNoPriceAvailable', + 'click .o_thread_message_snailmail_format_error': '_onClickFormatError', + 'click .o_thread_message_snailmail_unknown_error': '_onClickUnknownError', + }), + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this._enabledOptions = _.defaults(this._enabledOptions, { + displaySnailmailIcons: true, + }); + this._disabledOptions = _.defaults(this._disabledOptions, { + displaySnailmailIcons: false, + }); + }, + /** + * @override + */ + render: function (thread, options) { + this._super.apply(this, arguments); + var messages = _.clone(thread.getMessages({ domain: options.domain || [] })); + this._messages = messages; + this._renderMessageSnailmailPopover(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {integer} messageID + * @param {string} content + */ + _openCreditErrorDialog: function (messageID, content) { + var message = _.find(this._messages, function (message) { + return message.getID() === messageID; + }); + new Dialog(this, { + size: 'medium', + title: _t("Failed letter"), + $content: $('<div>').html(content), + buttons: [ + {text: _t("Re-send letter"), classes: 'btn-primary', close: true, click: _.bind(message.resendLetter, message)}, + {text: _t("Cancel letter"), close: true, click: _.bind(message.cancelLetter, message)}, + {text: _t("Close"), close: true}, + ], + }).open(); + }, + /** + * @private + * @param {integer} messageID + * @param {string} content + */ + _openGenericErrorDialog: function (messageID, content) { + var message = _.find(this._messages, function (message) { + return message.getID() === messageID; + }); + new Dialog(this, { + size: 'medium', + title: _t("Failed letter"), + $content: $('<div>').html(content), + buttons: [ + {text: _t("Cancel letter"), classes: 'btn-primary', close: true, click: _.bind(message.cancelLetter, message)}, + {text: _t("Close"), close: true}, + ], + }).open(); + }, + /** + * Render the popover when mouse-hovering on the mail icon of a message + * in the thread. There is at most one such popover at any given time. + * + * @private + * @param {mail.model.AbstractMessage[]} messages list of messages in the + * rendered thread, for which popover on mouseover interaction is + * permitted. + */ + _renderMessageSnailmailPopover: function () { + var self = this; + if (this._messageSnailmailPopover) { + this._messageSnailmailPopover.popover('hide'); + } + if (!this.$('.o_thread_snailmail_tooltip').length) { + return; + } + this._messageSnailmailPopover = this.$('.o_thread_snailmail_tooltip').popover({ + html: true, + boundary: 'viewport', + placement: 'auto', + trigger: 'hover', + offset: '0, 1', + content: function () { + var messageID = $(this).data('message-id'); + var message = _.find(self._messages, function (message) { + return message.getID() === messageID; + }); + return QWeb.render('snailmail.widget.Thread.Message.SnailmailTooltip', { + status: message.getSnailmailStatus() + }); + }, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} event + */ + _onClickCreditError: function (ev) { + var self = this; + var messageID = $(ev.currentTarget).data('message-id'); + this._rpc({ + model: 'iap.account', + method: 'get_credits_url', + args: ['snailmail'], + }).then(function (link) { + var content = _.str.sprintf(_t( + '<p>The letter could not be sent due to insufficient credits on your IAP account.</p>' + + '<div class= "text-right">' + + '<a class="btn btn-link buy_credits" href=%s>' + + '<i class= "fa fa-arrow-right"/> Buy credits' + + '</a>' + + '</div>' + ), link); + self._openCreditErrorDialog(messageID, content); + }); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onClickFormatError: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.do_action('snailmail.snailmail_letter_format_error_action', { + additional_context: { + message_id: messageID, + } + }); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onClickMissingRequiredFields: function (ev) { + var self = this; + + var messageID = $(ev.currentTarget).data('message-id'); + var domain = [['message_id', '=', messageID]]; + this._rpc({ + model: 'snailmail.letter', + method: 'search', + args: [domain], + }).then(function (letterIds) { + self.do_action('snailmail.snailmail_letter_missing_required_fields_action', { + additional_context: { + letter_id: letterIds[0] + }, + on_close: function () { + self.trigger_up('reload'); + } + }); + }); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onClickNoPriceAvailable: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + var content = _t('<p>The country to which you want to send the letter is not supported by our service.</p>'); + this._openGenericErrorDialog(messageID, content); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onClickTrialError: function (ev) { + var self = this; + var messageID = $(ev.currentTarget).data('message-id'); + this._rpc({ + model: 'iap.account', + method: 'get_credits_url', + args: ['snailmail', '', 0, true], + }).then(function (link) { + var content = _.str.sprintf(_t( + '<p>You need credits on your IAP account to send a letter.</p>' + + '<div class= "text-right">' + + '<a class="btn btn-link buy_credits" href=%s>' + + '<i class= "fa fa-arrow-right"/> Buy credits' + + '</a>' + + '</div>' + ), link); + self._openCreditErrorDialog(messageID, content); + }); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onClickUnknownError: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + var content = _t('<p>An unknown error occured. Please contact our <a href="https://www.odoo.com/help" target="new">support</a> for further assistance.</p>'); + this._openGenericErrorDialog(messageID, content); + }, +}); + +}); diff --git a/addons/snailmail/static/src/scss/thread.scss b/addons/snailmail/static/src/scss/thread.scss new file mode 100644 index 00000000000..ecf42cef033 --- /dev/null +++ b/addons/snailmail/static/src/scss/thread.scss @@ -0,0 +1,17 @@ +.o_thread_message_snailmail { + padding: 4px; + + &:not(.o_thread_message_snailmail_error) { + color: grey; + } + + &.o_thread_message_snailmail_error { + color: red; + opacity: 1; + cursor: pointer; + } +} + +.o_thread_tooltip_snailmail_icon { + min-width: 1rem; +} diff --git a/addons/snailmail/static/src/xml/thread.xml b/addons/snailmail/static/src/xml/thread.xml new file mode 100644 index 00000000000..aeda07445be --- /dev/null +++ b/addons/snailmail/static/src/xml/thread.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<template id="template" xml:space="preserve"> + + <t t-extend="mail.widget.Thread.Message"> + <t t-jquery=".o_thread_tooltip_container" t-operation="after"> + <span t-if="message.getType() === 'snailmail' and options.displaySnailmailIcons" class="o_thread_snailmail_tooltip_container"> + <t t-set="thread_icon_class" t-value="'o_thread_snailmail_tooltip o_thread_message_snailmail o_thread_message_snailmail_' + message.getSnailmailStatus()" /> + <t t-if="message.getSnailmailError()"> + <t t-set="thread_icon_class" t-value="thread_icon_class + ' o_thread_message_snailmail_error'"/> + </t> + <i t-att-class="thread_icon_class + ' fa fa-paper-plane'" t-att-data-message-id="message.getID()"/> + </span> + </t> + </t> + + <t t-name="snailmail.widget.Thread.Message.SnailmailTooltip"> + <span class="d-inline-block text-center o_thread_tooltip_snailmail"> + <t t-if="status === 'sent'"> + <i class='fa fa-check o_thread_tooltip_snailmail_icon' title="Sent" role="img" aria-label="Sent"/> + Sent + </t> + <t t-elif="status === 'canceled'"> + <i class='fa fa-trash-o o_thread_tooltip_snailmail_icon' title="Canceled" role="img" aria-label="Canceled"/> + Canceled + </t> + <t t-elif="status === 'pending'"> + <i class='fa fa-clock-o o_thread_tooltip_snailmail_icon' title="Awaiting Dispatch" role="img" aria-label="Awaiting Dispatch"/> + Awaiting Dispatch + </t> + <t t-else=""> + <i class='fa fa-exclamation text-danger o_thread_tooltip_snailmail_icon' title="Error" role="img" aria-label="Error"/> + Error + </t> + </span> + </t> + +</template> diff --git a/addons/snailmail/static/tests/chatter_snailmail_tests.js b/addons/snailmail/static/tests/chatter_snailmail_tests.js new file mode 100644 index 00000000000..312a77727c3 --- /dev/null +++ b/addons/snailmail/static/tests/chatter_snailmail_tests.js @@ -0,0 +1,503 @@ +odoo.define('snailmail.chatter_snailmail_tests', function (require) { +"use strict"; + +var mailTestUtils = require('mail.testUtils'); +var ThreadWidget = require('mail.widget.Thread'); + +var FormView = require('web.FormView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +var getArch = function () { + return '<form string="Invoice">' + + '<sheet>' + + '<field name="foo"/>' + + '</sheet>' + + '<div class="oe_chatter">' + + '<field name="message_ids" widget="mail_thread"/>' + + '</div>' + + '</form>'; +}; + +QUnit.module('snailmail', {}, function () { +QUnit.module('Chatter', { + before: function () { + this.services = mailTestUtils.getMailServices(); + this.data = { + 'account.invoice': { + fields: { + display_name: { string: "Displayed name", type: "char" }, + foo: {string: "Foo", type: "char", default: "My little Foo Value"}, + message_ids: { + string: "messages", + type: "one2many", + relation: 'mail.message', + relation_field: "res_id", + }, + message_attachment_count: { + string: 'Attachment count', + type: 'integer', + }, + }, + records: [{ + id: 1, + display_name: "Invoice 1", + foo: "HELLO", + message_ids: [11], + message_attachment_count: 0, + }] + }, + 'mail.message': { + fields: { + author_id: { + string: "Author", + relation: 'res.partner', + }, + body: { + string: "Contents", + type: 'html', + }, + date: { + string: "Date", + type: 'datetime', + }, + model: { + string: "Related Document Model", + type: 'char', + }, + res_id: { + string: "Related Document ID", + type: 'integer', + }, + snailmail_status: { + string: "Snailmail Status", + type: "char" + }, + snailmail_error: { + string: "Snailmail Error", + type: "boolean", + }, + message_type: { + string: "Type", + type: 'selection', + }, + }, + records: [{ + id: 11, + author_id: ["1", "John Doe"], + body: 'Message Body', + date: "2018-12-11 12:34:00", + model: 'account.invoice', + res_id: 1, + message_type: 'snailmail', + }], + } + }; + }, + afterEach: function () { + $('.popover').remove(); + $('.modal').remove(); + } +}); + +QUnit.test('Sent', function (assert) { + assert.expect(7); + + this.data['mail.message'].records[0].snailmail_status = 'sent'; + this.data['mail.message'].records[0].snailmail_error = false; + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_sent', + "Snailmail status of message should be 'sent'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_sent'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-check').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Sent")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_sent')); + assert.containsNone($('.modal'), "No modal should open on click"); + + form.destroy(); +}); + +QUnit.test('Canceled', function (assert) { + assert.expect(7); + + this.data['mail.message'].records[0].snailmail_status = 'canceled'; + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_canceled', + "Snailmail status of message should be 'canceled'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_canceled'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-trash-o').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Canceled")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_canceled')); + assert.containsNone($('.modal'), "No modal should open on click"); + + form.destroy(); +}); + +QUnit.test('Pending', function (assert) { + assert.expect(7); + + this.data['mail.message'].records[0].snailmail_status = 'pending'; + this.data['mail.message'].records[0].snailmail_error = false; + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_pending', + "Snailmail status of message should be 'pending'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_pending'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-clock-o').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Awaiting Dispatch")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_pending')); + assert.containsNone($('.modal'), "No modal should open on click"); + + form.destroy(); +}); + +QUnit.test('No Price Available', function (assert) { + assert.expect(11); + + this.data['mail.message'].records[0].snailmail_status = 'no_price_available'; + this.data['mail.message'].records[0].snailmail_error = true; + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + mockRPC: function (route, args) { + if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === 11) { + assert.step(args.method); + return $.when(); + } + return this._super.apply(this, arguments); + } + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_no_price_available', + "Snailmail status of message should be 'no_price_available'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_no_price_available'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_no_price_available')); + var $modal = $('.modal'); + assert.ok($modal.length, "A modal should open on click"); + + assert.containsOnce($modal, 'button:contains("Cancel letter")', + "Modal should have a 'Cancel letter' button"); + var $cancelButton = $('.modal').find('button:contains("Cancel letter")'); + testUtils.dom.click($cancelButton); + assert.notOk($('.modal').length, + "The modal should be closed after click on 'Cancel letter'"); + + assert.verifySteps(['cancel_letter'], + "Should have made a RPC call to 'cancel_letter'"); + + form.destroy(); +}); + +QUnit.test('Format Error', function (assert) { + assert.expect(8); + + this.data['mail.message'].records[0].snailmail_status = 'format_error'; + this.data['mail.message'].records[0].snailmail_error = true; + + testUtils.mock.patch(ThreadWidget, { + do_action: function (action, options) { + assert.step("do_action"); + }, + }); + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + mockRPC: function (route, args) { + if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === 11) { + assert.step(args.method); + return $.when(); + } + return this._super.apply(this, arguments); + } + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_format_error', + "Snailmail status of message should be 'format_error'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_format_error'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_format_error')); + assert.verifySteps(['do_action'], "'do_action' should have been called"); + + form.destroy(); + testUtils.mock.unpatch(ThreadWidget); +}); + +QUnit.test('Credit Error', function (assert) { + assert.expect(14); + + this.data['mail.message'].records[0].snailmail_status = 'credit_error'; + this.data['mail.message'].records[0].snailmail_error = true; + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + mockRPC: function (route, args) { + if (args.model === 'mail.message') { + if ((args.method === 'cancel_letter' && args.args[0][0] === 11) || args.method === 'send_letter') { + assert.step(args.method); + return $.when(); + } + } + if (args.method === 'get_credits_url' && args.model === 'iap.account') { + return $.when('credits_url'); + } + return this._super.apply(this, arguments); + } + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_credit_error', + "Snailmail status of message should be 'credit_error'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_credit_error'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error')); + var $modal = $('.modal'); + assert.ok($modal.length, "A modal should open on click"); + + assert.containsOnce($modal, 'button:contains("Re-send letter")', + "Modal should have a 'Re-send letter' button"); + var $resendButton = $('.modal').find('button:contains("Re-send letter")'); + testUtils.dom.click($resendButton); + assert.notOk($('.modal').length, + "The modal should be closed after click on 'Re-send letter'"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error')); + + assert.containsOnce($modal, 'button:contains("Cancel letter")', + "Modal should have a 'Cancel letter' button"); + var $cancelButton = $('.modal').find('button:contains("Cancel letter")'); + testUtils.dom.click($cancelButton); + assert.containsNone($('.modal'), + "The modal should be closed after click on 'Cancel letter'"); + + assert.verifySteps(['send_letter', 'cancel_letter'], + "Should have made RPC calls to 'send_letter' and 'cancel_letter'"); + + form.destroy(); +}); + +QUnit.test('Trial Error', function (assert) { + assert.expect(14); + + this.data['mail.message'].records[0].snailmail_status = 'trial_error'; + this.data['mail.message'].records[0].snailmail_error = true; + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + mockRPC: function (route, args) { + if (args.model === 'mail.message') { + if ((args.method === 'cancel_letter' && args.args[0][0] === 11) || args.method === 'send_letter') { + assert.step(args.method); + return $.when(); + } + } + if (args.method === 'get_credits_url' && args.model === 'iap.account') { + return $.when('credits_url'); + } + return this._super.apply(this, arguments); + } + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_trial_error', + "Snailmail status of message should be 'trial_error'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_trial_error'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error')); + var $modal = $('.modal'); + assert.ok($modal.length, "A modal should open on click"); + + assert.containsOnce($modal, 'button:contains("Re-send letter")', + "Modal should have a 'Re-send letter' button"); + var $resendButton = $('.modal').find('button:contains("Re-send letter")'); + testUtils.dom.click($resendButton); + assert.containsNone($('.modal'), + "The modal should be closed after click on 'Re-send letter'"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error')); + + $modal = $('.modal'); + assert.containsOnce($modal, 'button:contains("Cancel letter")', + "Modal should have a 'Cancel letter' button"); + var $cancelButton = $modal.find('button:contains("Cancel letter")'); + testUtils.dom.click($cancelButton); + assert.containsNone($('.modal'), + "The modal should be closed after click on 'Cancel letter'"); + + assert.verifySteps(['send_letter', 'cancel_letter'], + "Should have made RPC calls to 'send_letter' and 'cancel_letter'"); + + form.destroy(); +}); + +QUnit.test('Missing Required Fields', function (assert) { + assert.expect(8); + + this.data['mail.message'].records[0].snailmail_status = 'missing_required_fields'; + this.data['mail.message'].records[0].snailmail_error = true; + + testUtils.mock.patch(ThreadWidget, { + do_action: function (action, options) { + assert.step("do_action"); + }, + }); + + var form = createView({ + View: FormView, + model: 'account.invoice', + res_id: 1, + data: this.data, + services: this.services, + arch: getArch(), + mockRPC: function (route, args) { + if (args.model === 'snailmail.letter' && args.method === 'search') { + return $.when([2]); + } + return this._super.apply(this, arguments); + } + }); + + assert.containsOnce(form, '.o_thread_snailmail_tooltip_container', + "Snailmail icon should appear on message"); + assert.containsOnce(form, '.o_thread_message_snailmail_missing_required_fields', + "Snailmail status of message should be 'missing_required_fields'"); + assert.containsNone(form, '.o_thread_tooltip_snailmail', + "No tooltip should be present"); + + testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_missing_required_fields'), 'mouseenter'); + assert.ok($('.o_thread_tooltip_snailmail:visible').length, + "Tooltip should appear when hovering the Snailmail Icon"); + assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length, + "Tooltip should show correct icon"); + assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, + "Tooltip should show correct text"); + + testUtils.dom.click(form.$('.o_thread_message_snailmail_missing_required_fields')); + assert.verifySteps(['do_action'], "'do_action' should have been called"); + + form.destroy(); + testUtils.mock.unpatch(ThreadWidget); +}); + +}); + +}); diff --git a/addons/snailmail/views/assets.xml b/addons/snailmail/views/assets.xml new file mode 100644 index 00000000000..1dca06c355c --- /dev/null +++ b/addons/snailmail/views/assets.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_backend" name="iap assets" inherit_id="web.assets_backend"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/snailmail/static/src/js/mail_failure.js"></script> + <script type="text/javascript" src="/snailmail/static/src/js/message.js"></script> + <script type="text/javascript" src="/snailmail/static/src/js/snailmail_notification_manager.js"></script> + <script type="text/javascript" src="/snailmail/static/src/js/systray_messaging_menu.js"></script> + <script type="text/javascript" src="/snailmail/static/src/js/thread_widget.js"></script> + <link rel="stylesheet" type="text/scss" href="/snailmail/static/src/scss/thread.scss"/> + </xpath> + </template> + + <template id="qunit_suite" name="snailmail_tests" inherit_id="web.qunit_suite"> + <xpath expr="//t[@t-set='head']" position="inside"> + <script type="text/javascript" src="/snailmail/static/tests/chatter_snailmail_tests.js"></script> + </xpath> + </template> +</odoo> diff --git a/addons/snailmail/wizard/__init__.py b/addons/snailmail/wizard/__init__.py new file mode 100644 index 00000000000..86a507b287b --- /dev/null +++ b/addons/snailmail/wizard/__init__.py @@ -0,0 +1,3 @@ +from . import snailmail_letter_cancel +from . import snailmail_letter_format_error +from . import snailmail_letter_missing_required_fields diff --git a/addons/snailmail/wizard/snailmail_letter_cancel.py b/addons/snailmail/wizard/snailmail_letter_cancel.py new file mode 100644 index 00000000000..3cb5aa1621c --- /dev/null +++ b/addons/snailmail/wizard/snailmail_letter_cancel.py @@ -0,0 +1,28 @@ + +from odoo import _, api, fields, models + +class SnailmailLetterCancel(models.TransientModel): + _name = 'snailmail.letter.cancel' + _description = 'Dismiss notification for resend by model' + + model = fields.Char(string='Model') + help_message = fields.Char(string='Help message', compute='_compute_help_message') + + @api.multi + @api.depends('model') + def _compute_help_message(self): + for wizard in self: + wizard.help_message = _("Are you sure you want to discard %s snailmail delivery failures. You won't be able to re-send these letters later!") % (wizard._context.get('unread_counter')) + + @api.multi + def cancel_resend_action(self): + author_id = self.env.user.id + for wizard in self: + letters = self.env['snailmail.letter'].search([ + ('state', 'not in', ['sent', 'canceled', 'pending']), + ('user_id', '=', author_id), + ('model', '=', wizard.model) + ]) + for letter in letters: + letter.cancel() + return {'type': 'ir.actions.act_window_close'} diff --git a/addons/snailmail/wizard/snailmail_letter_cancel_views.xml b/addons/snailmail/wizard/snailmail_letter_cancel_views.xml new file mode 100644 index 00000000000..d244985b147 --- /dev/null +++ b/addons/snailmail/wizard/snailmail_letter_cancel_views.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="snailmail_letter_cancel" model="ir.ui.view"> + <field name="name">snailmail.letter.cancel.form</field> + <field name="model">snailmail.letter.cancel</field> + <field name="groups_id" eval="[(4,ref('base.group_user'))]"/> + <field name="arch" type="xml"> + <form string="Cancel notification in failure"> + <field name="model" invisible='1'/> + <field name="help_message"/> + <p>If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red paper-plane next to each message.</p> + <footer> + <button string="Discard delivery failures" name="cancel_resend_action" type="object" class="btn-primary" /> + <button string="Cancel" class="btn-secondary" special="cancel" /> + </footer> + </form> + </field> + </record> + + <record id="snailmail_letter_cancel_action" model="ir.actions.act_window"> + <field name="name">Discard snailmail delivery failures</field> + <field name="res_model">snailmail.letter.cancel</field> + <field name="type">ir.actions.act_window</field> + <field name="view_type">form</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> +</odoo> diff --git a/addons/snailmail/wizard/snailmail_letter_format_error.py b/addons/snailmail/wizard/snailmail_letter_format_error.py new file mode 100644 index 00000000000..1eb92654e42 --- /dev/null +++ b/addons/snailmail/wizard/snailmail_letter_format_error.py @@ -0,0 +1,34 @@ + +from odoo import api, fields, models + +class SnailmailLetterFormatError(models.TransientModel): + _name = 'snailmail.letter.format.error' + _description = 'Format Error Sending a Snailmail Letter' + + message_id = fields.Many2one('mail.message') + snailmail_cover = fields.Boolean(string='Add a Cover Page') + + @api.model + def default_get(self, fields): + res = super(SnailmailLetterFormatError, self).default_get(fields) + snailmail_cover = self.env.user.company_id.snailmail_cover + res.update({ + 'message_id': self.env.context.get('message_id'), + 'snailmail_cover': snailmail_cover, + }) + return res + + @api.multi + def update_resend_action(self): + self.env.user.company_id.write({'snailmail_cover': self.snailmail_cover}) + letters_to_resend = self.env['snailmail.letter'].search([ + ('error_code', '=', 'FORMAT_ERROR'), + ]) + for letter in letters_to_resend: + letter.attachment_id.unlink() + letter.write({'cover': self.snailmail_cover}) + letter.snailmail_print() + + @api.multi + def cancel_letter_action(self): + self.message_id.cancel_letter() diff --git a/addons/snailmail/wizard/snailmail_letter_format_error_views.xml b/addons/snailmail/wizard/snailmail_letter_format_error_views.xml new file mode 100644 index 00000000000..35ba55047fe --- /dev/null +++ b/addons/snailmail/wizard/snailmail_letter_format_error_views.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="snailmail_letter_format_error" model="ir.ui.view"> + <field name="name">snailmail.letter.format.error.form</field> + <field name="model">snailmail.letter.format.error</field> + <field name="groups_id" eval="[(4,ref('base.group_user'))]"/> + <field name="arch" type="xml"> + <form string="Cancel notification in failure"> + <p>Our service cannot read your letter due to its format.<br/> + Please modify the format of the template or update your settings + to automatically add a blank cover page to all letters.</p> + <field name="snailmail_cover"/> + <label string="Add a Cover Page" class="o_light_label" for="snailmail_cover"/> + <footer> + <button string="Update Config and Re-send" name="update_resend_action" type="object" class="btn-primary" /> + <button string="Cancel Letter" name="cancel_letter_action" type="object" class="btn-primary" /> + <button string="Close" class="btn-secondary" special="cancel" /> + </footer> + </form> + </field> + </record> + + <record id="snailmail_letter_format_error_action" model="ir.actions.act_window"> + <field name="name">Format Error</field> + <field name="res_model">snailmail.letter.format.error</field> + <field name="type">ir.actions.act_window</field> + <field name="view_type">form</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> +</odoo> diff --git a/addons/snailmail/wizard/snailmail_letter_missing_required_fields.py b/addons/snailmail/wizard/snailmail_letter_missing_required_fields.py new file mode 100644 index 00000000000..a6b82807be9 --- /dev/null +++ b/addons/snailmail/wizard/snailmail_letter_missing_required_fields.py @@ -0,0 +1,54 @@ + +from odoo import _, api, fields, models + +class SnailmailLetterMissingRequiredFields(models.TransientModel): + _name = 'snailmail.letter.missing.required.fields' + _description = 'Update address of partner' + + partner_id = fields.Many2one('res.partner') + letter_id = fields.Many2one('snailmail.letter') + + street = fields.Char('Street') + street2 = fields.Char('Street2') + zip = fields.Char('Zip') + city = fields.Char('City') + state_id = fields.Many2one("res.country.state", string='State') + country_id = fields.Many2one('res.country', string='Country') + + @api.model + def default_get(self, fields): + rec = super(SnailmailLetterMissingRequiredFields, self).default_get(fields) + letter_id = self.env['snailmail.letter'].browse(self.env.context.get('letter_id')) + rec.update({ + 'partner_id': letter_id.partner_id.id, + 'letter_id': letter_id.id, + 'street': letter_id.street, + 'street2': letter_id.street2, + 'zip': letter_id.zip, + 'city': letter_id.city, + 'state_id': letter_id.state_id.id, + 'country_id': letter_id.country_id.id, + }) + return rec + + @api.multi + def update_address_cancel(self): + self.letter_id.cancel() + + @api.multi + def update_address_save(self): + address_data = { + 'street': self.street, + 'street2': self.street2, + 'zip': self.zip, + 'city': self.city, + 'state_id': self.state_id.id, + 'country_id': self.country_id.id, + } + self.partner_id.write(address_data) + letters_to_resend = self.env['snailmail.letter'].search([ + ('partner_id', '=', self.partner_id.id), + ('error_code', '=', 'MISSING_REQUIRED_FIELDS'), + ]) + letters_to_resend.write(address_data) + letters_to_resend.snailmail_print() diff --git a/addons/snailmail/wizard/snailmail_letter_missing_required_fields_views.xml b/addons/snailmail/wizard/snailmail_letter_missing_required_fields_views.xml new file mode 100644 index 00000000000..b4be9c7d74a --- /dev/null +++ b/addons/snailmail/wizard/snailmail_letter_missing_required_fields_views.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="snailmail_letter_missing_required_fields" model="ir.ui.view"> + <field name="name">snailmail.letter.missing.required.fields.form</field> + <field name="model">snailmail.letter.missing.required.fields</field> + <field name="arch" type="xml"> + <form> + <p>The customer address is not complete. Update the address here and re-send the letter.</p> + <group> + <label for="partner_id" string="Address"/> + <div class="o_address_format"> + <field name="partner_id" readonly="1" options="{'no_open': True}"/> + <field name="street" placeholder="Street..." class="o_address_street"/> + <field name="street2" placeholder="Street 2..." class="o_address_street"/> + <field name="city" placeholder="City" class="o_address_city"/> + <field name="state_id" class="o_address_state" placeholder="State" options='{"no_open": True}'/> + <field name="zip" placeholder="ZIP" class="o_address_zip"/> + <field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'/> + </div> + </group> + <footer> + <button string="Update address and re-send" type="object" name="update_address_save" class="btn-primary"/> + <button string="Cancel letter" type="object" name="update_address_cancel" class="btn-secondary"/> + <button string="Close" special='cancel' class="btn-secondary"/> + </footer> + </form> + </field> + </record> + + <record id="snailmail_letter_missing_required_fields_action" model="ir.actions.act_window"> + <field name="name">Failed letter</field> + <field name="res_model">snailmail.letter.missing.required.fields</field> + <field name="type">ir.actions.act_window</field> + <field name="view_type">form</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> +</odoo> diff --git a/addons/snailmail_account/static/src/js/snailmail_account_notification_manager.js b/addons/snailmail_account/static/src/js/snailmail_account_notification_manager.js index 8c42b17c6be..0309930351b 100644 --- a/addons/snailmail_account/static/src/js/snailmail_account_notification_manager.js +++ b/addons/snailmail_account/static/src/js/snailmail_account_notification_manager.js @@ -4,7 +4,7 @@ odoo.define('snailmail_account.NotificationManager', function (require) { var AbstractService = require('web.AbstractService'); var core = require("web.core"); -var SnailmailNotificationManager = AbstractService.extend({ +var SnailmailAccountNotificationManager = AbstractService.extend({ dependencies: ['bus_service'], /** @@ -28,8 +28,8 @@ var SnailmailNotificationManager = AbstractService.extend({ }); -core.serviceRegistry.add('snailmail_notification_service', SnailmailNotificationManager); +core.serviceRegistry.add('snailmail_account_notification_service', SnailmailAccountNotificationManager); -return SnailmailNotificationManager; +return SnailmailAccountNotificationManager; }); diff --git a/addons/snailmail_account/views/res_config_settings_views.xml b/addons/snailmail_account/views/res_config_settings_views.xml index 167b5ee0d09..eb80ae4b860 100644 --- a/addons/snailmail_account/views/res_config_settings_views.xml +++ b/addons/snailmail_account/views/res_config_settings_views.xml @@ -27,6 +27,11 @@ <label for="snailmail_duplex"/> <span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/> </div> + <div class="row"> + <field name="snailmail_cover" class="col-lg-1 ml16"/> + <label for="snailmail_cover"/> + <span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/> + </div> </div> </div> <widget name="iap_buy_more_credits" service_name="snailmail"/> diff --git a/addons/snailmail_account/wizard/account_invoice_send.py b/addons/snailmail_account/wizard/account_invoice_send.py index 87c87a390a2..e8575b645d8 100644 --- a/addons/snailmail_account/wizard/account_invoice_send.py +++ b/addons/snailmail_account/wizard/account_invoice_send.py @@ -11,93 +11,77 @@ class AccountInvoiceSend(models.TransientModel): _description = 'Account Invoice Send' partner_id = fields.Many2one('res.partner', compute='_get_partner', string='Partner') - snailmail_is_letter = fields.Boolean('Send by Post', help='Allows to send the document by snail mail (coventional posting delivery service)', default=lambda self: self.env.user.company_id.invoice_is_snailmail) - snailmail_cost = fields.Float(string='Stamp(s)', compute='_snailmail_estimate', store=True, readonly=True) - letter_ids = fields.Many2many('snailmail.letter', 'snailmail_letter_account_invoice_send_rel', ondelete='cascade') - invalid_addresses = fields.Integer('Invalid Addresses', compute='_compute_invalid_addresses') - - @api.depends('snailmail_cost', 'letter_ids') - def _compute_invalid_addresses(self): - for wizard in self: - count_invalid_addresses = 0 - required_fields = ['street', 'city', 'zip', 'country_id'] - for letter in wizard.letter_ids: - for field in required_fields: - if not letter.partner_id[field]: - count_invalid_addresses += 1 - break - wizard.invalid_addresses = count_invalid_addresses + snailmail_is_letter = fields.Boolean('Send by Post', help='Allows to send the document by Snailmail (coventional posting delivery service)', default=lambda self: self.env.user.company_id.invoice_is_snailmail) + snailmail_cost = fields.Float(string='Stamp(s)', compute='_compute_snailmail_cost', readonly=True) + invalid_addresses = fields.Integer('Invalid Addresses Count', compute='_compute_invalid_addresses') + invalid_invoice_ids = fields.Many2many('account.invoice', string='Invalid Addresses', compute='_compute_invalid_addresses') @api.multi - @api.onchange('invoice_ids') - def _get_invoice_currency(self): + @api.depends('invoice_ids') + def _compute_invalid_addresses(self): for wizard in self: - if wizard.invoice_ids: - wizard.currency_id = wizard.invoice_ids.mapped('currency_id')[0].id + invalid_invoices = wizard.invoice_ids.filtered(lambda i: not self.env['snailmail.letter']._is_valid_address(i.partner_id)) + wizard.invalid_invoice_ids = invalid_invoices + wizard.invalid_addresses = len(invalid_invoices) @api.multi - @api.onchange('invoice_ids') + @api.depends('invoice_ids') def _get_partner(self): for wizard in self: if wizard.invoice_ids and len(wizard.invoice_ids) == 1: wizard.partner_id = wizard.invoice_ids.partner_id.id @api.multi - def unlink(self): - for agc in self: - agc.letter_ids.filtered(lambda l: l.state == 'draft').unlink() - return super(AccountInvoiceSend, self).unlink() - - @api.multi - def _fetch_letters(self): - self.ensure_one() - if not self.letter_ids: - - letters = self.env['snailmail.letter'] - for invoice in self.invoice_ids: - letter = letters.create({ - 'partner_id': invoice.partner_id.id, - 'model': 'account.invoice', - 'res_id': invoice.id, - 'user_id': self.env.user.id, - 'company_id': invoice.company_id.id, - 'report_template': self.env.ref('account.account_invoices').id - }) - letters |= letter - self.letter_ids = [(4, l.id) for l in letters] - return self.letter_ids - - @api.onchange('snailmail_is_letter') - def _snailmail_estimate(self): + @api.depends('snailmail_is_letter') + def _compute_snailmail_cost(self): for wizard in self: - if wizard.snailmail_is_letter: - letters = wizard._fetch_letters() - wizard.snailmail_cost = letters._snailmail_estimate() + wizard.snailmail_cost = wizard.env['snailmail.letter']._snailmail_estimate_from_documents(wizard.invoice_ids._name, wizard.invoice_ids.ids, 'partner_id') @api.multi def snailmail_print_action(self): - for wizard in self: - letters = wizard._fetch_letters() - letters.write({'state': 'pending'}) - wizard.invoice_ids.filtered(lambda inv: not inv.sent).write({'sent': True}) - if len(letters) == 1: - letters._snailmail_print() + self.ensure_one() + letters = self.env['snailmail.letter'] + for invoice in self.invoice_ids: + letter = self.env['snailmail.letter'].create({ + 'partner_id': invoice.partner_id.id, + 'model': 'account.invoice', + 'res_id': invoice.id, + 'user_id': self.env.user.id, + 'company_id': invoice.company_id.id, + 'report_template': self.env.ref('account.account_invoices').id + }) + letters |= letter + + self.invoice_ids.filtered(lambda inv: not inv.sent).write({'sent': True}) + if len(self.invoice_ids) == 1: + letters._snailmail_print() + else: + letters._snailmail_print(immediate=False) @api.multi def send_and_print_action(self): - if self.snailmail_is_letter and self.invalid_addresses: - if self.composition_mode == "mass_mail": - self.env['bus.bus'].sendone( - (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), - {'type': 'snailmail_invalid_address', 'title': _("Invalid Addresses"), - 'message': _("%s of the selected invoice(s) had an invalid address and were not sent") % self.invalid_addresses} - ) - else: - raise UserError(_( - '''Cannot send by post to an incomplete address. - Please update the customer's address or uncheck \'Send by Post\'.''' - )) - res = super(AccountInvoiceSend, self).send_and_print_action() if self.snailmail_is_letter: + if self.invalid_addresses and self.composition_mode == "mass_mail": + self.notify_invalid_addresses() self.snailmail_print_action() + res = super(AccountInvoiceSend, self).send_and_print_action() return res + + @api.multi + def notify_invalid_addresses(self): + self.ensure_one() + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), + {'type': 'snailmail_invalid_address', 'title': _("Invalid Addresses"), + 'message': _("%s of the selected invoice(s) had an invalid address and were not sent") % self.invalid_addresses} + ) + + @api.multi + def invalid_addresses_action(self): + return { + 'name': _('Invalid Addresses'), + 'type': 'ir.actions.act_window', + 'view_mode': 'kanban,tree,form', + 'res_model': 'account.invoice', + 'domain': [('id', 'in', self.mapped('invalid_invoice_ids').ids)], + } diff --git a/addons/snailmail_account/wizard/account_invoice_send_views.xml b/addons/snailmail_account/wizard/account_invoice_send_views.xml index f771712c36a..b2efb714bf6 100644 --- a/addons/snailmail_account/wizard/account_invoice_send_views.xml +++ b/addons/snailmail_account/wizard/account_invoice_send_views.xml @@ -8,7 +8,6 @@ <field name="arch" type="xml"> <xpath expr="//div[@name='option_email']" position='before'> <div name="option_letter"> - <field name="letter_ids" invisible="1"/> <field name="invalid_addresses" invisible="1"/> <div name="option" class="text-left d-inline-block"> <field name="snailmail_is_letter" /> @@ -34,16 +33,17 @@ <span attrs="{'invisible': [('invalid_addresses', '=', 0)]}"> <div class="text-right d-inline-block" attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}"> <span class="text-danger"> - Incomplete customer address + The customer address is not complete. </span> </div> </span> </span> <span attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}"> - <span attrs="{'invisible': [('snailmail_cost', '=', 0)]}"> - <span class="text-danger" attrs="{'invisible': [('invalid_addresses', '=', 0)]}"> - <field name="invalid_addresses" readonly="1" options="{'digits':[0,0]}"/> invoice(s) do not have a complete customer address + <span attrs="{'invisible': [('invalid_addresses', '=', 0)]}"> + <span class="text-danger"> + Some customer addresses are not complete. </span> + <button type="object" name="invalid_addresses_action" class="btn btn-link" role="button"><field name="invalid_addresses" readonly="1" options="{'digits':[0,0]}"/> invoices</button> </span> </span> </span> diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 3b4a2df8281..ff162bb1ecf 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -277,7 +277,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_inbox_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=32, emp=44): # com runbot 31 - 43 // test_mail only: 32 - 44 + with self.assertQueryCount(__system__=32, emp=46): # com runbot 31 - 45 // test_mail only: 32 - 44 record.message_post( body='<p>Test Post Performances with an inbox ping</p>', partner_ids=self.user_test.partner_id.ids, |
