472 lines
21 KiB
Python
472 lines
21 KiB
Python
import datetime
|
|
from unittest.mock import patch
|
|
|
|
from freezegun import freeze_time
|
|
|
|
from odoo import fields
|
|
from odoo.tests import Form, tagged
|
|
from odoo.tools import file_open, float_compare
|
|
|
|
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
|
from odoo.addons.hr_expense_extract.models.hr_expense import OCR_VERSION
|
|
from odoo.addons.hr_expense_predict_product.models.hr_expense import HrExpense
|
|
from odoo.addons.iap_extract.tests.test_extract_mixin import TestExtractMixin
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestExpenseExtractProcess(TestExpenseCommon, TestExtractMixin):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.expense = cls.env['hr.expense'].create({
|
|
'employee_id': cls.expense_employee.id,
|
|
'product_id': cls.product_c.id,
|
|
})
|
|
|
|
with file_open('base/tests/minimal.pdf', 'rb') as file:
|
|
pdf_bytes = file.read()
|
|
cls.attachment = cls.env['ir.attachment'].create([{
|
|
'name': 'Attachment 1',
|
|
'res_model': 'hr.expense',
|
|
'raw': pdf_bytes,
|
|
'mimetype': 'application/pdf',
|
|
}])
|
|
cls.env.company.expense_extract_show_ocr_option_selection = 'auto_send'
|
|
|
|
@classmethod
|
|
def default_env_context(cls):
|
|
# OVERRIDE to reactivate the tracking
|
|
return {}
|
|
|
|
def get_result_success_response(self):
|
|
return {
|
|
'status': 'success',
|
|
'results': [{
|
|
'description': {'selected_value': {'content': 'Pizzeria', 'candidates': []}},
|
|
'total': {'selected_value': {'content': 99.99, 'candidates': []}},
|
|
'date': {'selected_value': {'content': '2022-02-22', 'candidates': []}},
|
|
'currency': {'selected_value': {'content': 'euro', 'candidates': []}},
|
|
}],
|
|
}
|
|
|
|
def test_auto_send_for_digitization(self):
|
|
# test that the uploaded attachment is sent to the extract server when `auto_send` is set
|
|
expected_parse_params = {
|
|
'version': OCR_VERSION,
|
|
'account_token': 'test_token',
|
|
'dbuuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'),
|
|
'documents': [self.attachment.datas.decode('utf-8')],
|
|
'user_infos': {
|
|
'user_email': self.expense_user_employee.email,
|
|
'user_lang': self.env.ref('base.user_root').lang,
|
|
},
|
|
'webhook_url': f'{self.expense.get_base_url()}/hr_expense_extract/request_done',
|
|
}
|
|
|
|
usd_currency = self.env.ref('base.USD')
|
|
eur_currency = self.env.ref('base.EUR')
|
|
eur_currency.rate_ids.unlink()
|
|
eur_currency.active = True
|
|
|
|
Expense = self.env['hr.expense'].with_user(self.expense_user_employee)
|
|
with self._mock_iap_extract(
|
|
extract_response=self.parse_success_response(),
|
|
assert_params=expected_parse_params,
|
|
):
|
|
expense_id = Expense.sudo().create_expense_from_attachments(self.attachment.id)
|
|
expense = Expense.browse(expense_id)
|
|
|
|
self.assertEqual(expense.extract_state, 'waiting_extraction')
|
|
self.assertEqual(expense.extract_document_uuid, 'some_token')
|
|
self.assertTrue(expense.extract_state_processed)
|
|
self.assertFalse(expense.total_amount)
|
|
self.assertEqual(expense.currency_id, usd_currency)
|
|
|
|
extract_response = self.get_result_success_response()
|
|
expected_get_results_params = {
|
|
'version': OCR_VERSION,
|
|
'document_token': 'some_token',
|
|
'account_token': expense._get_iap_account().sudo().account_token,
|
|
}
|
|
with self._mock_iap_extract(
|
|
extract_response=extract_response,
|
|
assert_params=expected_get_results_params,
|
|
):
|
|
expense.check_all_status()
|
|
|
|
ext_result = extract_response['results'][0]
|
|
self.assertEqual(expense.extract_state, 'waiting_validation')
|
|
self.assertEqual(expense.name, 'Pizzeria')
|
|
self.assertEqual(float_compare(expense.total_amount, ext_result['total']['selected_value']['content'], 2), 0)
|
|
self.assertEqual(expense.currency_id, eur_currency)
|
|
self.assertEqual(str(expense.date), ext_result['date']['selected_value']['content'])
|
|
|
|
def test_manual_send_for_digitization(self):
|
|
# test the `manual_send` mode for digitization.
|
|
self.env.company.expense_extract_show_ocr_option_selection = 'manual_send'
|
|
extract_response = self.get_result_success_response()
|
|
|
|
eur_currency = self.env.ref('base.EUR')
|
|
eur_currency.active = True
|
|
|
|
self.assertEqual(self.expense.extract_state, 'no_extract_requested')
|
|
self.assertFalse(self.expense.extract_can_show_send_button)
|
|
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
|
|
self.assertEqual(self.expense.extract_state, 'no_extract_requested')
|
|
self.assertTrue(self.expense.extract_can_show_send_button)
|
|
|
|
with self._mock_iap_extract(extract_response=self.parse_success_response()):
|
|
self.expense.action_send_batch_for_digitization()
|
|
|
|
# upon success, no button shall be provided
|
|
self.assertFalse(self.expense.extract_can_show_send_button)
|
|
|
|
old_expense_name = self.expense.name
|
|
with self._mock_iap_extract(extract_response=extract_response):
|
|
self.expense.check_all_status()
|
|
|
|
ext_result = extract_response['results'][0]
|
|
self.assertEqual(self.expense.extract_state, 'waiting_validation')
|
|
self.assertEqual(self.expense.name, old_expense_name) # The expense already had a name, it shouldn't be updated
|
|
self.assertEqual(float_compare(self.expense.total_amount, ext_result['total']['selected_value']['content'], 2), 0)
|
|
self.assertEqual(self.expense.currency_id, eur_currency)
|
|
self.assertEqual(str(self.expense.date), ext_result['date']['selected_value']['content'])
|
|
self.assertEqual(self.expense.product_id, self.product_c)
|
|
|
|
def test_no_send_for_digitization(self):
|
|
# test that the `no_send` mode for digitization prevents the users from sending
|
|
self.env.company.expense_extract_show_ocr_option_selection = 'no_send'
|
|
|
|
with self._mock_iap_extract(extract_response=self.parse_success_response()):
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
|
|
self.assertEqual(self.expense.extract_state, 'no_extract_requested')
|
|
self.assertFalse(self.expense.extract_can_show_send_button)
|
|
|
|
def test_show_resend_button_when_not_enough_credits(self):
|
|
# test that upon not enough credit error, the retry button is provided
|
|
with self._mock_iap_extract(extract_response=self.parse_credit_error_response()):
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
|
|
self.assertFalse(self.expense.extract_can_show_send_button)
|
|
|
|
def test_status_not_ready(self):
|
|
# test the 'processing' ocr status effects
|
|
with self._mock_iap_extract(extract_response=self.parse_processing_response()):
|
|
self.expense._check_ocr_status()
|
|
|
|
self.assertEqual(self.expense.extract_state, 'extract_not_ready')
|
|
self.assertFalse(self.expense.extract_can_show_send_button)
|
|
|
|
def test_expense_validation(self):
|
|
# test that when the expense is submitted, the validation is sent to the server
|
|
with self._mock_iap_extract(extract_response=self.parse_success_response()):
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
|
|
with self._mock_iap_extract(self.get_result_success_response()):
|
|
self.expense._check_ocr_status()
|
|
|
|
self.assertEqual(self.expense.extract_state, 'waiting_validation')
|
|
|
|
# Test with an employee not linked to a user
|
|
self.env.user.group_ids += self.env.ref('hr.group_hr_user')
|
|
self.expense_employee.user_id = False
|
|
self.expense.employee_id = self.expense_employee.id
|
|
employee_without_user = self.expense
|
|
|
|
with self._mock_iap_extract(extract_response=self.parse_success_response()):
|
|
employee_without_user.message_post(attachment_ids=[self.attachment.id])
|
|
|
|
with self._mock_iap_extract(self.get_result_success_response()):
|
|
employee_without_user._check_ocr_status()
|
|
|
|
self.assertEqual(employee_without_user.extract_state, 'waiting_validation')
|
|
|
|
expected_validation_params = {
|
|
'version': OCR_VERSION,
|
|
'values': {
|
|
'total': {'content': self.expense.price_unit},
|
|
'date': {'content': str(self.expense.date)},
|
|
'description': {'content': self.expense.name},
|
|
'currency': {'content': self.expense.currency_id.name},
|
|
},
|
|
'document_token': 'some_token',
|
|
'account_token': self.expense._get_iap_account().sudo().account_token,
|
|
}
|
|
|
|
with self._mock_iap_extract(
|
|
extract_response=self.validate_success_response(),
|
|
assert_params=expected_validation_params,
|
|
):
|
|
self.expense.action_submit()
|
|
|
|
self.assertEqual(self.expense.extract_state, 'done')
|
|
|
|
def test_no_digitisation_for_posted_entries(self):
|
|
# Tests that if a move is created from an expense, it is not digitised again.
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
|
|
# We need to set a value, because if it is zero it would trigger non-zero constraints
|
|
self.expense.total_amount_currency = 1
|
|
|
|
self.expense.action_submit()
|
|
self.expense.with_user(self.expense_user_manager).action_approve()
|
|
self.post_expenses_with_wizard(self.expense)
|
|
|
|
move = self.expense.account_move_id
|
|
self.assertFalse(move._needs_auto_extract())
|
|
|
|
def test_no_change_in_price_unit_with_expense_no_extract(self):
|
|
"""
|
|
Test that the price unit does not change when the quantity changes after uploading an attachment
|
|
when there is no digitisation
|
|
"""
|
|
expense = self.env['hr.expense'].create({
|
|
'name': 'expense',
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_a.id,
|
|
})
|
|
with Form(expense) as form:
|
|
self.assertEqual(form.price_unit, 800)
|
|
|
|
self.attachment.res_id = expense.id
|
|
|
|
with Form(expense) as form:
|
|
form.quantity = 2
|
|
self.assertEqual(form.price_unit, 800)
|
|
|
|
def test_extract_multi_currencies(self):
|
|
"""Test that exchange rate is fetched during extraction"""
|
|
self.env.ref('base.EUR').active = True
|
|
self.env['res.currency.rate'].create({
|
|
'name': '2022-01-01',
|
|
'rate': 3,
|
|
'currency_id': self.env.ref('base.EUR').id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
ocr_results = self.get_result_success_response()['results'][0]
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
self.expense._fill_document_with_results(ocr_results=ocr_results)
|
|
|
|
self.assertEqual(self.expense.total_amount, 33.33)
|
|
|
|
def test_extract_multi_currencies_with_several_possible_currencies(self):
|
|
"""Test that the extraction does not crash"""
|
|
ocr_results = {
|
|
'description': {'selected_value': {'content': 'Pizzeria', 'candidates': []}},
|
|
'total': {'selected_value': {'content': 99.99, 'candidates': []}},
|
|
'date': {'selected_value': {'content': '2022-02-22', 'candidates': []}},
|
|
'currency': {'selected_value': {'content': '$', 'candidates': []}},
|
|
}
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
self.expense._fill_document_with_results(ocr_results=ocr_results)
|
|
|
|
self.assertTrue(self.expense.currency_id)
|
|
|
|
def test_extract_without_possible_currencies(self):
|
|
"""Test that the extraction does not crash"""
|
|
ocr_results = {
|
|
'description': {'selected_value': {'content': 'Pizzeria', 'candidates': []}},
|
|
'total': {'selected_value': {'content': 99.99, 'candidates': []}},
|
|
'date': {'selected_value': {'content': '2022-02-22', 'candidates': []}},
|
|
'currency': {'selected_value': {'content': 'undefined', 'candidates': []}},
|
|
}
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
self.expense._fill_document_with_results(ocr_results=ocr_results)
|
|
|
|
self.assertTrue(self.expense.currency_id)
|
|
|
|
def test_extract_no_total(self):
|
|
ocr_results = self.get_result_success_response()['results'][0]
|
|
del ocr_results['total']
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
self.expense._fill_document_with_results(ocr_results=ocr_results)
|
|
|
|
self.assertAlmostEqual(self.expense.total_amount_currency, 0, 2)
|
|
self.assertAlmostEqual(self.expense.total_amount, 0, 2)
|
|
self.assertEqual(self.expense.currency_id.name, 'EUR')
|
|
self.assertEqual(self.expense.date, fields.Date.to_date('2022-02-22'))
|
|
|
|
def test_extract_no_currency(self):
|
|
self.env['res.currency.rate'].create({
|
|
'name': '2022-01-01',
|
|
'rate': 3,
|
|
'currency_id': self.env.ref('base.EUR').id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
|
|
ocr_results = self.get_result_success_response()['results'][0]
|
|
del ocr_results['currency']
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
self.expense._fill_document_with_results(ocr_results=ocr_results)
|
|
|
|
self.assertAlmostEqual(self.expense.total_amount_currency, 99.99, 2)
|
|
self.assertAlmostEqual(self.expense.total_amount, 99.99, 2)
|
|
self.assertEqual(self.expense.currency_id.name, 'USD')
|
|
self.assertEqual(self.expense.date, fields.Date.to_date('2022-02-22'))
|
|
|
|
def test_expense_extract_product_with_cost_only_company_currency(self):
|
|
"""
|
|
Test that when an expense having a product whith a standard_price set up, we never change the currency
|
|
from the default company currency
|
|
"""
|
|
eur_currency = self.env.ref('base.EUR')
|
|
eur_currency.active = True
|
|
company_currency = self.env.company.currency_id
|
|
|
|
self.env['res.currency.rate'].sudo().create({
|
|
'name': '2022-01-01',
|
|
'rate': 10,
|
|
'currency_id': eur_currency.id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
|
|
attachment = self.env['ir.attachment'].with_user(self.expense_user_employee).create({
|
|
'name': 'test_attachment.png',
|
|
'res_model': 'hr.expense',
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
})
|
|
extract_response = self.get_result_success_response()
|
|
extract_response['document_token'] = 'some_token'
|
|
extract_response['results'][0]['total']['selected_value']['content'] = 100
|
|
extract_response['results'][0]['currency']['selected_value']['content'] = '€'
|
|
with (
|
|
self._mock_iap_extract(extract_response=extract_response),
|
|
patch.object(HrExpense, '_predict_product', side_effect=lambda *args, **kwargs: self.product_a.id)
|
|
):
|
|
self.env['hr.expense'].with_user(self.expense_user_employee).create_expense_from_attachments(attachment.ids)
|
|
expense = self.env['hr.expense'].search([('attachment_ids', '=', attachment.id)]).ensure_one()
|
|
expense._fill_document_with_results(ocr_results=extract_response['results'][0])
|
|
|
|
self.assertNotEqual(company_currency.id, eur_currency.id)
|
|
self.assertRecordValues(
|
|
expense,
|
|
[{'total_amount_currency': 800.0, 'total_amount': 800.0, 'currency_id': company_currency.id, 'product_id': self.product_a.id}],
|
|
)
|
|
|
|
@freeze_time('2024-01-01')
|
|
def test_extract_no_date(self):
|
|
self.env['res.currency.rate'].create({
|
|
'name': '2022-01-01',
|
|
'rate': 3,
|
|
'currency_id': self.env.ref('base.EUR').id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
|
|
ocr_results = self.get_result_success_response()['results'][0]
|
|
del ocr_results['date']
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
self.expense.date = None
|
|
self.expense._fill_document_with_results(ocr_results=ocr_results)
|
|
|
|
self.assertAlmostEqual(self.expense.total_amount_currency, 99.99, 2)
|
|
self.assertAlmostEqual(self.expense.total_amount, 33.33, 2)
|
|
self.assertEqual(self.expense.currency_id.name, 'EUR')
|
|
self.assertEqual(self.expense.date, fields.Date.to_date('2024-01-01'))
|
|
|
|
def test_expense_ocr_note_author(self):
|
|
with self._mock_iap_extract(extract_response=self.parse_success_response()):
|
|
self.expense.message_post(attachment_ids=[self.attachment.id])
|
|
|
|
self.env.cr.precommit.clear() # Clear the tracking values
|
|
with self._mock_iap_extract(extract_response=self.get_result_success_response()):
|
|
self.expense.check_all_status()
|
|
|
|
self.env.cr.flush() # Runs the precommit hooks to create the tracking message
|
|
message = self.env['mail.message'].search([
|
|
('model', '=', 'hr.expense'),
|
|
('res_id', '=', self.expense.id),
|
|
('tracking_value_ids', '!=', False),
|
|
]).ensure_one()
|
|
author_name = message.author_id.complete_name
|
|
self.assertEqual(author_name, 'OdooBot')
|
|
|
|
def test_action_create_sample_expense(self):
|
|
receipt_values = {
|
|
'name': 'Test Receipt',
|
|
'amount': 2000,
|
|
'date': datetime.date(2025, 3, 10)
|
|
}
|
|
|
|
self.env['expense.sample.receipt']._action_create_expense(receipt_values)
|
|
expense = self.env['hr.expense'].search([('name', '=', f"Sample Receipt: {receipt_values['name']}")], limit=1)
|
|
self.assertTrue(expense)
|
|
self.assertEqual(expense.total_amount, receipt_values['amount'])
|
|
|
|
def test_extract_conversion_when_currency_already_set(self):
|
|
""" Test that if EUR is already set on the record, the USD total still updates correctly from OCR """
|
|
eur_currency = self.env.ref('base.EUR')
|
|
eur_currency.active = True
|
|
|
|
self.expense.message_main_attachment_id = self.attachment
|
|
self.env['res.currency.rate'].create({
|
|
'name': '2022-02-22',
|
|
'rate': 1.0 / 3.0,
|
|
'currency_id': eur_currency.id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
|
|
self.expense.write({
|
|
'currency_id': eur_currency.id,
|
|
'total_amount_currency': 1.0,
|
|
'total_amount': 3.0,
|
|
})
|
|
|
|
ocr_results = self.get_result_success_response()['results'][0]
|
|
ocr_results['total']['selected_value']['content'] = 50.0
|
|
ocr_results['currency']['selected_value']['content'] = 'EUR'
|
|
ocr_results['date']['selected_value']['content'] = '2022-02-22'
|
|
|
|
self.expense.name = ""
|
|
self.expense._fill_document_with_results(ocr_results=ocr_results)
|
|
|
|
self.assertEqual(self.expense.total_amount_currency, 50.0)
|
|
self.assertAlmostEqual(self.expense.total_amount, 150.0)
|
|
|
|
def test_skip_extract_for_stripe_expense(self):
|
|
""" Test OCR should not be triggered for expense created from stripe payment. """
|
|
if 'hr_expense_stripe' not in self.env['ir.module.module']._installed():
|
|
self.skipTest("hr_expense_stripe module is not installed.")
|
|
|
|
self.company.stripe_id = 'acct_1234567890'
|
|
card = self.env['hr.expense.stripe.card'].with_user(self.expense_user_manager).create([{ # noqa: OLS03001
|
|
'employee_id': self.expense_employee.id,
|
|
'name': 'Test Card',
|
|
'company_id': self.company.id,
|
|
}])
|
|
expense_vals = {
|
|
'card_id': card.id,
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount_currency': 200.0,
|
|
}
|
|
|
|
# test skip ocr for auto ocr
|
|
self.company.expense_extract_show_ocr_option_selection = 'auto_send'
|
|
expense_1 = self.env['hr.expense'].create(expense_vals)
|
|
self.assertTrue(expense_1.is_card_expense)
|
|
with self._mock_iap_extract(extract_response=self.parse_success_response()):
|
|
expense_1.message_post(attachment_ids=[self.attachment.id])
|
|
with self._mock_iap_extract(extract_response=self.get_result_success_response()):
|
|
expense_1.check_all_status()
|
|
self.assertRecordValues(expense_1, [
|
|
{'total_amount_currency': 200.0, 'state': 'draft', 'extract_state': 'done'},
|
|
])
|
|
|
|
# test skip ocr for ocr on demand
|
|
self.company.expense_extract_show_ocr_option_selection = 'manual_send'
|
|
expense_2 = self.env['hr.expense'].create(expense_vals)
|
|
self.assertTrue(expense_2.is_card_expense)
|
|
expense_2.message_post(attachment_ids=[self.attachment.id])
|
|
with self._mock_iap_extract(extract_response=self.parse_success_response()):
|
|
expense_2.action_send_batch_for_digitization()
|
|
with self._mock_iap_extract(extract_response=self.get_result_success_response()):
|
|
expense_2.check_all_status()
|
|
self.assertRecordValues(expense_2, [
|
|
{'total_amount_currency': 200.0, 'state': 'draft', 'extract_state': 'done'},
|
|
])
|