[FIX] account*: Always send an invoice report using send & print
In case of error, the send & print wizard is crashing or log an error on the invoice chatter. In that case, nothing is sent to the end-customer. This is problematic for all flows in which we want to send a mail to the customer automatically. For example, e-commerce with automatic invoicing or subscription/recurring invoices. To avoid that, the current logic of the send & print has been reshaped. In case of error, a proforma PDF is sent instead. This is exactly the same document as the PDF but without the legal layer. To do that, a lot of refactoring has been necessary to always provide the cumulated data for invoices to be able to access the generated proforma report and to allow the overrides to know exactly in which mode the hooks are called. Also, this commit renames the method by something less generic about invoices. Indeed, this wizard needs to be usable for others documents than invoices. That's the purpose of the invoice_single/invoice_multi mode. For that reason, all methods about invoices are now expricitely prefixed by 'invoice'. Fix also a performance issue on multi-invoices since the invoice_pdf_report_id document was invalided for the whole model instead of the current record. When dealing with X invoices, the whole model was invalidated X times. Fix the managment of attachments: - The manual attachments wasn't send when sending a mail 'invoice_single' mode. - When changing to another mail template, the manual attachments were lost. Fix the double generation of PDF using a web-service. When opening again the send & print wizard, the PDF must not be regenerated but reloaded from the previous one. Task: 3339352 X-original-commit: e9e90811aeee46989a83b21f9b59071a9c7bc362 Part-of: odoo/odoo#124436
This commit is contained in:
@@ -4076,12 +4076,29 @@ class AccountMove(models.Model):
|
||||
'checkbox_download': False,
|
||||
}
|
||||
|
||||
def _generate_pdf_and_send_invoice(self, template, from_cron=True):
|
||||
def _generate_pdf_and_send_invoice(self, template, from_cron=True, allow_fallback_pdf=True):
|
||||
""" Generate the pdf for the current invoices and send them by mail using the send & print wizard.
|
||||
|
||||
:param from_cron: Flag indicating if the method is called from a cron. In that case, we avoid raising any
|
||||
error.
|
||||
:param allow_fallback_pdf: In case of error when generating the documents for invoices, generate a
|
||||
proforma PDF report instead.
|
||||
"""
|
||||
composer_vals = self._get_pdf_and_send_invoice_vals(template)
|
||||
composer = self.env['account.move.send'].create(composer_vals)
|
||||
|
||||
# from_cron=True to log errors in chatter instead of raise
|
||||
composer.action_send_and_print(from_cron=from_cron)
|
||||
composer.action_send_and_print(from_cron=from_cron, allow_fallback_pdf=allow_fallback_pdf)
|
||||
|
||||
def _get_invoice_pdf_report_filename(self):
|
||||
""" Get the filename of the generated PDF invoice report. """
|
||||
self.ensure_one()
|
||||
return f"{self.name.replace('/', '_')}.pdf"
|
||||
|
||||
def _get_invoice_proforma_pdf_report_filename(self):
|
||||
""" Get the filename of the generated proforma PDF invoice report. """
|
||||
self.ensure_one()
|
||||
return f"{self.name.replace('/', '_')}_proforma.pdf"
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TOOLING
|
||||
@@ -4300,7 +4317,7 @@ class AccountMove(models.Model):
|
||||
def _get_mail_thread_data_attachments(self):
|
||||
res = super()._get_mail_thread_data_attachments()
|
||||
# else, attachments with 'res_field' get excluded
|
||||
return res | self.env['account.move.send']._get_linked_attachments(self)
|
||||
return res | self.env['account.move.send']._get_invoice_extra_attachments(self)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TOOLING
|
||||
|
||||
@@ -47,6 +47,7 @@ export class MailAttachments extends Component {
|
||||
name: file.filename,
|
||||
mimetype: file.mimetype,
|
||||
placeholder: false,
|
||||
manual: true,
|
||||
});
|
||||
}
|
||||
this.props.record.update({ [this.props.name]: this.getValue().concat(extraFiles) });
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from ast import literal_eval
|
||||
|
||||
from datetime import date
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests.common import Form, users, warmup
|
||||
@@ -13,7 +13,7 @@ from odoo.tools import formataddr, mute_logger
|
||||
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class AccountMoveSendBase(AccountTestInvoicingCommon, MailCommon):
|
||||
class TestAccountComposerPerformance(AccountTestInvoicingCommon, MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -131,11 +131,6 @@ class AccountMoveSendBase(AccountTestInvoicingCommon, MailCommon):
|
||||
self.patch(self.env.registry, 'ready', True)
|
||||
self.flush_tracking()
|
||||
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestAccountComposerPerformance(AccountMoveSendBase):
|
||||
""" Test performance of custom composer for moves. """
|
||||
|
||||
def test_assert_initial_values(self):
|
||||
""" Test initial values to ease understanding of results and notifications """
|
||||
for move in self.test_account_moves:
|
||||
@@ -239,7 +234,7 @@ class TestAccountComposerPerformance(AccountMoveSendBase):
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False), \
|
||||
self.mock_mail_app():
|
||||
composer.action_send_and_print()
|
||||
composer.action_send_and_print(allow_fallback_pdf=True)
|
||||
self.env.cr.flush() # force tracking message
|
||||
|
||||
self.assertEqual(len(self._new_msgs), 2, 'Should produce 2 messages: one for posting template, one for tracking')
|
||||
@@ -254,7 +249,7 @@ class TestAccountComposerPerformance(AccountMoveSendBase):
|
||||
)
|
||||
|
||||
# print: template-based message
|
||||
self.assertEqual(len(print_msg.attachment_ids), 3) # + factur-x.xml in tests
|
||||
self.assertEqual(len(print_msg.attachment_ids), 3)
|
||||
self.assertNotIn(self.attachments, print_msg.attachment_ids,
|
||||
'Attachments should be duplicated, not just linked')
|
||||
self.assertEqual(print_msg.author_id, self.user_account_other.partner_id,
|
||||
@@ -301,7 +296,6 @@ class TestAccountComposerPerformance(AccountMoveSendBase):
|
||||
)
|
||||
|
||||
# composer configuration
|
||||
self.assertEqual(len(composer.mail_attachments_widget), 3)
|
||||
self.assertIn(f'TemplateBody for {test_move.name}', composer.mail_body)
|
||||
self.assertEqual(composer.move_ids, test_move)
|
||||
self.assertTrue(composer.checkbox_send_mail)
|
||||
@@ -342,7 +336,7 @@ class TestAccountComposerPerformance(AccountMoveSendBase):
|
||||
)
|
||||
|
||||
# print: template-based message
|
||||
self.assertEqual(len(print_msg.attachment_ids), 3) # + factur-x.xml in tests
|
||||
self.assertEqual(len(print_msg.attachment_ids), 3)
|
||||
self.assertNotIn(self.attachments, print_msg.attachment_ids,
|
||||
'Attachments should be duplicated, not just linked')
|
||||
self.assertEqual(print_msg.author_id, self.user_account_other.partner_id,
|
||||
@@ -389,7 +383,6 @@ class TestAccountComposerPerformance(AccountMoveSendBase):
|
||||
)
|
||||
|
||||
# composer configuration
|
||||
self.assertEqual(len(composer.mail_attachments_widget), 3)
|
||||
self.assertIn(f'SpanishBody for {test_move.name}', composer.mail_body,
|
||||
'Should be translated, based on template')
|
||||
self.assertEqual(composer.move_ids, test_move)
|
||||
@@ -435,3 +428,295 @@ class TestAccountComposerPerformance(AccountMoveSendBase):
|
||||
author=self.user_account_other.partner_id,
|
||||
content='access_token='
|
||||
)
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestAccountMoveSendCommon(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
cls.partner_a.email = "partner_a@tsointsoin"
|
||||
cls.partner_b.email = "partner_b@tsointsoin"
|
||||
|
||||
def _assert_mail_attachments_widget(self, wizard, expected_values_list):
|
||||
self.assertEqual(len(wizard.mail_attachments_widget), len(expected_values_list))
|
||||
for values, expected_values in zip(wizard.mail_attachments_widget, expected_values_list):
|
||||
try:
|
||||
int(values['id'])
|
||||
check_id_needed = True
|
||||
except ValueError:
|
||||
check_id_needed = False
|
||||
self.assertDictEqual(
|
||||
{k: v for k, v in values.items() if not check_id_needed and k != 'id'},
|
||||
{k: v for k, v in expected_values.items() if not check_id_needed and k != 'id'},
|
||||
)
|
||||
|
||||
def create_send_and_print(self, invoices, **kwargs):
|
||||
template = self.env.ref(invoices._get_mail_template())
|
||||
return self.env['account.move.send']\
|
||||
.with_context(active_model='account.move', active_ids=invoices.ids)\
|
||||
.create({
|
||||
'mail_template_id': template.id,
|
||||
**kwargs,
|
||||
})
|
||||
|
||||
def _get_mail_message(self, move):
|
||||
return self.env['mail.message'].search([('model', '=', move._name), ('res_id', '=', move.id)], limit=1)
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestAccountMoveSend(TestAccountMoveSendCommon):
|
||||
|
||||
def test_send_mail_warning_message(self):
|
||||
invoice = self.init_invoice("out_invoice", amounts=[1000], post=True)
|
||||
self.partner_a.email = None
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
self.assertTrue(wizard.send_mail_warning_message)
|
||||
|
||||
# Fix the partner.
|
||||
self.partner_a.email = "turlututu@tsointsoin"
|
||||
wizard.invalidate_recordset(fnames=['send_mail_warning_message'])
|
||||
self.assertFalse(wizard.send_mail_warning_message)
|
||||
|
||||
def test_invoice_single(self):
|
||||
invoice = self.init_invoice("out_invoice", amounts=[1000], post=True)
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
self.assertRecordValues(wizard, [{
|
||||
'mode': 'invoice_single',
|
||||
'enable_download': True,
|
||||
'checkbox_download': True,
|
||||
'enable_send_mail': True,
|
||||
'display_mail_composer': True,
|
||||
'send_mail_readonly': False,
|
||||
'checkbox_send_mail': True,
|
||||
'mail_lang': 'en_US',
|
||||
'mail_partner_ids': wizard.move_ids.partner_id.ids,
|
||||
}])
|
||||
self.assertTrue(wizard.mail_subject)
|
||||
self.assertTrue(wizard.mail_body)
|
||||
self._assert_mail_attachments_widget(wizard, [{
|
||||
'mimetype': 'application/pdf',
|
||||
'name': 'INV_2019_00001.pdf',
|
||||
'placeholder': True,
|
||||
}])
|
||||
|
||||
# Process.
|
||||
results = wizard.action_send_and_print()
|
||||
self.assertEqual(results['type'], 'ir.actions.act_url')
|
||||
self.assertRecordValues(wizard, [{'mode': 'done'}])
|
||||
|
||||
# The PDF has been successfully generated.
|
||||
pdf_report = invoice.invoice_pdf_report_id
|
||||
self.assertTrue(pdf_report)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice._name),
|
||||
('res_id', '=', invoice.id),
|
||||
('res_field', '=', 'invoice_pdf_report_file'),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 1)
|
||||
|
||||
# Send it again. The PDF must not be created again.
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
results = wizard.action_send_and_print()
|
||||
self.assertEqual(results['type'], 'ir.actions.act_url')
|
||||
self.assertRecordValues(wizard, [{'mode': 'done'}])
|
||||
self.assertRecordValues(invoice, [{'invoice_pdf_report_id': pdf_report.id}])
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice._name),
|
||||
('res_id', '=', invoice.id),
|
||||
('res_field', '=', 'invoice_pdf_report_file'),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 1)
|
||||
|
||||
def test_invoice_multi(self):
|
||||
invoice1 = self.init_invoice("out_invoice", partner=self.partner_a, amounts=[1000], post=True)
|
||||
invoice2 = self.init_invoice("out_invoice", partner=self.partner_b, amounts=[1000], post=True)
|
||||
|
||||
wizard = self.create_send_and_print(invoice1 + invoice2)
|
||||
wizard_values = {
|
||||
'mode': 'invoice_multi',
|
||||
'enable_download': False,
|
||||
'checkbox_download': False,
|
||||
'enable_send_mail': True,
|
||||
'display_mail_composer': False,
|
||||
'send_mail_readonly': False,
|
||||
'checkbox_send_mail': True,
|
||||
'mail_lang': 'en_US',
|
||||
'mail_partner_ids': [],
|
||||
'mail_subject': False,
|
||||
'mail_body': False,
|
||||
'mail_attachments_widget': False,
|
||||
}
|
||||
self.assertRecordValues(wizard, [wizard_values])
|
||||
|
||||
# Process.
|
||||
results = wizard.action_send_and_print()
|
||||
self.assertEqual(results['type'], 'ir.actions.act_window_close')
|
||||
self.assertRecordValues(wizard, [{'mode': 'invoice_multi'}])
|
||||
|
||||
# Awaiting the CRON.
|
||||
self.assertFalse(invoice1.invoice_pdf_report_id)
|
||||
self.assertFalse(invoice2.invoice_pdf_report_id)
|
||||
|
||||
# Run the CRON.
|
||||
wizard.action_send_and_print(from_cron=True)
|
||||
self.assertTrue(invoice1.invoice_pdf_report_id)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice1._name),
|
||||
('res_id', '=', invoice1.id),
|
||||
('res_field', '=', 'invoice_pdf_report_file'),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 1)
|
||||
self.assertTrue(invoice2.invoice_pdf_report_id)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice2._name),
|
||||
('res_id', '=', invoice2.id),
|
||||
('res_field', '=', 'invoice_pdf_report_file'),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 1)
|
||||
|
||||
# Mix already sent invoice with a new one.
|
||||
invoice3 = self.init_invoice("out_invoice", partner=self.partner_b, amounts=[1000], post=True)
|
||||
wizard = self.create_send_and_print(invoice1 + invoice2 + invoice3)
|
||||
self.assertRecordValues(wizard, [wizard_values])
|
||||
wizard.action_send_and_print(from_cron=True)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice1._name),
|
||||
('res_id', '=', invoice1.id),
|
||||
('res_field', '=', 'invoice_pdf_report_file'),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 1)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice2._name),
|
||||
('res_id', '=', invoice2.id),
|
||||
('res_field', '=', 'invoice_pdf_report_file'),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 1)
|
||||
self.assertTrue(invoice1.invoice_pdf_report_id)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice3._name),
|
||||
('res_id', '=', invoice3.id),
|
||||
('res_field', '=', 'invoice_pdf_report_file'),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 1)
|
||||
|
||||
def test_invoice_mail_attachments_widget(self):
|
||||
invoice = self.init_invoice("out_invoice", amounts=[1000], post=True)
|
||||
|
||||
# Add a new attachment on the mail_template.
|
||||
template = self.env.ref(invoice._get_mail_template())
|
||||
extra_attachment = self.env['ir.attachment'].create({'name': "extra_attachment", 'raw': b'bar'})
|
||||
template.attachment_ids = [Command.link(extra_attachment.id)]
|
||||
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
pdf_report_values = {
|
||||
'mimetype': 'application/pdf',
|
||||
'name': 'INV_2019_00001.pdf',
|
||||
'placeholder': True,
|
||||
}
|
||||
extra_attachment_values = {
|
||||
'mimetype': 'application/octet-stream',
|
||||
'name': extra_attachment.name,
|
||||
'placeholder': False,
|
||||
'mail_template_id': template.id,
|
||||
}
|
||||
self._assert_mail_attachments_widget(wizard, [pdf_report_values, extra_attachment_values])
|
||||
|
||||
# Add a new attachment manually.
|
||||
manual_attachment = self.env['ir.attachment'].create({'name': "manual_attachment", 'raw': b'foo'})
|
||||
manual_attachment_values = {
|
||||
'id': manual_attachment.id,
|
||||
'name': manual_attachment.name,
|
||||
'mimetype': manual_attachment.mimetype,
|
||||
'placeholder': False,
|
||||
'manual': True,
|
||||
}
|
||||
wizard.mail_attachments_widget = wizard.mail_attachments_widget + [manual_attachment_values]
|
||||
|
||||
# Add an attachment to a new mail_template and change it.
|
||||
new_mail_template = wizard.mail_template_id.copy()
|
||||
extra_attachment2 = self.env['ir.attachment'].create({'name': "extra_attachment2", 'raw': b'bar'})
|
||||
new_mail_template.attachment_ids = [Command.set(extra_attachment2.ids)]
|
||||
extra_attachment2_values = {
|
||||
'mimetype': 'application/octet-stream',
|
||||
'name': extra_attachment2.name,
|
||||
'placeholder': False,
|
||||
'mail_template_id': new_mail_template.id,
|
||||
}
|
||||
|
||||
wizard.mail_template_id = new_mail_template
|
||||
self._assert_mail_attachments_widget(wizard, [
|
||||
pdf_report_values,
|
||||
extra_attachment2_values,
|
||||
manual_attachment_values,
|
||||
])
|
||||
|
||||
# Send.
|
||||
wizard.action_send_and_print()
|
||||
message = self._get_mail_message(invoice)
|
||||
self.assertRecordValues(message, [{
|
||||
'attachment_ids': [
|
||||
manual_attachment.id,
|
||||
invoice.invoice_pdf_report_id.id,
|
||||
extra_attachment2.id,
|
||||
],
|
||||
}])
|
||||
|
||||
# Resend.
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
pdf_report_values['id'] = invoice.invoice_pdf_report_id.id
|
||||
self._assert_mail_attachments_widget(wizard, [
|
||||
pdf_report_values,
|
||||
extra_attachment_values,
|
||||
])
|
||||
|
||||
# Switch the template.
|
||||
wizard.mail_template_id = new_mail_template
|
||||
self._assert_mail_attachments_widget(wizard, [
|
||||
pdf_report_values,
|
||||
extra_attachment2_values,
|
||||
])
|
||||
|
||||
# Send.
|
||||
wizard.action_send_and_print()
|
||||
message = self._get_mail_message(invoice)
|
||||
self.assertRecordValues(message, [{
|
||||
'attachment_ids': [
|
||||
invoice.invoice_pdf_report_id.id,
|
||||
extra_attachment2.id,
|
||||
],
|
||||
}])
|
||||
|
||||
def test_proforma_pdf(self):
|
||||
invoice = self.init_invoice("out_invoice", amounts=[1000], post=True)
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
|
||||
def _hook_invoice_document_before_pdf_report_render(self, invoice, invoice_data):
|
||||
invoice_data['error'] = 'test_proforma_pdf'
|
||||
|
||||
# Process.
|
||||
with patch.object(type(wizard), '_hook_invoice_document_before_pdf_report_render', _hook_invoice_document_before_pdf_report_render):
|
||||
results = wizard.action_send_and_print(allow_fallback_pdf=True)
|
||||
|
||||
self.assertEqual(results['type'], 'ir.actions.act_url')
|
||||
self.assertRecordValues(wizard, [{'mode': 'done'}])
|
||||
|
||||
# The PDF is not generated but a proforma.
|
||||
self.assertFalse(invoice.invoice_pdf_report_id)
|
||||
|
||||
def test_error_but_continue(self):
|
||||
invoice = self.init_invoice("out_invoice", amounts=[1000], post=True)
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
|
||||
def _hook_invoice_document_before_pdf_report_render(self, invoice, invoice_data):
|
||||
invoice_data['error'] = 'prout'
|
||||
invoice_data['error_but_continue'] = True
|
||||
|
||||
# Process.
|
||||
with patch.object(type(wizard), '_hook_invoice_document_before_pdf_report_render', _hook_invoice_document_before_pdf_report_render):
|
||||
results = wizard.action_send_and_print(allow_fallback_pdf=True)
|
||||
|
||||
self.assertEqual(results['type'], 'ir.actions.act_url')
|
||||
self.assertRecordValues(wizard, [{'mode': 'done'}])
|
||||
|
||||
# The PDF is generated even in case of error.
|
||||
self.assertTrue(invoice.invoice_pdf_report_id)
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<div class="mt-5 clearfix">
|
||||
<div class="page mb-4">
|
||||
<h2>
|
||||
<span t-if="proforma">PROFORMA</span>
|
||||
<span t-if="o.move_type == 'out_invoice' and o.state == 'posted'">Invoice</span>
|
||||
<span t-elif="o.move_type == 'out_invoice' and o.state == 'draft'">Draft Invoice</span>
|
||||
<span t-elif="o.move_type == 'out_invoice' and o.state == 'cancel'">Cancelled Invoice</span>
|
||||
|
||||
@@ -95,45 +95,66 @@ class AccountMoveSend(models.Model):
|
||||
.with_context(lang=lang)\
|
||||
._render_field(field, move.ids, **kwargs)[move._origin.id]
|
||||
|
||||
@api.model
|
||||
def _get_default_mode_from_moves(self, moves):
|
||||
if all(x.is_invoice(include_receipts=True) for x in moves):
|
||||
return 'invoice_single' if len(moves) == 1 else 'invoice_multi'
|
||||
return None
|
||||
def _get_available_field_values_in_multi(self, move):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'move_ids': [Command.set(move.ids)],
|
||||
'mail_template_id': self.mail_template_id.id,
|
||||
'checkbox_send_mail': self.checkbox_send_mail,
|
||||
}
|
||||
|
||||
def _get_placeholder_mail_attachments_data(self, move):
|
||||
""" Returns all the placeholder data.
|
||||
Should be extended to add placeholder based on the checkboxes.
|
||||
|
||||
:param: move: The current move.
|
||||
:returns: A list of dictionary for each placeholder.
|
||||
* id: str: The (fake) id of the attachment, this is needed in rendering in t-key.
|
||||
* name: str: The name of the attachment.
|
||||
* mimetype: str: The mimetype of the attachment.
|
||||
* placeholder bool: Should be true to prevent download / deletion.
|
||||
"""
|
||||
if move.invoice_pdf_report_id:
|
||||
return []
|
||||
|
||||
filename = move._get_invoice_pdf_report_filename()
|
||||
return [{
|
||||
'id': f'placeholder_{filename}',
|
||||
'name': filename,
|
||||
'mimetype': 'application/pdf',
|
||||
'placeholder': True,
|
||||
}]
|
||||
|
||||
@api.model
|
||||
def _get_default_lang(self, mail_template, move):
|
||||
if mail_template:
|
||||
return mail_template._render_lang([move.id]).get(move.id)
|
||||
else:
|
||||
return get_lang(self.env).code
|
||||
def _get_invoice_extra_attachments(self, move):
|
||||
return move.invoice_pdf_report_id
|
||||
|
||||
@api.model
|
||||
def _get_default_mail_partners(self, mail_template, lang, move):
|
||||
partners = self.env['res.partner'].with_company(move.company_id)
|
||||
if mail_template.email_to:
|
||||
for mail_data in tools.email_split(mail_template.email_to):
|
||||
partners |= partners.find_or_create(mail_data)
|
||||
if mail_template.email_cc:
|
||||
for mail_data in tools.email_split(mail_template.email_cc):
|
||||
partners |= partners.find_or_create(mail_data)
|
||||
if mail_template.partner_to:
|
||||
partner_to = self._get_mail_default_field_value_from_template(mail_template, lang, move, 'partner_to')
|
||||
partner_ids = [int(pid) for pid in partner_to.split(',') if pid]
|
||||
partners |= self.env['res.partner'].sudo().browse(partner_ids).exists()
|
||||
return partners
|
||||
def _get_invoice_extra_attachments_data(self, move):
|
||||
return [
|
||||
{
|
||||
'id': attachment.id,
|
||||
'name': attachment.name,
|
||||
'mimetype': attachment.mimetype,
|
||||
'placeholder': False,
|
||||
}
|
||||
for attachment in self._get_invoice_extra_attachments(move)
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _get_default_mail_body(self, mail_template, lang, move):
|
||||
return self._get_mail_default_field_value_from_template(mail_template, lang, move, 'body_html', options={'post_process': True})
|
||||
|
||||
@api.model
|
||||
def _get_default_mail_subject(self, mail_template, lang, move):
|
||||
return self._get_mail_default_field_value_from_template(mail_template, lang, move, 'subject')
|
||||
|
||||
@api.model
|
||||
def _get_default_email_from(self, mail_template, lang, move):
|
||||
return self._get_mail_default_field_value_from_template(mail_template, lang, move, 'email_from')
|
||||
def _get_mail_template_attachments_data(self, mail_template):
|
||||
""" Returns all the placeholder data and mail template data
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'id': attachment.id,
|
||||
'name': attachment.name,
|
||||
'mimetype': attachment.mimetype,
|
||||
'placeholder': False,
|
||||
'mail_template_id': mail_template.id,
|
||||
}
|
||||
for attachment in mail_template.attachment_ids
|
||||
]
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
@@ -149,10 +170,10 @@ class AccountMoveSend(models.Model):
|
||||
@api.depends('move_ids')
|
||||
def _compute_mode(self):
|
||||
for wizard in self:
|
||||
if wizard.move_ids:
|
||||
wizard.mode = wizard._get_default_mode_from_moves(wizard.move_ids)
|
||||
else:
|
||||
wizard.mode = 'done'
|
||||
mode = 'done'
|
||||
if wizard.move_ids and all(x.is_invoice(include_receipts=True) for x in wizard.move_ids):
|
||||
mode = 'invoice_single' if len(wizard.move_ids) == 1 else 'invoice_multi'
|
||||
wizard.mode = mode
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_enable_download(self):
|
||||
@@ -162,7 +183,7 @@ class AccountMoveSend(models.Model):
|
||||
@api.depends('enable_download')
|
||||
def _compute_checkbox_download(self):
|
||||
for wizard in self:
|
||||
wizard.checkbox_download = wizard.company_id.invoice_is_download
|
||||
wizard.checkbox_download = wizard.enable_download and wizard.company_id.invoice_is_download
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_send_mail_extra_fields(self):
|
||||
@@ -196,16 +217,31 @@ class AccountMoveSend(models.Model):
|
||||
@api.depends('mail_template_id')
|
||||
def _compute_mail_lang(self):
|
||||
for wizard in self:
|
||||
if wizard.mode == 'invoice_single':
|
||||
wizard.mail_lang = wizard._get_default_lang(wizard.mail_template_id, wizard.move_ids)
|
||||
if wizard.mail_template_id and wizard.mode == 'invoice_single':
|
||||
move = wizard.move_ids
|
||||
wizard.mail_lang = wizard.mail_template_id._render_lang([move.id]).get(move.id)
|
||||
else:
|
||||
wizard.mail_lang = False
|
||||
wizard.mail_lang = get_lang(self.env).code
|
||||
|
||||
@api.depends('mail_template_id', 'mail_lang')
|
||||
def _compute_mail_partner_ids(self):
|
||||
for wizard in self:
|
||||
if wizard.mail_template_id and wizard.mode == 'invoice_single':
|
||||
wizard.mail_partner_ids = wizard._get_default_mail_partners(wizard.mail_template_id, wizard.mail_lang, wizard.move_ids)
|
||||
move = wizard.move_ids
|
||||
partners = self.env['res.partner'].with_company(move.company_id)
|
||||
mail_template = wizard.mail_template_id
|
||||
if mail_template.email_to:
|
||||
for mail_data in tools.email_split(mail_template.email_to):
|
||||
partners |= partners.find_or_create(mail_data)
|
||||
if mail_template.email_cc:
|
||||
for mail_data in tools.email_split(mail_template.email_cc):
|
||||
partners |= partners.find_or_create(mail_data)
|
||||
if mail_template.partner_to:
|
||||
partner_to = wizard._get_mail_default_field_value_from_template(mail_template, wizard.mail_lang, move,
|
||||
'partner_to')
|
||||
partner_ids = [int(pid) for pid in partner_to.split(',') if pid]
|
||||
partners |= self.env['res.partner'].sudo().browse(partner_ids).exists()
|
||||
wizard.mail_partner_ids = partners
|
||||
else:
|
||||
wizard.mail_partner_ids = []
|
||||
|
||||
@@ -213,7 +249,12 @@ class AccountMoveSend(models.Model):
|
||||
def _compute_mail_subject(self):
|
||||
for wizard in self:
|
||||
if wizard.mail_template_id and wizard.mode == 'invoice_single':
|
||||
wizard.mail_subject = wizard._get_default_mail_subject(wizard.mail_template_id, wizard.mail_lang, wizard.move_ids)
|
||||
wizard.mail_subject = wizard._get_mail_default_field_value_from_template(
|
||||
wizard.mail_template_id,
|
||||
wizard.mail_lang,
|
||||
wizard.move_ids,
|
||||
'subject',
|
||||
)
|
||||
else:
|
||||
wizard.mail_subject = None
|
||||
|
||||
@@ -221,112 +262,147 @@ class AccountMoveSend(models.Model):
|
||||
def _compute_mail_body(self):
|
||||
for wizard in self:
|
||||
if wizard.mail_template_id and wizard.mode == 'invoice_single':
|
||||
wizard.mail_body = wizard._get_default_mail_body(wizard.mail_template_id, wizard.mail_lang, wizard.move_ids)
|
||||
wizard.mail_body = wizard._get_mail_default_field_value_from_template(
|
||||
wizard.mail_template_id,
|
||||
wizard.mail_lang,
|
||||
wizard.move_ids,
|
||||
'body_html',
|
||||
options={'post_process': True},
|
||||
)
|
||||
else:
|
||||
wizard.mail_body = None
|
||||
|
||||
@api.depends('mail_template_id', 'mail_lang')
|
||||
def _compute_mail_attachments_widget(self):
|
||||
for wizard in self:
|
||||
if wizard.mail_attachments_widget: # When checkboxes are ticked in UI
|
||||
wizard.mail_attachments_widget = wizard._get_placeholder_mail_attachments_data(wizard.move_ids)\
|
||||
+ wizard._get_selected_attachments_data()
|
||||
elif wizard.mode == 'invoice_single':
|
||||
wizard.mail_attachments_widget = wizard\
|
||||
._get_default_mail_attachments_data(wizard.mail_template_id, wizard.move_ids)
|
||||
if wizard.mode == 'invoice_single':
|
||||
manual_attachments_data = [x for x in wizard.mail_attachments_widget or [] if x.get('manual')]
|
||||
wizard.mail_attachments_widget = \
|
||||
wizard._get_placeholder_mail_attachments_data(wizard.move_ids) \
|
||||
+ wizard._get_invoice_extra_attachments_data(wizard.move_ids) \
|
||||
+ wizard._get_mail_template_attachments_data(wizard.mail_template_id) \
|
||||
+ manual_attachments_data
|
||||
else:
|
||||
wizard.mail_attachments_widget = []
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# ATTACHMENTS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _get_linked_attachments(self, move):
|
||||
""" Returns all the attachments ids linked to the move to be sent in the email.
|
||||
Should be extended to add attachments based on the checkboxes.
|
||||
"""
|
||||
return move.invoice_pdf_report_id
|
||||
|
||||
def _get_mail_attachments_ids(self, mail_template, move):
|
||||
""" Returns all the attachments ids linked to the move to be sent in the email
|
||||
AND the attachments selected by the user or the one from the template (invoice multi)
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.mode == 'invoice_single':
|
||||
return self._get_selected_attachments_ids() + self._get_linked_attachments(move).ids
|
||||
else:
|
||||
return mail_template.attachment_ids.ids + self._get_linked_attachments(move).ids
|
||||
|
||||
def _get_selected_attachments_ids(self):
|
||||
""" Returns all the attachments ids selected by the user (invoice single)
|
||||
"""
|
||||
return [vals['id'] for vals in self._get_selected_attachments_data()]
|
||||
|
||||
def _get_selected_attachments_data(self):
|
||||
""" Returns all the attachments data selected by the user (invoice single)
|
||||
"""
|
||||
return list(filter(lambda x: not x.get('placeholder'), self.mail_attachments_widget))
|
||||
|
||||
def _get_placeholder_mail_attachments_data(self, move):
|
||||
""" Returns all the placeholder data.
|
||||
Should be extended to add placeholder based on the checkboxes.
|
||||
|
||||
:returns: A list of dictionary for each placeholder.
|
||||
* id: str: The (fake) id of the attachment, this is needed in rendering in t-key.
|
||||
* name: str: The name of the attachment.
|
||||
* mimetype: str: The mimetype of the attachment.
|
||||
* placeholder bool: should be true to prevent download / deletion
|
||||
"""
|
||||
self.ensure_one()
|
||||
filename = self._get_invoice_pdf_report_filename(move)
|
||||
return [{
|
||||
'id': f'placeholder_{filename}',
|
||||
'name': filename,
|
||||
'mimetype': 'application/pdf',
|
||||
'placeholder': True,
|
||||
}]
|
||||
|
||||
def _get_default_mail_attachments_data(self, mail_template, move):
|
||||
""" Returns all the placeholder data and mail template data
|
||||
"""
|
||||
results = []
|
||||
|
||||
for attachment in mail_template.attachment_ids:
|
||||
results.append({
|
||||
'id': attachment.id,
|
||||
'name': attachment.name,
|
||||
'mimetype': attachment.mimetype,
|
||||
'placeholder': False,
|
||||
})
|
||||
|
||||
return self._get_placeholder_mail_attachments_data(move) + results
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# BUSINESS ACTIONS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _get_invoice_pdf_report_filename(self, move):
|
||||
return f"{move.name.replace('/', '_')}.pdf"
|
||||
def _need_invoice_document(self, invoice):
|
||||
""" Determine if we need to generate the documents for the invoice passed as parameter.
|
||||
|
||||
@api.model
|
||||
def _need_document(self, move):
|
||||
return not move.invoice_pdf_report_id and move.state == 'posted'
|
||||
:param invoice: An account.move record.
|
||||
:return: True if the PDF / electronic documents must be generated, False otherwise.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.mode == 'invoice_single' and not invoice.invoice_pdf_report_id and invoice.state == 'posted'
|
||||
|
||||
@api.model
|
||||
def _download(self, move):
|
||||
""" Download the PDF. """
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f'/web/content/{move.invoice_pdf_report_id.id}?download=true',
|
||||
'close': True, # close the wizard
|
||||
def _hook_invoice_document_before_pdf_report_render(self, invoice, invoice_data):
|
||||
""" Hook allowing to add some extra data for the invoice passed as parameter before the rendering of the pdf
|
||||
report.
|
||||
|
||||
:param invoice: An account.move record.
|
||||
:param invoice_data: The collected data for the invoice so far.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
def _prepare_invoice_pdf_report(self, invoice, invoice_data):
|
||||
""" Prepare the pdf report for the invoice passed as parameter.
|
||||
|
||||
:param invoice: An account.move record.
|
||||
:param invoice_data: The collected data for the invoice so far.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if invoice.invoice_pdf_report_id:
|
||||
return
|
||||
|
||||
content, _report_format = self.env['ir.actions.report']._render('account.account_invoices', invoice.ids)
|
||||
|
||||
invoice_data['pdf_attachment_values'] = {
|
||||
'raw': content,
|
||||
'name': invoice._get_invoice_pdf_report_filename(),
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': invoice._name,
|
||||
'res_id': invoice.id,
|
||||
'res_field': 'invoice_pdf_report_file', # Binary field
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _send_mail(self, mail_template, move, **kwargs):
|
||||
""" Send the journal entries passed as parameter by mail. """
|
||||
def _prepare_invoice_proforma_pdf_report(self, invoice, invoice_data):
|
||||
""" Prepare the proforma pdf report for the invoice passed as parameter.
|
||||
|
||||
:param invoice: An account.move record.
|
||||
:param invoice_data: The collected data for the invoice so far.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
content, _report_format = self.env['ir.actions.report']._render('account.account_invoices', invoice.ids, data={'proforma': True})
|
||||
|
||||
invoice_data['proforma_pdf_attachment_values'] = {
|
||||
'raw': content,
|
||||
'name': invoice._get_invoice_proforma_pdf_report_filename(),
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': invoice._name,
|
||||
'res_id': invoice.id,
|
||||
}
|
||||
|
||||
def _hook_invoice_document_after_pdf_report_render(self, invoice, invoice_data):
|
||||
""" Hook allowing to add some extra data for the invoice passed as parameter after the rendering of the
|
||||
(proforma) pdf report.
|
||||
|
||||
:param invoice: An account.move record.
|
||||
:param invoice_data: The collected data for the invoice so far.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
def _link_invoice_documents(self, invoice, invoice_data):
|
||||
""" Create the attachments containing the pdf/electronic documents for the invoice passed as parameter.
|
||||
|
||||
:param invoice: An account.move record.
|
||||
:param invoice_data: The collected data for the invoice so far.
|
||||
"""
|
||||
# create an attachment that will become 'invoice_pdf_report_file'
|
||||
# note: Binary is used for security reason
|
||||
invoice.message_main_attachment_id = self.env['ir.attachment'].create(invoice_data['pdf_attachment_values'])
|
||||
invoice.invalidate_recordset(fnames=['invoice_pdf_report_id', 'invoice_pdf_report_file'])
|
||||
self.env.add_to_compute(invoice._fields['is_move_sent'], invoice)
|
||||
|
||||
def _hook_if_errors(self, moves_data, from_cron=False, allow_fallback_pdf=False):
|
||||
""" Process errors found so far when generating the documents.
|
||||
|
||||
:param from_cron: Flag indicating if the method is called from a cron. In that case, we avoid raising any
|
||||
error.
|
||||
:param allow_fallback_pdf: In case of error when generating the documents for invoices, generate a
|
||||
proforma PDF report instead.
|
||||
"""
|
||||
allow_raising = not from_cron and not allow_fallback_pdf
|
||||
for move, move_data in moves_data.items():
|
||||
error = move_data['error']
|
||||
if allow_raising:
|
||||
raise UserError(error)
|
||||
|
||||
move.with_context(no_new_invoice=True).message_post(body=error)
|
||||
|
||||
def _hook_if_success(self, moves_data, from_cron=False, allow_fallback_pdf=False):
|
||||
""" Process successful documents.
|
||||
|
||||
:param from_cron: Flag indicating if the method is called from a cron. In that case, we avoid raising any
|
||||
error.
|
||||
:param allow_fallback_pdf: In case of error when generating the documents for invoices, generate a
|
||||
proforma PDF report instead.
|
||||
"""
|
||||
send_mail = self.enable_send_mail and self.checkbox_send_mail
|
||||
|
||||
# Send mail.
|
||||
if send_mail:
|
||||
self._send_mails(moves_data)
|
||||
|
||||
def _send_mail(self, move, **kwargs):
|
||||
""" Send the journal entry passed as parameter by mail. """
|
||||
self.ensure_one()
|
||||
partner_ids = kwargs.get('partner_ids', [])
|
||||
mail_template = self.mail_template_id
|
||||
|
||||
move\
|
||||
.with_context(
|
||||
@@ -345,135 +421,158 @@ class AccountMoveSend(models.Model):
|
||||
},
|
||||
)
|
||||
|
||||
def _send_mails(self, moves):
|
||||
def _get_mail_params(self, move):
|
||||
self.ensure_one()
|
||||
|
||||
if self.mode != 'invoice_single':
|
||||
return
|
||||
|
||||
# We must ensure the newly created PDF are added. At this point, the PDF has been generated but not added
|
||||
# to 'mail_attachments_widget'.
|
||||
attachment_ids = list(set([
|
||||
x['id']
|
||||
for x in self.mail_attachments_widget + self._get_invoice_extra_attachments_data(self.move_ids)
|
||||
if not x.get('placeholder')
|
||||
]))
|
||||
|
||||
return {
|
||||
'body': self.mail_body,
|
||||
'subject': self.mail_subject,
|
||||
'partner_ids': self.mail_partner_ids.ids,
|
||||
'attachment_ids': attachment_ids,
|
||||
}
|
||||
|
||||
def _send_mails(self, moves_data):
|
||||
self.ensure_one()
|
||||
subtype = self.env.ref('mail.mt_comment')
|
||||
mail_template = self.mail_template_id
|
||||
|
||||
for move in moves:
|
||||
attachment_ids = self._get_mail_attachments_ids(mail_template, move)
|
||||
mail_lang = self.mail_lang or self._get_default_lang(mail_template, move)
|
||||
partners = self.mail_partner_ids or self._get_default_mail_partners(mail_template, mail_lang, move)
|
||||
body = self.mail_body or self._get_default_mail_body(mail_template, mail_lang, move)
|
||||
subject = self.mail_subject or self._get_default_mail_subject(mail_template, mail_lang, move)
|
||||
email_from = self._get_default_email_from(mail_template, mail_lang, move)
|
||||
for move, move_data in moves_data.items():
|
||||
form = move_data['_form']
|
||||
mail_params = form._get_mail_params(move)
|
||||
if not mail_params:
|
||||
continue
|
||||
|
||||
if move_data.get('proforma_pdf_attachment'):
|
||||
mail_params['attachment_ids'].append(move_data['proforma_pdf_attachment'].id)
|
||||
|
||||
mail_lang = form.mail_lang
|
||||
email_from = form._get_mail_default_field_value_from_template(mail_template, mail_lang, move, 'email_from')
|
||||
model_description = move.with_context(lang=mail_lang).type_name
|
||||
|
||||
self._send_mail(
|
||||
mail_template,
|
||||
form._send_mail(
|
||||
move,
|
||||
body=body,
|
||||
subject=subject,
|
||||
email_from=email_from,
|
||||
subtype_id=subtype.id,
|
||||
partner_ids=partners.ids,
|
||||
attachment_ids=attachment_ids,
|
||||
model_description=model_description,
|
||||
email_from=email_from,
|
||||
**mail_params,
|
||||
)
|
||||
|
||||
def _generate_documents_success_hook(self, moves, from_cron):
|
||||
send_mail = self.enable_send_mail and self.checkbox_send_mail
|
||||
|
||||
# Send mail.
|
||||
if send_mail:
|
||||
self._send_mails(moves)
|
||||
|
||||
return moves
|
||||
|
||||
def _generate_documents_failed_hook(self, errors, from_cron):
|
||||
for move, error in errors:
|
||||
if from_cron:
|
||||
move\
|
||||
.with_context(no_new_invoice=True)\
|
||||
.message_post(body=error)
|
||||
else:
|
||||
raise UserError(error)
|
||||
|
||||
def _link_document(self, invoice, prepared_data):
|
||||
""" Link prepared_data to the record
|
||||
"""
|
||||
# create an attachment that will become 'invoice_pdf_report_file'
|
||||
# note: Binary is used for security reason
|
||||
self.env['ir.attachment'].create(prepared_data['pdf_attachment_values'])
|
||||
invoice.invalidate_model(fnames=['invoice_pdf_report_id', 'invoice_pdf_report_file'])
|
||||
self.env.add_to_compute(invoice._fields['is_move_sent'], invoice)
|
||||
|
||||
def _postprocess_document(self, invoice, prepared_data):
|
||||
""" Postprocess prepared_data before it gets linked to the record
|
||||
"""
|
||||
def _call_web_service(self, invoices_data):
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
|
||||
def _render_document(self, invoice, prepared_data):
|
||||
""" Extend prepared_data with the rendered documents
|
||||
def _generate_invoice_documents(self, invoices_data, allow_fallback_pdf=False):
|
||||
""" Generate the invoice PDF and electronic documents.
|
||||
|
||||
:param allow_fallback_pdf: In case of error when generating the documents for invoices, generate a
|
||||
proforma PDF report instead.
|
||||
:param invoices_data: The collected data for invoices so far.
|
||||
"""
|
||||
self.ensure_one()
|
||||
for invoice, invoice_data in invoices_data.items():
|
||||
form = invoice_data['_form']
|
||||
if form._need_invoice_document(invoice):
|
||||
form._hook_invoice_document_before_pdf_report_render(invoice, invoice_data)
|
||||
invoice_data['blocking_error'] = invoice_data.get('error') \
|
||||
and not (allow_fallback_pdf and invoice_data.get('error_but_continue'))
|
||||
if invoice_data['blocking_error']:
|
||||
continue
|
||||
|
||||
content, _report_format = self.env['ir.actions.report']._render('account.account_invoices', invoice.ids)
|
||||
invoices_data_web_service = {
|
||||
invoice: invoice_data
|
||||
for invoice, invoice_data in invoices_data.items()
|
||||
if not invoice_data.get('error')
|
||||
}
|
||||
if invoices_data_web_service:
|
||||
self._call_web_service(invoices_data_web_service)
|
||||
|
||||
prepared_data['pdf_attachment_values'] = {
|
||||
'raw': content,
|
||||
'name': self._get_invoice_pdf_report_filename(invoice),
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': invoice._name,
|
||||
'res_id': invoice.id,
|
||||
'res_field': 'invoice_pdf_report_file', # Binary field
|
||||
invoices_data_pdf = {
|
||||
invoice: invoice_data
|
||||
for invoice, invoice_data in invoices_data.items()
|
||||
if not invoice_data.get('error') or not invoice_data.get('blocking_error', True)
|
||||
}
|
||||
for invoice, invoice_data in invoices_data_pdf.items():
|
||||
form = invoice_data['_form']
|
||||
if form._need_invoice_document(invoice):
|
||||
form._prepare_invoice_pdf_report(invoice, invoice_data)
|
||||
form._hook_invoice_document_after_pdf_report_render(invoice, invoice_data)
|
||||
form._link_invoice_documents(invoice, invoice_data)
|
||||
|
||||
def _generate_invoice_fallback_documents(self, invoices_data):
|
||||
""" Generate the invoice PDF and electronic documents.
|
||||
|
||||
:param invoices_data: The collected data for invoices so far.
|
||||
"""
|
||||
for invoice, invoice_data in invoices_data.items():
|
||||
if self._need_invoice_document(invoice) and invoice_data.get('error'):
|
||||
invoice_data.pop('error')
|
||||
self._prepare_invoice_proforma_pdf_report(invoice, invoice_data)
|
||||
self._hook_invoice_document_after_pdf_report_render(invoice, invoice_data)
|
||||
invoice_data['proforma_pdf_attachment'] = self.env['ir.attachment']\
|
||||
.create(invoice_data.pop('proforma_pdf_attachment_values'))
|
||||
|
||||
@api.model
|
||||
def _download(self, attachment_id):
|
||||
""" Download the PDF. """
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f'/web/content/{attachment_id}?download=true',
|
||||
'close': True, # close the wizard
|
||||
}
|
||||
|
||||
def _prepare_document(self, invoice):
|
||||
""" To be overridden by modules adding support for different action in send&print.
|
||||
:return: 'prepared_data'
|
||||
{ 'error' : str - if any, will be raised
|
||||
'xxx_values': {} - prefered naming for object to be linked as attachments
|
||||
'xxx_options': {} - prefered naming for object to be used later on }
|
||||
def action_send_and_print(self, from_cron=False, allow_fallback_pdf=False):
|
||||
""" Create the documents and send them to the end customers.
|
||||
|
||||
:param from_cron: Flag indicating if the method is called from a cron. In that case, we avoid raising any
|
||||
error.
|
||||
:param allow_fallback_pdf: In case of error when generating the documents for invoices, generate a
|
||||
proforma PDF report instead.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return {}
|
||||
|
||||
def _call_web_service(self, prepared_data_list):
|
||||
# TO OVERRIDE
|
||||
return
|
||||
|
||||
def _generate_documents(self, moves, from_cron):
|
||||
""" Main entry point to generate the invoice pdf and the related attachments.
|
||||
"""
|
||||
prepared_data_list = []
|
||||
success, errors = self.env['account.move'], []
|
||||
for move in moves:
|
||||
try:
|
||||
if self._need_document(move):
|
||||
prepared_data = self._prepare_document(move)
|
||||
if prepared_data.get('error'):
|
||||
raise UserError(prepared_data['error'])
|
||||
else:
|
||||
prepared_data_list.append((move, prepared_data))
|
||||
except UserError as e:
|
||||
errors.append((move, str(e)))
|
||||
|
||||
try:
|
||||
self._call_web_service(prepared_data_list)
|
||||
except UserError as e:
|
||||
for move, _prepared_data in prepared_data_list:
|
||||
errors.append((move, str(e)))
|
||||
return success, errors
|
||||
|
||||
for move, prepared_data in prepared_data_list:
|
||||
self._render_document(move, prepared_data)
|
||||
self._postprocess_document(move, prepared_data)
|
||||
self._link_document(move, prepared_data)
|
||||
success |= move
|
||||
|
||||
return success, errors
|
||||
|
||||
def action_send_and_print(self, from_cron=False):
|
||||
self.ensure_one()
|
||||
|
||||
download = self.enable_download and self.checkbox_download
|
||||
generate_documents = self.mode == 'invoice_single' or from_cron
|
||||
generate_invoice_documents = self.mode == 'invoice_single' or (self.mode == 'invoice_multi' and from_cron)
|
||||
moves_data = {move: {} for move in self.move_ids}
|
||||
|
||||
if generate_documents:
|
||||
success_moves, errors = self._generate_documents(self.move_ids, from_cron)
|
||||
if len(moves_data) == 1:
|
||||
moves_data[self.move_ids]['_form'] = self
|
||||
else:
|
||||
for move in self.move_ids:
|
||||
moves_data[move]['_form'] = self.new(self._get_available_field_values_in_multi(move))
|
||||
|
||||
self._generate_documents_success_hook(success_moves, from_cron)
|
||||
self._generate_documents_failed_hook(errors, from_cron)
|
||||
if generate_invoice_documents:
|
||||
# Generate all invoice documents.
|
||||
self._generate_invoice_documents(moves_data, allow_fallback_pdf=allow_fallback_pdf)
|
||||
|
||||
# Manage errors.
|
||||
errors = {move: move_data for move, move_data in moves_data.items() if move_data.get('error')}
|
||||
if errors:
|
||||
self._hook_if_errors(errors, from_cron=from_cron, allow_fallback_pdf=allow_fallback_pdf)
|
||||
|
||||
# Cleanup the error if we don't want to block the regular pdf generation.
|
||||
for move_data in errors.values():
|
||||
if move_data.get('pdf_attachment_values'):
|
||||
move_data.pop('error')
|
||||
|
||||
# Fallback in case of error.
|
||||
errors = {move: move_data for move, move_data in moves_data.items() if move_data.get('error')}
|
||||
if allow_fallback_pdf and errors:
|
||||
self._generate_invoice_fallback_documents(errors)
|
||||
|
||||
# Send mail.
|
||||
success = {move: move_data for move, move_data in moves_data.items() if not move_data.get('error')}
|
||||
if success:
|
||||
self._hook_if_success(success, from_cron=from_cron, allow_fallback_pdf=allow_fallback_pdf)
|
||||
|
||||
self.mode = 'done'
|
||||
|
||||
@@ -481,7 +580,11 @@ class AccountMoveSend(models.Model):
|
||||
self.env.ref('account.ir_cron_account_move_send')._trigger()
|
||||
|
||||
if download:
|
||||
return self._download(self.move_ids)
|
||||
attachment = self.move_ids.invoice_pdf_report_id
|
||||
if not attachment:
|
||||
attachment = list(moves_data.values())[0].get('proforma_pdf_attachment')
|
||||
if attachment:
|
||||
return self._download(attachment.id)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
|
||||
@@ -22,7 +22,16 @@ class AccountMoveSend(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_default_enable_ubl_cii_xml(self, move):
|
||||
return not move.invoice_pdf_report_id and move.is_sale_document() and move.partner_id.ubl_cii_format
|
||||
return not move.invoice_pdf_report_id \
|
||||
and not move.ubl_cii_xml_id \
|
||||
and move.is_sale_document() \
|
||||
and bool(move.partner_id.ubl_cii_format)
|
||||
|
||||
def _get_available_field_values_in_multi(self, move):
|
||||
# EXTENDS 'account'
|
||||
values = super()._get_available_field_values_in_multi(move)
|
||||
values['checkbox_ubl_cii_xml'] = self.checkbox_ubl_cii_xml
|
||||
return values
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
@@ -31,18 +40,18 @@ class AccountMoveSend(models.Model):
|
||||
@api.depends('move_ids')
|
||||
def _compute_checkbox_ubl_cii_label(self):
|
||||
for wizard in self:
|
||||
code_to_label = dict(wizard.move_ids.partner_id._fields['ubl_cii_format'].selection)
|
||||
codes = wizard.move_ids.partner_id.mapped('ubl_cii_format')
|
||||
if any(codes):
|
||||
wizard.checkbox_ubl_cii_label = ", ".join(code_to_label[c] for c in set(codes) if c)
|
||||
else:
|
||||
wizard.checkbox_ubl_cii_label = False
|
||||
wizard.checkbox_ubl_cii_label = False
|
||||
if wizard.mode in ('invoice_single', 'invoice_multi'):
|
||||
code_to_label = dict(wizard.move_ids.partner_id._fields['ubl_cii_format'].selection)
|
||||
codes = wizard.move_ids.partner_id.mapped('ubl_cii_format')
|
||||
if any(codes):
|
||||
wizard.checkbox_ubl_cii_label = ", ".join(code_to_label[c] for c in set(codes) if c)
|
||||
|
||||
def _compute_send_mail_extra_fields(self):
|
||||
# EXTENDS 'account'
|
||||
super()._compute_send_mail_extra_fields()
|
||||
for wizard in self:
|
||||
wizard.enable_ubl_cii_xml = any(self._get_default_enable_ubl_cii_xml(m) for m in wizard.move_ids)
|
||||
wizard.enable_ubl_cii_xml = any(wizard._get_default_enable_ubl_cii_xml(m) for m in wizard.move_ids)
|
||||
|
||||
@api.depends('checkbox_ubl_cii_xml')
|
||||
def _compute_mail_attachments_widget(self):
|
||||
@@ -52,16 +61,16 @@ class AccountMoveSend(models.Model):
|
||||
@api.depends('enable_ubl_cii_xml')
|
||||
def _compute_checkbox_ubl_cii_xml(self):
|
||||
for wizard in self:
|
||||
wizard.checkbox_ubl_cii_xml = wizard.company_id.invoice_is_ubl_cii
|
||||
wizard.checkbox_ubl_cii_xml = wizard.enable_ubl_cii_xml and wizard.company_id.invoice_is_ubl_cii
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# ATTACHMENTS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _get_linked_attachments(self, move):
|
||||
def _get_invoice_extra_attachments(self, move):
|
||||
# EXTENDS 'account'
|
||||
return super()._get_linked_attachments(move) + move.ubl_cii_xml_id
|
||||
return super()._get_invoice_extra_attachments(move) + move.ubl_cii_xml_id
|
||||
|
||||
def _needs_ubl_cii_placeholder(self):
|
||||
return self.enable_ubl_cii_xml and self.checkbox_ubl_cii_xml
|
||||
@@ -70,7 +79,7 @@ class AccountMoveSend(models.Model):
|
||||
# EXTENDS 'account'
|
||||
results = super()._get_placeholder_mail_attachments_data(move)
|
||||
|
||||
if self._needs_ubl_cii_placeholder():
|
||||
if self.mode == 'invoice_single' and self._needs_ubl_cii_placeholder():
|
||||
builder = move.partner_id._get_edi_builder()
|
||||
filename = builder._export_invoice_filename(move)
|
||||
results.append({
|
||||
@@ -86,11 +95,11 @@ class AccountMoveSend(models.Model):
|
||||
# BUSINESS ACTIONS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _prepare_document(self, invoice):
|
||||
def _hook_invoice_document_before_pdf_report_render(self, invoice, invoice_data):
|
||||
# EXTENDS 'account'
|
||||
results = super()._prepare_document(invoice)
|
||||
super()._hook_invoice_document_before_pdf_report_render(invoice, invoice_data)
|
||||
|
||||
if self.checkbox_ubl_cii_xml and self._get_default_enable_ubl_cii_xml(invoice):
|
||||
if self.mode == 'invoice_single' and self.checkbox_ubl_cii_xml and self._get_default_enable_ubl_cii_xml(invoice):
|
||||
builder = invoice.partner_id._get_edi_builder()
|
||||
|
||||
xml_content, errors = builder._export_invoice(invoice)
|
||||
@@ -98,41 +107,38 @@ class AccountMoveSend(models.Model):
|
||||
|
||||
# Failed.
|
||||
if errors:
|
||||
return {
|
||||
'error': "".join([
|
||||
_("Errors occured while creating the EDI document (format: %s):", builder._description),
|
||||
"\n",
|
||||
"<p><li>" + "</li><li>".join(errors) + "</li></p>" if self.mode == 'invoice_multi' \
|
||||
else "\n".join(errors)
|
||||
]),
|
||||
invoice_data['error'] = "".join([
|
||||
_("Errors occured while creating the EDI document (format: %s):", builder._description),
|
||||
"\n",
|
||||
"<p><li>" + "</li><li>".join(errors) + "</li></p>" if self.mode == 'invoice_multi' \
|
||||
else "\n".join(errors)
|
||||
])
|
||||
invoice_data['error_but_continue'] = True
|
||||
else:
|
||||
invoice_data['ubl_cii_xml_attachment_values'] = {
|
||||
'name': filename,
|
||||
'raw': xml_content,
|
||||
'mimetype': 'application/xml',
|
||||
'res_model': invoice._name,
|
||||
'res_id': invoice.id,
|
||||
'res_field': 'ubl_cii_xml_file', # Binary field
|
||||
}
|
||||
invoice_data['ubl_cii_xml_options'] = {
|
||||
'ubl_cii_format': invoice.partner_id.ubl_cii_format,
|
||||
'builder': builder,
|
||||
}
|
||||
|
||||
results['ubl_cii_xml_attachment_values'] = {
|
||||
'name': filename,
|
||||
'raw': xml_content,
|
||||
'mimetype': 'application/xml',
|
||||
'res_model': invoice._name,
|
||||
'res_id': invoice.id,
|
||||
'res_field': 'ubl_cii_xml_file', # Binary field
|
||||
}
|
||||
results['ubl_cii_xml_options'] = {
|
||||
'ubl_cii_format': invoice.partner_id.ubl_cii_format,
|
||||
'builder': builder,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
def _postprocess_document(self, invoice, prepared_data):
|
||||
def _hook_invoice_document_after_pdf_report_render(self, invoice, invoice_data):
|
||||
# EXTENDS 'account'
|
||||
super()._postprocess_document(invoice, prepared_data)
|
||||
super()._hook_invoice_document_after_pdf_report_render(invoice, invoice_data)
|
||||
|
||||
# Add PDF to XML
|
||||
if 'ubl_cii_xml_options' in prepared_data and prepared_data['ubl_cii_xml_options']['ubl_cii_format'] != 'facturx':
|
||||
self._postprocess_invoice_ubl_xml(invoice, prepared_data)
|
||||
if 'ubl_cii_xml_options' in invoice_data and invoice_data['ubl_cii_xml_options']['ubl_cii_format'] != 'facturx':
|
||||
self._postprocess_invoice_ubl_xml(invoice, invoice_data)
|
||||
|
||||
# Always silently generate a Factur-X and embed it inside the PDF (inter-portability)
|
||||
if 'ubl_cii_xml_options' in prepared_data and prepared_data['ubl_cii_xml_options']['ubl_cii_format'] == 'facturx':
|
||||
xml_facturx = prepared_data['ubl_cii_xml_attachment_values']['raw']
|
||||
if 'ubl_cii_xml_options' in invoice_data and invoice_data['ubl_cii_xml_options']['ubl_cii_format'] == 'facturx':
|
||||
xml_facturx = invoice_data['ubl_cii_xml_attachment_values']['raw']
|
||||
else:
|
||||
xml_facturx = self.env['account.edi.xml.cii']._export_invoice(invoice)[0]
|
||||
|
||||
@@ -147,7 +153,7 @@ class AccountMoveSend(models.Model):
|
||||
return
|
||||
|
||||
# Read pdf content.
|
||||
reader_buffer = io.BytesIO(prepared_data['pdf_attachment_values']['raw'])
|
||||
reader_buffer = io.BytesIO(invoice_data['pdf_attachment_values']['raw'])
|
||||
reader = OdooPdfFileReader(reader_buffer, strict=False)
|
||||
|
||||
# Post-process.
|
||||
@@ -157,8 +163,8 @@ class AccountMoveSend(models.Model):
|
||||
writer.addAttachment('factur-x.xml', xml_facturx, subtype='text/xml')
|
||||
|
||||
# PDF-A.
|
||||
if 'ubl_cii_xml_options' in prepared_data \
|
||||
and prepared_data['ubl_cii_xml_options']['ubl_cii_format'] == 'facturx' \
|
||||
if 'ubl_cii_xml_options' in invoice_data \
|
||||
and invoice_data['ubl_cii_xml_options']['ubl_cii_format'] == 'facturx' \
|
||||
and not writer.is_pdfa:
|
||||
try:
|
||||
writer.convert_to_pdfa()
|
||||
@@ -178,19 +184,19 @@ class AccountMoveSend(models.Model):
|
||||
# Replace the current content.
|
||||
writer_buffer = io.BytesIO()
|
||||
writer.write(writer_buffer)
|
||||
prepared_data['pdf_attachment_values']['raw'] = writer_buffer.getvalue()
|
||||
invoice_data['pdf_attachment_values']['raw'] = writer_buffer.getvalue()
|
||||
reader_buffer.close()
|
||||
writer_buffer.close()
|
||||
|
||||
def _postprocess_invoice_ubl_xml(self, invoice, prepared_data):
|
||||
def _postprocess_invoice_ubl_xml(self, invoice, invoice_data):
|
||||
# Add PDF to XML
|
||||
tree = etree.fromstring(prepared_data['ubl_cii_xml_attachment_values']['raw'])
|
||||
tree = etree.fromstring(invoice_data['ubl_cii_xml_attachment_values']['raw'])
|
||||
anchor_elements = tree.xpath("//*[local-name()='AccountingSupplierParty']")
|
||||
if not anchor_elements:
|
||||
return
|
||||
|
||||
filename = prepared_data['pdf_attachment_values']['name']
|
||||
content = prepared_data['pdf_attachment_values']['raw']
|
||||
filename = invoice_data['pdf_attachment_values']['name']
|
||||
content = invoice_data['pdf_attachment_values']['raw']
|
||||
to_inject = f'''
|
||||
<cac:AdditionalDocumentReference
|
||||
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
@@ -209,14 +215,14 @@ class AccountMoveSend(models.Model):
|
||||
|
||||
anchor_index = tree.index(anchor_elements[0])
|
||||
tree.insert(anchor_index, etree.fromstring(to_inject))
|
||||
prepared_data['ubl_cii_xml_attachment_values']['raw'] = b"<?xml version='1.0' encoding='UTF-8'?>\n" \
|
||||
invoice_data['ubl_cii_xml_attachment_values']['raw'] = b"<?xml version='1.0' encoding='UTF-8'?>\n" \
|
||||
+ etree.tostring(cleanup_xml_node(tree))
|
||||
|
||||
def _link_document(self, invoice, prepared_data):
|
||||
def _link_invoice_documents(self, invoice, invoice_data):
|
||||
# EXTENDS 'account'
|
||||
super()._link_document(invoice, prepared_data)
|
||||
super()._link_invoice_documents(invoice, invoice_data)
|
||||
|
||||
attachment_vals = prepared_data.get('ubl_cii_xml_attachment_values')
|
||||
attachment_vals = invoice_data.get('ubl_cii_xml_attachment_values')
|
||||
if attachment_vals:
|
||||
self.env['ir.attachment'].create(attachment_vals)
|
||||
invoice.invalidate_model(fnames=['ubl_cii_xml_id', 'ubl_cii_xml_file'])
|
||||
invoice.invalidate_recordset(fnames=['ubl_cii_xml_id', 'ubl_cii_xml_file'])
|
||||
|
||||
@@ -17,10 +17,11 @@ class AccountMoveSend(models.Model):
|
||||
help='Send the invoice via PEPPOL',
|
||||
)
|
||||
peppol_proxy_state = fields.Selection(related='company_id.account_peppol_proxy_state')
|
||||
# technical field needed for computing a warning text about the peppol configuration
|
||||
peppol_warning = fields.Char(
|
||||
string="Warning",
|
||||
compute="_compute_peppol_warning",
|
||||
) # technical field needed for computing a warning text about the peppol configuration
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
@@ -62,7 +63,7 @@ class AccountMoveSend(models.Model):
|
||||
# BUSINESS ACTIONS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def action_send_and_print(self, from_cron=False):
|
||||
def action_send_and_print(self, from_cron=False, allow_fallback_pdf=False):
|
||||
# Extends the wizard to mark the move to be sent via PEPPOL first
|
||||
self.ensure_one()
|
||||
|
||||
@@ -71,72 +72,60 @@ class AccountMoveSend(models.Model):
|
||||
if not move.peppol_move_state:
|
||||
move.peppol_move_state = 'to_send'
|
||||
|
||||
return super().action_send_and_print(from_cron)
|
||||
return super().action_send_and_print(from_cron=from_cron, allow_fallback_pdf=allow_fallback_pdf)
|
||||
|
||||
def _call_web_service(self, prepared_data_list):
|
||||
def _call_web_service(self, invoices_data):
|
||||
# Overrides 'account'
|
||||
super()._call_web_service(invoices_data)
|
||||
|
||||
if not self.checkbox_send_peppol:
|
||||
return super()._call_web_service(prepared_data_list)
|
||||
return
|
||||
|
||||
params = {'documents': []}
|
||||
all_moves = self.env['account.move']
|
||||
|
||||
if not prepared_data_list:
|
||||
# if a user tries to resend an invoice that has a peppol error state,
|
||||
# the prepared_data_list is going to be empty, as the xml has been aready generated
|
||||
error_moves = self.move_ids.filtered(lambda m: m.peppol_move_state == 'error')
|
||||
if not error_moves:
|
||||
return
|
||||
|
||||
for error_move in error_moves:
|
||||
receiver_identification = f"{error_move.partner_id.peppol_eas}:{error_move.partner_id.peppol_endpoint}"
|
||||
if not error_move.ubl_cii_xml_id:
|
||||
# this happens when a user manually deletes the xml file
|
||||
# but doesn't delete the pdf file
|
||||
# they should delete both if they want to regenerate attachments
|
||||
raise UserError(_('The following invoice does not have an XML attachment: %s', error_move.name))
|
||||
|
||||
params['documents'].append({
|
||||
'filename': error_move.name,
|
||||
'receiver': receiver_identification,
|
||||
'ubl': b64encode(error_move.ubl_cii_xml_id.raw).decode(),
|
||||
})
|
||||
all_moves |= error_move
|
||||
|
||||
for move, prepared_data in prepared_data_list:
|
||||
if not prepared_data:
|
||||
move.peppol_move_state = False
|
||||
raise UserError(_('Please select the UBL format'))
|
||||
|
||||
if move.peppol_move_state not in ('to_send', 'error'):
|
||||
invoices_data_peppol = {}
|
||||
for invoice, invoice_data in invoices_data.items():
|
||||
if invoice.peppol_move_state not in ('to_send', 'error'):
|
||||
continue
|
||||
|
||||
xml_file = prepared_data['ubl_cii_xml_attachment_values']['raw']
|
||||
filename = prepared_data['ubl_cii_xml_attachment_values']['name']
|
||||
if invoice_data.get('ubl_cii_xml_attachment_values'):
|
||||
xml_file = invoice_data['ubl_cii_xml_attachment_values']['raw']
|
||||
filename = invoice_data['ubl_cii_xml_attachment_values']['name']
|
||||
elif invoice.ubl_cii_xml_id:
|
||||
xml_file = invoice.ubl_cii_xml_id.raw
|
||||
filename = invoice.ubl_cii_xml_id.name
|
||||
else:
|
||||
continue
|
||||
|
||||
if not move.partner_id.peppol_eas or not move.partner_id.peppol_endpoint:
|
||||
if not invoice.partner_id.peppol_eas or not invoice.partner_id.peppol_endpoint:
|
||||
# should never happen but in case it does, we need to handle it
|
||||
move.peppol_move_state = 'error'
|
||||
move._message_log(body=_('The partner is missing Peppol EAS and/or Endpoint identifier.'))
|
||||
invoice.peppol_move_state = 'error'
|
||||
invoice_data['error'] = _('The partner is missing Peppol EAS and/or Endpoint identifier.')
|
||||
continue
|
||||
|
||||
if not move.partner_id.account_peppol_is_endpoint_valid:
|
||||
move.peppol_move_state = 'error'
|
||||
move._message_log(body=_('Please verify partner configuration in partner settings.'))
|
||||
if not invoice.partner_id.account_peppol_is_endpoint_valid:
|
||||
invoice.peppol_move_state = 'error'
|
||||
invoice_data['error'] = _('Please verify partner configuration in partner settings.')
|
||||
continue
|
||||
|
||||
receiver_identification = f"{move.partner_id.peppol_eas}:{move.partner_id.peppol_endpoint}"
|
||||
receiver_identification = f"{invoice.partner_id.peppol_eas}:{invoice.partner_id.peppol_endpoint}"
|
||||
params['documents'].append({
|
||||
'filename': filename,
|
||||
'receiver': receiver_identification,
|
||||
'ubl': b64encode(xml_file).decode(),
|
||||
})
|
||||
all_moves |= move
|
||||
invoices_data_peppol[invoice] = invoice_data
|
||||
|
||||
edi_user = self.env['account_edi_proxy_client.user'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('proxy_type', '=', 'peppol'),
|
||||
])
|
||||
if not params['documents']:
|
||||
return
|
||||
|
||||
edi_user = self.env['account_edi_proxy_client.user'].search(
|
||||
[
|
||||
('company_id', '=', self.company_id.id),
|
||||
('proxy_type', '=', 'peppol'),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
try:
|
||||
response = edi_user._make_request(
|
||||
@@ -145,18 +134,20 @@ class AccountMoveSend(models.Model):
|
||||
)
|
||||
if response.get('error'):
|
||||
# at the moment the only error that can happen here is ParticipantNotReady error
|
||||
all_moves._message_log_batch(bodies=dict((move.id, response['error']['message']) for move in all_moves))
|
||||
all_moves.peppol_move_state = 'error'
|
||||
raise UserError(response['error']['message'])
|
||||
for invoice, invoice_data in invoices_data_peppol.items():
|
||||
invoice.peppol_move_state = 'error'
|
||||
invoice_data['error'] = response['error']['message']
|
||||
except AccountEdiProxyError as e:
|
||||
all_moves._message_log_batch(bodies=dict((move.id, e.message) for move in all_moves))
|
||||
all_moves.peppol_move_state = 'error'
|
||||
raise UserError(e.message)
|
||||
for invoice, invoice_data in invoices_data_peppol.items():
|
||||
invoice.peppol_move_state = 'error'
|
||||
invoice_data['error'] = e.message
|
||||
else:
|
||||
# the response only contains message uuids,
|
||||
# so we have to rely on the order to connect peppol messages to account.move
|
||||
for i, move in enumerate(all_moves):
|
||||
move.peppol_message_uuid = response['messages'][i]['message_uuid']
|
||||
move.peppol_move_state = 'processing'
|
||||
invoices = self.env['account.move']
|
||||
for i, (invoice, invoice_data) in enumerate(invoices_data_peppol.items()):
|
||||
invoice.peppol_message_uuid = response['messages'][i]['message_uuid']
|
||||
invoice.peppol_move_state = 'processing'
|
||||
invoices |= invoice
|
||||
log_message = _('The document has been sent to the Peppol Access Point for processing')
|
||||
all_moves._message_log_batch(bodies=dict((move.id, log_message) for move in all_moves))
|
||||
invoices._message_log_batch(bodies=dict((invoice.id, log_message) for invoice in invoices_data_peppol))
|
||||
|
||||
@@ -184,7 +184,7 @@ class TestUBLCommon(AccountTestInvoicingCommon):
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@freeze_time('2017-01-01')
|
||||
def _generate_move(self, seller, buyer, **invoice_kwargs):
|
||||
def _generate_move(self, seller, buyer, send=True, **invoice_kwargs):
|
||||
"""
|
||||
Create and post an account.move.
|
||||
"""
|
||||
@@ -221,7 +221,8 @@ class TestUBLCommon(AccountTestInvoicingCommon):
|
||||
})
|
||||
|
||||
account_move.action_post()
|
||||
account_move._generate_pdf_and_send_invoice(self.move_template)
|
||||
if send:
|
||||
account_move._generate_pdf_and_send_invoice(self.move_template)
|
||||
return account_move
|
||||
|
||||
def _assert_invoice_attachment(self, attachment, xpaths, expected_file):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import Command
|
||||
from odoo.addons.l10n_account_edi_ubl_cii_tests.tests.common import TestUBLCommon
|
||||
from odoo.addons.account.tests.test_account_move_send import TestAccountMoveSendCommon
|
||||
from odoo.tests import tagged
|
||||
import base64
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestUBLBE(TestUBLCommon):
|
||||
class TestUBLBE(TestUBLCommon, TestAccountMoveSendCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref="be"):
|
||||
@@ -67,6 +69,8 @@ class TestUBLBE(TestUBLCommon):
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
})
|
||||
|
||||
cls.env.company.invoice_is_ubl_cii = True
|
||||
|
||||
@classmethod
|
||||
def setup_company_data(cls, company_name, chart_template):
|
||||
# OVERRIDE
|
||||
@@ -495,3 +499,84 @@ class TestUBLBE(TestUBLCommon):
|
||||
list_line_subtotals=[99], currency_id=self.currency_data['currency'].id, list_line_price_unit=[99],
|
||||
list_line_discount=[0], list_line_taxes=[tax_21+self.recupel], move_type='out_invoice',
|
||||
)
|
||||
|
||||
####################################################
|
||||
# Test Send & print
|
||||
####################################################
|
||||
|
||||
def test_send_and_print(self):
|
||||
invoice = self._generate_move(
|
||||
self.partner_1,
|
||||
self.partner_2,
|
||||
send=False,
|
||||
move_type='out_invoice',
|
||||
invoice_line_ids=[
|
||||
{
|
||||
'product_id': self.product_a.id,
|
||||
'tax_ids': [Command.set(self.tax_21.ids)],
|
||||
},
|
||||
],
|
||||
)
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
wizard._compute_send_mail_extra_fields()
|
||||
self.assertRecordValues(wizard, [{
|
||||
'mode': 'invoice_single',
|
||||
'checkbox_ubl_cii_label': "Peppol BIS Billing 3.0",
|
||||
'enable_ubl_cii_xml': True,
|
||||
'checkbox_ubl_cii_xml': True,
|
||||
}])
|
||||
self._assert_mail_attachments_widget(wizard, [
|
||||
{
|
||||
'mimetype': 'application/pdf',
|
||||
'name': 'INV_2017_00001.pdf',
|
||||
'placeholder': True,
|
||||
},
|
||||
{
|
||||
'mimetype': 'application/xml',
|
||||
'name': 'INV_2017_00001_ubl_bis3.xml',
|
||||
'placeholder': True,
|
||||
},
|
||||
])
|
||||
self.assertFalse(invoice.invoice_pdf_report_id)
|
||||
self.assertFalse(invoice.ubl_cii_xml_id)
|
||||
|
||||
# Send.
|
||||
wizard.action_send_and_print()
|
||||
self.assertTrue(invoice.invoice_pdf_report_id)
|
||||
self.assertTrue(invoice.ubl_cii_xml_id)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice._name),
|
||||
('res_id', '=', invoice.id),
|
||||
('res_field', 'in', ('invoice_pdf_report_file', 'ubl_cii_xml_file')),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 2)
|
||||
|
||||
# Send again.
|
||||
wizard = self.create_send_and_print(invoice)
|
||||
self.assertRecordValues(wizard, [{
|
||||
'mode': 'invoice_single',
|
||||
'checkbox_ubl_cii_label': 'Peppol BIS Billing 3.0',
|
||||
'enable_ubl_cii_xml': False,
|
||||
'checkbox_ubl_cii_xml': False,
|
||||
}])
|
||||
self._assert_mail_attachments_widget(wizard, [
|
||||
{
|
||||
'id': invoice.invoice_pdf_report_id.id,
|
||||
'mimetype': 'application/pdf',
|
||||
'name': 'INV_2019_00001.pdf',
|
||||
},
|
||||
{
|
||||
'id': invoice.ubl_cii_xml_id.id,
|
||||
'mimetype': 'application/xml',
|
||||
'name': 'INV_2019_00001_ubl_bis3.xml',
|
||||
},
|
||||
])
|
||||
wizard.action_send_and_print()
|
||||
self.assertTrue(invoice.invoice_pdf_report_id)
|
||||
self.assertTrue(invoice.ubl_cii_xml_id)
|
||||
invoice_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', invoice._name),
|
||||
('res_id', '=', invoice.id),
|
||||
('res_field', 'in', ('invoice_pdf_report_file', 'ubl_cii_xml_file')),
|
||||
])
|
||||
self.assertEqual(len(invoice_attachments), 2)
|
||||
|
||||
@@ -80,7 +80,11 @@ class AccountMove(models.Model):
|
||||
|
||||
def _l10n_es_edi_facturae_get_default_enable(self):
|
||||
self.ensure_one()
|
||||
return self.company_id.country_code == 'ES' and self.company_id.currency_id.name == 'EUR'
|
||||
return not self.invoice_pdf_report_id \
|
||||
and self.l10n_es_edi_facturae_xml_id \
|
||||
and self.is_invoice(include_receipts=True) \
|
||||
and self.company_id.country_code == 'ES' \
|
||||
and self.company_id.currency_id.name == 'EUR'
|
||||
|
||||
def _l10n_es_edi_facturae_get_filename(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -37,7 +37,7 @@ class AccountMoveSend(models.Model):
|
||||
# EXTENDS 'account'
|
||||
results = super()._get_placeholder_mail_attachments_data(move)
|
||||
|
||||
if self.l10n_es_edi_facturae_enable_xml and self.l10n_es_edi_facturae_checkbox_xml:
|
||||
if self.mode == 'invoice_single' and self.l10n_es_edi_facturae_enable_xml and self.l10n_es_edi_facturae_checkbox_xml:
|
||||
filename = f'{move.name.replace("/", "_")}_facturae_signed.xml'
|
||||
results.append({
|
||||
'id': f'placeholder_{filename}',
|
||||
@@ -52,18 +52,19 @@ class AccountMoveSend(models.Model):
|
||||
# BUSINESS ACTIONS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _prepare_document(self, invoice):
|
||||
def _hook_invoice_document_before_pdf_report_render(self, invoice, invoice_data):
|
||||
# EXTENDS 'account'
|
||||
results = super()._prepare_document(invoice)
|
||||
results = super()._hook_invoice_document_before_pdf_report_render(invoice, invoice_data)
|
||||
|
||||
if self.l10n_es_edi_facturae_checkbox_xml and invoice._l10n_es_edi_facturae_get_default_enable():
|
||||
try:
|
||||
xml_content = invoice._l10n_es_edi_facturae_render_facturae()
|
||||
except Exception as error:
|
||||
return {
|
||||
'error': "".join((_("Errors occured while creating the EDI document (format: %s):", "Facturae"),
|
||||
"\n", str(error))),
|
||||
}
|
||||
invoice_data['error'] = "".join((
|
||||
_("Errors occured while creating the EDI document (format: %s):", "Facturae"),
|
||||
"\n",
|
||||
str(error),
|
||||
))
|
||||
|
||||
results['l10n_es_edi_facturae_attachment_values'] = {
|
||||
'name': invoice._l10n_es_edi_facturae_get_filename(),
|
||||
@@ -76,11 +77,11 @@ class AccountMoveSend(models.Model):
|
||||
|
||||
return results
|
||||
|
||||
def _link_document(self, invoice, prepared_data):
|
||||
def _link_invoice_documents(self, invoice, invoice_data):
|
||||
# EXTENDS 'account'
|
||||
super()._link_document(invoice, prepared_data)
|
||||
super()._link_invoice_documents(invoice, invoice_data)
|
||||
|
||||
attachment_vals = prepared_data.get('l10n_es_edi_facturae_attachment_values')
|
||||
attachment_vals = invoice_data.get('l10n_es_edi_facturae_attachment_values')
|
||||
if attachment_vals:
|
||||
self.env['ir.attachment'].create(attachment_vals)
|
||||
invoice.invalidate_model(fnames=['l10n_es_edi_facturae_xml_id', 'l10n_es_edi_facturae_xml_file'])
|
||||
invoice.invalidate_recordset(fnames=['l10n_es_edi_facturae_xml_id', 'l10n_es_edi_facturae_xml_file'])
|
||||
|
||||
@@ -15,6 +15,16 @@ class AccountMoveSend(models.Model):
|
||||
)
|
||||
send_by_post_warning_message = fields.Text(compute='_compute_send_by_post_warning_message')
|
||||
|
||||
def _get_available_field_values_in_multi(self, move):
|
||||
# EXTENDS 'account'
|
||||
values = super()._get_available_field_values_in_multi(move)
|
||||
values['checkbox_send_by_post'] = self.checkbox_send_by_post
|
||||
return values
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('mode')
|
||||
def _compute_enable_send_by_post(self):
|
||||
for wizard in self:
|
||||
@@ -54,18 +64,24 @@ class AccountMoveSend(models.Model):
|
||||
'attachment_id': move.invoice_pdf_report_id.id,
|
||||
}
|
||||
|
||||
def _generate_documents_success_hook(self, moves, from_cron):
|
||||
def _hook_if_success(self, moves_data, from_cron=False, allow_fallback_pdf=False):
|
||||
# EXTENDS 'account'
|
||||
moves = super()._generate_documents_success_hook(moves, from_cron)
|
||||
super()._hook_if_success(moves_data, from_cron=from_cron, allow_fallback_pdf=allow_fallback_pdf)
|
||||
|
||||
send_by_post = self.enable_send_by_post and self.checkbox_send_by_post
|
||||
if not send_by_post:
|
||||
return
|
||||
|
||||
if send_by_post:
|
||||
self.env['snailmail.letter']\
|
||||
.create([
|
||||
self._prepare_snailmail_letter_values(move)
|
||||
for move in moves
|
||||
])\
|
||||
._snailmail_print(immediate=False)
|
||||
moves = self.env['account.move']
|
||||
for move in moves_data:
|
||||
moves |= move
|
||||
moves = moves.filtered('invoice_pdf_report_id')
|
||||
if not moves:
|
||||
return
|
||||
|
||||
return moves
|
||||
self.env['snailmail.letter']\
|
||||
.create([
|
||||
self._prepare_snailmail_letter_values(move)
|
||||
for move in moves
|
||||
])\
|
||||
._snailmail_print(immediate=False)
|
||||
|
||||
Reference in New Issue
Block a user