394 lines
16 KiB
Python
394 lines
16 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import patch
|
|
from urllib.parse import urljoin
|
|
|
|
from freezegun import freeze_time
|
|
|
|
from odoo.addons.account.tests.test_account_move_send import TestAccountMoveSendCommon
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests import tagged
|
|
from odoo.tests.common import HttpCase
|
|
|
|
CALL_API_METHOD = 'odoo.addons.l10n_tw_edi_ecpay.models.account_move.call_ecpay_api'
|
|
|
|
|
|
@tagged('post_install_l10n', 'post_install', '-at_install')
|
|
class L10nTWITestEdi(TestAccountMoveSendCommon, HttpCase):
|
|
|
|
@classmethod
|
|
@TestAccountMoveSendCommon.setup_country('tw')
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.company_data['company'].write({
|
|
'l10n_tw_edi_ecpay_staging_mode': True,
|
|
'l10n_tw_edi_ecpay_merchant_id': '1234',
|
|
'l10n_tw_edi_ecpay_hashkey': 'aaBBccDDeeFFggHH',
|
|
'l10n_tw_edi_ecpay_hashIV': 'bbCCDDeeFFggHHaa',
|
|
'phone': '+886 123 456 781',
|
|
})
|
|
cls.partner_a.write({
|
|
'phone': '+886 123 456 789',
|
|
'contact_address': 'test address',
|
|
'company_type': 'person',
|
|
})
|
|
|
|
# We can reuse this invoice for the flow tests.
|
|
cls.basic_invoice = cls.init_invoice(
|
|
'out_invoice', partner=cls.partner_a, products=cls.product_a,
|
|
)
|
|
cls.basic_invoice.action_post()
|
|
|
|
def test_01_can_generate_file(self):
|
|
"""
|
|
Simply test that with a valid configuration, we can generate the file.
|
|
"""
|
|
with patch(CALL_API_METHOD, new=self._test_01_mock):
|
|
json_data = self.basic_invoice._l10n_tw_edi_generate_invoice_json()
|
|
self.assertTrue(json_data)
|
|
|
|
# Validate the customer data
|
|
self.assertEqual(json_data.get("MerchantID"), "1234")
|
|
self.assertEqual(json_data.get("CustomerName"), "partner_a")
|
|
self.assertEqual(json_data.get("CustomerEmail"), "partner_a@tsointsoin")
|
|
self.assertEqual(json_data.get("CustomerPhone"), "0123456789")
|
|
self.assertEqual(json_data.get("SalesAmount"), 1050.0)
|
|
|
|
@freeze_time("2025-01-06 15:00:00")
|
|
def test_02_basic_submission(self):
|
|
"""
|
|
This tests the most basic flow: an invoice is successfully sent to the ECpay platform, and then pass validation.
|
|
"""
|
|
send_and_print = self.create_send_and_print(self.basic_invoice)
|
|
with patch(CALL_API_METHOD, new=self._test_02_mock):
|
|
send_and_print.action_send_and_print()
|
|
|
|
# Now that the invoice has been sent successfully, we assert that some info have been saved correctly.
|
|
self.assertRecordValues(
|
|
self.basic_invoice,
|
|
[{
|
|
'l10n_tw_edi_ecpay_invoice_id': 'AB11100099',
|
|
'l10n_tw_edi_invoice_create_date': datetime.strptime('2025-01-06 15:00:00', '%Y-%m-%d %H:%M:%S'),
|
|
'l10n_tw_edi_state': 'valid',
|
|
}]
|
|
)
|
|
|
|
self.assertTrue(self.basic_invoice.l10n_tw_edi_file_id)
|
|
|
|
def test_03_failed_submission(self):
|
|
"""
|
|
This test will test a flow that fails when sending the invoice to the ECpay platform, an UserError to be raised
|
|
"""
|
|
send_and_print = self.create_send_and_print(self.basic_invoice)
|
|
with patch(CALL_API_METHOD, new=self._test_03_mock):
|
|
with self.assertRaises(UserError):
|
|
send_and_print.action_send_and_print()
|
|
|
|
@freeze_time("2025-01-06 15:00:00")
|
|
def test_04_invalid_invoice(self):
|
|
"""
|
|
This tests the flow of invalidating an invoice that has already been sent to the ECpay platform.
|
|
"""
|
|
send_and_print = self.create_send_and_print(self.basic_invoice)
|
|
with patch(CALL_API_METHOD, new=self._test_04_mock):
|
|
send_and_print.action_send_and_print()
|
|
wizard_vals = {'journal_id': self.basic_invoice.journal_id.id, 'reason': 'refund'}
|
|
wizard_reverse = self.env['account.move.reversal']\
|
|
.with_context(active_ids=self.basic_invoice.id, active_model='account.move').create(wizard_vals)
|
|
wizard_reverse.reverse_moves(is_modify=True)
|
|
self.assertEqual(self.basic_invoice.l10n_tw_edi_state, 'invalid')
|
|
|
|
@freeze_time("2025-01-06 15:00:00")
|
|
def test_05_refund_invoice(self):
|
|
"""
|
|
This tests the flow of refunding an invoice that has already been sent to the ECpay platform with an offline
|
|
agreement type.
|
|
And make sure that the credit note is created with the correct fields and values from the wizard
|
|
"""
|
|
send_and_print = self.create_send_and_print(self.basic_invoice)
|
|
with patch(CALL_API_METHOD, new=self._test_05_mock):
|
|
send_and_print.action_send_and_print()
|
|
wizard_vals = {
|
|
'journal_id': self.basic_invoice.journal_id.id,
|
|
'l10n_tw_edi_refund_agreement_type': 'offline',
|
|
'l10n_tw_edi_allowance_notify_way': 'email',
|
|
'reason': 'refund',
|
|
}
|
|
wizard_reverse = self.env['account.move.reversal']\
|
|
.with_context(active_ids=self.basic_invoice.id, active_model='account.move').create(wizard_vals)
|
|
wizard_reverse.reverse_moves(is_modify=False)
|
|
credit_note = wizard_reverse.new_move_ids
|
|
credit_note.action_post()
|
|
send_and_print_credit_note = self.create_send_and_print(credit_note)
|
|
send_and_print_credit_note.action_send_and_print()
|
|
self.assertRecordValues(
|
|
credit_note,
|
|
[{
|
|
'reversed_entry_id': self.basic_invoice.id,
|
|
'l10n_tw_edi_refund_agreement_type': 'offline',
|
|
'l10n_tw_edi_refund_invoice_number': '20250106000000021',
|
|
'l10n_tw_edi_refund_state': 'agreed',
|
|
'l10n_tw_edi_ecpay_invoice_id': 'AB11100099',
|
|
'l10n_tw_edi_invoice_create_date': datetime(2025, 1, 6, 15, 0, 0),
|
|
}]
|
|
)
|
|
|
|
@freeze_time("2025-01-06 15:00:00")
|
|
def test_06_refund_invoice_agreed_invoice_allowance(self):
|
|
"""
|
|
This tests the flow of refunding an invoice that has already been sent to the ECpay platform with an online
|
|
agreement type including the flow that customer agreeing to the invoice allowance.
|
|
And make sure that the credit note is created with the correct fields and values from the wizard
|
|
"""
|
|
send_and_print = self.create_send_and_print(self.basic_invoice)
|
|
with patch(CALL_API_METHOD, new=self._test_06_mock):
|
|
send_and_print.action_send_and_print()
|
|
wizard_vals = {
|
|
'journal_id': self.basic_invoice.journal_id.id,
|
|
'l10n_tw_edi_refund_agreement_type': 'online',
|
|
'l10n_tw_edi_allowance_notify_way': 'email',
|
|
'reason': 'refund',
|
|
}
|
|
wizard_reverse = self.env['account.move.reversal']\
|
|
.with_context(active_ids=self.basic_invoice.id, active_model='account.move').create(wizard_vals)
|
|
wizard_reverse.reverse_moves(is_modify=False)
|
|
credit_note = wizard_reverse.new_move_ids
|
|
credit_note.action_post()
|
|
send_and_print_credit_note = self.create_send_and_print(credit_note)
|
|
send_and_print_credit_note.action_send_and_print()
|
|
self.assertRecordValues(
|
|
credit_note,
|
|
[{
|
|
'reversed_entry_id': self.basic_invoice.id,
|
|
'l10n_tw_edi_refund_agreement_type': 'online',
|
|
'l10n_tw_edi_refund_invoice_number': '20250106000000021',
|
|
'l10n_tw_edi_refund_state': 'to_be_agreed',
|
|
'l10n_tw_edi_ecpay_invoice_id': 'AB11100099',
|
|
'l10n_tw_edi_invoice_create_date': datetime(2025, 1, 6, 15, 0, 0),
|
|
}]
|
|
)
|
|
|
|
# test the step that the customer agrees invoice allowance
|
|
api_url = urljoin(
|
|
credit_note.get_base_url(),
|
|
f"/invoice/ecpay/agreed_invoice_allowance/{credit_note.id}?access_token={credit_note._portal_ensure_token()}")
|
|
response = self.url_open(
|
|
api_url,
|
|
data={"RtnCode": "1"}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertRecordValues(
|
|
credit_note,
|
|
[{
|
|
'l10n_tw_edi_refund_invoice_number': '20250106000000021',
|
|
'l10n_tw_edi_refund_state': 'agreed',
|
|
}]
|
|
)
|
|
|
|
def test_07_fail_data_validation(self):
|
|
"""
|
|
This tests the data validation when trying to send to the ECpay platform.
|
|
"""
|
|
# the partner is b2b but has no tax id
|
|
test_partner = self.env['res.partner'].create({
|
|
'name': 'Test Partner',
|
|
'phone': '+886 123 456 789',
|
|
'contact_address': 'test address',
|
|
'company_type': 'company',
|
|
})
|
|
invoice_a = self.init_invoice(
|
|
'out_invoice', partner=test_partner, products=self.product_a,
|
|
)
|
|
invoice_a.action_post()
|
|
send_and_print = self.create_send_and_print(invoice_a)
|
|
with self.assertRaises(UserError):
|
|
send_and_print.action_send_and_print()
|
|
|
|
# the partner is b2b and has an invalid tax id
|
|
test_partner.vat = '1234567A'
|
|
invoice_b = self.init_invoice(
|
|
'out_invoice', partner=test_partner, products=self.product_a,
|
|
)
|
|
invoice_b.action_post()
|
|
send_and_print = self.create_send_and_print(invoice_b)
|
|
with self.assertRaises(UserError):
|
|
send_and_print.action_send_and_print()
|
|
|
|
# the partner's phone number is invalid
|
|
test_partner.vat = '12345678'
|
|
test_partner.phone = '123+456+789'
|
|
invoice_c = self.init_invoice(
|
|
'out_invoice', partner=test_partner, products=self.product_a,
|
|
)
|
|
invoice_c.action_post()
|
|
send_and_print = self.create_send_and_print(invoice_c)
|
|
with self.assertRaises(UserError):
|
|
send_and_print.action_send_and_print()
|
|
# the invoice type is invalid
|
|
test_partner.phone = '+886 123 456 789'
|
|
invoice_d = self.init_invoice(
|
|
'out_invoice', partner=test_partner, products=self.product_a,
|
|
)
|
|
invoice_d.l10n_tw_edi_invoice_type = '08'
|
|
invoice_d.action_post()
|
|
send_and_print = self.create_send_and_print(invoice_d)
|
|
with self.assertRaises(UserError):
|
|
send_and_print.action_send_and_print()
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Patched methods
|
|
# -------------------------------------------------------------------------
|
|
def _test_01_mock(self, endpoint, params, company_id, is_b2b=False):
|
|
if endpoint == "/GetCompanyNameByTaxID":
|
|
return {
|
|
"RtnCode": 1,
|
|
"CompanyName": "Test Company",
|
|
}
|
|
else:
|
|
raise UserError('Unexpected endpoint called during a test: %s with params %s.' % (endpoint, params))
|
|
|
|
def _test_02_mock(self, endpoint, json_data, company_id, is_b2b=False):
|
|
if endpoint == "/Issue":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"InvoiceNo": "AB11100099",
|
|
"InvoiceDate": "2025-01-06 23:00:00",
|
|
"RandomNumber": "6868"
|
|
}
|
|
elif endpoint == "/GetIssue":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"IIS_Sales_Amount": self.basic_invoice.amount_total,
|
|
"IIS_Invalid_Status": "0",
|
|
"IIS_Issue_Status": "1",
|
|
"IIS_Relate_Number": "20250106000000020",
|
|
"IIS_Remain_Allowance_Amt": 0,
|
|
}
|
|
elif endpoint == "/GetCompanyNameByTaxID":
|
|
return {
|
|
"RtnCode": 1,
|
|
"CompanyName": "Test Company",
|
|
}
|
|
else:
|
|
raise UserError('Unexpected endpoint called during a test: %s with params %s.' % (endpoint, json_data))
|
|
|
|
def _test_03_mock(self, endpoint, params, company_id, is_b2b=False):
|
|
if endpoint == "/Issue":
|
|
return {
|
|
"RtnCode": 0,
|
|
"RtnMsg": "Error",
|
|
}
|
|
else:
|
|
raise UserError('Unexpected endpoint called during a test: %s with params %s.' % (endpoint, params))
|
|
|
|
def _test_04_mock(self, endpoint, params, company_id, is_b2b=False):
|
|
if endpoint == "/Issue":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"InvoiceNo": "AB11100099",
|
|
"InvoiceDate": "2025-01-06 23:00:00",
|
|
"RandomNumber": "6868"
|
|
}
|
|
elif endpoint == "/GetIssue":
|
|
return_data = {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"IIS_Sales_Amount": self.basic_invoice.amount_total,
|
|
"IIS_Invalid_Status": "1" if self.basic_invoice.l10n_tw_edi_invalidate_reason else "0",
|
|
"IIS_Issue_Status": "1",
|
|
"IIS_Relate_Number": "20250106000000020",
|
|
"IIS_Remain_Allowance_Amt": 0,
|
|
}
|
|
return return_data
|
|
elif endpoint == "/Invalid":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"InvoiceNo": "AB11100099"
|
|
}
|
|
elif endpoint == "/GetCompanyNameByTaxID":
|
|
return {
|
|
"RtnCode": 1,
|
|
"CompanyName": "Test Company",
|
|
}
|
|
else:
|
|
raise UserError('Unexpected endpoint called during a test: %s with params %s.' % (endpoint, params))
|
|
|
|
def _test_05_mock(self, endpoint, params, company_id, is_b2b=False):
|
|
if endpoint == "/Issue":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"InvoiceNo": "AB11100099",
|
|
"InvoiceDate": "2025-01-06 23:00:00",
|
|
"RandomNumber": "6868"
|
|
}
|
|
elif endpoint == "/GetIssue":
|
|
return_data = {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"IIS_Sales_Amount": self.basic_invoice.amount_total,
|
|
"IIS_Invalid_Status": "0",
|
|
"IIS_Issue_Status": "1",
|
|
"IIS_Relate_Number": "20250106000000020",
|
|
"IIS_Remain_Allowance_Amt": 0,
|
|
}
|
|
return return_data
|
|
elif endpoint == "/Allowance":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"IA_Allow_No": "20250106000000021",
|
|
"IA_Invoice_No": "AB11100099",
|
|
"IA_Date": "2025-01-06 23:00:00",
|
|
"IA_Remain_Allowance_Amt": 0,
|
|
}
|
|
elif endpoint == "/GetCompanyNameByTaxID":
|
|
return {
|
|
"RtnCode": 1,
|
|
"CompanyName": "Test Company",
|
|
}
|
|
else:
|
|
raise UserError('Unexpected endpoint called during a test: %s with params %s.' % (endpoint, params))
|
|
|
|
def _test_06_mock(self, endpoint, params, company_id, is_b2b=False):
|
|
if endpoint == "/Issue":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"InvoiceNo": "AB11100099",
|
|
"InvoiceDate": "2025-01-06 23:00:00",
|
|
"RandomNumber": "6868"
|
|
}
|
|
elif endpoint == "/GetIssue":
|
|
return_data = {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"IIS_Sales_Amount": self.basic_invoice.amount_total,
|
|
"IIS_Invalid_Status": "0",
|
|
"IIS_Issue_Status": "1",
|
|
"IIS_Relate_Number": "20250106000000020",
|
|
"IIS_Remain_Allowance_Amt": 0,
|
|
}
|
|
return return_data
|
|
elif endpoint == "/AllowanceByCollegiate":
|
|
return {
|
|
"RtnCode": 1,
|
|
"RtnMsg": "Success",
|
|
"IA_Allow_No": "20250106000000021",
|
|
"IA_Invoice_No": "AB11100099",
|
|
"IA_Date": "2025-01-06 23:00:00",
|
|
"IA_Remain_Allowance_Amt": 0,
|
|
}
|
|
elif endpoint == "/GetCompanyNameByTaxID":
|
|
return {
|
|
"RtnCode": 1,
|
|
"CompanyName": "Test Company",
|
|
}
|
|
else:
|
|
raise UserError('Unexpected endpoint called during a test: %s with params %s.' % (endpoint, params))
|