# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import textwrap from odoo import fields from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.addons.account.tests.test_account_incoming_supplier_invoice import TestAccountInvoiceImportMixin from odoo.addons.iap_extract.tests.test_extract_mixin import TestExtractMixin from odoo.addons.mail.tests.common import MailCase from odoo.tests import tagged from odoo.tools import file_open from ..models.account_invoice import OCR_VERSION @tagged('post_install', '-at_install') class TestInvoiceExtract(AccountTestInvoicingCommon, TestExtractMixin, TestAccountInvoiceImportMixin, MailCase): @classmethod def setUpClass(cls): super().setUpClass() # give default values for all email aliases and domain cls._init_mail_gateway() cls._init_mail_servers() cls.env.user.group_ids |= cls.env.ref('base.group_system') # Required for `price_total` to be visible in the view config = cls.env['res.config.settings'].create({}) config.execute() cls.journal_with_alias = cls.env['account.journal'].search( [('company_id', '=', cls.env.user.company_id.id), ('type', '=', 'sale')], limit=1, ) # Make sure only the USD currency is active cls.env['res.currency'].search([('name', '!=', 'USD')]).with_context(force_deactivate=True).active = False with file_open('base/tests/minimal.pdf', 'rb') as file: cls.sample_pdf_bytes = file.read() @classmethod def default_env_context(cls): # OVERRIDE to reactivate the tracking return {} def get_result_success_response(self): return { 'results': [{ 'client': {'selected_value': {'content': "Test"}, 'candidates': []}, 'supplier': {'selected_value': {'content': "Test"}, 'candidates': []}, 'total': {'selected_value': {'content': 330}, 'candidates': []}, 'subtotal': {'selected_value': {'content': 300}, 'candidates': []}, 'invoice_id': {'selected_value': {'content': 'INV0001'}, 'candidates': []}, 'total_tax_amount': {'selected_value': {'content': 30.0}, 'words': []}, 'currency': {'selected_value': {'content': 'EUR'}, 'candidates': []}, 'VAT_Number': {'selected_value': {'content': 'BE0477472701'}, 'candidates': []}, 'date': {'selected_value': {'content': '2019-04-12 00:00:00'}, 'candidates': []}, 'due_date': {'selected_value': {'content': '2019-04-19 00:00:00'}, 'candidates': []}, 'email': {'selected_value': {'content': 'test@email.com'}, 'candidates': []}, 'website': {'selected_value': {'content': 'www.test.com'}, 'candidates': []}, 'payment_ref': {'selected_value': {'content': '+++123/1234/12345+++'}, 'candidates': []}, 'iban': {'selected_value': {'content': 'BE01234567890123'}, 'candidates': []}, 'invoice_lines': [ { 'description': 'Test 1', 'unit_price': 100, 'quantity': 1, 'taxes': [15], 'subtotal': 100, 'total': 115, }, { 'description': 'Test 2', 'unit_price': 50, 'quantity': 2, 'taxes': [0], 'subtotal': 100, 'total': 100, }, { 'description': 'Test 3', 'unit_price': 20, 'quantity': 5, 'taxes': [15], 'subtotal': 100, 'total': 115, }, ], }], 'status': 'success', } def _get_email_for_journal_alias(self, message_id='some_msg_id'): attachment = base64.b64encode(self.sample_pdf_bytes).decode() alias = self.journal_with_alias.alias_id return textwrap.dedent(f'''\ MIME-Version: 1.0 Date: Fri, 26 Nov 2021 16:27:45 +0100 Message-ID: {message_id} Subject: Incoming bill From: Someone To: {alias.display_name} Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" --000000000000a47519057e029630 Content-Type: text/plain; charset=\"UTF-8\" --000000000000a47519057e029630 Content-Type: application/pdf Content-Transfer-Encoding: base64 {attachment} --000000000000a47519057e029630-- ''') def get_partner_autocomplete_response(self): return { 'data': { 'name': 'Partner', 'country_code': 'BE', 'vat': 'BE0477472701', 'city': 'Namur', 'bank_ids': [], 'zip': '2110', 'street': 'OCR street' } } def test_no_merge_check_ocr_status(self): # test check_ocr_status without lines merging self.env.company.extract_single_line_per_tax = False self.env.company.quick_edit_mode = "out_and_in_invoices" # Fiduciary mode is necessary for out_invoice for move_type in ('in_invoice', 'out_invoice'): invoice = self.env['account.move'].create({ 'move_type': move_type, 'extract_state': 'waiting_extraction', 'extract_document_uuid': 'some_token', }) extract_response = self.get_result_success_response() expected_get_results_params = { 'version': OCR_VERSION, 'document_token': 'some_token', 'account_token': invoice._get_iap_account().sudo().account_token, } with self._mock_iap_extract( extract_response=extract_response, assert_params=expected_get_results_params, ): invoice._check_ocr_status() self.assertEqual(invoice.extract_state, 'waiting_validation') self.assertEqual(invoice.extract_status, 'success') self.assertEqual(invoice.amount_total, 330) self.assertEqual(invoice.amount_untaxed, 300) self.assertEqual(invoice.amount_tax, 30) self.assertEqual(invoice.invoice_date, fields.Date.from_string('2019-04-12')) self.assertEqual(invoice.invoice_date_due, fields.Date.from_string('2019-04-19')) self.assertEqual(invoice.payment_reference, "+++123/1234/12345+++") if move_type == 'in_invoice': self.assertEqual(invoice.ref, 'INV0001') else: self.assertEqual(invoice.name, 'INV0001') self.assertEqual(len(invoice.invoice_line_ids), 3) for i, invoice_line in enumerate(invoice.invoice_line_ids): self.assertEqual(invoice_line.name, extract_response['results'][0]['invoice_lines'][i]['description']) self.assertEqual(invoice_line.price_unit, extract_response['results'][0]['invoice_lines'][i]['unit_price']) self.assertEqual(invoice_line.quantity, extract_response['results'][0]['invoice_lines'][i]['quantity']) tax = extract_response['results'][0]['invoice_lines'][i]['taxes'][0] if tax == 0: self.assertEqual(len(invoice_line.tax_ids), 0) else: self.assertEqual(len(invoice_line.tax_ids), 1) self.assertEqual(invoice_line.tax_ids[0].amount, tax) self.assertEqual(invoice_line.tax_ids[0].amount_type, 'percent') self.assertEqual(invoice_line.price_subtotal, extract_response['results'][0]['invoice_lines'][i]['subtotal']) self.assertEqual(invoice_line.price_total, extract_response['results'][0]['invoice_lines'][i]['total']) def test_included_default_tax(self): # test that a tax included coming from the account is not removed from the lines even if it's not detected tax_10_included = self.env['account.tax'].create({ 'name': 'Tax 10% included', 'amount': 10, 'type_tax_use': 'purchase', 'price_include_override': 'tax_included', }) self.company_data['default_account_expense'].write({ 'tax_ids': tax_10_included }) invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) extract_response = self.get_result_success_response() extract_response['results'][0]['total']['selected_value']['content'] = 300 for line in extract_response['results'][0]['invoice_lines']: line['total'] = line['subtotal'] line['taxes'] = [] with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertEqual(invoice.amount_total, 300) for line in invoice.invoice_line_ids: self.assertEqual(line.tax_ids[0], tax_10_included) # test that the default purchase included tax is the only tax used if it matches the detected tax tax_15_included = self.env['account.tax'].create({ 'name': 'Tax 15% included', 'amount': 15, 'type_tax_use': 'purchase', 'price_include_override': 'tax_included', }) self.company_data['default_account_expense'].write({ 'tax_ids': tax_15_included }) invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice._check_ocr_status() self.assertEqual(invoice.amount_total, 330) for line in invoice.invoice_line_ids: self.assertEqual(line.tax_ids[0], tax_15_included) self.assertTrue(line.is_imported) def test_merge_check_ocr_status(self): # test check_ocr_status with lines merging for move_type in ('in_invoice', 'out_invoice'): invoice = self.env['account.move'].create({'move_type': move_type, 'extract_state': 'waiting_extraction'}) self.env.company.extract_single_line_per_tax = True with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice._check_ocr_status() self.assertEqual(len(invoice.invoice_line_ids), 2) # line 1 and 3 should be merged as they both have a 15% tax self.assertEqual(invoice.invoice_line_ids[0].name, "Test - 2019-04-12") self.assertEqual(invoice.invoice_line_ids[0].price_unit, 200) self.assertEqual(invoice.invoice_line_ids[0].quantity, 1) self.assertEqual(len(invoice.invoice_line_ids[0].tax_ids), 1) self.assertEqual(invoice.invoice_line_ids[0].tax_ids[0].amount, 15) self.assertEqual(invoice.invoice_line_ids[0].tax_ids[0].amount_type, 'percent') self.assertEqual(invoice.invoice_line_ids[0].price_subtotal, 200) self.assertEqual(invoice.invoice_line_ids[0].price_total, 230) # line 2 has no tax self.assertEqual(invoice.invoice_line_ids[1].name, "Test - 2019-04-12") self.assertEqual(invoice.invoice_line_ids[1].price_unit, 100) self.assertEqual(invoice.invoice_line_ids[1].quantity, 1) self.assertEqual(len(invoice.invoice_line_ids[1].tax_ids), 0) self.assertEqual(invoice.invoice_line_ids[1].price_subtotal, 100) self.assertEqual(invoice.invoice_line_ids[1].price_total, 100) def test_partner_creation_from_vat(self): # test that the partner isn't created if the VAT number isn't valid invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice._check_ocr_status() self.assertFalse(invoice.partner_id) # test that the partner is created if the VAT number is valid invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract( extract_response=self.get_result_success_response(), partner_autocomplete_response=self.get_partner_autocomplete_response(), ): invoice._check_ocr_status() self.assertEqual(invoice.partner_id.name, 'Partner') self.assertEqual(invoice.partner_id.vat, 'BE0477472701') def test_partner_selection_from_vat(self): # test that if a partner with the VAT found already exists in database it is selected invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) existing_partner = self.env['res.partner'].create({'name': 'Existing partner', 'vat': 'BE0477472701'}) with self._mock_iap_extract( extract_response=self.get_result_success_response(), partner_autocomplete_response={'name': 'A new partner', 'vat': 'BE0477472701'}, ): invoice._check_ocr_status() self.assertEqual(invoice.partner_id, existing_partner) def test_partner_selection_from_iban_and_good_name(self): # test that if the IBAN found already exists in database and the name is close enough, it is selected invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) existing_partner = self.env['res.partner'].create({ 'name': 'test', 'bank_ids': [(0, 0, {'acc_number': "BE01 2345 6789 0123"})], }) with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice._check_ocr_status() self.assertEqual(invoice.partner_id, existing_partner) def test_partner_selection_from_iban_and_bad_name(self): # test that if the IBAN found already exists in database but the name is too different, it is not selected invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) self.env['res.partner'].create({ 'name': 'Existing partner', 'bank_ids': [(0, 0, {'acc_number': "BE01 2345 6789 0123"})], }) with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice._check_ocr_status() self.assertFalse(invoice.partner_id) def test_partner_selection_from_name(self): # test that if a partner with a similar name already exists in database it is selected invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) existing_partner = self.env['res.partner'].create({'name': 'Test'}) self.env['res.partner'].create({'name': 'Partner'}) self.env['res.partner'].create({'name': 'Another supplier'}) with self._mock_iap_extract( extract_response=self.get_result_success_response(), partner_autocomplete_response={'name': 'A new partner', 'vat': 'BE0477472701'} ): invoice._check_ocr_status() self.assertEqual(invoice.partner_id, existing_partner) # test that if no partner with a similar name exists, the partner isn't set invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) extract_response = self.get_result_success_response() extract_response['results'][0]['supplier']['selected_value']['content'] = 'Blablablablabla' with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertFalse(invoice.partner_id) def test_multi_currency(self): # test that if the multi currency is disabled, the currency isn't changed invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) test_user = self.env.ref('base.user_root') test_user.group_ids = [(3, self.env.ref('base.group_multi_currency').id)] usd_currency = self.env['res.currency'].search([('name', '=', 'USD')]) eur_currency = self.env['res.currency'].with_context({'active_test': False}).search([('name', '=', 'EUR')]) invoice.currency_id = usd_currency.id with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice.with_user(test_user)._check_ocr_status() self.assertEqual(invoice.currency_id, usd_currency) # test that if multi currency is enabled, the currency is changed # group_multi_currency is automatically activated on currency activation eur_currency.active = True # test with the name of the currency invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) invoice.currency_id = usd_currency.id with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice.with_user(test_user)._check_ocr_status() self.assertEqual(invoice.currency_id, eur_currency) # test with the symbol of the currency invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) invoice.currency_id = usd_currency.id extract_response = self.get_result_success_response() extract_response['results'][0]['currency']['selected_value']['content'] = '€' with self._mock_iap_extract(extract_response=extract_response): invoice.with_user(test_user)._check_ocr_status() self.assertEqual(invoice.currency_id, eur_currency) # test with the invoice having an invoice line invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) invoice.currency_id = usd_currency.id self.env['account.move.line'].create({ 'move_id': invoice.id, 'account_id': self.company_data['default_account_expense'].id, 'name': 'Test Invoice Line', }) extract_response = self.get_result_success_response() extract_response['results'][0]['currency']['selected_value']['content'] = '€' with self._mock_iap_extract(extract_response, {}): invoice.with_user(test_user)._check_ocr_status() # test if the currency is still the same after extracting the invoice self.assertEqual(invoice.currency_id, usd_currency) def test_same_name_currency(self): # test that when we have several currencies with the same name, and no antecedants with the partner, we take the one that is on our company. cad_currency = self.env['res.currency'].with_context({'active_test': False}).search([('name', '=', 'CAD')]) usd_currency = self.env['res.currency'].with_context({'active_test': False}).search([('name', '=', 'USD')]) (cad_currency | usd_currency).active = True test_user = self.env.user test_user.group_ids = [(3, self.env.ref('base.group_multi_currency').id)] self.assertEqual(test_user.currency_id, usd_currency) extract_response = self.get_result_success_response() extract_response['results'][0]['currency']['selected_value']['content'] = 'dollars' invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract(extract_response=extract_response): invoice.with_user(test_user)._check_ocr_status() self.assertEqual(invoice.currency_id, usd_currency) # test that the currency of the last invoice (with a currency) of the partner is used for its next invoice partner = self.env['res.partner'].create({'name': 'O Canada'}) # create an existing invoice with a currency for this partner self.env['account.move'].create({'move_type': 'in_invoice', 'partner_id': partner.id, 'currency_id': cad_currency.id}) invoice = self.env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': partner.id, 'extract_state': 'waiting_extraction', }) with self._mock_iap_extract(extract_response=extract_response): invoice.with_user(test_user)._check_ocr_status() self.assertEqual(invoice.currency_id, cad_currency) def test_tax_adjustments(self): # test that if the total computed by Odoo doesn't exactly match the total found by the OCR, the tax are adjusted accordingly for move_type in ('in_invoice', 'out_invoice'): invoice = self.env['account.move'].create({'move_type': move_type, 'extract_state': 'waiting_extraction'}) extract_response = self.get_result_success_response() extract_response['results'][0]['total']['selected_value']['content'] += 0.01 with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertEqual(invoice.amount_tax, 30.01) self.assertEqual(invoice.amount_untaxed, 300) self.assertEqual(invoice.amount_total, 330.01) def test_non_existing_tax(self): # test that if there is an invoice line with a tax which doesn't exist in database it is ignored invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) extract_response = self.get_result_success_response() extract_response['results'][0]['total']['selected_value']['content'] = 123.4 extract_response['results'][0]['subtotal']['selected_value']['content'] = 100 extract_response['results'][0]['invoice_lines'] = [ { 'description': 'Test 1', 'unit_price': 100, 'quantity': 1, 'taxes': [12.34], 'subtotal': 100, 'total': 123.4, }, ] with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertEqual(len(invoice.invoice_line_ids), 1) self.assertEqual(invoice.invoice_line_ids[0].price_unit, 100) self.assertEqual(invoice.invoice_line_ids[0].quantity, 1) self.assertEqual(len(invoice.invoice_line_ids[0].tax_ids), 0) self.assertEqual(invoice.invoice_line_ids[0].price_subtotal, 100) self.assertEqual(invoice.invoice_line_ids[0].price_total, 100) def test_server_error(self): # test that the extract state is set to 'error' if the OCR returned an error invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract(extract_response={'status': 'error_internal'}): invoice._check_ocr_status() self.assertEqual(invoice.extract_state, 'error_status') self.assertEqual(invoice.extract_status, 'error_internal') def test_server_not_ready(self): # test that the extract state is set to 'not_ready' if the OCR didn't finish to process the invoice invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract(extract_response=self.parse_processing_response()): invoice._check_ocr_status() self.assertEqual(invoice.extract_state, 'extract_not_ready') self.assertEqual(invoice.extract_status, 'processing') def test_preupdate_other_waiting_invoices(self): # test that when we update an invoice, other invoices waiting for extraction are updated as well invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) invoice2 = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice.check_ocr_status() self.assertEqual(invoice.extract_state, 'waiting_validation') self.assertEqual(invoice2.extract_state, 'waiting_validation') def test_no_overwrite_client_values(self): # test that we are not overwriting the values entered by the client partner = self.env['res.partner'].create({'name': 'Blabla', 'vat': 'BE0477472701'}) invoice = self.env['account.move'].create({ 'move_type': 'in_invoice', 'extract_state': 'waiting_extraction', 'invoice_date': '2019-04-01', 'date': '2019-04-01', 'invoice_date_due': '2019-05-01', 'ref': 'INV1234', 'partner_id': partner.id, 'invoice_line_ids': [(0, 0, { 'name': 'Blabla', 'price_unit': 13.0, 'quantity': 2.0, 'account_id': self.company_data['default_account_revenue'].id, })], }) self.env['res.partner'].create({'name': 'Test', 'vat': 'BE0477472701'}) # this match the partner found in the server response with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice.check_ocr_status() self.assertEqual(invoice.extract_state, 'waiting_validation') self.assertEqual(invoice.ref, 'INV1234') self.assertEqual(invoice.invoice_date, fields.Date.from_string('2019-04-01')) self.assertEqual(invoice.invoice_date_due, fields.Date.from_string('2019-05-01')) self.assertEqual(invoice.partner_id, partner) self.assertEqual(len(invoice.invoice_line_ids), 1) self.assertEqual(invoice.invoice_line_ids[0].name, "Blabla") self.assertEqual(invoice.invoice_line_ids[0].price_unit, 13) self.assertEqual(invoice.invoice_line_ids[0].quantity, 2) def test_invoice_validation(self): # test that when we post the invoice, the validation is sent to the server invoice = self.env['account.move'].create({ 'move_type': 'in_invoice', 'extract_state': 'waiting_extraction', 'extract_document_uuid': 'some_token', }) with self._mock_iap_extract( extract_response=self.get_result_success_response(), partner_autocomplete_response=self.get_partner_autocomplete_response(), ): invoice._check_ocr_status() expected_validation_params = { 'version': OCR_VERSION, 'values': { 'total': {'content': invoice.amount_total}, 'subtotal': {'content': invoice.amount_untaxed}, 'total_tax_amount': {'content': invoice.amount_tax}, 'date': {'content': str(invoice.invoice_date)}, 'due_date': {'content': str(invoice.invoice_date_due)}, 'invoice_id': {'content': invoice.ref}, 'partner': {'content': invoice.partner_id.name}, 'VAT_Number': {'content': invoice.partner_id.vat}, 'currency': {'content': invoice.currency_id.name}, 'payment_ref': {'content': invoice.payment_reference}, 'iban': {'content': invoice.partner_bank_id.acc_number}, 'SWIFT_code': {'content': invoice.partner_bank_id.bank_bic}, 'merged_lines': True, 'invoice_lines': { 'lines': [ { 'description': il.name, 'quantity': il.quantity, 'unit_price': il.price_unit, 'product': il.product_id.id, 'taxes_amount': round(il.price_total - il.price_subtotal, 2), 'taxes': [ { 'amount': tax.amount, 'type': tax.amount_type, 'price_include': tax.price_include, } for tax in il.tax_ids ], 'subtotal': il.price_subtotal, 'total': il.price_total, } for il in invoice.invoice_line_ids ] } }, 'document_token': 'some_token', 'account_token': invoice._get_iap_account().sudo().account_token, } with self._mock_iap_extract( extract_response=self.validate_success_response(), assert_params=expected_validation_params, ): invoice.action_post() self.assertEqual(invoice.extract_state, 'done') self.assertEqual(invoice._get_validation('total')['content'], invoice.amount_total) self.assertEqual(invoice._get_validation('subtotal')['content'], invoice.amount_untaxed) self.assertEqual(invoice._get_validation('date')['content'], str(invoice.invoice_date)) self.assertEqual(invoice._get_validation('due_date')['content'], str(invoice.invoice_date_due)) self.assertEqual(invoice._get_validation('invoice_id')['content'], invoice.ref) self.assertEqual(invoice._get_validation('partner')['content'], invoice.partner_id.name) self.assertEqual(invoice._get_validation('total_tax_amount')['content'], invoice.amount_tax) self.assertEqual(invoice._get_validation('VAT_Number')['content'], invoice.partner_id.vat) self.assertEqual(invoice._get_validation('currency')['content'], invoice.currency_id.name) self.assertEqual(invoice._get_validation('payment_ref')['content'], invoice.payment_reference) validation_invoice_lines = invoice._get_validation('invoice_lines')['lines'] for i, il in enumerate(invoice.invoice_line_ids): self.assertDictEqual(validation_invoice_lines[i], { 'description': il.name, 'quantity': il.quantity, 'unit_price': il.price_unit, 'product': il.product_id.id, 'taxes_amount': round(il.price_total - il.price_subtotal, 2), 'taxes': [{ 'amount': tax.amount, 'type': tax.amount_type, 'price_include': tax.price_include} for tax in il.tax_ids], 'subtotal': il.price_subtotal, 'total': il.price_total, }) def test_automatic_sending_vendor_bill_message_post(self): # test that a vendor bill is automatically sent to the OCR server when a message with attachment is posted and the option is enabled self.env.company.extract_in_invoice_digitalization_mode = 'auto_send' invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'no_extract_requested'}) test_attachment = self.env['ir.attachment'].create({ 'name': "attachment.pdf", 'raw': self.sample_pdf_bytes, }) expected_parse_params = { 'version': OCR_VERSION, 'account_token': 'test_token', 'dbuuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'), 'documents': [test_attachment.datas.decode('utf-8')], 'user_infos': { 'perspective': 'client', 'user_company_VAT': invoice.company_id.vat, 'user_company_country_code': invoice.company_id.country_id.code, 'user_company_name': invoice.company_id.name, 'user_email': self.env.user.email, 'user_lang': self.env.ref('base.user_root').lang, }, 'webhook_url': f'{invoice.get_base_url()}/account_invoice_extract/request_done', } if self.env['ir.module.module']._get('account_invoice_extract_purchase').state == 'installed': expected_parse_params['user_infos']['purchase_order_regex'] = r'P\d{5}' with self._mock_iap_extract( extract_response=self.parse_success_response(), assert_params=expected_parse_params, ): invoice.message_post(message_type='comment', attachment_ids=test_attachment.ids) self.assertEqual(invoice.extract_state, 'waiting_extraction') self.assertEqual(invoice.extract_document_uuid, 'some_token') def test_automatic_sending_vendor_bill_main_attachment(self): # test that a vendor bill is automatically sent to the OCR server when a main attachment is registered and the option is enabled self.env.company.extract_in_invoice_digitalization_mode = 'auto_send' invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'no_extract_requested'}) test_attachment = self.env['ir.attachment'].create({ 'name': "an attachment", "mimetype": "application/pdf", 'raw': self.sample_pdf_bytes, 'res_model': 'account.move', 'res_id': invoice.id, }) with self._mock_iap_extract(extract_response=self.parse_success_response()): test_attachment.register_as_main_attachment() self.assertEqual(invoice.extract_state, 'waiting_extraction') self.assertEqual(invoice.extract_document_uuid, 'some_token') def test_not_automatic_sending_entry_main_attachment(self): # test that an entry is not automatically sent to the OCR server when a main attachment is registered and the option is enabled self.env.company.extract_in_invoice_digitalization_mode = 'auto_send' invoice = self.env['account.move'].create({'move_type': 'entry', 'extract_state': 'no_extract_requested'}) test_attachment = self.env['ir.attachment'].create({ 'name': "an attachment", 'raw': b'My attachment', 'res_model': 'account.move', 'res_id': invoice.id, }) with self._mock_iap_extract(extract_response=self.parse_success_response()): test_attachment.register_as_main_attachment() self.assertEqual(invoice.extract_state, 'no_extract_requested') self.assertFalse(invoice.extract_document_uuid) def test_automatic_sending_multiple_vendor_bill_message_post(self): # test that when multiple pdf attachments are posted and the option is enabled each one is split # into a separate move self.env.company.extract_in_invoice_digitalization_mode = 'auto_send' attachments_vals = [ { 'name': 'Attachment 1', 'raw': self.sample_pdf_bytes, 'mimetype': 'application/pdf', }, { 'name': 'Attachment 2', 'raw': self.sample_pdf_bytes, 'mimetype': 'application/pdf', }, ] with self._mock_iap_extract( extract_response=self.parse_success_response(), ): attachments, _messages, invoices = self._upload_and_import_attachments(origin='mail_alias', attachments_vals=attachments_vals) self.assertEqual(len(invoices), 2, "Two separate bills should have been created") for inv, att in zip(invoices, attachments): self.assertEqual(inv.extract_state, 'waiting_extraction') self.assertEqual(inv.extract_document_uuid, 'some_token') self.assertEqual(inv.message_main_attachment_id, att) def test_automatic_sending_customer_invoice_upload(self): # test that a customer invoice is automatically sent to the OCR server when uploaded and the option is enabled self.env.company.extract_out_invoice_digitalization_mode = 'auto_send' test_attachment = self.env['ir.attachment'].create({ 'name': "attachment.pdf", 'raw': self.sample_pdf_bytes, }) with self._mock_iap_extract(extract_response=self.parse_success_response()): action = self.env['account.journal'].with_context(default_move_type='out_invoice').create_document_from_attachment(test_attachment.id) invoice = self.env['account.move'].browse(action['res_id']) self.assertEqual(invoice.extract_state, 'waiting_extraction') self.assertEqual(invoice.extract_document_uuid, 'some_token') def test_automatic_sending_customer_invoice_email_alias(self): # test that a customer invoice is automatically sent to the OCR server when sent via email alias and the option is enabled self.env.company.extract_out_invoice_digitalization_mode = 'auto_send' mail = self._get_email_for_journal_alias( message_id='message_2' ) with self._mock_iap_extract(self.parse_success_response()): invoice = self.env['account.move'].browse(self.env['mail.thread'].message_process('account.move', mail)) self.assertEqual(invoice.extract_state, 'waiting_extraction') self.assertEqual(invoice.extract_document_uuid, 'some_token') def test_no_automatic_sending_customer_invoice_email_alias(self): # test that a customer invoice isn't automatically sent to the OCR server when sent via email alias and the option is disabled self.env.company.extract_out_invoice_digitalization_mode = 'manual_send' mail = self._get_email_for_journal_alias() with self._mock_iap_extract(self.parse_success_response()): invoice = self.env['account.move'].browse(self.env['mail.thread'].message_process('account.move', mail)) self.assertEqual(invoice.extract_state, 'no_extract_requested') def test_no_automatic_sending_customer_invoice_message_post(self): # test that a customer invoice isn't automatically sent to the OCR server when a message with attachment is posted and the option is enabled self.env.company.extract_out_invoice_digitalization_mode = 'auto_send' invoice = self.env['account.move'].create({'move_type': 'out_invoice', 'extract_state': 'no_extract_requested'}) test_attachment = self.env['ir.attachment'].create({ 'name': "an attachment", 'raw': b'My attachment', }) with self._mock_iap_extract(extract_response=self.parse_success_response()): invoice.message_post(attachment_ids=[test_attachment.id]) self.assertEqual(invoice.extract_state, 'no_extract_requested') self.assertFalse(invoice.extract_document_uuid) def test_no_automatic_sending_customer_invoice_main_attachment(self): # test that a customer invoice isn't automatically sent to the OCR server when a main attachment is registered and the option is enabled self.env.company.extract_out_invoice_digitalization_mode = 'auto_send' invoice = self.env['account.move'].create({'move_type': 'out_invoice', 'extract_state': 'no_extract_requested'}) test_attachment = self.env['ir.attachment'].create({ 'name': "an attachment", 'raw': b'My attachment', 'res_model': 'account.move', 'res_id': invoice.id, }) with self._mock_iap_extract(extract_response=self.parse_success_response()): test_attachment.register_as_main_attachment() self.assertEqual(invoice.extract_state, 'no_extract_requested') self.assertFalse(invoice.extract_document_uuid) def test_no_automatic_sending_option_disabled(self): # test that an invoice isn't automatically sent to the OCR server when the option is disabled self.env.company.extract_in_invoice_digitalization_mode = 'manual_send' self.env.company.extract_out_invoice_digitalization_mode = 'manual_send' for move_type in ('in_invoice', 'out_invoice'): # test with message_post() invoice = self.env['account.move'].create({'move_type': move_type, 'extract_state': 'no_extract_requested'}) test_attachment = self.env['ir.attachment'].create({ 'name': "an attachment", 'raw': b'My attachment', }) with self._mock_iap_extract(extract_response=self.parse_success_response()): invoice.message_post(attachment_ids=[test_attachment.id]) self.assertEqual(invoice.extract_state, 'no_extract_requested') # test with register_as_main_attachment() invoice = self.env['account.move'].create({'move_type': move_type, 'extract_state': 'no_extract_requested'}) test_attachment = self.env['ir.attachment'].create({ 'name': "another attachment", 'raw': b'My other attachment', 'res_model': 'account.move', 'res_id': invoice.id, }) with self._mock_iap_extract(extract_response=self.parse_success_response()): test_attachment.register_as_main_attachment() self.assertEqual(invoice.extract_state, 'no_extract_requested') self.assertFalse(invoice.extract_document_uuid) def test_bank_account(self): # test that the bank account is set when an iban is found # test that an account is created if no existing matches the account number invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract( extract_response=self.get_result_success_response(), partner_autocomplete_response=self.get_partner_autocomplete_response(), ): invoice._check_ocr_status() self.assertEqual(invoice.partner_bank_id.acc_number, 'BE01234567890123') # test that it uses the existing bank account if it exists and if it is trusted created_bank_account = invoice.partner_bank_id created_bank_account.allow_out_payment = True invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice._check_ocr_status() self.assertEqual(invoice.partner_bank_id, created_bank_account) def test_tax_price_included(self): self.env['account.tax'].create({ 'name': 'Tax 12% included', 'amount': 12, 'amount_type': 'percent', 'type_tax_use': 'purchase', 'price_include_override': 'tax_included', 'company_id': self.company_data['company'].id }) invoice = self._create_invoice_with_tax() self.assertRecordValues(invoice.invoice_line_ids, [{ 'quantity': 1, 'price_subtotal': 100, 'price_total': 112, }]) self.assertAlmostEqual(invoice.invoice_line_ids.price_unit, 112) def test_tax_price_excluded(self): self.env['account.tax'].create({ 'name': 'Tax 12% excluded', 'amount': 12, 'amount_type': 'percent', 'type_tax_use': 'purchase', 'company_id': self.company_data['company'].id, 'price_include_override': 'tax_excluded', }) invoice = self._create_invoice_with_tax() self.assertRecordValues(invoice.invoice_line_ids, [{ 'price_unit': 100, 'quantity': 1, 'price_subtotal': 100, 'price_total': 112, }]) def _create_invoice_with_tax(self): invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) extract_response = self.get_result_success_response() extract_response['results'][0]['total']['selected_value']['content'] = 112 extract_response['results'][0]['subtotal']['selected_value']['content'] = 100 extract_response['results'][0]['invoice_lines'] = [ { 'description': 'Test 1', 'unit_price': 100, 'quantity': 1, 'taxes': [12], 'subtotal': 100, 'total': 112, }, ] with self._mock_iap_extract(extract_response, {}): invoice._check_ocr_status() return invoice def test_credit_note_detection(self): # test that move type changes, if and only if the type in the ocr results is refund the current move type is invoice invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) extract_response = self.get_result_success_response() extract_response['results'][0]['type'] = 'refund' with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertEqual(invoice.move_type, 'in_refund') invoice = self.env['account.move'].create({'move_type': 'out_refund', 'extract_state': 'waiting_extraction'}) extract_response['results'][0]['type'] = 'invoice' with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertEqual(invoice.move_type, 'out_refund') def test_action_reload_ai_data(self): # test that the "Reload AI data" button overwrites the content of the invoice with the OCR results self.env.company.extract_single_line_per_tax = False ocr_partner = self.env['res.partner'].create({'name': 'Test', 'vat': 'BE0477472701'}) invoice = self.env['account.move'].create({ 'move_type': 'in_invoice', 'extract_state': 'waiting_validation', 'invoice_date': '2019-04-01', 'date': '2019-04-01', 'invoice_date_due': '2019-05-01', 'ref': 'INV1234', 'payment_reference': '+++111/2222/33333+++', 'partner_id': self.partner_a.id, 'invoice_line_ids': [(0, 0, { 'name': 'Blabla', 'price_unit': 13.0, 'quantity': 2.0, 'account_id': self.company_data['default_account_revenue'].id, })], }) extract_response = self.get_result_success_response() with self._mock_iap_extract(extract_response=extract_response): invoice.action_reload_ai_data() self.assertEqual(invoice.extract_state, 'waiting_validation') # Check that the fields have been overwritten with the OCR results self.assertEqual(invoice.amount_total, 330) self.assertEqual(invoice.amount_untaxed, 300) self.assertEqual(invoice.amount_tax, 30) self.assertEqual(invoice.partner_id, ocr_partner) self.assertEqual(invoice.invoice_date, fields.Date.from_string('2019-04-12')) self.assertEqual(invoice.invoice_date_due, fields.Date.from_string('2019-04-19')) self.assertEqual(invoice.payment_reference, '+++123/1234/12345+++') self.assertEqual(invoice.ref, 'INV0001') self.assertEqual(invoice.invoice_line_ids.mapped('name'), ["Test 1", "Test 2", "Test 3"]) def test_autopost_bills_ocr(self): # Test that when we validate 3 bills without modification from the OCR data, we show # the user a wizard to automate the posting for that vendor def create_bill_with_ocr(ref, date='2017-01-01'): move = self.env['account.move'].create({ 'move_type': 'in_invoice', 'ref': ref, 'extract_state': 'waiting_extraction', 'extract_document_uuid': 'some_token', 'invoice_date': date, }) with self._mock_iap_extract( extract_response=self.get_result_success_response(), partner_autocomplete_response=self.get_partner_autocomplete_response(), ): move._check_ocr_status() return move # Do it two times, no wizard should be shown for _ in range(2): bill = create_bill_with_ocr("ref1") self.assertEqual(bill.state, "draft") autopost_bills_wizard = bill.action_post() self.assertFalse(autopost_bills_wizard) self.assertFalse(bill.is_manually_modified) # Create a third invoice, No modification for the third time, we should show the wizard bill = create_bill_with_ocr("ref2") self.assertEqual(bill.state, "draft") post_result = bill.action_post() self.assertEqual(post_result.get('res_model'), 'account.autopost.bills.wizard') autopost_bills_wizard = self.env[post_result.get('res_model')].browse(post_result.get('res_id')) self.assertTrue(autopost_bills_wizard) autopost_bills_wizard.action_automate_partner() # Now, next time the OCR is finished, we should autopost the bill bill = create_bill_with_ocr("ref3", date="2017-01-02") self.assertEqual(bill.state, "posted") def test_invoice_ocr_note_author(self): invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'no_extract_requested'}) self.env.cr.flush() attachment = self.env['ir.attachment'].create({ 'name': 'test_attachment.png', 'res_model': 'account.move', 'raw': b'My invoice', }) with self._mock_iap_extract(extract_response=self.parse_success_response()): invoice.message_post(message_type='comment', attachment_ids=attachment.ids) with self._mock_iap_extract(extract_response=self.get_result_success_response()): invoice.check_all_status() self.env.cr.flush() message = self.env['mail.message'].search([ ('model', '=', 'account.move'), ('res_id', '=', invoice.id), ('tracking_value_ids', '!=', False), ]).ensure_one() author_name = message.author_id.complete_name self.assertEqual(author_name, 'OdooBot')