395 lines
20 KiB
Python
395 lines
20 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import base64
|
|
import logging
|
|
import re
|
|
import time
|
|
from contextlib import contextmanager
|
|
from unittest import mock
|
|
|
|
from lxml import etree
|
|
from requests import Response
|
|
|
|
from odoo.addons.l10n_ar.tests.common import TestArCommon
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests import tagged
|
|
from odoo.tools import file_open
|
|
from odoo.tools.zeep import Transport
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ArMockedClient:
|
|
@staticmethod
|
|
def get_type(_param):
|
|
return lambda arg: arg
|
|
|
|
|
|
class TestArEdiCommon(TestArCommon):
|
|
|
|
@staticmethod
|
|
def setup_afip_ws(afip_ws="wsfe"):
|
|
def _decorator(function):
|
|
def wrapper(self):
|
|
self.afip_ws = afip_ws
|
|
function(self)
|
|
return wrapper
|
|
|
|
return _decorator
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.subfolder = ""
|
|
cls.ar_private_key = cls.env['certificate.key'].create({
|
|
'name': 'AR Test Private key 1',
|
|
'content': base64.b64encode(file_open("l10n_ar_edi/tests/private_key.pem", 'rb').read()),
|
|
})
|
|
|
|
cls.ar_certificate_1 = cls.env['certificate.certificate'].create({
|
|
'name': 'AR Test certificate 1',
|
|
'content': base64.b64encode(file_open("l10n_ar_edi/tests/test_cert1.crt", 'rb').read()),
|
|
'private_key_id': cls.ar_private_key.id,
|
|
})
|
|
|
|
cls.ar_certificate_2 = cls.env['certificate.certificate'].create({
|
|
'name': 'AR Test certificate 2',
|
|
'content': base64.b64encode(file_open("l10n_ar_edi/tests/test_cert2.crt", 'rb').read()),
|
|
'private_key_id': cls.ar_private_key.id,
|
|
})
|
|
|
|
cls.ar_certificate_3 = cls.env['certificate.certificate'].create({
|
|
'name': 'AR Test certificate 3',
|
|
'content': base64.b64encode(file_open("l10n_ar_edi/tests/test_cert3.crt", 'rb').read()),
|
|
'private_key_id': cls.ar_private_key.id,
|
|
})
|
|
|
|
cls.company_ri.write({
|
|
'l10n_ar_afip_ws_environment': 'testing',
|
|
'l10n_ar_afip_ws_key_id': cls.ar_private_key.id,
|
|
})
|
|
cls.company_mono.write({
|
|
'l10n_ar_afip_ws_environment': 'testing',
|
|
'l10n_ar_afip_ws_key_id': cls.ar_private_key.id,
|
|
})
|
|
|
|
if 'external' in cls.test_tags:
|
|
cls.company_ri.l10n_ar_afip_ws_crt_id = cls.ar_certificate_1
|
|
cls._create_afip_connections(cls.company_ri, cls.afip_ws)
|
|
else:
|
|
cls.company_ri.l10n_ar_afip_ws_crt_id = False
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Initialization
|
|
# -------------------------------------------------------------------------
|
|
|
|
@classmethod
|
|
def _create_afip_connections(cls, company, afip_ws):
|
|
""" Method used to create afip connections and commit then to re use this connections in all the test.
|
|
If a connection can not be set because another instance is already using the certificate then we assign a
|
|
random certificate and try again to create the connections. """
|
|
# In order to connect ARCA we need to create a token which depend on the configured ARCA certificate.
|
|
# If the certificate is been used by another testing instance will raise an error telling us that the token
|
|
# can not be used and need to wait 10 minuts or change with another certificate.
|
|
# To avoid this and always run the unit tests we randonly change the certificate and try to create the
|
|
# connection until there is not token error.
|
|
_logger.log(25, 'Setting homologation private key to company %s', company.name)
|
|
company = company.with_context(l10n_ar_invoice_skip_commit=True)
|
|
|
|
for p in range(5):
|
|
try:
|
|
company._l10n_ar_get_connection(afip_ws)
|
|
return
|
|
except Exception as error:
|
|
if 'El CEE ya posee un TA valido para el acceso al WSN solicitado' in repr(error):
|
|
_logger.log(25, 'Connection Failed')
|
|
elif 'Missing certificate' in repr(error):
|
|
_logger.log(25, 'Not certificate configured yet')
|
|
else:
|
|
raise error
|
|
|
|
# Set testing certificate
|
|
old = company.l10n_ar_afip_ws_crt_id.name or 'NOT DEFINED'
|
|
current_cert_num = re.findall(r"OdootTestsCert(.)", old)
|
|
current_cert_num = current_cert_num and int(current_cert_num[0]) or 0
|
|
new_cert_number = 1 if current_cert_num == 3 else current_cert_num + 1
|
|
|
|
company.l10n_ar_afip_ws_crt_id = cls.env['certificate.certificate'].search([('name', '=', 'AR Test certificate %d' % new_cert_number)], limit=1)
|
|
_logger.log(25, 'Setting demo certificate from %s to %s in %s company' % (
|
|
old, company.l10n_ar_afip_ws_crt_id.name, company.name))
|
|
time.sleep(2**p)
|
|
|
|
def _prepare_multicurrency_values(self):
|
|
super()._prepare_multicurrency_values()
|
|
# Set Rates for USD currency takint into account the value from ARCA
|
|
USD = self.env.ref('base.USD')
|
|
if 'external' in self.test_tags:
|
|
_date, value = USD.with_context(l10n_ar_invoice_skip_commit=True)._l10n_ar_get_afip_ws_currency_rate(self.journal.l10n_ar_afip_ws)
|
|
else:
|
|
# Mock the currency value for standard tests
|
|
value = 1125.3
|
|
self._set_today_rate(USD, 1.0 / value)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Common test methods
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _test_ar_edi_common_external(self):
|
|
# Ensure that the connection is made and all the documents are synchronized
|
|
with self.subTest("Test connection"):
|
|
with self.assertRaisesRegex(UserError, '"Check Available AFIP PoS" is not implemented in testing mode for webservice'):
|
|
self.journal.with_context(l10n_ar_invoice_skip_commit=True).l10n_ar_check_afip_pos_number()
|
|
|
|
# Ensure basic invoice creation, validation, & consultation process
|
|
with self.subTest("Test consult invoice"):
|
|
invoice = self._create_invoice_ar()
|
|
self._validate_and_review(invoice, "")
|
|
|
|
# Consult the info about the last invoice
|
|
last = invoice.journal_id._l10n_ar_get_afip_last_invoice_number(invoice.l10n_latam_document_type_id)
|
|
document_parts = invoice._l10n_ar_get_document_number_parts(invoice.l10n_latam_document_number, invoice.l10n_latam_document_type_id.code)
|
|
self.assertEqual(last, document_parts['invoice_number'])
|
|
|
|
# Consult the info about specific invoice
|
|
with self.assertRaisesRegex(UserError, '(CodAutorizacion|Cae).*%s' % invoice.l10n_ar_afip_auth_code):
|
|
self.env['l10n_ar_afip.ws.consult'].create([{
|
|
'number': last,
|
|
'journal_id': invoice.journal_id.id,
|
|
'document_type_id': invoice.l10n_latam_document_type_id.id,
|
|
}]).button_confirm()
|
|
|
|
def _test_connection(self):
|
|
""" Review that the connection is made and all the documents are syncronized"""
|
|
with self.assertRaisesRegex(UserError, '"Check Available ARCA PoS" is not implemented in testing mode for webservice'):
|
|
self.journal.with_context(l10n_ar_invoice_skip_commit=True).l10n_ar_check_afip_pos_number()
|
|
|
|
def _test_consult_invoice(self):
|
|
invoice = self._create_invoice_ar()
|
|
self._validate_and_review(invoice, "", skip_assert_json=True)
|
|
|
|
# Consult the info about the last invoice
|
|
last = invoice.journal_id._l10n_ar_get_afip_last_invoice_number(invoice.l10n_latam_document_type_id)
|
|
document_parts = invoice._l10n_ar_get_document_number_parts(invoice.l10n_latam_document_number, invoice.l10n_latam_document_type_id.code)
|
|
self.assertEqual(last, document_parts['invoice_number'])
|
|
|
|
# Consult the info about specific invoice
|
|
with self.assertRaisesRegex(UserError, '(CodAutorizacion|Cae).*%s' % invoice.l10n_ar_afip_auth_code):
|
|
self.env['l10n_ar_afip.ws.consult'].create({'number': last,
|
|
'journal_id': invoice.journal_id.id,
|
|
'document_type_id': invoice.l10n_latam_document_type_id.id}).button_confirm()
|
|
return invoice
|
|
|
|
def _test_ar_edi_flow(self, test_name: str, move_type: str, document_code: str, concept: str, **invoice_args):
|
|
_logger.info("Testing subtest: %s", test_name)
|
|
if document_code == 'b':
|
|
invoice_args.setdefault('partner_id', self.partner_cf)
|
|
if concept == 'product':
|
|
invoice_args.setdefault('invoice_line_ids', [self._prepare_invoice_line(product_id=self.product_iva_21, price_unit=100)])
|
|
elif concept == 'service':
|
|
invoice_args.setdefault('invoice_line_ids', [self._prepare_invoice_line(product_id=self.service_iva_27, price_unit=100)])
|
|
else: # concept == 'product_service'
|
|
invoice_args.setdefault('invoice_line_ids', [
|
|
self._prepare_invoice_line(product_id=self.product_iva_21, price_unit=100),
|
|
self._prepare_invoice_line(product_id=self.service_iva_27, price_unit=100),
|
|
])
|
|
|
|
invoice = self._create_invoice_ar(**invoice_args)
|
|
document_type = f"invoice_{document_code}"
|
|
expected_document = self.document_type[document_type]
|
|
self.assertEqual(invoice.l10n_latam_document_type_id.display_name, expected_document.display_name, 'The document should be %s' % expected_document.display_name)
|
|
self._validate_and_review(invoice, test_name or f"{document_type}_{concept}", skip_assert_json=move_type != 'invoice')
|
|
|
|
if move_type in ('credit_note', 'debit_note'):
|
|
if move_type == 'credit_note':
|
|
note_move = self._reverse_invoice(invoice, reason='Mercadería defectuosa')
|
|
else:
|
|
note_move = self._create_debit_note(invoice, reason='Mercadería defectuosa')
|
|
expected_document = self.document_type[f"{move_type}_{document_code}"]
|
|
self.assertEqual(note_move.l10n_latam_document_type_id.display_name, expected_document.display_name, 'The document should be %s' % expected_document.display_name)
|
|
self._validate_and_review(note_move, test_name)
|
|
|
|
return invoice
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Helpers
|
|
# -------------------------------------------------------------------------
|
|
|
|
@classmethod
|
|
def _get_afip_pos_system_real_name(cls):
|
|
mapping = super()._get_afip_pos_system_real_name()
|
|
mapping.update({'WSFE': 'RAW_MAW', 'WSFEX': 'FEEWS', 'WSBFE': 'BFEWS'})
|
|
return mapping
|
|
|
|
@contextmanager
|
|
def _handler_afip_internal_error(self):
|
|
try:
|
|
yield
|
|
except Exception as exc:
|
|
error_msg = repr(exc)
|
|
if 'Code 500' in error_msg or 'Code 501' in error_msg or 'Code 502' in error_msg:
|
|
self.skipTest("We receive an internal error from ARCA so skip this test")
|
|
else:
|
|
raise
|
|
|
|
def _post(self, invoice):
|
|
with self._handler_afip_internal_error():
|
|
invoice.with_context(l10n_ar_invoice_skip_commit=True).action_post()
|
|
|
|
def _l10n_ar_verify_on_afip(self, bill):
|
|
with self._handler_afip_internal_error():
|
|
bill.l10n_ar_verify_on_afip()
|
|
|
|
def _validate_and_review(self, invoice, test_name: str, document_number='12345-12345678', skip_assert_json=False):
|
|
""" Validate electronic invoice and review that the invoice has been proper validated """
|
|
self._post(invoice)
|
|
|
|
if 'external' in self.test_tags:
|
|
# EDI validations
|
|
expected_result = 'A'
|
|
error_msg = 'This test return a result different from the expected (%s)' % expected_result
|
|
self.assertEqual(invoice.l10n_ar_afip_auth_mode, 'CAE', error_msg)
|
|
detail_info = f"{error_msg}\nResponse\n{invoice.l10n_ar_afip_xml_response}\nMsg\n{invoice.message_ids[:1].body}"
|
|
self.assertEqual(invoice.l10n_ar_afip_result, expected_result, detail_info)
|
|
|
|
self.assertTrue(invoice.l10n_ar_afip_auth_code, error_msg)
|
|
self.assertTrue(invoice.l10n_ar_afip_auth_code_due, error_msg)
|
|
self.assertTrue(invoice.l10n_ar_afip_xml_request, error_msg)
|
|
self.assertTrue(invoice.l10n_ar_afip_xml_response, error_msg)
|
|
else:
|
|
mocked_client = ArMockedClient()
|
|
invoice.l10n_latam_document_number = document_number
|
|
if invoice.journal_id.l10n_ar_afip_ws == 'wsfe':
|
|
request_data = invoice.wsfe_get_cae_request(mocked_client)
|
|
elif invoice.journal_id.l10n_ar_afip_ws == 'wsfex':
|
|
request_data = invoice.wsfex_get_cae_request(111, mocked_client)
|
|
elif invoice.journal_id.l10n_ar_afip_ws == 'wsbfe':
|
|
request_data = invoice.wsbfe_get_cae_request(222, mocked_client)
|
|
else:
|
|
self.fail(f"Unexpected l10n_ar_afip_ws value on the invoice journal: {invoice.journal_id.l10n_ar_afip_ws}")
|
|
|
|
if test_name and not skip_assert_json:
|
|
self.assert_json(request_data, test_name, self.subfolder)
|
|
|
|
def _l10n_ar_xml_tag(self, afip_ws, data):
|
|
""" Easy helper to get XML tag for a given purpose data """
|
|
xml_tags = {
|
|
'wsfe': {'currency': 'MonId', 'rate': 'MonCotiz'},
|
|
'wsfex': {'currency': 'Moneda_Id', 'rate': 'Moneda_ctz'},
|
|
'wsbfe': {'currency': 'Imp_moneda_Id', 'rate': 'Imp_moneda_ctz'}}
|
|
return xml_tags[afip_ws][data]
|
|
|
|
def _test_payment_foreign_currency(self):
|
|
""" Payment in Foreign Currency """
|
|
USD = self.env.ref('base.USD')
|
|
self.assertEqual(USD.rate, 1.0)
|
|
self._prepare_multicurrency_values()
|
|
self.assertNotEqual(USD.rate, 1.0)
|
|
afip_ws = self.journal.l10n_ar_afip_ws
|
|
|
|
# No + any rate (does not matter rate): Will work always is the current behavior, is the default value
|
|
invoice = self._create_invoice_ar(currency_id=USD)
|
|
self.assertEqual(invoice.l10n_ar_payment_foreign_currency, "No")
|
|
self._validate_and_review(invoice, f"test_{self.subfolder.split('/')[0]}_foreign_currency_no")
|
|
currency_tag = self._l10n_ar_xml_tag(afip_ws, 'currency')
|
|
if 'external' in self.test_tags:
|
|
self.assertIn(f"<ns0:{currency_tag}>DOL</ns0:{currency_tag}>", invoice.l10n_ar_afip_xml_request)
|
|
self.assertIn("<ns0:CanMisMonExt>N</ns0:CanMisMonExt>", invoice.l10n_ar_afip_xml_request)
|
|
|
|
# Yes + Correct last business day rate: Will work
|
|
self.env['ir.config_parameter'].sudo().set_param(
|
|
f"l10n_ar_edi.{self.env.company.id}_foreign_currency_payment", "Yes")
|
|
invoice = self._create_invoice_ar(currency_id=USD)
|
|
invoice.company_id._compute_l10n_ar_payment_foreign_currency()
|
|
invoice._compute_l10n_ar_payment_foreign_currency()
|
|
self.assertEqual(invoice.l10n_ar_payment_foreign_currency, "Yes")
|
|
self._validate_and_review(invoice, f"test_{self.subfolder.split('/')[0]}_foreign_currency_yes", document_number='12345-12345679')
|
|
|
|
if 'external' in self.test_tags:
|
|
self.assertIn(f"<ns0:{currency_tag}>DOL</ns0:{currency_tag}>", invoice.l10n_ar_afip_xml_request)
|
|
self.assertIn("<ns0:CanMisMonExt>S</ns0:CanMisMonExt>", invoice.l10n_ar_afip_xml_request)
|
|
|
|
# Yes + bad rate: Will fail because is not last business day
|
|
USD.rate_ids.rate = USD.rate_ids.rate * 0.10
|
|
invoice = self._create_invoice_ar(currency_id=USD)
|
|
with self.assertRaisesRegex(UserError,
|
|
"The rate to be reported.*differs from that of ARCA Remember that if you pay in foreign currency you must use the same rate of the last business day of ARCA"):
|
|
self._validate_and_review(invoice, "", document_number='12345-12345680')
|
|
|
|
# Yes + No rate defined: Will raise error
|
|
USD.rate_ids.rate = 1.0
|
|
invoice = self._create_invoice_ar(currency_id=USD)
|
|
with self.assertRaisesRegex(UserError, "The currency rate to be reported.*is not valid. It must be between"):
|
|
self._validate_and_review(invoice, "", document_number='12345-12345681')
|
|
|
|
|
|
class TestArEdiMockedCommon(TestArEdiCommon):
|
|
@contextmanager
|
|
def patch_client(self, responses):
|
|
""" Patch zeep.Transport in l10n_ar_edi/models/l10n_ar_afipws_connection.py"""
|
|
|
|
self.maxDiff = None
|
|
# This method can be called from within a @classmethod, instantiate a TestCase when that happens, so we can use test_case.assert*
|
|
test_case = self if hasattr(self.assertEqual, '__self__') else self()
|
|
|
|
responses = iter(responses)
|
|
|
|
class MockedTransport(Transport):
|
|
def _load_remote_data(self, url):
|
|
""" Before we make any interactions with the server, we first need to get the
|
|
schema of datatypes so we know what services are available. There are only two
|
|
URLS for Argentina (LoginCms, and services.asmx?WSDL) we are testing so we can go
|
|
view these files directly.
|
|
"""
|
|
service = url.rpartition("/")[2].partition("?")[0]
|
|
module = 'l10n_ar_edi'
|
|
with file_open(f'{module}/tests/expected_requests/{service}-schema.xml', 'rb') as fd:
|
|
expected_tree = fd.read()
|
|
return expected_tree
|
|
|
|
def post(self, address, message, headers):
|
|
expected_service, expected_request_filename, response_filename = next(responses)
|
|
if 'service.asmx' in address:
|
|
_, _, service = headers.get('SOAPAction').rpartition("/")
|
|
service = service[:-1]
|
|
else:
|
|
_, _, service = address.rpartition("/")
|
|
|
|
test_case.assertEqual(service, expected_service)
|
|
|
|
module = 'l10n_ar_edi'
|
|
with file_open(f'{module}/tests/expected_requests/{expected_request_filename}.xml', 'rb') as fd:
|
|
expected_tree = etree.fromstring(fd.read())
|
|
|
|
request_tree = etree.fromstring(message)
|
|
try:
|
|
test_case.assertXmlTreeEqual(request_tree, expected_tree)
|
|
except AssertionError:
|
|
_logger.error('Unexpected request XML for service %s', service)
|
|
raise
|
|
|
|
with file_open(f'{module}/tests/mocked_responses/{response_filename}.xml', 'rb') as fd:
|
|
response_content = fd.read()
|
|
|
|
response = mock.Mock(spec=Response)
|
|
response.status_code = 200
|
|
response.content = response_content
|
|
response.headers = {'Content-Type': 'text/xml;charset=utf-8'}
|
|
self.xml_request = etree.tostring(
|
|
request_tree, pretty_print=True).decode('utf-8')
|
|
self.xml_response = etree.tostring(
|
|
etree.fromstring(response_content), pretty_print=True).decode('utf-8')
|
|
return response
|
|
|
|
with mock.patch('odoo.addons.l10n_ar_edi.models.l10n_ar_afipws_connection.ARTransport', new=MockedTransport):
|
|
yield
|
|
|
|
if next(responses, None):
|
|
test_case.fail('Not all expected calls were made!')
|
|
|
|
@classmethod
|
|
def _create_afip_connections(cls, company, afip_ws):
|
|
# Override to mock the connection instead of actually making the network requests.
|
|
# No need to call super as it is mainly just running the same code in a loop.
|
|
company = company.with_context(l10n_ar_invoice_skip_commit=True)
|
|
with cls.patch_client(cls, [('LoginCms', 'LoginCms-final', 'LoginCms-final')]):
|
|
company._l10n_ar_get_connection(afip_ws)
|