forked from Mapan/odoo17e
229 lines
9.1 KiB
Python
229 lines
9.1 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import os
|
|
import json
|
|
from unittest import mock
|
|
import contextlib
|
|
import requests
|
|
import logging
|
|
import time
|
|
|
|
from odoo import Command, fields
|
|
from odoo.tools.misc import file_open
|
|
from odoo.addons.account.tests.test_account_move_send import TestAccountMoveSendCommon
|
|
from odoo.addons.base.models.ir_cron import ir_cron
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TestKeEdiCommon(TestAccountMoveSendCommon):
|
|
@classmethod
|
|
def setUpClass(cls, chart_template_ref='ke'):
|
|
super().setUpClass(chart_template_ref=chart_template_ref)
|
|
cls.is_live_test = False
|
|
cls.parent_cmc_key = os.getenv('KE_PARENT_CMC_KEY', 'cmc_key')
|
|
cls.branch_cmc_key = os.getenv('KE_BRANCH_CMC_KEY', 'cmc_key')
|
|
|
|
cls.company_data['company'].write({
|
|
'vat': 'P052112956W',
|
|
'l10n_ke_branch_code': '00',
|
|
'l10n_ke_server_mode': 'test',
|
|
'l10n_ke_oscu_cmc_key': cls.parent_cmc_key,
|
|
'l10n_ke_oscu_user_agreement': True,
|
|
})
|
|
|
|
# 20 * (seconds since epoch) modulo 2^31-1 is a pretty good initializer that leaves space for 20 invoices
|
|
# and also happens to fit in an int32. Note that 2^31 - 1 is a prime number! ;)
|
|
cls.sale_sequence_number = (int(time.time()) * 10) % (2**31 - 1)
|
|
|
|
cls.partner_a.write({
|
|
'name': 'Ralph Jr',
|
|
'street': 'The Cucumber Lounge',
|
|
'city': 'Vineland',
|
|
'zip': '00500',
|
|
'country_id': cls.env.ref('base.ke').id,
|
|
'vat': 'A000123456F',
|
|
})
|
|
|
|
cls.standard_rate_sales_tax = cls.env['account.chart.template'].ref('ST16')
|
|
cls.standard_rate_purchase_tax = cls.env['account.chart.template'].ref('PT16')
|
|
cls.reduced_rate_sales_tax = cls.env['account.chart.template'].ref('ST8')
|
|
cls.reduced_rate_purchase_tax = cls.env['account.chart.template'].ref('PT8')
|
|
|
|
cls.product_service = cls.env['product.product'].create([{
|
|
'name': 'Fiscal Optimization Consultancy',
|
|
'type': 'service',
|
|
'taxes_id': [Command.set(cls.standard_rate_sales_tax.ids)],
|
|
'supplier_taxes_id': [Command.set(cls.standard_rate_purchase_tax.ids)],
|
|
'standard_price': 100.0,
|
|
'l10n_ke_product_type_code': '3',
|
|
'l10n_ke_origin_country_id': cls.env.ref('base.ke').id,
|
|
'unspsc_code_id': cls.env['product.unspsc.code'].search([
|
|
('code', '=', '81121500'),
|
|
], limit=1).id,
|
|
'l10n_ke_packaging_unit_id': cls.env.ref('l10n_ke_edi_oscu.code_17_OU').id,
|
|
'l10n_ke_packaging_quantity': 1,
|
|
}])
|
|
|
|
currency_eur = cls.env.ref('base.EUR')
|
|
currency_eur.active = True
|
|
cls.env['res.currency.rate'].create({
|
|
'name': fields.Date.today(),
|
|
'currency_id': currency_eur.id,
|
|
'company_id': cls.company_data['company'].id,
|
|
'inverse_company_rate': '100.00',
|
|
})
|
|
|
|
cls.excise_tax = cls.env['account.tax'].create({
|
|
'name': 'Excise tax',
|
|
'amount_type': 'percent',
|
|
'amount': 30.0,
|
|
'country_id': cls.company_data['company'].account_fiscal_country_id.id,
|
|
'tax_exigibility': 'on_invoice',
|
|
'price_include': True,
|
|
'include_base_amount': True,
|
|
'invoice_repartition_line_ids': [
|
|
Command.create({'repartition_type': 'base'}),
|
|
Command.create({
|
|
'repartition_type': 'tax',
|
|
'account_id': cls.company_data['default_account_tax_sale'].id,
|
|
}),
|
|
],
|
|
'refund_repartition_line_ids': [
|
|
Command.create({'repartition_type': 'base'}),
|
|
Command.create({
|
|
'repartition_type': 'tax',
|
|
'account_id': cls.company_data['default_account_tax_sale'].id,
|
|
}),
|
|
],
|
|
})
|
|
|
|
def _test_create_branches(self):
|
|
parent = self.company_data['company']
|
|
parent.action_l10n_ke_create_branches()
|
|
branches = self.env['res.company'].search([('parent_id', '=', parent.id)])
|
|
|
|
self.assertTrue(branches[0].name.startswith('KAKAMEGA'))
|
|
self.assertTrue(branches[1].name.startswith('MOMBASA'))
|
|
|
|
expected_branches = [
|
|
{
|
|
'vat': 'P052112956W',
|
|
'l10n_ke_branch_code': '02',
|
|
},
|
|
{
|
|
'vat': 'P052112956W',
|
|
'l10n_ke_branch_code': '01',
|
|
},
|
|
]
|
|
self.assertRecordValues(branches, expected_branches)
|
|
|
|
for branch in branches:
|
|
branch.write({'country_id': self.env.ref('base.ke')})
|
|
(self.product_a | self.product_b).with_company(branch).write({'standard_price': 30})
|
|
|
|
@contextlib.contextmanager
|
|
def patch_session(self, responses):
|
|
""" Patch requests.Session in l10n_ke_edi_oscu/models/company.py """
|
|
|
|
def replace_ignore(dict_to_replace):
|
|
""" Replace `___ignore___` in the expected request JSONs by unittest.mock.ANY,
|
|
which is equal to everything. """
|
|
for k, v in dict_to_replace.items():
|
|
if v == '___ignore___':
|
|
dict_to_replace[k] = mock.ANY
|
|
return dict_to_replace
|
|
|
|
self.maxDiff = None
|
|
test_case = self
|
|
json_module = json
|
|
|
|
responses = iter(responses)
|
|
|
|
class MockedSession:
|
|
def __init__(self):
|
|
self.headers = {}
|
|
|
|
def post(self, url, json=None, timeout=None):
|
|
expected_service, expected_request_filename, response_filename = next(responses)
|
|
_, _, service = url.rpartition('/')
|
|
|
|
test_case.assertEqual(service, expected_service)
|
|
|
|
stock_services = (
|
|
'insertStockIO',
|
|
'saveStockMaster',
|
|
'selectImportItemList',
|
|
'selectItemList',
|
|
'updateImportItem',
|
|
)
|
|
|
|
module = 'l10n_ke_edi_oscu_stock' if service in stock_services else 'l10n_ke_edi_oscu'
|
|
|
|
with file_open(f'{module}/tests/expected_requests/{expected_request_filename}.json', 'rb') as expected_request_file:
|
|
try:
|
|
test_case.assertEqual(json_module.loads(expected_request_file.read(), object_hook=replace_ignore), json)
|
|
except AssertionError:
|
|
_logger.error('Unexpected request JSON for service %s', service)
|
|
raise
|
|
|
|
mock_response = mock.Mock(spec=requests.Response)
|
|
mock_response.status_code = 200
|
|
mock_response.headers = ''
|
|
|
|
with file_open(f'{module}/tests/mocked_responses/{response_filename}.json', 'rb') as response_file:
|
|
mock_response.content = response_file.read()
|
|
mock_response.text = mock_response.content.decode()
|
|
|
|
mock_response.json.side_effect = lambda: json_module.loads(mock_response.content)
|
|
|
|
return mock_response
|
|
|
|
with mock.patch('odoo.addons.l10n_ke_edi_oscu.models.res_company.requests.Session', side_effect=MockedSession, autospec=True) as mock_session:
|
|
yield mock_session
|
|
|
|
try:
|
|
next(responses)
|
|
except StopIteration:
|
|
pass
|
|
else:
|
|
test_case.fail('Not all expected calls were made!')
|
|
|
|
@contextlib.contextmanager
|
|
def patch_cron_trigger(self):
|
|
""" Decorator for patching ir.cron.trigger so that the cron gets run right after this context manager's exit. """
|
|
crons_to_trigger = []
|
|
|
|
def mock_trigger(cron, at=None):
|
|
crons_to_trigger.append(cron)
|
|
|
|
with mock.patch.object(ir_cron, '_trigger', side_effect=mock_trigger, autospec=True) as mocked_trigger:
|
|
yield mocked_trigger
|
|
|
|
# Run cron as current user (not superuser) to limit ourselves to the test company
|
|
self.env.invalidate_all()
|
|
self.env['ir.cron'].union(*crons_to_trigger).ir_actions_server_id.run()
|
|
|
|
def create_reversal(self, invoice, is_modify=False):
|
|
""" Create a credit note that reverses an invoice. """
|
|
wizard_vals = {'journal_id': invoice.journal_id.id}
|
|
wizard_reverse = self.env['account.move.reversal'].with_context(active_ids=invoice.ids, active_model='account.move').create(wizard_vals)
|
|
wizard_reverse.write({
|
|
'reason': 'Return',
|
|
'l10n_ke_reason_code_id': self.env.ref('l10n_ke_edi_oscu.code_32_06').id,
|
|
})
|
|
wizard_reverse.reverse_moves(is_modify=is_modify)
|
|
return wizard_reverse.new_move_ids
|
|
|
|
@classmethod
|
|
@contextlib.contextmanager
|
|
def set_invoice_number(cls, invoice):
|
|
if not invoice.is_sale_document():
|
|
raise NotImplementedError('`set_invoice_number` is only needed for out_invoice / out_refund!')
|
|
sequence = invoice._l10n_ke_get_invoice_sequence()
|
|
sequence.number_next_actual = cls.sale_sequence_number
|
|
try:
|
|
yield
|
|
finally:
|
|
cls.sale_sequence_number = sequence.number_next
|