19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo import models
class IrActionsReport(models.Model):
@ -11,12 +9,11 @@ class IrActionsReport(models.Model):
# using snailmail
if self.env.context.get('snailmail_layout'):
return False
return super(IrActionsReport, self).retrieve_attachment(record)
return super().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()
res = super().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

View file

@ -2,30 +2,30 @@
from odoo import api, fields, models
class Message(models.Model):
class MailMessage(models.Model):
_inherit = 'mail.message'
snailmail_error = fields.Boolean("Snailmail message in error", compute="_compute_snailmail_error", search="_search_snailmail_error")
snailmail_error = fields.Boolean(
string="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'})})
message_type = fields.Selection(
selection_add=[('snailmail', 'Snailmail')],
ondelete={'snailmail': lambda recs: recs.write({'message_type': ' comment'})})
@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
self.snailmail_error = False
for message in self.filtered(lambda msg: msg.message_type == 'snailmail' and msg.letter_ids):
message.snailmail_error = message.letter_ids[0].state == 'error'
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)]
if operator != 'in':
return NotImplemented
return ['&', ('letter_ids.state', '=', 'error'), ('letter_ids.user_id', '=', self.env.user.id)]
def cancel_letter(self):
self.mapped('letter_ids').cancel()
self.letter_ids.cancel()
def send_letter(self):
self.mapped('letter_ids')._snailmail_print()
self.letter_ids._snailmail_print()

View file

@ -3,7 +3,7 @@
from odoo import fields, models
class Notification(models.Model):
class MailNotification(models.Model):
_inherit = 'mail.notification'
notification_type = fields.Selection(selection_add=[('snail', 'Snailmail')], ondelete={'snail': 'cascade'})

View file

@ -3,9 +3,10 @@
from odoo import fields, models
class Company(models.Model):
class ResCompany(models.Model):
_inherit = "res.company"
snailmail_color = fields.Boolean(string='Color', default=True)
snailmail_color = fields.Boolean(default=True)
snailmail_cover = fields.Boolean(string='Add a Cover Page', default=False)
snailmail_duplex = fields.Boolean(string='Both sides', default=False)

View file

@ -1,12 +1,30 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
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)
snailmail_cover_readonly = fields.Boolean(compute="_compute_cover_readonly")
def _is_layout_cover_required(self):
return self.external_report_layout_id in {
self.env.ref(f'web.external_layout_{layout}')
for layout in ('boxed', 'bold', 'striped')
}
@api.onchange('external_report_layout_id')
def _onchange_layout(self):
for record in self:
if record._is_layout_cover_required():
record.company_id.snailmail_cover = True
@api.depends('external_report_layout_id')
def _compute_cover_readonly(self):
for record in self:
record.snailmail_cover_readonly = self._is_layout_cover_required()

View file

@ -38,6 +38,12 @@ 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') and self.country_id.code == 'DE':
# Germany requires specific address formatting for Pingen
result = "%(street)s"
if self.street2:
result += " // %(street2)s"
return result + "\n%(zip)s %(city)s\n%(country_name)s"
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"

View file

