# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import Command from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.addons.iap_extract.tests.test_extract_mixin import TestExtractMixin from odoo.tests import Form, tagged @tagged('post_install', '-at_install') class TestInvoiceExtractPurchase(AccountTestInvoicingCommon, TestExtractMixin): @classmethod def setUpClass(cls): super().setUpClass() cls.env.user.group_ids |= cls.env.ref('base.group_system') cls.env.company.write({'account_purchase_tax_id': None}) # Required for `price_total` to be visible in the view config = cls.env['res.config.settings'].create({}) config.execute() cls.vendor = cls.env['res.partner'].create({'name': 'Odoo', 'vat': 'BE0477472701'}) cls.product1 = cls.env['product.product'].create({'name': 'Test 1', 'list_price': 100.0}) cls.product2 = cls.env['product.product'].create({'name': 'Test 2', 'list_price': 50.0}) cls.product3 = cls.env['product.product'].create({'name': 'Test 3', 'list_price': 20.0}) po = Form(cls.env['purchase.order']) po.partner_id = cls.vendor po.partner_ref = "INV1234" with po.order_line.new() as po_line: po_line.product_id = cls.product1 po_line.product_qty = 1 po_line.price_unit = 100 with po.order_line.new() as po_line: po_line.product_id = cls.product2 po_line.product_qty = 2 po_line.price_unit = 50 with po.order_line.new() as po_line: po_line.product_id = cls.product3 po_line.product_qty = 5 po_line.price_unit = 20 cls.purchase_order = po.save() cls.purchase_order.button_confirm() for line in cls.purchase_order.order_line: line.qty_received = line.product_qty def get_result_success_response(self): return { 'results': [{ 'supplier': {'selected_value': {'content': "Test"}, 'candidates': []}, 'total': {'selected_value': {'content': 300}, 'candidates': []}, 'subtotal': {'selected_value': {'content': 300}, 'candidates': []}, 'total_tax_amount': {'selected_value': {'content': 0.0}, 'words': []}, 'invoice_id': {'selected_value': {'content': 'INV0001'}, 'candidates': []}, 'currency': {'selected_value': {'content': 'EUR'}, 'candidates': []}, 'VAT_Number': {'selected_value': {'content': 'BE123456789'}, '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': []}, 'purchase_order': {'selected_values': [{'content': self.purchase_order.name}], 'candidates': []}, 'invoice_lines': [ { 'description': 'Test 1', 'unit_price': 50, 'quantity': 1, 'taxes': [0], 'subtotal': 50, 'total': 50, }, { 'description': 'Test 2', 'unit_price': 75, 'quantity': 2, 'taxes': [0], 'subtotal': 150, 'total': 150, }, { 'description': 'Test 3', 'unit_price': 20, 'quantity': 5, 'taxes': [0], 'subtotal': 100, 'total': 100, }, ], }], 'status': 'success', } def test_match_po_by_name(self): invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'extract_state': 'waiting_extraction'}) extract_response = self.get_result_success_response() with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertTrue(invoice.id in self.purchase_order.invoice_ids.ids) def test_match_po_by_supplier_and_total(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]['supplier']['selected_value']['content'] = self.purchase_order.partner_id.name with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertTrue(invoice.id in self.purchase_order.invoice_ids.ids) def test_match_subset_of_order_lines(self): # Test the case were only one subset of order lines match the total found by the OCR 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'] = 200 extract_response['results'][0]['subtotal']['selected_value']['content'] = 200 extract_response['results'][0]['invoice_lines'] = extract_response['results'][0]['invoice_lines'][:2] with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertTrue(invoice.id in self.purchase_order.invoice_ids.ids) self.assertEqual(invoice.amount_total, 200) def test_no_match_subset_of_order_lines(self): # Test the case were two subsets of order lines match the total found by the OCR 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'] = 150 extract_response['results'][0]['subtotal']['selected_value']['content'] = 150 extract_response['results'][0]['invoice_lines'] = [extract_response['results'][0]['invoice_lines'][1]] with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertTrue(invoice.id in self.purchase_order.invoice_ids.ids) # The PO should be used instead of the OCR result self.assertEqual(invoice.amount_total, 300) def test_no_match(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]['purchase_order']['selected_values'][0]['content'] = self.purchase_order.name + '123' with self._mock_iap_extract(extract_response=extract_response): invoice._check_ocr_status() self.assertTrue(invoice.id not in self.purchase_order.invoice_ids.ids) def test_action_reload_ai_data(self): 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': 'INV0000', '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() # Check that the fields have been overwritten with the content of the PO matched by the OCR self.assertTrue(invoice.id in self.purchase_order.invoice_ids.ids) self.assertEqual(invoice.amount_total, 300) self.assertEqual(invoice.amount_untaxed, 300) self.assertEqual(invoice.amount_tax, 0) self.assertEqual(invoice.partner_id, self.vendor) self.assertEqual(invoice.ref, 'INV0001') self.assertEqual(invoice.invoice_line_ids.mapped('product_id'), self.product1 | self.product2 | self.product3) def test_no_purchase_extraction_when_bill_lines(self): ''' Tests that the purchase matching extraction does not update a bill that already has lines.''' # Step 1: create 2 identical POs partner = self.company_data['company'].partner_id po1 = self.env['purchase.order'].create({ "partner_id": partner.id, "order_line": [Command.create({ 'product_id': self.product_a.id, 'name': self.product_a.name, 'product_qty': 1.0, 'price_unit': 100, 'tax_ids': False, })], }) po2 = po1.copy() po2.order_line.write({'price_unit': 200}) (po1 + po2).button_confirm() (po1 + po2).order_line.write({'qty_received': 1}) # Step 2: Create a bill from PO1 bill_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) bill_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po1.id) bill = bill_form.save() self.assertEqual(bill.invoice_origin, po1.name) # Step 3: Extend the bill with PO2 extract_response = { 'results': [{ 'supplier': {'selected_value': {'content': "company_1_data"}, 'candidates': []}, 'total': {'selected_value': {'content': 200}, 'candidates': []}, 'subtotal': {'selected_value': {'content': 200}, 'candidates': []}, 'total_tax_amount': {'selected_value': {'content': 0.0}, 'words': []}, 'currency': {'selected_value': {'content': 'EUR'}, 'candidates': []}, 'purchase_order': {'selected_values': [{'content': po2.name}], 'candidates': []}, 'invoice_lines': [ { 'description': {'selected_value': {'content': 'product_a'}}, 'unit_price': {'selected_value': {'content': 200}}, 'quantity': {'selected_value': {'content': 1}}, 'taxes': {'selected_values': [{'content': 0, 'amount_type': 'percent'}]}, 'subtotal': {'selected_value': {'content': 200}}, 'total': {'selected_value': {'content': 200}}, }, ], }], 'status': 'success', } bill.extract_state = 'waiting_extraction' with self._mock_iap_extract(extract_response=extract_response): bill._check_ocr_status() self.assertEqual(bill.extract_status, 'success') self.assertEqual(bill.invoice_origin, po1.name)