Odoo18-Base/addons/l10n_vn_edi_viettel/tests/test_edi.py
2025-01-06 10:57:38 +07:00

387 lines
17 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from unittest import mock
from unittest.mock import patch
from freezegun import freeze_time
from odoo import fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestVNEDI(AccountTestInvoicingCommon):
@classmethod
@AccountTestInvoicingCommon.setup_country('vn')
def setUpClass(cls):
super().setUpClass()
# Setup the default symbol and template.
cls.template = cls.env['l10n_vn_edi_viettel.sinvoice.template'].create({
'name': '1/001',
'template_invoice_type': '1',
})
cls.symbol = cls.env['l10n_vn_edi_viettel.sinvoice.symbol'].create({
'name': 'K24TUT',
'invoice_template_id': cls.template.id,
})
cls.env['ir.default'].set(
'res.partner',
'l10n_vn_edi_symbol',
cls.symbol.id,
company_id=cls.env.company.id
)
# Setup a vietnamese address on the partner and company.
cls.partner_a.write({
'street': '121 Hang Bac Street',
'state_id': cls.env.ref('base.state_vn_VN-HN').id,
'city': 'Hoan Kiem District',
'country_id': cls.env.ref('base.vn').id,
'vat': '0100109106-505',
'phone': '3825 7670',
'email': 'partner_a@gmail.com',
})
cls.env.company.write({
'street': '3 Alley 45 Phan Dinh Phung, Quan Thanh Ward',
'state_id': cls.env.ref('base.state_vn_VN-HN').id,
'country_id': cls.env.ref('base.vn').id,
'vat': '0100109106-506',
'phone': '6266 1275',
'email': 'test_company@gmail.com',
'website': 'test_company.com',
'l10n_vn_edi_password': 'a',
'l10n_vn_edi_username': 'b',
})
cls.product_a.default_code = 'BN/1035'
cls.other_currency = cls.setup_other_currency('EUR')
@freeze_time('2024-01-01')
def test_invoice_creation(self):
""" Create an invoice, and post it. Ensure that the status and symbol is set correctly during this flow. """
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
)
self.assertFalse(invoice.l10n_vn_edi_invoice_state) # State should be False before posting.
self.assertEqual(invoice.l10n_vn_edi_invoice_symbol.id, self.symbol.id)
invoice.action_post()
self.assertEqual(invoice.l10n_vn_edi_invoice_state, 'ready_to_send')
@freeze_time('2024-01-01')
def test_default_symbol_on_partner(self):
""" Ensure that the default symbol is set correctly if set on the partner of the invoice. """
self.partner_a.l10n_vn_edi_symbol = self.env['l10n_vn_edi_viettel.sinvoice.symbol'].create({
'name': 'K24TUD',
'invoice_template_id': self.template.id,
})
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
)
self.assertEqual(invoice.l10n_vn_edi_invoice_symbol.id, self.partner_a.l10n_vn_edi_symbol.id)
@freeze_time('2024-01-01')
def test_json_data_generation(self):
""" Test the data dict generated to ensure consistency with the data we set in the system. """
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
taxes=self.tax_sale_a,
post=True,
)
self.assertDictEqual(
invoice._l10n_vn_edi_generate_invoice_json(),
{
'generalInvoiceInfo': {
'transactionUuid': mock.ANY, # Random, not important.
'invoiceType': '1',
'templateCode': '1/001',
'invoiceSeries': 'K24TUT',
'invoiceIssuedDate': 1704067200000,
'currencyCode': 'VND',
'adjustmentType': '1',
'paymentStatus': False,
'cusGetInvoiceRight': True,
'validation': 1,
},
'buyerInfo': {
'buyerName': 'partner_a',
'buyerLegalName': 'partner_a',
'buyerTaxCode': '0100109106-505',
'buyerAddressLine': '121 Hang Bac Street',
'buyerPhoneNumber': '38257670',
'buyerEmail': 'partner_a@gmail.com',
'buyerDistrictName': 'Hà Nội',
'buyerCityName': 'Hoan Kiem District',
'buyerCountryCode': 'VN',
'buyerNotGetInvoice': 0,
},
'sellerInfo': {
'sellerLegalName': 'company_1_data',
'sellerTaxCode': '0100109106-506',
'sellerAddressLine': '3 Alley 45 Phan Dinh Phung, Quan Thanh Ward',
'sellerPhoneNumber': '62661275',
'sellerEmail': 'test_company@gmail.com',
'sellerDistrictName': 'Hà Nội',
'sellerCountryCode': 'VN',
'sellerWebsite': 'http://test_company.com',
},
'payments': [{'paymentMethodName': 'TM/CK'}],
'itemInfo': [{
'itemCode': 'BN/1035',
'itemName': 'product_a',
'unitName': 'Units',
'unitPrice': 1000.0,
'quantity': 1.0,
'itemTotalAmountWithoutTax': 1000.0,
'taxPercentage': 10.0,
'taxAmount': 100.0,
'discount': 0.0,
'itemTotalAmountAfterDiscount': 1000.0,
'itemTotalAmountWithTax': 1100.0,
'selection': 1,
}],
'taxBreakdowns': [{
'taxPercentage': 10.0,
'taxableAmount': 1000.0,
'taxAmount': 100.0,
'taxableAmountPos': True,
'taxAmountPos': True
}]
}
)
@freeze_time('2024-01-01')
def test_adjustment_invoice(self):
"""
Create an invoice, then create an adjustment invoice from it. Ensure that when generating the data dict,
the related fields are set correctly.
"""
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
taxes=self.tax_sale_a,
post=True,
)
invoice.write({ # Would be set by sending it to the edi
'l10n_vn_edi_invoice_number': 'K24TUT01',
'l10n_vn_edi_issue_date': fields.Datetime.now(),
'l10n_vn_edi_invoice_state': 'sent',
})
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({
'reason': 'Correcting price',
'journal_id': invoice.journal_id.id,
'l10n_vn_edi_adjustment_type': '1',
'l10n_vn_edi_agreement_document_name': 'N/A',
'l10n_vn_edi_agreement_document_date': fields.Datetime.now(),
})
reversal = move_reversal.reverse_moves()
reverse_move = self.env['account.move'].browse(reversal['res_id'])
reverse_move.invoice_line_ids[0].price_unit = 100 # We invoiced 100 too much
json_data = reverse_move._l10n_vn_edi_generate_invoice_json()
# 1. Check the general info values, ensure correct adjustment type, and that the data were correctly fetched from the original invoice.
expected = {
'adjustmentType': '5',
'adjustmentInvoiceType': '1',
'originalInvoiceId': 'K24TUT01',
'originalInvoiceIssueDate': 1704067200000,
'originalTemplateCode': '1/001',
'additionalReferenceDesc': 'N/A',
'additionalReferenceDate': 1704067200000,
}
actual = json_data['generalInvoiceInfo']
self.assertDictEqual(actual, actual | expected)
# 2. Check the itemInfo to ensure that the values make sense
expected = {
'unitPrice': -100.0,
'itemTotalAmountWithoutTax': -100.0,
'taxAmount': -10.0,
'itemTotalAmountWithTax': -110.0,
'adjustmentTaxAmount': -10.0,
'isIncreaseItem': False,
}
actual = json_data['itemInfo'][0]
self.assertDictEqual(actual, actual | expected)
@freeze_time('2024-01-01')
def test_replacement_invoice(self):
"""
Create an invoice, then create a replacement invoice from it. Ensure that when generating the data dict,
the related fields are set correctly.
"""
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
taxes=self.tax_sale_a,
post=True,
)
invoice.write({ # Would be set by sending it to the edi
'l10n_vn_edi_invoice_number': 'K24TUT01',
'l10n_vn_edi_issue_date': fields.Datetime.now(),
'l10n_vn_edi_invoice_state': 'sent',
})
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({
'reason': 'Correcting price',
'journal_id': invoice.journal_id.id,
'l10n_vn_edi_adjustment_type': '1',
'l10n_vn_edi_agreement_document_name': 'N/A',
'l10n_vn_edi_agreement_document_date': fields.Datetime.now(),
})
reversal = move_reversal.reverse_moves(is_modify=True)
reverse_move = self.env['account.move'].browse(reversal['res_id'])
reverse_move.invoice_line_ids[0].price_unit = 900 # New price is 900 and not 1000
json_data = reverse_move._l10n_vn_edi_generate_invoice_json()
# 1. Check the general info values, ensure correct adjustment type, and that the data were correctly fetched from the original invoice.
expected = {
'adjustmentType': '3',
'adjustmentInvoiceType': '1',
'originalInvoiceId': 'K24TUT01',
'originalInvoiceIssueDate': 1704067200000,
'originalTemplateCode': '1/001',
'additionalReferenceDesc': 'N/A',
'additionalReferenceDate': 1704067200000,
}
actual = reverse_move._l10n_vn_edi_generate_invoice_json()['generalInvoiceInfo']
self.assertDictEqual(actual, actual | expected)
# 2. Check the itemInfo to ensure that the values make sense
expected = {
'unitPrice': 900.0,
'itemTotalAmountWithoutTax': 900.0,
'taxAmount': 90.0,
'itemTotalAmountWithTax': 990.0,
}
actual = json_data['itemInfo'][0]
self.assertDictEqual(actual, actual | expected)
@freeze_time('2024-01-01')
def test_invoice_foreign_currency(self):
""" When invoicing in a foreign currency, we are required to include the rate at the time of the invoice. """
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
taxes=self.tax_sale_a,
post=True,
currency=self.other_currency,
)
json_data = invoice._l10n_vn_edi_generate_invoice_json()
self.assertEqual(json_data['generalInvoiceInfo']['exchangeRate'], 0.5)
@freeze_time('2024-01-01')
def test_send_and_print(self):
""" Test the send & print settings and flows.
Note: we are not trying to test the API, thus the few api call will be mocked to not happen.
"""
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
taxes=self.tax_sale_a,
post=True,
currency=self.other_currency,
)
self.assertEqual(invoice.l10n_vn_edi_invoice_state, 'ready_to_send')
self._send_invoice(invoice)
# Check a few things that should be set by the send & print: invoice number, attachments, state, reservation code.
self.assertRecordValues(
invoice,
[{
'l10n_vn_edi_invoice_number': 'K24TUT01',
'l10n_vn_edi_reservation_code': '123456',
'l10n_vn_edi_invoice_state': 'sent',
}]
)
self.assertNotEqual(invoice.l10n_vn_edi_sinvoice_xml_file, False)
self.assertNotEqual(invoice.l10n_vn_edi_sinvoice_pdf_file, False)
self.assertNotEqual(invoice.l10n_vn_edi_sinvoice_file, False)
@freeze_time('2024-01-01')
def test_cancel_invoice(self):
""" Ensure that trying to cancel a sent invoice returns the wizard action, and test the wizard flow. """
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
taxes=self.tax_sale_a,
post=True,
currency=self.other_currency,
)
self._send_invoice(invoice)
# Trying to cancel a sent invoice should result in an action to open the cancellation wizard.
action = invoice.button_request_cancel()
self.assertEqual(action['res_model'], 'l10n_vn_edi_viettel.cancellation')
with patch('odoo.addons.l10n_vn_edi_viettel.models.account_move._l10n_vn_edi_send_request', return_value=(None, None)):
self.env['l10n_vn_edi_viettel.cancellation'].create({
'invoice_id': invoice.id,
'reason': 'Unwanted',
'agreement_document_name': 'N/A',
'agreement_document_date': fields.Datetime.now(),
}).button_request_cancel()
# Both states should be canceled, but the e-invoicing data should still be there
self.assertEqual(invoice.l10n_vn_edi_invoice_state, 'canceled')
self.assertEqual(invoice.state, 'cancel')
self.assertNotEqual(invoice.l10n_vn_edi_invoice_number, False)
def test_access_token(self):
""" Ensure that we can fetch access tokens as you would expect. """
invoice = self.init_invoice(
move_type='out_invoice',
products=self.product_a,
taxes=self.tax_sale_a,
post=True,
currency=self.other_currency,
)
request_response = {
'access_token': '123', # In reality, it wouldn't be set here, but for convenience in the tests we'll "cheat"
'expires_in': '600', # 10m
}
# Do a few tests to ensure that the access token is handled correctly.
with patch('odoo.addons.l10n_vn_edi_viettel.models.account_move._l10n_vn_edi_send_request', return_value=(request_response, None)):
# First ensure that fetching the token will set the value correctly on the company.
with freeze_time('2024-01-01 02:00:00'):
invoice._l10n_vn_edi_get_access_token()
self.assertEqual(invoice.company_id.l10n_vn_edi_token, '123')
self.assertEqual(invoice.company_id.l10n_vn_edi_token_expiry, datetime.strptime('2024-01-01 02:10:00', '%Y-%m-%d %H:%M:%S'))
# Second fetch should not set anything as the token isn't expired.
with freeze_time('2024-01-01 02:05:00'):
invoice._l10n_vn_edi_get_access_token()
self.assertEqual(invoice.company_id.l10n_vn_edi_token, '123')
self.assertEqual(invoice.company_id.l10n_vn_edi_token_expiry, datetime.strptime('2024-01-01 02:10:00', '%Y-%m-%d %H:%M:%S'))
# Third fetch will get a new token due as it expired
with freeze_time('2024-01-01 02:15:00'):
invoice._l10n_vn_edi_get_access_token()
self.assertEqual(invoice.company_id.l10n_vn_edi_token, '123')
self.assertEqual(invoice.company_id.l10n_vn_edi_token_expiry, datetime.strptime('2024-01-01 02:25:00', '%Y-%m-%d %H:%M:%S'))
def _send_invoice(self, invoice):
pdf_response = {
'name': 'sinvoice.pdf',
'mimetype': 'application/pdf',
'raw': b'pdf file',
'res_field': 'l10n_vn_edi_sinvoice_pdf_file',
}, ""
xml_response = {
'name': 'sinvoice.xml',
'mimetype': 'application/xml',
'raw': b'xml file',
'res_field': 'l10n_vn_edi_sinvoice_xml_file',
}, ""
request_response = {
'result': {
'reservationCode': '123456',
'invoiceNo': 'K24TUT01',
},
'access_token': '123', # In reality, it wouldn't be set here, but for convenience in the tests we'll "cheat"
'expires_in': '60',
}
with patch('odoo.addons.l10n_vn_edi_viettel.models.account_move.AccountMove._l10n_vn_edi_fetch_invoice_pdf_file_data', return_value=pdf_response), \
patch('odoo.addons.l10n_vn_edi_viettel.models.account_move.AccountMove._l10n_vn_edi_fetch_invoice_xml_file_data', return_value=xml_response), \
patch('odoo.addons.l10n_vn_edi_viettel.models.account_move._l10n_vn_edi_send_request', return_value=(request_response, None)):
self.env['account.move.send.wizard'].with_context(active_model=invoice._name, active_ids=invoice.ids).create({}).action_send_and_print()