@ -4,23 +4,6 @@ import re
import base64
import io
try:
from PyPDF2 import PdfWriter, PdfReader
# Create compatibility classes for PyPDF2 3.0+
class PdfFileWriter(PdfWriter):
def addPage(self, page):
return self.add_page(page)
def appendPagesFromReader(self, reader, after_page_append=None):
return self.append_pages_from_reader(reader, after_page_append)
class PdfFileReader(PdfReader):
def getPage(self, page_num):
return self.pages[page_num]
except ImportError:
from PyPDF2 import PdfFileWriter, PdfFileReader
from reportlab.platypus import Frame, Paragraph, KeepInFrame
from reportlab.lib.units import mm
from reportlab.lib.pagesizes import A4
@ -30,6 +13,7 @@ from reportlab.pdfgen.canvas import Canvas
from odoo import fields, models, api, _
from odoo.addons.iap.tools import iap_tools
from odoo.exceptions import AccessError, UserError
from odoo.tools.pdf import PdfFileReader, PdfFileWriter
from odoo.tools.safe_eval import safe_eval
DEFAULT_ENDPOINT = 'https://iap-snailmail.odoo.com'
@ -43,6 +27,7 @@ ERROR_CODES = [
'NO_PRICE_AVAILABLE',
'FORMAT_ERROR',
'UNKNOWN_ERROR',
'ATTACHMENT_ERROR',
]
@ -68,14 +53,13 @@ class SnailmailLetter(models.Model):
('pending', 'In Queue'),
('sent', 'Sent'),
('error', 'Error'),
('canceled', 'Canceled')
('canceled', 'Cancelled')
], '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.Html('Information')
display_name = fields.Char('Display Name', compute="_compute_display_name")
reference = fields.Char(string='Related Record', compute='_compute_reference', readonly=True, store=False)
@ -89,11 +73,11 @@ class SnailmailLetter(models.Model):
state_id = fields.Many2one("res.country.state", string='State')
country_id = fields.Many2one('res.country', string='Country')
@api.depends('reference', 'partner_id')
@api.depends('attachment_id', '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)
letter.display_name = f"{letter.attachment_id.name} - {letter.partner_id.name}"
else:
letter.display_name = letter.partner_id.name
@ -136,22 +120,33 @@ class SnailmailLetter(models.Model):
self.env['mail.notification'].sudo().create(notification_vals)
letters.attachment_id.check('read')
letters.attachment_id.check_access('read')
return letters
def write(self, vals):
res = super().write(vals)
if 'attachment_id' in vals:
self.attachment_id.check('read')
self.attachment_id.check_access('read')
return res
def _generate_report_pdf(self, report):
obj = self.env[self.model].browse(self.res_id)
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 = self.env['ir.actions.report'].with_context(snailmail_layout=not self.cover, lang='en_US')._render_qweb_pdf(report, self.res_id)[0]
return filename, pdf_bin
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:
@ -161,18 +156,19 @@ class SnailmailLetter(models.Model):
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")
paperformat = report.get_paperformat()
if (paperformat.format == 'custom' and paperformat.page_width != 210 and paperformat.page_height != 297) or paperformat.format != 'A4':
raise UserError(_("Please use an A4 Paper format."))
pdf_bin, unused_filetype = self.env['ir.actions.report'].with_context(snailmail_layout=not self.cover, lang='en_US')._render_qweb_pdf(report, self.res_id)
# The external_report_layout_id is changed just for the snailmail pdf generation if the layout is not supported
prev = self.company_id.external_report_layout_id
if prev in {
self.env.ref(f'web.external_layout_{layout}')
for layout in ('bubble', 'wave', 'folder')
}:
self.company_id.sudo().external_report_layout_id = self.env.ref('web.external_layout_standard')
filename, pdf_bin = self._generate_report_pdf(report)
self.company_id.sudo().external_report_layout_id = prev
pdf_bin = self._overwrite_margins(pdf_bin)
if self.cover:
pdf_bin = self._append_cover_page(pdf_bin)
@ -192,7 +188,7 @@ class SnailmailLetter(models.Model):
:param bin_pdf : binary content of the pdf file
"""
pages = 0
for match in re.compile(br"/Count\s+(\d+)").finditer(bin_pdf):
for match in re.compile(rb"/Count\s+(\d+)").finditer(bin_pdf):
pages = int(match.group(1))
return pages
@ -234,11 +230,10 @@ class SnailmailLetter(models.Model):
}
}
"""
account_token = self.env['iap.account'].get('snailmail').account_token
account_token = self.env['iap.account'].get('snailmail').sudo().account_token
dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
documents = []
batch = len(self) > 1
for letter in self:
recipient_name = letter.partner_id.name or letter.partner_id.parent_id and letter.partner_id.parent_id.name
if not recipient_name:
@ -253,7 +248,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],
'contact_address': letter.partner_id.with_context(snailmail_layout=True, show_address=True).display_name,
'address': {
'name': recipient_name,
'street': letter.partner_id.street,
@ -293,7 +288,7 @@ class SnailmailLetter(models.Model):
letter.write({
'info_msg': 'The attachment could not be generated.',
'state': 'error',
'error_code': 'UNKNOWN_ERROR'
'error_code': 'ATTACHMENT_ERROR'
})
continue
if letter.company_id.external_report_layout_id == self.env.ref('l10n_de.external_layout_din5008', False):
@ -321,7 +316,7 @@ class SnailmailLetter(models.Model):
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)
link = self.env['iap.account'].get_credits_url(service_name='snailmail')
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.')
@ -398,9 +393,8 @@ class SnailmailLetter(models.Model):
raise ae
for doc in response['request']['documents']:
if doc.get('sent') and response['request_code'] == 200:
self.env['iap.account']._send_iap_bus_notification(
service_name='snailmail',
title=_("Snail Mails are successfully sent"))
self.env['iap.account']._send_success_notification(
message=_("Snail Mails are successfully 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}
notification_data = {
@ -412,10 +406,9 @@ class SnailmailLetter(models.Model):
error = doc['error'] if response['request_code'] == 200 else response['reason']
if error == 'CREDIT_ERROR':
self.env['iap.account']._send_iap_bus_notification(
self.env['iap.account']._send_no_credit_notification(
service_name='snailmail',
title=_("Not enough credits for Snail Mail"),
error_type="credit")
title=_("Not enough credits for Snail Mail"))
note = _('An error occurred when sending the document by post.<br>Error: %s', self._get_error_message(error))
letter_data = {
'info_msg': note,
@ -458,7 +451,7 @@ class SnailmailLetter(models.Model):
('state', '=', 'pending'),
'&',
('state', '=', 'error'),
('error_code', 'in', ['TRIAL_ERROR', 'CREDIT_ERROR', 'MISSING_REQUIRED_FIELDS'])
('error_code', 'in', ['TRIAL_ERROR', 'CREDIT_ERROR', 'ATTACHMENT_ERROR', 'MISSING_REQUIRED_FIELDS'])
])
for letter in letters_send:
letter._snailmail_print()
@ -474,9 +467,18 @@ class SnailmailLetter(models.Model):
required_keys = ['street', 'city', 'zip', 'country_id']
return all(record[key] for key in required_keys)
def _get_cover_address_split(self):
address_split = self.partner_id.with_context(show_address=True, lang='en_US').display_name.split('\n')
if self.country_id.code == 'DE':
# Germany requires specific address formatting for Pingen
if self.street2:
address_split[1] = f'{self.street} // {self.street2}'
address_split[2] = f'{self.zip} {self.city}'
return address_split
def _append_cover_page(self, invoice_bin: bytes):
out_writer = PdfFileWriter()
address_split = self.partner_id.with_context(show_address=True, lang='en_US')._get_name().split('\n')
address_split = self._get_cover_address_split()
address_split[0] = self.partner_id.name or self.partner_id.parent_id and self.partner_id.parent_id.name or address_split[0]
address = '<br/>'.join(address_split)
address_x = 118 * mm