diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/snailmail/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/snailmail/models')
| -rw-r--r-- | addons/snailmail/models/__init__.py | 10 | ||||
| -rw-r--r-- | addons/snailmail/models/ir_actions_report.py | 24 | ||||
| -rw-r--r-- | addons/snailmail/models/ir_qweb_fields.py | 19 | ||||
| -rw-r--r-- | addons/snailmail/models/mail_message.py | 31 | ||||
| -rw-r--r-- | addons/snailmail/models/mail_notification.py | 18 | ||||
| -rw-r--r-- | addons/snailmail/models/res_company.py | 11 | ||||
| -rw-r--r-- | addons/snailmail/models/res_config_settings.py | 12 | ||||
| -rw-r--r-- | addons/snailmail/models/res_partner.py | 44 | ||||
| -rw-r--r-- | addons/snailmail/models/snailmail_letter.py | 406 |
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) |
