summaryrefslogtreecommitdiff
path: root/addons/snailmail/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/snailmail/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/snailmail/models')
-rw-r--r--addons/snailmail/models/__init__.py10
-rw-r--r--addons/snailmail/models/ir_actions_report.py24
-rw-r--r--addons/snailmail/models/ir_qweb_fields.py19
-rw-r--r--addons/snailmail/models/mail_message.py31
-rw-r--r--addons/snailmail/models/mail_notification.py18
-rw-r--r--addons/snailmail/models/res_company.py11
-rw-r--r--addons/snailmail/models/res_config_settings.py12
-rw-r--r--addons/snailmail/models/res_partner.py44
-rw-r--r--addons/snailmail/models/snailmail_letter.py406
9 files changed, 575 insertions, 0 deletions
diff --git a/addons/snailmail/models/__init__.py b/addons/snailmail/models/__init__.py
new file mode 100644
index 00000000..37c70029
--- /dev/null
+++ b/addons/snailmail/models/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+from . import res_company
+from . import res_partner
+from . import res_config_settings
+from . import mail_notification
+from . import snailmail_letter
+from . import ir_actions_report
+from . import ir_qweb_fields
+from . import mail_message
diff --git a/addons/snailmail/models/ir_actions_report.py b/addons/snailmail/models/ir_actions_report.py
new file mode 100644
index 00000000..91573ef4
--- /dev/null
+++ b/addons/snailmail/models/ir_actions_report.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, fields, api, _
+
+
+class IrActionsReport(models.Model):
+ _inherit = 'ir.actions.report'
+
+ def retrieve_attachment(self, record):
+ # Override this method in order to force to re-render the pdf in case of
+ # using snailmail
+ if self.env.context.get('snailmail_layout'):
+ return False
+ return super(IrActionsReport, self).retrieve_attachment(record)
+
+ @api.model
+ def get_paperformat(self):
+ # force the right format (euro/A4) when sending letters, only if we are not using the l10n_DE layout
+ res = super(IrActionsReport, self).get_paperformat()
+ if self.env.context.get('snailmail_layout') and res != self.env.ref('l10n_de.paperformat_euro_din', False):
+ paperformat_id = self.env.ref('base.paperformat_euro')
+ return paperformat_id
+ else:
+ return res
diff --git a/addons/snailmail/models/ir_qweb_fields.py b/addons/snailmail/models/ir_qweb_fields.py
new file mode 100644
index 00000000..b005fdd3
--- /dev/null
+++ b/addons/snailmail/models/ir_qweb_fields.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, models
+
+
+class Contact(models.AbstractModel):
+ _inherit = 'ir.qweb.field.contact'
+
+ @api.model
+ def value_to_html(self, value, options):
+ if self.env.context.get('snailmail_layout'):
+ value = value.with_context(snailmail_layout=self.env.context['snailmail_layout'])
+ return super(Contact, self).value_to_html(value, options)
+
+ @api.model
+ def record_to_html(self, record, field_name, options):
+ if self.env.context.get('snailmail_layout'):
+ record = record.with_context(snailmail_layout=self.env.context['snailmail_layout'])
+ return super(Contact, self).record_to_html(record, field_name, options)
diff --git a/addons/snailmail/models/mail_message.py b/addons/snailmail/models/mail_message.py
new file mode 100644
index 00000000..3607ff8b
--- /dev/null
+++ b/addons/snailmail/models/mail_message.py
@@ -0,0 +1,31 @@
+
+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")
+ letter_ids = fields.One2many(comodel_name='snailmail.letter', inverse_name='message_id')
+ message_type = fields.Selection(selection_add=[
+ ('snailmail', 'Snailmail')
+ ], ondelete={'snailmail': lambda recs: recs.write({'message_type': 'email'})})
+
+ @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'
+ else:
+ message.snailmail_error = False
+
+ 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)]
+
+ def cancel_letter(self):
+ self.mapped('letter_ids').cancel()
+
+ def send_letter(self):
+ self.mapped('letter_ids')._snailmail_print()
diff --git a/addons/snailmail/models/mail_notification.py b/addons/snailmail/models/mail_notification.py
new file mode 100644
index 00000000..a368c0a7
--- /dev/null
+++ b/addons/snailmail/models/mail_notification.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class Notification(models.Model):
+ _inherit = 'mail.notification'
+
+ notification_type = fields.Selection(selection_add=[('snail', 'Snailmail')], ondelete={'snail': 'cascade'})
+ letter_id = fields.Many2one('snailmail.letter', string="Snailmail Letter", index=True, ondelete='cascade')
+ failure_type = fields.Selection(selection_add=[
+ ('sn_credit', "Snailmail Credit Error"),
+ ('sn_trial', "Snailmail Trial Error"),
+ ('sn_price', "Snailmail No Price Available"),
+ ('sn_fields', "Snailmail Missing Required Fields"),
+ ('sn_format', "Snailmail Format Error"),
+ ('sn_error', "Snailmail Unknown Error"),
+ ])
diff --git a/addons/snailmail/models/res_company.py b/addons/snailmail/models/res_company.py
new file mode 100644
index 00000000..2aa46f3c
--- /dev/null
+++ b/addons/snailmail/models/res_company.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+class 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
new file mode 100644
index 00000000..84365f21
--- /dev/null
+++ b/addons/snailmail/models/res_config_settings.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+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
new file mode 100644
index 00000000..c7f40f7b
--- /dev/null
+++ b/addons/snailmail/models/res_partner.py
@@ -0,0 +1,44 @@
+
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+from odoo.addons.snailmail.country_utils import SNAILMAIL_COUNTRIES
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ 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 letter_address_vals:
+ letters = self.env['snailmail.letter'].search([
+ ('state', 'not in', ['sent', 'canceled']),
+ ('partner_id', 'in', self.ids),
+ ])
+ letters.write(letter_address_vals)
+
+ return super(ResPartner, self).write(vals)
+
+ 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
+ # dictionary imported in country_utils.py
+ country_code = self.country_id.code
+ if self.env.context.get('snailmail_layout') and country_code in SNAILMAIL_COUNTRIES:
+ return SNAILMAIL_COUNTRIES.get(country_code)
+
+ return super(ResPartner, self)._get_country_name()
+
+ @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') 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()
diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py
new file mode 100644
index 00000000..d4be829b
--- /dev/null
+++ b/addons/snailmail/models/snailmail_letter.py
@@ -0,0 +1,406 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import re
+import base64
+
+from odoo import fields, models, api, _
+from odoo.addons.iap.tools import iap_tools
+from odoo.tools.safe_eval import safe_eval
+
+DEFAULT_ENDPOINT = 'https://iap-snailmail.odoo.com'
+PRINT_ENDPOINT = '/iap/snailmail/1/print'
+DEFAULT_TIMEOUT = 30
+
+ERROR_CODES = [
+ 'MISSING_REQUIRED_FIELDS',
+ 'CREDIT_ERROR',
+ 'TRIAL_ERROR',
+ 'NO_PRICE_AVAILABLE',
+ 'FORMAT_ERROR',
+ 'UNKNOWN_ERROR',
+]
+
+
+class SnailmailLetter(models.Model):
+ _name = 'snailmail.letter'
+ _description = 'Snailmail Letter'
+
+ user_id = fields.Many2one('res.users', 'Sent by')
+ 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.company.id)
+ report_template = fields.Many2one('ir.actions.report', 'Optional report to print and attach')
+
+ 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.name')
+ color = fields.Boolean(string='Color', default=lambda self: self.env.company.snailmail_color)
+ cover = fields.Boolean(string='Cover Page', default=lambda self: self.env.company.snailmail_cover)
+ duplex = fields.Boolean(string='Both side', default=lambda self: self.env.company.snailmail_duplex)
+ state = fields.Selection([
+ ('pending', 'In Queue'),
+ ('sent', 'Sent'),
+ ('error', 'Error'),
+ ('canceled', 'Canceled')
+ ], '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")
+ notification_ids = fields.One2many('mail.notification', 'letter_id', "Notifications")
+
+ 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:
+ if letter.attachment_id:
+ letter.display_name = "%s - %s" % (letter.attachment_id.name, letter.partner_id.name)
+ else:
+ letter.display_name = letter.partner_id.name
+
+ @api.depends('model', 'res_id')
+ 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,
+ })
+ letter = super(SnailmailLetter, self).create(vals)
+
+ self.env['mail.notification'].sudo().create({
+ 'mail_message_id': msg_id.id,
+ 'res_partner_id': partner_id.id,
+ 'notification_type': 'snail',
+ 'letter_id': letter.id,
+ 'is_read': True, # discard Inbox notification
+ 'notification_status': 'ready',
+ })
+
+ return letter
+
+ 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.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),
+ '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
+
+ 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),
+ currency_name: char
+ }
+ }
+ """
+ account_token = self.env['iap.account'].get('snailmail').account_token
+ dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
+ 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,
+ '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,
+ '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: 1 object = 1 page
+ # If we are printing -> attach the pdf
+ if route == 'estimate':
+ document.update(pages=1)
+ else:
+ # adding the web logo from the company for future possible customization
+ document.update({
+ 'company_logo': letter.company_id.logo_web and letter.company_id.logo_web.decode('utf-8') or False,
+ })
+ attachment = letter._fetch_attachment()
+ if attachment:
+ document.update({
+ 'pdf_bin': route == 'print' and attachment.datas.decode('utf-8'),
+ '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',
+ 'error_code': 'ATTACHMENT_ERROR'
+ })
+ continue
+ if letter.company_id.external_report_layout_id == self.env.ref('l10n_de.external_layout_din5008', False):
+ document.update({
+ 'rightaddress': 0,
+ })
+ documents.append(document)
+
+ return {
+ 'account_token': account_token,
+ 'dbuuid': dbuuid,
+ 'documents': documents,
+ 'options': {
+ 'color': self and self[0].color,
+ 'cover': self and self[0].cover,
+ 'duplex': self and self[0].duplex,
+ 'currency_name': 'EUR',
+ },
+ # 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.<br>Please go to your <a href=%s target="new">iap account</a>.', link)
+ if error == 'TRIAL_ERROR':
+ link = self.env['iap.account'].get_credits_url(service_name='snailmail', trial=True)
+ return _('You don\'t have an IAP account registered for this service.<br>Please go to <a href=%s target="new">iap.odoo.com</a> to claim your free credits.', 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 == 'FORMAT_ERROR':
+ return _('The attachment of the letter could not be sent. Please check its content and contact the support if the problem persists.')
+ else:
+ return _('An unknown error happened. Please contact the support.')
+ return error
+
+ def _get_failure_type(self, error):
+ if error == 'CREDIT_ERROR':
+ return 'sn_credit'
+ if error == 'TRIAL_ERROR':
+ return 'sn_trial'
+ if error == 'NO_PRICE_AVAILABLE':
+ return 'sn_price'
+ if error == 'MISSING_REQUIRED_FIELDS':
+ return 'sn_fields'
+ if error == 'FORMAT_ERROR':
+ return 'sn_format'
+ else:
+ return 'sn_error'
+
+ 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:
+ for letter in valid_address_letters:
+ letter._snailmail_print_valid_address()
+ self.env.cr.commit()
+
+ def _snailmail_print_invalid_address(self):
+ error = 'MISSING_REQUIRED_FIELDS'
+ error_message = _("The address of the recipient is not complete")
+ self.write({
+ 'state': 'error',
+ 'error_code': error,
+ 'info_msg': error_message,
+ })
+ self.notification_ids.sudo().write({
+ 'notification_status': 'exception',
+ 'failure_type': self._get_failure_type(error),
+ 'failure_reason': error_message,
+ })
+ self.message_id._notify_message_notification_update()
+
+ def _snailmail_print_valid_address(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
+ }
+ }
+ }
+ """
+ endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
+ timeout = int(self.env['ir.config_parameter'].sudo().get_param('snailmail.timeout', DEFAULT_TIMEOUT))
+ params = self._snailmail_create('print')
+ response = iap_tools.iap_jsonrpc(endpoint + PRINT_ENDPOINT, params=params, timeout=timeout)
+ for doc in response['request']['documents']:
+ if doc.get('sent') and response['request_code'] == 200:
+ 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}
+ notification_data = {
+ 'notification_status': 'sent',
+ 'failure_type': False,
+ 'failure_reason': False,
+ }
+ else:
+ 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'
+ }
+ notification_data = {
+ 'notification_status': 'exception',
+ 'failure_type': self._get_failure_type(error),
+ 'failure_reason': note,
+ }
+
+ letter = self.browse(doc['letter_id'])
+ letter.write(letter_data)
+ letter.notification_ids.sudo().write(notification_data)
+ self.message_id._notify_message_notification_update()
+
+ def snailmail_print(self):
+ self.write({'state': 'pending'})
+ self.notification_ids.sudo().write({
+ 'notification_status': 'ready',
+ 'failure_type': False,
+ 'failure_reason': False,
+ })
+ self.message_id._notify_message_notification_update()
+ if len(self) == 1:
+ self._snailmail_print()
+
+ def cancel(self):
+ self.write({'state': 'canceled', 'error_code': False})
+ self.notification_ids.sudo().write({
+ 'notification_status': 'canceled',
+ })
+ self.message_id._notify_message_notification_update()
+
+ @api.model
+ def _snailmail_cron(self, autocommit=True):
+ letters_send = self.search([
+ '|',
+ ('state', '=', 'pending'),
+ '&',
+ ('state', '=', 'error'),
+ ('error_code', 'in', ['TRIAL_ERROR', 'CREDIT_ERROR', 'ATTACHMENT_ERROR', 'MISSING_REQUIRED_FIELDS'])
+ ])
+ for letter in letters_send:
+ letter._snailmail_print()
+ # Commit after every letter sent to avoid to send it again in case of a rollback
+ if autocommit:
+ self.env.cr.commit()
+
+ @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)