# -*- coding: utf-8 -*- from contextlib import contextmanager from freezegun import freeze_time from unittest.mock import patch from odoo.tests import Form, tagged from odoo.addons.account_reports.tests.common import TestAccountReportsCommon from odoo.addons.account_followup.tests.common import TestAccountFollowupCommon from odoo.tools.misc import file_open from odoo import Command, fields @tagged('post_install', '-at_install') class TestAccountFollowupReports(TestAccountReportsCommon, TestAccountFollowupCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.partner_a.email = 'partner_a@mypartners.xyz' cls.report = cls.env.ref('account_reports.followup_report') def test_followup_report(self): ''' Test report lines when printing the follow-up report. ''' # Init options. report = self.env['account.followup.report'] options = { 'partner_id': self.partner_a.id, 'multi_currency': True, } # 2016-01-01: First invoice, partially paid. invoice_1 = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice_1.action_post() payment_1 = self.env['account.move'].create({ # pylint: disable=C0326 'move_type': 'entry', 'date': '2016-01-01', 'journal_id': self.company_data['default_journal_misc'].id, 'line_ids': [ (0, 0, {'debit': 0.0, 'credit': 200.0, 'account_id': self.company_data['default_account_receivable'].id}), (0, 0, {'debit': 200.0, 'credit': 0.0, 'account_id': self.company_data['default_journal_bank'].default_account_id.id}), ], }) payment_1.action_post() (payment_1 + invoice_1).line_ids\ .filtered(lambda line: line.account_id == self.company_data['default_account_receivable'])\ .reconcile() with freeze_time('2016-01-01'): self.assertLinesValues( # pylint: disable=C0326 report._get_followup_report_lines(options), # Name Date, Due Date, Doc. Total Due [ 0, 1, 2, 3, 5], [ ('INV/2016/00001', '01/01/2016', '01/01/2016', '', 300.0), ('', '', '', '', 300.0), ], options, ) # 2016-01-05: Credit note due at 2016-01-10. invoice_2 = self.env['account.move'].create({ 'move_type': 'out_refund', 'invoice_date': '2016-01-05', 'invoice_date_due': '2016-01-10', 'partner_id': self.partner_a.id, 'invoice_payment_term_id': False, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 200, 'tax_ids': [], })] }) invoice_2.action_post() with freeze_time('2016-01-05'): self.assertLinesValues( # pylint: disable=C0326 report._get_followup_report_lines(options), # Name Date, Due Date, Doc. Total Due [ 0, 1, 2, 3, 5], [ ('RINV/2016/00001', '01/05/2016', '01/10/2016', '', -200.0), ('INV/2016/00001', '01/01/2016', '01/01/2016', '', 300.0), ('', '', '', '', 100.0), ('', '', '', '', 300.0), ], options, ) # 2016-01-15: Draft invoice + previous credit note reached the date_maturity + first invoice reached the delay # of the first followup level. self.env['account.move'].create({ 'move_type': 'out_refund', 'invoice_date': '2016-01-15', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 1000, 'tax_ids': [], })] }) with freeze_time('2016-01-15'): self.assertLinesValues( # pylint: disable=C0326 report._get_followup_report_lines(options), # Name Date, Due Date, Doc. Total Due [ 0, 1, 2, 3, 5], [ ('RINV/2016/00001', '01/05/2016', '01/10/2016', '', -200.0), ('INV/2016/00001', '01/01/2016', '01/01/2016', '', 300.0), ('', '', '', '', 100.0), ('', '', '', '', 100.0), ], options, ) # Trigger the followup report notice. invoice_attachments = self.env['ir.attachment'] for invoice in invoice_1 + invoice_2: invoice_attachment = self.env['ir.attachment'].create({ 'name': 'some_attachment.pdf', 'res_id': invoice.id, 'res_model': 'account.move', 'res_field': 'invoice_pdf_report_file', # simulates send & print 'datas': 'test', 'type': 'binary', }) invoice_attachments += invoice_attachment invoice._message_set_main_attachment_id(invoice_attachment) self.partner_a._compute_unpaid_invoices() options.update( attachment_ids=invoice_attachments.ids, email=True, manual_followup=True, join_invoices=True, ) with patch.object(type(self.env['mail.mail']), 'unlink', lambda self: None): with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a.execute_followup(options) sent_attachments = self.env['mail.message'].search([('partner_ids', '=', self.partner_a.id)]).attachment_ids self.assertEqual(sent_attachments.mapped('name'), [f'{self.partner_a.name} - fake_partner_ledger.pdf', 'some_attachment.pdf', 'some_attachment.pdf']) options.update( attachment_ids=[], email=False, join_invoices=False, ) with ( patch.object(self.env.registry['mail.mail'], 'unlink', lambda self: None), patch.object( self.env.registry['res.partner'], '_get_partner_account_report_attachment', autospec=True, side_effect=lambda *args, **kwargs: invoice_attachments[0], ), ): self.partner_a.execute_followup(options) self.assertEqual(options['attachment_ids'], [invoice_attachments.ids[0]], "The report attachment should be included regardless of join_invoices and email checkboxes.") attachaments_domain = [('attachment_ids', '=', attachment.id) for attachment in invoice_attachments] mail = self.env['mail.mail'].search([('recipient_ids', '=', self.partner_a.id)] + attachaments_domain) self.assertTrue(mail, "A payment reminder email should have been sent.") def test_followup_report_journal_option_disabled(self): options = { 'partner_id': self.partner_a.id, 'manual_followup': True, } self.report.filter_journals = False with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a.execute_followup(options) def test_followup_lines_branches(self): branch = self.env['res.company'].create({ 'name': 'branch', 'parent_id': self.env.company.id }) self.cr.precommit.run() # load the COA report = self.env['account.followup.report'] options = { 'partner_id': self.partner_a.id, } invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'company_id': branch.id, 'invoice_payment_term_id': self.pay_terms_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice.action_post() self.assertLinesValues( # pylint: disable=C0326 report._get_followup_report_lines(options), # Name Date, Due Date, Doc. Total Due [ 0, 1, 2, 3, 5], [ ('INV/2016/00001', '01/01/2016', '01/01/2016', '', f'{self.env.company.currency_id.symbol}\xa0500.00'), ('', '', '', '', f'{self.env.company.currency_id.symbol}\xa0500.00'), ('', '', '', '', f'{self.env.company.currency_id.symbol}\xa0500.00'), ], options, ) def test_followup_report_address_1(self): ''' Test child contact priorities: the company will be used when there is no followup or billing contacts ''' Partner = self.env['res.partner'] self.partner_a.is_company = True options = { 'partner_id': self.partner_a.id, } child_partner = Partner.create({ 'name': "Child contact", 'type': "contact", 'parent_id': self.partner_a.id, }) mail = self.env['mail.mail'].search([('recipient_ids', '=', self.partner_a.id)]) self.init_invoice('out_invoice', partner=child_partner, invoice_date='2016-01-01', amounts=[500], post=True) self.partner_a._compute_unpaid_invoices() with patch.object(type(self.env['mail.mail']), 'unlink', lambda self: None): with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.env['account.followup.report']._send_email(options) mail = self.env['mail.mail'].search([('recipient_ids', '=', self.partner_a.id)]) self.assertTrue(mail, "The payment reminder email should have been sent to the company.") def test_followup_report_address_2(self): ''' Test child contact priorities: the follow up contact will be preferred over the billing contact ''' Partner = self.env['res.partner'] self.partner_a.is_company = True options = { 'partner_id': self.partner_a.id, } # Testing followup sent to billing address if used in invoice child_partner = Partner.create({ 'name': "Child contact", 'type': "contact", 'parent_id': self.partner_a.id, }) invoice_partner = Partner.create({ 'name' : "Child contact invoice", 'type' : "invoice", 'email' : "test-invoice@example.com", 'parent_id': child_partner.id, }) self.init_invoice('out_invoice', partner=invoice_partner, invoice_date='2016-01-01', amounts=[500], post=True) self.partner_a._compute_unpaid_invoices() with patch.object(type(self.env['mail.mail']), 'unlink', lambda self: None): with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.env['account.followup.report']._send_email(options) mail = self.env['mail.mail'].search([('recipient_ids', '=', invoice_partner.id)]) self.assertTrue(mail, "The payment reminder email should have been sent to the invoice partner.") mail.unlink() def test_followup_invoice_no_amount(self): # Init options. report = self.env['account.followup.report'] options = { 'partner_id': self.partner_a.id, } invoice_move = self.env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, 'date': '2022-01-01', 'invoice_line_ids': [ (0, 0, {'quantity': 0, 'price_unit': 30}), ], }) invoice_move.action_post() lines = report._get_followup_report_lines(options) self.assertEqual(len(lines), 0, "There should be no line displayed") def test_negative_followup_report(self): ''' Test negative or null followup reports: if a contact has an overdue invoice but has a negative of null total due, no action is needed. ''' followup_line = self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': False, }) self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }).action_post() self.env['account.move'].create({ 'move_type': 'out_refund', 'invoice_date': '2016-01-15', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 300, 'tax_ids': [], })] }).action_post() self.assertEqual(self.partner_a.total_due, 200) self.assertPartnerFollowup(self.partner_a, 'in_need_of_action', followup_line) self.env['account.payment'].create({ 'partner_id': self.partner_a.id, 'amount': 400, }).action_post() self.assertEqual(self.partner_a.total_due, -200) self.assertPartnerFollowup(self.partner_a, 'no_action_needed', followup_line) def test_followup_report_style(self): """ This report is often broken in terms of styling, this test will check the styling of the lines. (This test will not work if we modify the template it self) """ report = self.env['account.followup.report'] options = { 'partner_id': self.partner_a.id, 'multi_currency': True, } # 2016-01-01: First invoice, partially paid. invoice_1 = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice_1.action_post() payment_1 = self.env['account.move'].create({ 'move_type': 'entry', 'date': '2016-01-01', 'journal_id': self.company_data['default_journal_misc'].id, 'line_ids': [ Command.create({ 'debit': 0.0, 'credit': 200.0, 'account_id': self.company_data['default_account_receivable'].id }), Command.create({ 'debit': 200.0, 'credit': 0.0, 'account_id': self.company_data['default_journal_bank'].default_account_id.id }), ], }) payment_1.action_post() (payment_1 + invoice_1).line_ids \ .filtered(lambda line: line.account_id == self.company_data['default_account_receivable']) \ .reconcile() lines = report._get_followup_report_lines(options) # The variable lines is composed of 3 lines, the line of the move and two lines of total (due and overdue) # First line line_0_style = [ 'white-space:nowrap;text-align:left;', 'white-space:nowrap;text-align:left;color: red;', 'text-align:center; white-space:normal;', 'text-align:left; white-space:normal;', 'text-align:right; white-space:normal;', ] for expected_style, column in zip(line_0_style, lines[0]['columns']): self.assertEqual(expected_style, column['style']) # Second line self.assertEqual(lines[1]['columns'][3].get('style'), 'text-align:right; white-space:normal; font-weight: bold;') # Total due title self.assertEqual(lines[1]['columns'][4].get('style'), 'text-align:right; white-space:normal; font-weight: bold;') # Total due value # Third line self.assertEqual(lines[2]['columns'][3].get('style'), 'text-align:right; white-space:normal; font-weight: bold;') # Total overdue title self.assertEqual(lines[2]['columns'][4].get('style'), 'text-align:right; white-space:normal; font-weight: bold;') # Total overdue value # Check template used for line in lines: for column in line['columns']: self.assertEqual(column['template'], 'account_followup.line_template') def test_followup_send_email(self): """ Tests that the email address in the mail.template is used to send the followup email.""" self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }).action_post() @contextmanager def create_and_send_email(email_from, subject): """ Create a mail.template, open the followup wizard and send the followup email.""" mail_template = self.env['mail.template'].create({ 'name': "Payment Reminder", 'model_id': self.env.ref('base.model_res_partner').id, 'email_from': email_from, 'partner_to': '{{ object.id }}', 'subject': subject, }) wizard = self.env['account_followup.manual_reminder'].with_context( active_model='res.partner', active_ids=self.partner_a.ids, ).create({}) wizard.email = True # tick the 'email' checkbox wizard.template_id = mail_template with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): wizard.process_followup() yield self.assertEqual(len(message), 1) self.assertEqual(message.author_id, self.env.user.partner_id) # case 1: the email_from is dynamically set with create_and_send_email( email_from="{{ object._get_followup_responsible().email_formatted }}", subject="{{ (object.company_id or object._get_followup_responsible().company_id).name }} Pay me now !", ): message = self.env['mail.message'].search([('subject', 'like', "Pay me now !")]) self.assertEqual(message.email_from, self.env.user.partner_id.email_formatted) # case 2: the email_from is hardcoded in the template with create_and_send_email( email_from="test@odoo.com", subject="{{ (object.company_id or object._get_followup_responsible().company_id).name }} Pay me noooow !", ): message = self.env['mail.message'].search([('subject', 'like', "Pay me noooow !")]) self.assertEqual(message.email_from, "test@odoo.com") def test_process_automatic_followup_send_email(self): """ Tests that the email address in the mail.template is used to send the followup email from the cron.""" self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, }) self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }).action_post() @contextmanager def create_and_send_email(email_from, subject): """ Create a mail.template, link it with the followup line and execute followups.""" mail_template = self.env['mail.template'].create({ 'name': "Payment Reminder", 'model_id': self.env.ref('base.model_res_partner').id, 'email_from': email_from, 'partner_to': '{{ object.id }}', 'subject': subject, }) self.partner_a.followup_line_id.mail_template_id = mail_template self.partner_a.followup_next_action_date = False with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a._execute_followup_partner(options={'snailmail': False}) yield self.assertEqual(len(message), 1) self.assertEqual(message.author_id, self.partner_a._get_followup_responsible().partner_id, "Automatic followups should have the followup responsible as the author.") # case 1: the email_from is dynamically set with create_and_send_email( email_from="{{ object._get_followup_responsible().email_formatted }}", subject="{{ (object.company_id or object._get_followup_responsible().company_id).name }} Pay me now !", ): message = self.env['mail.message'].search([('subject', 'like', "Pay me now !")]) self.assertEqual(message.email_from, self.env.user.partner_id.email_formatted) # we have to create a new overdue invoice to test the second case as the previous invoice # will no longer trigger the followup self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 600, 'tax_ids': [], })] }).action_post() # case 2: the email_from is hardcoded in the template with create_and_send_email( email_from="test@odoo.com", subject="{{ (object.company_id or object._get_followup_responsible().company_id).name }} Pay me noooow !", ): message = self.env['mail.message'].search([('subject', 'like', "Pay me noooow !")]) self.assertEqual(message.email_from, "test@odoo.com") def test_compute_render_model(self): with Form(self.env['account_followup.manual_reminder'].with_context( active_model='res.partner', active_ids=self.partner_a.ids, )) as wizard: self.assertEqual(wizard.render_model, "res.partner") def test_followup_report_with_levels_on_main_company(self): cron = self.env.ref('account_followup.ir_cron_follow_up') self.env['account_followup.followup.line'].create([{ 'company_id': self.company_data['company'].id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, 'auto_execute': True, }, { 'company_id': self.company_data['company'].id, 'name': 'Second Reminder', 'delay': 30, 'send_email': True, 'auto_execute': True, }]) self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'invoice_date_due': '2016-01-01', 'date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, })] }).action_post() with ( freeze_time('2022-01-10'), patch.object(self.env.registry['res.partner'], '_send_followup') as patched, self.enter_registry_test_mode(), patch.object(self.env.registry['ir.actions.report'], '_run_wkhtmltopdf', return_value=b"0"), ): cron.method_direct_trigger() # For the same reason we clear the cache in assertPartnerFollowup, to avoid this test change the state of the cache, # which could break other tests. self.env.cr.cache.pop('res_partner_all_followup', None) self.assertEqual(patched.call_count, 1) count_mail = self.env['mail.message'].search_count([('record_company_id', '=', self.company_data['company'].id)]) # We should have 1 email : # 1 for the main company self.assertEqual(count_mail, 1) def test_followup_report_with_levels_on_one_branch(self): cron = self.env.ref('account_followup.ir_cron_follow_up') branch_a, branch_b = self.env['res.company'].create([{ 'name': 'Branch number 1', 'parent_id': self.company_data['company'].id, }, { 'name': 'Branch number 2', 'parent_id': self.company_data['company'].id, }]) self.cr.precommit.run() # load the COA self.env['account_followup.followup.line'].create([{ 'company_id': branch_a.id, 'name': 'First Reminder (A)', 'delay': 15, 'send_email': True, 'auto_execute': True, }, { 'company_id': branch_a.id, 'name': 'Second Reminder (A)', 'delay': 30, 'send_email': True, 'auto_execute': True, }]) self.env['account.move'].create([{ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'invoice_date_due': '2016-01-01', 'date': '2016-01-01', 'partner_id': self.partner_a.id, 'company_id': branch_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 400, })] }, { 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'invoice_date_due': '2016-01-01', 'date': '2016-01-01', 'partner_id': self.partner_a.id, 'company_id': branch_b.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 800, })] }]).action_post() with ( freeze_time('2022-01-10'), patch.object(self.env.registry['res.partner'], '_send_followup') as patched, self.enter_registry_test_mode(), patch.object(self.env.registry['ir.actions.report'], '_run_wkhtmltopdf', return_value=b"0"), ): cron.method_direct_trigger() # For the same reason we clear the cache in assertPartnerFollowup, to avoid this test change the state of the cache, # which could break other tests. self.env.cr.cache.pop('res_partner_all_followup', None) self.assertEqual(patched.call_count, 1) count_mail = self.env['mail.message'].search_count([('record_company_id', '=', branch_a.id)]) # We should have 1 email : # 1 for the Branch number 1 self.assertEqual(count_mail, 1) def test_followup_report_with_levels_on_branches_and_main_company(self): cron = self.env.ref('account_followup.ir_cron_follow_up') branch_a, branch_b = self.env['res.company'].create([{ 'name': 'Branch number 1', 'parent_id': self.company_data['company'].id, }, { 'name': 'Branch number 2', 'parent_id': self.company_data['company'].id, }]) self.cr.precommit.run() # load the COA self.env['account_followup.followup.line'].create([{ 'company_id': branch_a.id, 'name': 'First Reminder (A)', 'delay': 15, 'send_email': True, 'auto_execute': True, }, { 'company_id': branch_a.id, 'name': 'Second Reminder (A)', 'delay': 30, 'send_email': True, 'auto_execute': True, }, { 'company_id': branch_b.id, 'name': 'First Reminder (B)', 'delay': 10, 'send_email': True, 'auto_execute': True, }, { 'company_id': branch_b.id, 'name': 'Second Reminder (B)', 'delay': 20, 'send_email': True, 'auto_execute': True, }, { 'company_id': self.company_data['company'].id, 'name': 'First Reminder', 'delay': 20, 'send_email': True, 'auto_execute': True, }, { 'company_id': self.company_data['company'].id, 'name': 'Second Reminder', 'delay': 40, 'send_email': True, 'auto_execute': True, }]) self.env['account.move'].create([{ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'invoice_date_due': '2016-01-01', 'date': '2016-01-01', 'partner_id': self.partner_a.id, 'company_id': branch_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 400, })] }, { 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'invoice_date_due': '2016-01-01', 'date': '2016-01-01', 'partner_id': self.partner_a.id, 'company_id': branch_b.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 800, })] }, { 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'invoice_date_due': '2016-01-01', 'date': '2016-01-01', 'partner_id': self.partner_a.id, 'company_id': self.company_data['company'].id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 200, })] }]).action_post() with ( freeze_time('2022-01-10'), patch.object(self.env.registry['res.partner'], '_send_followup') as patched, self.enter_registry_test_mode(), patch.object(self.env.registry['ir.actions.report'], '_run_wkhtmltopdf', return_value=b"0"), ): cron.method_direct_trigger() # For the same reason we clear the cache in assertPartnerFollowup, to avoid this test change the state of the cache, # which could break other tests. self.env.cr.cache.pop('res_partner_all_followup', None) self.assertEqual(patched.call_count, 3) count_mail = self.env['mail.message'].search_count([('record_company_id', 'in', [self.company_data['company'].id, branch_a.id, branch_b.id])]) # We should have 3 emails : # 1 for the main company # 1 for the Branch number 1 # 1 for the Branch number 2 self.assertEqual(count_mail, 3) # Now we check the amounts overdue # Expected : 200 (main_company) + 400 (branch_a) + 800 (branch_b) self.assertEqual(self.partner_a.total_overdue, 1400) # Expected : 400 self.assertEqual(self.partner_a.with_company(branch_a).total_overdue, 400) # Expected : 800 self.assertEqual(self.partner_a.with_company(branch_b).total_overdue, 800) def test_partner_total_due_with_payable(self): """ Test that the total due for a partner also reflects payable accounts and is coherent with the customer statement report. """ # Init options. report = self.env.ref('account_reports.customer_statement_report') default_options = { 'partner_id': self.partner_a.id, 'multi_currency': True, 'unfold_all': True, } options = self._generate_options(report, fields.Date.from_string('2016-01-01'), fields.Date.from_string('2016-12-31'), default_options=default_options) self.init_invoice('out_invoice', self.partner_a, '2016-01-01', True, amounts=[500]) self.assertRecordValues(self.partner_a, [{'total_due': 500.0, 'total_all_due': 500.0}]) with freeze_time('2016-01-01'): self.assertLinesValues( # pylint: disable=C0326 report._get_lines(options), # Name Date, Due Date, Amount, Balance [ 0, 1, 2, 3, 5], [ ('partner_a', '', '', 500.0, 500.0), ('INV/2016/00001', '01/01/2016', '01/01/2016', 500.0, 500.0), ('Total partner_a', '', '', 500.0, 500.0), ('Total', '', '', 500.0, 500.0), ], options, ) self.init_invoice('in_invoice', self.partner_a, '2016-01-01', True, amounts=[200]) self.assertRecordValues(self.partner_a, [{'total_due': 500.0, 'total_all_due': 300.0}]) with freeze_time('2016-01-01'): self.assertLinesValues( # pylint: disable=C0326 report._get_lines(options), # Name Date, Due Date, Amount, Balance [ 0, 1, 2, 3, 5], [ ('partner_a', '', '', 300.0, 300.0), ('INV/2016/00001', '01/01/2016', '01/01/2016', 500.0, 500.0), ('BILL/2016/01/0001', '01/01/2016', '01/01/2016', -200.0, 300.0), ('Total partner_a', '', '', 300.0, 300.0), ('Total', '', '', 300.0, 300.0), ], options, ) def test_automatic_followup_report_attachments(self): followup_line = self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, }) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice.action_post() self.assertPartnerFollowup(self.partner_a, 'in_need_of_action', followup_line) invoice_attachment = self.env['ir.attachment'].create({ 'name': 'some_attachment.pdf', 'res_id': invoice.id, 'res_model': 'account.move', 'datas': 'test', 'type': 'binary', }) send_wizard = self.env['account.move.send.wizard']\ .with_context(active_model='account.move', active_ids=invoice.ids)\ .create({'sending_methods': ['manual']}) send_wizard.action_send_and_print() self.partner_a._compute_unpaid_invoices() with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a.action_manually_process_automatic_followups() sent_attachments = self.env['mail.message'].search([('partner_ids', '=', self.partner_a.id)]).attachment_ids self.assertEqual(sent_attachments.mapped('name'), [f'{self.partner_a.name} - fake_partner_ledger.pdf', invoice._get_invoice_report_filename()]) def test_automatic_followup_no_followup_invoice_not_attached(self): followup_line = self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, }) invoice = self._create_invoice(invoice_date='2016-01-01', post=True) self.assertPartnerFollowup(self.partner_a, 'in_need_of_action', followup_line) send_wizard = self.env['account.move.send.wizard']\ .with_context(active_model='account.move', active_ids=invoice.ids)\ .create({'sending_methods': ['manual']}) send_wizard.action_send_and_print() # Simulate toggling "No Follow-Up" in the customer statement. invoice.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable').no_followup = True self.partner_a._compute_unpaid_invoices() with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a.action_manually_process_automatic_followups() sent_attachments = self.env['mail.message'].search([ ('partner_ids', '=', self.partner_a.id), ], order='id desc', limit=1).attachment_ids self.assertNotIn(invoice._get_invoice_report_filename(), sent_attachments.mapped('name')) def test_manual_followup_report_invoices_removed(self): followup_line = self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, }) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice.action_post() self.assertPartnerFollowup(self.partner_a, 'in_need_of_action', followup_line) invoice_attachment = self.env['ir.attachment'].create({ 'name': 'some_attachment.pdf', 'res_id': invoice.id, 'res_model': 'account.move', 'datas': 'test', 'type': 'binary', }) invoice._message_set_main_attachment_id(invoice_attachment) self.partner_a._compute_unpaid_invoices() with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a._execute_followup_partner(options={ 'partner_id': self.partner_a.id, 'manual_followup': True, 'snailmail': False, 'join_invoices': True, 'attachment_ids': [], }) sent_attachments = self.env['mail.message'].search([('partner_ids', '=', self.partner_a.id)]).attachment_ids self.assertEqual(sent_attachments.mapped('name'), [f'{self.partner_a.name} - fake_partner_ledger.pdf']) def test_manual_followup_report_join_invoices(self): followup_line = self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, }) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice.action_post() self.assertPartnerFollowup(self.partner_a, 'in_need_of_action', followup_line) invoice_attachment = self.env['ir.attachment'].create({ 'name': 'some_attachment.pdf', 'res_id': invoice.id, 'res_model': 'account.move', 'datas': 'test', 'type': 'binary', }) invoice._message_set_main_attachment_id(invoice_attachment) self.partner_a._compute_unpaid_invoices() with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a._execute_followup_partner(options={ 'partner_id': self.partner_a.id, 'manual_followup': True, 'snailmail': False, 'join_invoices': False, 'attachment_ids': invoice_attachment.ids, }) sent_attachments = self.env['mail.message'].search([('partner_ids', '=', self.partner_a.id)]).attachment_ids self.assertEqual(sent_attachments.mapped('name'), [f'{self.partner_a.name} - fake_partner_ledger.pdf']) def _prepare_invoices_and_attachments(self): invoice_1 = self.init_invoice("out_invoice", amounts=[1000], post=True) invoice_2 = self.init_invoice("out_invoice", amounts=[2000], post=True) attachment_1 = self.env['ir.attachment'].create({ 'name': 'att_1.pdf', 'res_id': invoice_1.id, 'res_model': 'account.move', 'datas': 'test', 'type': 'binary', }) invoice_1._message_set_main_attachment_id(attachment_1) attachment_2 = self.env['ir.attachment'].create([{ 'name': 'att_2.pdf', 'res_id': invoice_2.id, 'res_model': 'account.move', 'res_field': 'invoice_pdf_report_file', # simulates send & print 'datas': 'test', 'type': 'binary', }]) return invoice_1 + invoice_2, attachment_1 + attachment_2 def test_manual_followup_invoice_attachments_pdf_report_file(self): invoices, attachments = self._prepare_invoices_and_attachments() wizard = self.env['account_followup.manual_reminder'].with_context( active_model='res.partner', active_ids=invoices[1].partner_id.ids, ).create({}) self.assertEqual(invoices[1].partner_id.unreconciled_aml_ids.move_id, invoices) self.assertEqual(wizard.attachment_ids, attachments[1], "The manually uploaded PDF should not be attached to the follow-up.") def test_auto_followup_invoice_attachments_pdf_report_file(self): invoices, attachments = self._prepare_invoices_and_attachments() self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, }) with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a.action_manually_process_automatic_followups() sent_attachments = self.env['mail.message'].search([('partner_ids', '=', invoices.partner_id.id)]).attachment_ids self.assertEqual(sent_attachments.mapped('name'), [f'{self.partner_a.name} - fake_partner_ledger.pdf', attachments[1].name]) def test_followup_report_with_entries(self): """ Entries shouldn't have a due date or be added to total_overdue on the followup report and on the partner. """ report = self.env['account.followup.report'] options = { 'partner_id': self.partner_a.id, } with freeze_time('2016-01-02'): invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'invoice_date_due': '2016-01-01', 'invoice_payment_term_id': False, 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 300, 'tax_ids': [], })] }) invoice.action_post() entry = self.env['account.move'].create({ 'move_type': 'entry', 'date': fields.Date.from_string('2016-01-02'), 'partner_id': self.partner_a.id, 'invoice_line_ids': [ Command.create({ 'name': 'line1', 'account_id': self.company_data['default_account_receivable'].id, 'debit': 500.0, 'credit': 0.0, 'no_followup': False, }), Command.create({ 'name': 'counterpart line', 'account_id': self.company_data['default_account_revenue'].id, 'debit': 0.0, 'credit': 500.0, }) ] }) entry.action_post() with freeze_time('2016-01-15'): self.assertLinesValues( # pylint: disable=C0326 report._get_followup_report_lines(options), # Name Date, Due Date, Doc. Total Due [ 0, 1, 2, 3, 5], [ ('MISC/2016/01/0001', '01/02/2016', '', '', f'{self.env.company.currency_id.symbol}\xa0500.00'), ('INV/2016/00001', '01/01/2016', '01/01/2016', '', f'{self.env.company.currency_id.symbol}\xa0300.00'), ('', '', '', '', f'{self.env.company.currency_id.symbol}\xa0800.00'), ('', '', '', '', f'{self.env.company.currency_id.symbol}\xa0300.00'), ], options, ) self.assertEqual(self.partner_a.total_due, 800) self.assertEqual(self.partner_a.total_overdue, 300) def test_action_report_followup(self): def _run_wkhtmltopdf(*args, **kwargs): return file_open('base/tests/minimal.pdf', 'rb').read() followup_line = self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, }) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice.action_post() self.assertPartnerFollowup(self.partner_a, 'in_need_of_action', followup_line) with patch.object(self.env.registry['ir.actions.report'], '_run_wkhtmltopdf', _run_wkhtmltopdf): followup_letter = self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account_followup.report_followup_print_all', self.partner_a.id)[0] self.assertTrue(followup_letter) def test_automatic_followup_report_attachments_from_template(self): mail_template = self.env['mail.template'].create({ 'name': 'reminder', 'model_id': self.env['ir.model']._get_id('res.partner'), 'email_cc': 'john.carmac@example.me', }) template_attachment = self.env['ir.attachment'].create({ 'name': 'template_attachment.pdf', 'res_id': mail_template.id, 'res_model': 'mail.template', 'datas': 'test', 'type': 'binary', }) mail_template.attachment_ids = [template_attachment.id] dynamic_report = self.env['ir.actions.report'].create({ 'name': 'Test Report Partner', 'model': 'res.partner', 'report_name': 'account_followup.report_followup_print_all', 'print_report_name': "'followup_dynamic_report'", }) mail_template.report_template_ids = [dynamic_report.id] followup_line = self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, 'mail_template_id': mail_template.id, }) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': '2016-01-01', 'partner_id': self.partner_a.id, 'invoice_line_ids': [Command.create({ 'quantity': 1, 'price_unit': 500, 'tax_ids': [], })] }) invoice.action_post() self.assertPartnerFollowup(self.partner_a, 'in_need_of_action', followup_line) invoice_attachment = self.env['ir.attachment'].create({ 'name': 'invoice_attachment.pdf', 'res_id': invoice.id, 'res_model': 'account.move', 'res_field': 'invoice_pdf_report_file', # simulates send & print 'datas': 'test', 'type': 'binary', }) invoice._message_set_main_attachment_id(invoice_attachment) self.partner_a._compute_unpaid_invoices() with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a.action_manually_process_automatic_followups() sent_attachments = self.env['mail.message'].search([('partner_ids', '=', self.partner_a.id)]).attachment_ids self.assertEqual(sent_attachments.mapped('name'), [f'{self.partner_a.name} - fake_partner_ledger.pdf', 'template_attachment.pdf', 'followup_dynamic_report.html', 'invoice_attachment.pdf']) def test_manual_followup_no_followup_invoice_not_attached(self): mail_template = self.env['mail.template'].create({ 'name': 'reminder', 'model_id': self.env['ir.model']._get_id('res.partner'), }) self.env['account_followup.followup.line'].create({ 'company_id': self.env.company.id, 'name': 'First Reminder', 'delay': 15, 'send_email': True, 'mail_template_id': mail_template.id, }) invoice = self._create_invoice(invoice_date='2016-01-01', post=True) invoice_attachment = self.env['ir.attachment'].create({ 'name': 'invoice_attachment.pdf', 'res_id': invoice.id, 'res_model': 'account.move', 'res_field': 'invoice_pdf_report_file', 'datas': 'test', 'type': 'binary', }) invoice._message_set_main_attachment_id(invoice_attachment) # Simulate toggling "No Follow-Up" in the customer statement. invoice.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable').no_followup = True wizard = self.env['account_followup.manual_reminder'].with_context( active_model='res.partner', active_ids=self.partner_a.ids, ).create({}) self.assertNotIn( invoice_attachment.id, wizard.attachment_ids.ids, "The invoice marked as No Follow-Up should not be part of the wizard attachments list.", ) options = wizard._get_wizard_options() options['followup_line'] = self.partner_a.followup_line_id or self.partner_a._get_first_followup_level() with patch.object(self.env.registry['account.report'], 'export_to_pdf', autospec=True, side_effect=lambda *args, **kwargs: {'file_name': 'fake_partner_ledger.pdf', 'file_content': b'', 'file_type': 'pdf'}): self.partner_a._get_followup_attachments(options) self.assertNotIn(invoice_attachment.id, options['attachment_ids'])