Files

1228 lines
54 KiB
Python

# -*- 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'])