mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-21 22:42:06 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -1,45 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_account_move_reconcile
|
||||
from . import test_account_move_payments_widget
|
||||
from . import test_account_move_out_invoice
|
||||
from . import test_account_move_out_refund
|
||||
from . import test_account_move_in_invoice
|
||||
from . import test_account_move_in_refund
|
||||
from . import test_account_move_duplicate
|
||||
from . import test_account_move_entry
|
||||
from . import test_account_move_date_algorithm
|
||||
from . import test_invoice_tax_totals
|
||||
from . import test_account_inalterable_hash
|
||||
from . import test_account_journal
|
||||
from . import test_account_account
|
||||
from . import test_account_tax
|
||||
from . import test_account_to_check
|
||||
from . import test_account_analytic
|
||||
from . import test_account_payment
|
||||
from . import test_account_payment_method_line
|
||||
from . import test_account_payment_duplicate
|
||||
from . import test_account_bank_statement
|
||||
from . import test_account_move_partner_count
|
||||
from . import test_account_move_rounding
|
||||
from . import test_account_invoice_report
|
||||
from . import test_account_move_line_tax_details
|
||||
from . import test_account_journal_dashboard_common
|
||||
from . import test_account_journal_dashboard
|
||||
from . import test_account_lock_exception
|
||||
from . import test_audit_trail
|
||||
from . import test_chart_template
|
||||
from . import test_company_branch
|
||||
from . import test_digest
|
||||
from . import test_download_docs
|
||||
from . import test_fiscal_position
|
||||
from . import test_kpi_provider
|
||||
from . import test_sequence_mixin
|
||||
from . import test_settings
|
||||
from . import test_tax
|
||||
from . import test_taxes_base_lines_tax_details
|
||||
from . import test_taxes_computation
|
||||
from . import test_taxes_tax_totals_summary
|
||||
from . import test_taxes_global_discount
|
||||
from . import test_taxes_downpayment
|
||||
from . import test_taxes_dispatching_base_lines
|
||||
from . import test_invoice_taxes
|
||||
from . import test_templates_consistency
|
||||
from . import test_account_move_send
|
||||
from . import test_account_all_l10n
|
||||
from . import test_reconciliation_matching_rules
|
||||
from . import test_account_onboarding
|
||||
from . import test_portal_attachment
|
||||
from . import test_product
|
||||
from . import test_portal_invoice
|
||||
from . import test_tax_report
|
||||
from . import test_transfer_wizard
|
||||
from . import test_account_incoming_supplier_invoice
|
||||
from . import test_payment_term
|
||||
from . import test_account_payment_items
|
||||
from . import test_account_payment_register
|
||||
from . import test_account_report
|
||||
from . import test_account_section_and_subsection
|
||||
from . import test_tour
|
||||
from . import test_early_payment_discount
|
||||
from . import test_ir_actions_report
|
||||
from . import test_download_xsds
|
||||
from . import test_multivat
|
||||
from . import test_account_partner
|
||||
from . import test_setup_wizard
|
||||
from . import test_structured_reference
|
||||
from . import test_product
|
||||
from . import test_unexpected_invoice
|
||||
from . import test_mail_tracking_value
|
||||
from . import test_res_partner_merge
|
||||
from . import test_account_merge_wizard
|
||||
from . import test_account_move_attachment
|
||||
from . import test_account_bill_deductibility
|
||||
from . import test_dict_to_xml
|
||||
from . import test_duplicate_res_partner_bank
|
||||
from . import test_account_move_import_template
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,20 +1,226 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
from odoo.addons.account.tests.common import TestAccountMergeCommon
|
||||
from odoo.tests import Form, tagged, new_test_user
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import mute_logger
|
||||
import psycopg2
|
||||
from freezegun import freeze_time
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountAccount(AccountTestInvoicingCommon):
|
||||
class TestAccountAccount(TestAccountMergeCommon):
|
||||
|
||||
def test_changing_account_company(self):
|
||||
''' Ensure you can't change the company of an account.account if there are some journal entries '''
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
|
||||
def test_shared_accounts(self):
|
||||
''' Test that creating an account with a given company in company_ids sets the code for that company.
|
||||
Test that copying an account creates a new account with the code set for the new account's company_ids company,
|
||||
and that copying an account that belongs to multiple companies works, even if the copied account had
|
||||
check_company fields that had values belonging to several companies.
|
||||
'''
|
||||
|
||||
company_1 = self.company_data['company']
|
||||
company_2 = self.company_data_2['company']
|
||||
company_3 = self.setup_other_company(name='company_3')['company']
|
||||
|
||||
# Test specifying company_ids in account creation.
|
||||
account = self.env['account.account'].create({
|
||||
'code': '180001',
|
||||
'name': 'My Account in Company 2',
|
||||
'company_ids': [Command.link(company_2.id)]
|
||||
})
|
||||
self.assertRecordValues(
|
||||
account.with_company(company_2),
|
||||
[{'code': '180001', 'company_ids': company_2.ids}]
|
||||
)
|
||||
|
||||
# Test that adding a company to an account fails if the code is not defined for that account and that company.
|
||||
with self.assertRaises(ValidationError):
|
||||
account.write({'company_ids': [Command.link(company_1.id)]})
|
||||
|
||||
# Test that you can add a company to an account if you add the code at the same time
|
||||
account.write({
|
||||
'code': '180011',
|
||||
'company_ids': [Command.link(company_1.id)],
|
||||
'tax_ids': [Command.link(self.company_data['default_tax_sale'].id), Command.link(self.company_data_2['default_tax_sale'].id)],
|
||||
})
|
||||
self.assertRecordValues(account, [{'code': '180011', 'company_ids': [company_1.id, company_2.id]}])
|
||||
|
||||
# Test that you can create an account with multiple codes and companies if you specify the codes in `code_mapping_ids`
|
||||
account_2 = self.env['account.account'].create({
|
||||
'code_mapping_ids': [
|
||||
Command.create({'company_id': company_1.id, 'code': '180021'}),
|
||||
Command.create({'company_id': company_2.id, 'code': '180022'}),
|
||||
Command.create({'company_id': company_3.id, 'code': '180023'}),
|
||||
],
|
||||
'name': 'My second account',
|
||||
'company_ids': [Command.set([company_1.id, company_2.id, company_3.id])],
|
||||
'tax_ids': [Command.set([self.company_data_2['default_tax_sale'].id])],
|
||||
})
|
||||
self.assertRecordValues(account_2, [{'code': '180021', 'company_ids': [company_1.id, company_2.id, company_3.id]}])
|
||||
self.assertRecordValues(account_2.with_company(company_2), [{'code': '180022'}])
|
||||
self.assertRecordValues(account_2.with_company(company_3), [{'code': '180023'}])
|
||||
|
||||
# Test copying an account belonging to multiple companies, specifying the company the new account should belong to.
|
||||
account_copy_1 = account.copy({'company_ids': [Command.set(company_1.ids)], 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)]})
|
||||
self.assertRecordValues(
|
||||
account_copy_1,
|
||||
[{'code': '180012', 'company_ids': company_1.ids, 'tax_ids': self.company_data['default_tax_sale'].ids}],
|
||||
)
|
||||
|
||||
# Test copying an account belonging to multiple companies, without specifying the company. Both companies should be copied.
|
||||
account_copy_2 = account.copy()
|
||||
self.assertRecordValues(
|
||||
account_copy_2,
|
||||
[{
|
||||
'code': '180013',
|
||||
'company_ids': [company_1.id, company_2.id],
|
||||
'tax_ids': [self.company_data['default_tax_sale'].id, self.company_data_2['default_tax_sale'].id],
|
||||
}],
|
||||
)
|
||||
self.assertRecordValues(account_copy_2.with_company(company_2), [{'code': '180002'}])
|
||||
|
||||
# Test copying an account belonging to 3 companies
|
||||
account_copy_3 = account_2.copy()
|
||||
self.assertRecordValues(account_copy_3, [{'code': '180022', 'company_ids': [company_1.id, company_2.id, company_3.id]}])
|
||||
self.assertRecordValues(account_copy_3.with_company(company_2), [{'code': '180023'}])
|
||||
self.assertRecordValues(account_copy_3.with_company(company_3), [{'code': '180024'}])
|
||||
|
||||
# Test that you can modify the code of an account in another company by writing on `code_mapping_ids`.
|
||||
account_copy_3.code_mapping_ids[2].code = '180025'
|
||||
self.assertRecordValues(account_copy_3.with_company(company_3), [{'code': '180025'}])
|
||||
|
||||
# Test that you can modify the code of an account in another company by passing a CREATE value to `code_mapping_ids` (needed for import).
|
||||
account_copy_3.write({'code_mapping_ids': [Command.create({'company_id': company_3.id, 'code': '180026'})]})
|
||||
self.assertRecordValues(account_copy_3.with_company(company_3), [{'code': '180026'}])
|
||||
|
||||
def test_write_on_code_from_branch(self):
|
||||
""" Ensure that when writing on account.code from a company, the old code isn't erroneously kept
|
||||
on other companies that share the same root_id """
|
||||
branch = self.env['res.company'].create([{
|
||||
'name': "My Test Branch",
|
||||
'parent_id': self.company_data['company'].id,
|
||||
}])
|
||||
|
||||
account = self.env['account.account'].create([{
|
||||
'name': 'My Test Account',
|
||||
'code': '180001',
|
||||
}])
|
||||
|
||||
# Change the code from the branch
|
||||
account.with_company(branch).code = '180002'
|
||||
|
||||
# Ensure it's changed from the perspective of the root company
|
||||
self.assertRecordValues(account, [{'code': '180002'}])
|
||||
|
||||
def test_ensure_code_unique(self):
|
||||
''' Test the `_ensure_code_unique` check method.
|
||||
|
||||
Check that it allows a code to be set on an account if and only if
|
||||
there is no other account accessible from the child or parent companies of the account
|
||||
that has the same code in that company.
|
||||
|
||||
Simultaneously, check that the `_search_new_account_code` method proposes codes that
|
||||
would be accepted and skips codes that would be disallowed.
|
||||
'''
|
||||
# Create company hierarchy:
|
||||
# parent_company -> {child_company_1, child_company_2}
|
||||
# other_company is disjoint.
|
||||
|
||||
parent_company = self.company_data['company']
|
||||
child_company_1 = self.env['res.company'].create([{
|
||||
'name': 'Child Company 1',
|
||||
'parent_id': parent_company.id,
|
||||
}])
|
||||
child_company_2 = self.env['res.company'].create([{
|
||||
'name': 'Child Company 2',
|
||||
'parent_id': parent_company.id,
|
||||
}])
|
||||
other_company = self.company_data_2['company']
|
||||
|
||||
# Set up an existing account in the other company.
|
||||
self.env['account.account'].with_context({'allowed_company_ids': other_company.ids}).create([{
|
||||
'name': 'Existing account in other company',
|
||||
'company_ids': [Command.set(other_company.ids)],
|
||||
'code_mapping_ids': [
|
||||
Command.create({'company_id': other_company.id, 'code': '180001'}),
|
||||
Command.create({'company_id': parent_company.id, 'code': '180001'}),
|
||||
]
|
||||
}])
|
||||
|
||||
# 1. Check that the existing account in the other company does not prevent
|
||||
# an account from being created with the same code in `parent_company`.
|
||||
self.assertEqual(
|
||||
self.env['account.account'].with_company(parent_company)._search_new_account_code('180001', cache=set()),
|
||||
'180001',
|
||||
)
|
||||
self.env['account.account'].create([{
|
||||
'name': 'Account in parent company',
|
||||
'code': '180001',
|
||||
'company_ids': [Command.set(parent_company.ids)],
|
||||
}])
|
||||
|
||||
# 2. Check that now that there is an account in `parent_company` with code
|
||||
# 180001, because that account is accessible in `child_company_1`, we cannot
|
||||
# create another account with the same code in `child_company_1`.
|
||||
self.assertEqual(
|
||||
self.env['account.account'].with_company(child_company_1)._search_new_account_code('180001', cache=set()),
|
||||
'180002',
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['account.account'].create([{
|
||||
'name': 'Account in child company 1 (this should fail)',
|
||||
'code': '180001',
|
||||
'company_ids': [Command.set(child_company_1.ids)],
|
||||
}])
|
||||
|
||||
# Now, create an account in `child_company_1` with a new code.
|
||||
self.env['account.account'].create([{
|
||||
'name': 'Account in child company 1',
|
||||
'code': '180002',
|
||||
'company_ids': [Command.set(child_company_1.ids)],
|
||||
}])
|
||||
|
||||
# 3. Check that now that there is an account in `child_company_1` with code
|
||||
# 180002, because any new accounts in `parent_company` are also accessible
|
||||
# from `child_company_1`, we cannot create another account with the same
|
||||
# code in `parent_company`.
|
||||
self.assertEqual(
|
||||
self.env['account.account'].with_company(parent_company)._search_new_account_code('180002', cache=set()),
|
||||
'180003',
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['account.account'].create([{
|
||||
'name': 'Account in parent company (this should fail)',
|
||||
'code': '180002',
|
||||
'company_ids': [Command.set(child_company_1.ids)],
|
||||
}])
|
||||
|
||||
# 4. Check that we can create an account in `child_company_2` with code
|
||||
# 180002, because it will not interfere with the account in `child_company_1`
|
||||
# with code 180002.
|
||||
self.assertEqual(
|
||||
self.env['account.account'].with_company(child_company_2)._search_new_account_code('180002', cache=set()),
|
||||
'180002',
|
||||
)
|
||||
self.env['account.account'].create([{
|
||||
'name': 'Account in child company 2',
|
||||
'code': '180002',
|
||||
'company_ids': [Command.set(child_company_2.ids)],
|
||||
}])
|
||||
|
||||
def test_account_company(self):
|
||||
''' Test the constraint on `account.company_ids`. '''
|
||||
# Test that at least one company is required on accounts.
|
||||
with self.assertRaises(UserError):
|
||||
self.company_data['default_account_revenue'].sudo().company_ids = False
|
||||
|
||||
# Test that unassigning a company from an account fails if there already are journal items
|
||||
# for that company and that account.
|
||||
self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2019-01-01',
|
||||
|
|
@ -30,8 +236,8 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
self.company_data['default_account_revenue'].company_id = self.company_data_2['company']
|
||||
with self.assertRaises(UserError):
|
||||
self.company_data['default_account_revenue'].company_ids = self.company_data_2['company']
|
||||
|
||||
def test_toggle_reconcile(self):
|
||||
''' Test the feature when the user sets an account as reconcile/not reconcile with existing journal entries. '''
|
||||
|
|
@ -43,14 +249,14 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': account.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 200.0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': account.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'debit': 0.0,
|
||||
'credit': 100.0,
|
||||
'amount_currency': -200.0,
|
||||
|
|
@ -100,21 +306,21 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': account.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 200.0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': account.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'debit': 0.0,
|
||||
'credit': 50.0,
|
||||
'amount_currency': -100.0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': self.company_data['default_account_expense'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'debit': 0.0,
|
||||
'credit': 50.0,
|
||||
'amount_currency': -100.0,
|
||||
|
|
@ -125,23 +331,13 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
|
||||
# Set the account as reconcile and partially reconcile something.
|
||||
account.reconcile = True
|
||||
self.env.invalidate_all()
|
||||
|
||||
move.line_ids.filtered(lambda line: line.account_id == account).reconcile()
|
||||
|
||||
# Try to set the account as a not-reconcile one.
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
with self.assertRaises(UserError):
|
||||
account.reconcile = False
|
||||
|
||||
def test_toggle_reconcile_outstanding_account(self):
|
||||
''' Test the feature when the user sets an account as not reconcilable when a journal
|
||||
is configured with this account as the payment credit or debit account.
|
||||
Since such an account should be reconcilable by nature, a ValidationError is raised.'''
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
self.company_data['default_journal_bank'].company_id.account_journal_payment_debit_account_id.reconcile = False
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
self.company_data['default_journal_bank'].company_id.account_journal_payment_credit_account_id.reconcile = False
|
||||
|
||||
def test_remove_account_from_account_group(self):
|
||||
"""Test if an account is well removed from account group"""
|
||||
group = self.env['account.group'].create({
|
||||
|
|
@ -158,6 +354,9 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
|
||||
group.code_prefix_end = 401000
|
||||
|
||||
# Because group_id must depend on the group start and end, but there is no way of making this dependency explicit.
|
||||
(account_1 | account_2).invalidate_recordset(fnames=['group_id'])
|
||||
|
||||
self.assertRecordValues(account_1 + account_2, [{'group_id': group.id}, {'group_id': False}])
|
||||
|
||||
def test_name_create(self):
|
||||
|
|
@ -177,24 +376,62 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
def test_compute_account_type(self):
|
||||
existing_account = self.company_data['default_account_revenue']
|
||||
# account_type should be computed
|
||||
new_account_code = self.env['account.account']._search_new_account_code(
|
||||
company=existing_account.company_id,
|
||||
digits=len(existing_account.code),
|
||||
prefix=existing_account.code[:-1])
|
||||
new_account_code = self.env['account.account']._search_new_account_code(existing_account.code)
|
||||
new_account = self.env['account.account'].create({
|
||||
'code': new_account_code,
|
||||
'name': 'A new account'
|
||||
})
|
||||
self.assertEqual(new_account.account_type, existing_account.account_type)
|
||||
# account_type should not be altered
|
||||
alternate_account = self.env['account.account'].search([('account_type', '!=', existing_account.account_type)], limit=1)
|
||||
alternate_code = self.env['account.account']._search_new_account_code(
|
||||
company=alternate_account.company_id,
|
||||
digits=len(alternate_account.code),
|
||||
prefix=alternate_account.code[:-1])
|
||||
alternate_account = self.env['account.account'].search([
|
||||
('account_type', '!=', existing_account.account_type),
|
||||
('company_ids', '=', self.company_data['company'].id),
|
||||
], limit=1)
|
||||
alternate_code = self.env['account.account']._search_new_account_code(alternate_account.code)
|
||||
new_account.code = alternate_code
|
||||
self.assertEqual(new_account.account_type, existing_account.account_type)
|
||||
|
||||
def test_get_closest_parent_account(self):
|
||||
self.env['account.account'].create({
|
||||
'code': 99998,
|
||||
'name': 'This name will be transferred to the child one',
|
||||
'account_type': 'expense',
|
||||
'tag_ids': [
|
||||
Command.create({'name': 'Test tag'}),
|
||||
],
|
||||
})
|
||||
account_to_process = self.env['account.account'].create({
|
||||
'code': 99999,
|
||||
'name': 'This name will be erase',
|
||||
})
|
||||
|
||||
# The account type and tags will be transferred automatically with the computes
|
||||
self.assertEqual(account_to_process.account_type, 'expense')
|
||||
self.assertEqual(account_to_process.tag_ids.name, 'Test tag')
|
||||
|
||||
def test_search_new_account_code(self):
|
||||
""" Test whether the account codes tested for availability are the ones we expect. """
|
||||
# pylint: disable=bad-whitespace
|
||||
tests = [
|
||||
# start_code Expected tested codes
|
||||
('102100', ['102101', '102102', '102103', '102104']),
|
||||
('1598', ['1599', '1600', '1601', '1602']),
|
||||
('10.01.08', ['10.01.09', '10.01.10', '10.01.11', '10.01.12']),
|
||||
('10.01.97', ['10.01.98', '10.01.99', '10.01.97.copy', '10.01.97.copy2']),
|
||||
('1021A', ['1022A', '1023A', '1024A', '1025A']),
|
||||
('hello', ['hello.copy', 'hello.copy2', 'hello.copy3', 'hello.copy4']),
|
||||
('9998', ['9999', '9998.copy', '9998.copy2', '9998.copy3']),
|
||||
]
|
||||
for start_code, expected_tested_codes in tests:
|
||||
start_account = self.env['account.account'].create({
|
||||
'code': start_code,
|
||||
'name': 'Test',
|
||||
'account_type': 'asset_receivable',
|
||||
})
|
||||
tested_codes = [start_account.copy().code for _ in expected_tested_codes]
|
||||
|
||||
self.assertListEqual(tested_codes, expected_tested_codes)
|
||||
|
||||
def test_compute_current_balance(self):
|
||||
""" Test if an account's current_balance is computed correctly """
|
||||
|
||||
|
|
@ -278,7 +515,7 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
self.assertEqual(account.name, "A new account")
|
||||
|
||||
# name split is only possible through name_create, so an error should be raised
|
||||
with self.assertRaises(psycopg2.DatabaseError), mute_logger('odoo.sql_db'):
|
||||
with self.assertRaises(ValidationError):
|
||||
account = self.env['account.account'].create({
|
||||
'name': '314159 A new account',
|
||||
'account_type': 'expense',
|
||||
|
|
@ -297,7 +534,11 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
"""
|
||||
Test various scenarios when creating an account via a form
|
||||
"""
|
||||
account_form = Form(self.env['account.account'])
|
||||
# We set `allowed_company_ids` here so that the `with_company(other_company)` in
|
||||
# `account.code.mapping._inverse_code` creates a context with both the first active
|
||||
# company and the other company, rather than with just the other company.
|
||||
# In a client-side form, 'allowed_company_ids' will always be set in the context.
|
||||
account_form = Form(self.env['account.account'].with_context({'allowed_company_ids': self.env.company.ids}))
|
||||
account_form.name = "A New Account 1"
|
||||
|
||||
# code should not be set
|
||||
|
|
@ -342,7 +583,7 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
account_form.code = False
|
||||
account_form.name = "Only letters"
|
||||
# saving a form without a code should not be possible
|
||||
with self.assertRaises(AssertionError):
|
||||
with self.assertRaises(ValidationError):
|
||||
account_form.save()
|
||||
|
||||
@freeze_time('2023-09-30')
|
||||
|
|
@ -352,7 +593,7 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
|
||||
- Creates: partner and a account move of that partner.
|
||||
- Checks if the most frequent account for the partner matches created account (with recent move).
|
||||
- Sets the account as deprecated and checks that it no longer appears in the suggestions.
|
||||
- Sets the account as archived and checks that it no longer appears in the suggestions.
|
||||
|
||||
* since tested function takes into account last 2 years, we use freeze_time
|
||||
"""
|
||||
|
|
@ -372,16 +613,78 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
)
|
||||
self.assertEqual(account.id, results_1[0], "Account with most account_moves should be listed first")
|
||||
|
||||
account.deprecated = True
|
||||
account.flush_recordset(['deprecated'])
|
||||
account.active = False
|
||||
account.flush_recordset(['active'])
|
||||
results_2 = self.env['account.account']._get_most_frequent_accounts_for_partner(
|
||||
company_id=self.env.company.id,
|
||||
partner_id=partner.id,
|
||||
move_type="out_invoice"
|
||||
)
|
||||
self.assertFalse(account.id in results_2, "Deprecated account should NOT appear in account suggestions")
|
||||
self.assertFalse(account.id in results_2, "Archived account should NOT appear in account suggestions")
|
||||
|
||||
@freeze_time('2017-01-01')
|
||||
def test_placeholder_code(self):
|
||||
""" Test that the placeholder code is '{code_in_company} ({company})'
|
||||
where `company` is the first of the user's companies that is
|
||||
in `account.company_ids`.
|
||||
|
||||
Check that `_field_to_sql` gives the same value.
|
||||
"""
|
||||
def get_placeholder_code_via_sql(account):
|
||||
account_query = account._as_query()
|
||||
placeholder_code_sql = account_query.select(account._field_to_sql('account_account', 'placeholder_code', account_query))
|
||||
placeholder_code = self.env.execute_query(placeholder_code_sql)[0][0]
|
||||
return placeholder_code
|
||||
|
||||
# This user cannot access company 2, so it can't access the created account.
|
||||
user_2 = new_test_user(
|
||||
self.env,
|
||||
name="User that can't access company 2",
|
||||
login='user_that_cannot_access_company_2',
|
||||
password='user_that_cannot_access_company_2',
|
||||
email='user_that_cannot_access_company_2@test.com',
|
||||
group_ids=self.get_default_groups().ids,
|
||||
company_id=self.env.company.id,
|
||||
)
|
||||
|
||||
account = self.env['account.account'].create([{
|
||||
'name': 'My account',
|
||||
'company_ids': [Command.set(self.company_data_2['company'].ids)],
|
||||
'code': '180001',
|
||||
}])
|
||||
|
||||
self.assertEqual(account.placeholder_code, '180001 (company_2)')
|
||||
self.assertEqual(get_placeholder_code_via_sql(account), '180001 (company_2)')
|
||||
|
||||
self.assertEqual(account.with_company(self.company_data_2['company']).placeholder_code, '180001')
|
||||
self.assertEqual(get_placeholder_code_via_sql(account.with_company(self.company_data_2['company'])), '180001')
|
||||
|
||||
# Invalidate in order to recompute `placeholder_code` with `user_2`
|
||||
account.invalidate_recordset(fnames=['placeholder_code'])
|
||||
self.assertEqual(account.with_user(user_2).sudo().placeholder_code, False)
|
||||
self.assertEqual(get_placeholder_code_via_sql(account.with_user(user_2).sudo()), None)
|
||||
|
||||
def test_account_accessible_by_search_in_sudo_mode(self):
|
||||
""" Test that even if an account isn't accessible by the current user, it is returned by a search in sudo mode. """
|
||||
account = self.env['account.account'].with_company(self.company_data_2['company']).create([{
|
||||
'name': 'Account in Company 2',
|
||||
'code': '180002',
|
||||
}])
|
||||
|
||||
# This user can't access company 2, so it can't access the created account.
|
||||
user_that_cannot_access_company_2 = new_test_user(
|
||||
self.env,
|
||||
name="User that can't access company 2",
|
||||
login='user_that_cannot_access_company_2',
|
||||
password='user_that_cannot_access_company_2',
|
||||
email='user_that_cannot_access_company_2@test.com',
|
||||
group_ids=self.get_default_groups().ids,
|
||||
company_id=self.env.company.id,
|
||||
)
|
||||
|
||||
searched_account = self.env['account.account'].with_user(user_that_cannot_access_company_2).sudo().search([('id', '=', account.id)])
|
||||
self.assertEqual(searched_account, account)
|
||||
|
||||
@freeze_time('2018-01-01')
|
||||
def test_account_opening_balance(self):
|
||||
company = self.env.company
|
||||
account = self.company_data['default_account_revenue']
|
||||
|
|
@ -402,15 +705,15 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 300.0},
|
||||
{'account_id': balancing_account.id, 'balance': 200.0},
|
||||
{'account_id': account.id, 'balance': -500.0},
|
||||
{'account_id': balancing_account.id, 'balance': 200.0},
|
||||
])
|
||||
|
||||
account.opening_balance = 0
|
||||
self.cr.precommit.run()
|
||||
self.assertFalse(company.account_opening_move_id.line_ids)
|
||||
|
||||
account.currency_id = self.currency_data['currency']
|
||||
account.currency_id = self.other_currency
|
||||
account.opening_debit = 100
|
||||
self.cr.precommit.run()
|
||||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
|
|
@ -444,9 +747,9 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
|
||||
{'account_id': balancing_account.id, 'balance': 800.0, 'amount_currency': 800.0},
|
||||
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
|
||||
{'account_id': account.id, 'balance': -1000.0, 'amount_currency': -2000.0},
|
||||
{'account_id': balancing_account.id, 'balance': 800.0, 'amount_currency': 800.0},
|
||||
])
|
||||
|
||||
account.opening_debit = 1000
|
||||
|
|
@ -456,3 +759,317 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
|||
{'account_id': account.id, 'balance': 1000.0, 'amount_currency': 2000.0},
|
||||
{'account_id': account.id, 'balance': -1000.0, 'amount_currency': -2000.0},
|
||||
])
|
||||
|
||||
def test_unmerge(self):
|
||||
company_1 = self.company_data['company']
|
||||
company_2 = self.company_data_2['company']
|
||||
|
||||
# 1. Create a merged account.
|
||||
# First, set-up various fields pointing to the accounts before merging
|
||||
accounts = self.env['account.account']._load_records([
|
||||
{
|
||||
'xml_id': f'account.{company_1.id}_test_account_1',
|
||||
'values': {
|
||||
'name': 'My First Account',
|
||||
'code': '100234',
|
||||
'account_type': 'asset_receivable',
|
||||
'company_ids': [Command.link(company_1.id)],
|
||||
'tax_ids': [Command.link(self.company_data['default_tax_sale'].id)],
|
||||
'tag_ids': [Command.link(self.env.ref('account.account_tag_operating').id)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'xml_id': f'account.{company_2.id}_test_account_2',
|
||||
'values': {
|
||||
'name': 'My Second Account',
|
||||
'code': '100235',
|
||||
'account_type': 'asset_receivable',
|
||||
'company_ids': [Command.link(company_2.id)],
|
||||
'tax_ids': [Command.link(self.company_data_2['default_tax_sale'].id)],
|
||||
'tag_ids': [Command.link(self.env.ref('account.account_tag_investing').id)],
|
||||
},
|
||||
},
|
||||
])
|
||||
referencing_records = {
|
||||
account: self._create_references_to_account(account)
|
||||
for account in accounts
|
||||
}
|
||||
|
||||
# Create the merged account by merging `accounts`
|
||||
wizard = self._create_account_merge_wizard(accounts)
|
||||
wizard.action_merge()
|
||||
self.assertFalse(accounts[1].exists())
|
||||
|
||||
# Check that the merged account has correct values
|
||||
account_to_unmerge = accounts[0]
|
||||
self.assertRecordValues(account_to_unmerge, [{
|
||||
'company_ids': [company_1.id, company_2.id],
|
||||
'name': 'My First Account',
|
||||
'code': '100234',
|
||||
'tax_ids': [self.company_data['default_tax_sale'].id, self.company_data_2['default_tax_sale'].id],
|
||||
'tag_ids': [self.env.ref('account.account_tag_operating').id, self.env.ref('account.account_tag_investing').id],
|
||||
}])
|
||||
self.assertRecordValues(account_to_unmerge.with_company(company_2), [{'code': '100235'}])
|
||||
self.assertEqual(self.env['account.chart.template'].ref('test_account_1'), account_to_unmerge)
|
||||
self.assertEqual(self.env['account.chart.template'].with_company(company_2).ref('test_account_2'), account_to_unmerge)
|
||||
|
||||
for referencing_records_for_account in referencing_records.values():
|
||||
for referencing_record, fname in referencing_records_for_account.items():
|
||||
expected_field_value = account_to_unmerge.ids if referencing_record._fields[fname].type == 'many2many' else account_to_unmerge.id
|
||||
self.assertRecordValues(referencing_record, [{fname: expected_field_value}])
|
||||
|
||||
# Step 2: Unmerge the account
|
||||
new_account = account_to_unmerge.with_context({
|
||||
'account_unmerge_confirm': True,
|
||||
'allowed_company_ids': [company_1.id, company_2.id],
|
||||
})._action_unmerge()
|
||||
|
||||
# Check that the account fields are correct
|
||||
self.assertRecordValues(account_to_unmerge, [{
|
||||
'company_ids': [company_1.id],
|
||||
'name': 'My First Account',
|
||||
'code': '100234',
|
||||
'tax_ids': self.company_data['default_tax_sale'].ids,
|
||||
'tag_ids': [self.env.ref('account.account_tag_operating').id, self.env.ref('account.account_tag_investing').id],
|
||||
}])
|
||||
self.assertRecordValues(account_to_unmerge.with_company(company_2), [{'code': False}])
|
||||
self.assertRecordValues(new_account.with_company(company_2), [{
|
||||
'company_ids': [company_2.id],
|
||||
'name': 'My First Account',
|
||||
'code': '100235',
|
||||
'tax_ids': self.company_data_2['default_tax_sale'].ids,
|
||||
'tag_ids': [self.env.ref('account.account_tag_operating').id, self.env.ref('account.account_tag_investing').id],
|
||||
}])
|
||||
self.assertRecordValues(new_account, [{'code': False}])
|
||||
|
||||
# Check that the referencing records were correctly unmerged
|
||||
new_account_by_old_account = {
|
||||
account_to_unmerge: account_to_unmerge,
|
||||
accounts[1]: new_account,
|
||||
}
|
||||
for account, referencing_records_for_account in referencing_records.items():
|
||||
for referencing_record, fname in referencing_records_for_account.items():
|
||||
expected_account = new_account_by_old_account[account]
|
||||
expected_field_value = expected_account.ids if referencing_record._fields[fname].type == 'many2many' else expected_account.id
|
||||
self.assertRecordValues(referencing_record, [{fname: expected_field_value}])
|
||||
|
||||
# Check that the XMLids were correctly unmerged
|
||||
self.assertEqual(self.env['account.chart.template'].ref('test_account_1'), account_to_unmerge)
|
||||
self.assertEqual(self.env['account.chart.template'].with_company(company_2).ref('test_account_2'), new_account)
|
||||
|
||||
def test_account_code_mapping(self):
|
||||
company_3 = self.env['res.company'].create({'name': 'company_3'})
|
||||
account = self.env['account.account'].create({
|
||||
'code': 'test1',
|
||||
'name': 'Test Account',
|
||||
'account_type': 'asset_current',
|
||||
})
|
||||
|
||||
# Write to DB so that the account gets an ID, and invalidate cache for code_mapping_ids so that they will be looked up
|
||||
account.invalidate_recordset(['code_mapping_ids'])
|
||||
|
||||
account = account.with_context({'allowed_company_ids': [self.company_data['company'].id, self.company_data_2['company'].id, company_3.id]})
|
||||
|
||||
with Form(account) as account_form:
|
||||
# Test that the code mapping gives correct values once the form has been opened (which should call search)
|
||||
self.assertRecordValues(account.code_mapping_ids, [
|
||||
{'company_id': self.company_data['company'].id, 'code': 'test1'},
|
||||
{'company_id': self.company_data_2['company'].id, 'code': False},
|
||||
{'company_id': company_3.id, 'code': False},
|
||||
])
|
||||
|
||||
# Test that we are able to set a new code for companies 2 and 3 via the company mapping
|
||||
with account_form.code_mapping_ids.edit(1) as code_mapping_form:
|
||||
code_mapping_form.code = 'test2'
|
||||
with account_form.code_mapping_ids.edit(2) as code_mapping_form:
|
||||
code_mapping_form.code = 'test3'
|
||||
|
||||
# Test that writing codes and companies at the same time doesn't trigger the constraint
|
||||
# that the code must be set for each company in company_ids
|
||||
account_form.company_ids.add(self.company_data_2['company'])
|
||||
account_form.company_ids.add(company_3)
|
||||
|
||||
self.assertRecordValues(account.with_company(self.company_data_2['company'].id), [{'code': 'test2'}])
|
||||
self.assertRecordValues(account.with_company(company_3.id), [{'code': 'test3'}])
|
||||
|
||||
def test_account_code_mapping_create(self):
|
||||
""" Similar as above, except test that you can create an account while specifying multiple codes in the code mapping tab. """
|
||||
|
||||
company_3 = self.env['res.company'].create({'name': 'company_3'})
|
||||
|
||||
AccountAccount = self.env['account.account'].with_context(
|
||||
{'allowed_company_ids': [self.company_data['company'].id, self.company_data_2['company'].id, company_3.id]}
|
||||
)
|
||||
|
||||
with Form(AccountAccount) as account_form:
|
||||
expected_code_mapping_vals_list = [
|
||||
{'company_id': self.company_data['company'].id, 'code': False},
|
||||
{'company_id': self.company_data_2['company'].id, 'code': False},
|
||||
{'company_id': company_3.id, 'code': False},
|
||||
]
|
||||
actual_code_mapping_vals_list = account_form.code_mapping_ids._records
|
||||
|
||||
for expected_code_mapping_vals, actual_code_mapping_vals in zip(expected_code_mapping_vals_list, actual_code_mapping_vals_list):
|
||||
for key, expected_val in expected_code_mapping_vals.items():
|
||||
self.assertEqual(actual_code_mapping_vals[key], expected_val)
|
||||
|
||||
account_form.name = "My Test Account"
|
||||
account_form.code = 'test1'
|
||||
account_form.account_type = 'asset_current'
|
||||
with account_form.code_mapping_ids.edit(1) as code_mapping_form:
|
||||
code_mapping_form.code = 'test2'
|
||||
with account_form.code_mapping_ids.edit(2) as code_mapping_form:
|
||||
code_mapping_form.code = 'test3'
|
||||
|
||||
# Test that writing codes and companies at the same time doesn't trigger the constraint
|
||||
# that the code must be set for each company in company_ids
|
||||
account_form.company_ids.add(self.company_data_2['company'])
|
||||
account_form.company_ids.add(company_3)
|
||||
|
||||
account = account_form.record
|
||||
|
||||
self.assertRecordValues(account, [{
|
||||
'company_ids': [self.company_data['company'].id, self.company_data_2['company'].id, company_3.id],
|
||||
'code': 'test1'
|
||||
}])
|
||||
self.assertRecordValues(account.with_company(self.company_data_2['company'].id), [{'code': 'test2'}])
|
||||
self.assertRecordValues(account.with_company(company_3.id), [{'code': 'test3'}])
|
||||
|
||||
def test_account_group_hierarchy_consistency(self):
|
||||
""" Test if the hierarchy of account groups is consistent when creating, deleting and recreating an account group """
|
||||
def create_account_group(name, code_prefix, company):
|
||||
return self.env['account.group'].create({
|
||||
'name': name,
|
||||
'code_prefix_start': code_prefix,
|
||||
'code_prefix_end': code_prefix,
|
||||
'company_id': company.id
|
||||
})
|
||||
|
||||
group_1 = create_account_group('group_1', 1, self.env.company)
|
||||
group_10 = create_account_group('group_10', 10, self.env.company)
|
||||
group_100 = create_account_group('group_100', 100, self.env.company)
|
||||
group_101 = create_account_group('group_101', 101, self.env.company)
|
||||
|
||||
self.assertEqual(len(group_1.parent_id), 0)
|
||||
self.assertEqual(group_10.parent_id, group_1)
|
||||
self.assertEqual(group_100.parent_id, group_10)
|
||||
self.assertEqual(group_101.parent_id, group_10)
|
||||
|
||||
# Delete group_101 and recreate it
|
||||
group_101.unlink()
|
||||
group_101 = create_account_group('group_101', 101, self.env.company)
|
||||
|
||||
self.assertEqual(len(group_1.parent_id), 0)
|
||||
self.assertEqual(group_10.parent_id, group_1)
|
||||
self.assertEqual(group_100.parent_id, group_10)
|
||||
self.assertEqual(group_101.parent_id, group_10)
|
||||
|
||||
# The root becomes a child and vice versa
|
||||
group_3 = create_account_group('group_3', 3, self.env.company)
|
||||
group_31 = create_account_group('group_31', 31, self.env.company)
|
||||
group_3.code_prefix_start = 312
|
||||
self.assertEqual(len(group_31.parent_id), 0)
|
||||
self.assertEqual(group_3.parent_id, group_31)
|
||||
|
||||
def test_muticompany_account_groups(self):
|
||||
"""
|
||||
Ensure that account groups are always in a root company
|
||||
Ensure that accounts and account groups from a same company tree match
|
||||
"""
|
||||
|
||||
branch_company = self.env['res.company'].create({
|
||||
'name': 'Branch Company',
|
||||
'parent_id': self.env.company.id,
|
||||
})
|
||||
|
||||
parent_group = self.env['account.group'].create({
|
||||
'name': 'Parent Group',
|
||||
'code_prefix_start': '123',
|
||||
'code_prefix_end': '124'
|
||||
})
|
||||
child_group = self.env['account.group'].with_company(branch_company).create({
|
||||
'name': 'Child Group',
|
||||
'code_prefix_start': '125',
|
||||
'code_prefix_end': '126',
|
||||
})
|
||||
self.assertEqual(
|
||||
child_group.company_id,
|
||||
child_group.company_id.root_id,
|
||||
"company_id should never be a branch company"
|
||||
)
|
||||
|
||||
branch_account = self.env['account.account'].with_company(branch_company).create({
|
||||
'name': 'Branch Account',
|
||||
'code': '1234',
|
||||
})
|
||||
self.assertEqual(
|
||||
branch_account.group_id,
|
||||
parent_group,
|
||||
"group_id computation should work for accounts that are not in the root company"
|
||||
)
|
||||
|
||||
parent_account = self.env['account.account'].create({
|
||||
'name': 'Parent Account',
|
||||
'code': '1235'
|
||||
})
|
||||
parent_account.with_company(branch_company).code = '1256'
|
||||
self.assertEqual(
|
||||
parent_account.with_company(branch_company).group_id,
|
||||
child_group,
|
||||
"group_id computation should work if company_id is not in self.env.companies"
|
||||
)
|
||||
|
||||
def test_compute_account(self):
|
||||
account_sale = self.company_data['default_account_revenue'].copy()
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'account_id': account_sale.id,
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 100,
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertEqual(invoice.invoice_line_ids.account_id, account_sale)
|
||||
|
||||
invoice.line_ids._compute_account_id()
|
||||
|
||||
self.assertEqual(invoice.invoice_line_ids.account_id, self.company_data['default_account_revenue'])
|
||||
|
||||
def test_access_to_parent_accounts_from_branch(self):
|
||||
""" Ensure that a user with access to a branch can access to the accounts of the parent company """
|
||||
parent_company = self.env['res.company'].create([{
|
||||
'name': "Parent Company",
|
||||
}])
|
||||
branch = self.env['res.company'].create([{
|
||||
'name': "Branch Company",
|
||||
'parent_id': parent_company.id,
|
||||
}])
|
||||
self.env['account.account'].create([{
|
||||
'name': 'Parent Account',
|
||||
'code': '444719',
|
||||
'company_ids': [Command.link(parent_company.id)]
|
||||
}])
|
||||
# create a user with account rights and access to the branch company only
|
||||
branch_user = self.env['res.users'].create({
|
||||
'login': 'branch',
|
||||
'name': 'XYZ',
|
||||
'email': 'xyz@example.com',
|
||||
'group_ids': [Command.link(self.env.ref('account.group_account_user').id)],
|
||||
'company_ids': [Command.link(branch.id)],
|
||||
'company_id': branch.id,
|
||||
})
|
||||
|
||||
parent_accounts = self.env['account.account'].search([('company_ids', '=', parent_company.id)])
|
||||
self.assertEqual(len(parent_accounts), 1, "There should be 1 account in the parent company")
|
||||
|
||||
branch_accounts = self.env['account.account'].search([('company_ids', '=', branch.id)])
|
||||
self.assertEqual(len(branch_accounts), 0, "There should be no account in the branch company")
|
||||
# get the accounts from the parent company with the branch user
|
||||
accounts = self.env['account.account'].with_user(branch_user.id).search([('company_ids', 'parent_of', [branch.id])])
|
||||
self.assertEqual(len(accounts), 1, "Branch user should have access to the accounts of the parent company")
|
||||
|
|
|
|||
|
|
@ -1,41 +1,144 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import time
|
||||
|
||||
from odoo.fields import Domain
|
||||
from odoo.modules.loading import force_demo
|
||||
from odoo.tools import make_index_name, SQL
|
||||
from odoo.tools.translate import TranslationImporter
|
||||
from odoo.tests import standalone
|
||||
|
||||
from odoo.addons.account.models.chart_template import AccountChartTemplate
|
||||
from unittest.mock import patch
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_file(self, filepath, lang, xmlids=None, module=None, original=TranslationImporter.load_file):
|
||||
self.imported_langs.add(lang)
|
||||
return original(self, filepath, lang, xmlids=xmlids, module=module)
|
||||
|
||||
|
||||
@standalone('all_l10n')
|
||||
def test_all_l10n(env):
|
||||
""" This test will install all the l10n_* modules.
|
||||
As the module install is not yet fully transactional, the modules will
|
||||
remain installed after the test.
|
||||
"""
|
||||
assert env.ref('base.module_account').demo, "Need the demo to test with data"
|
||||
l10n_mods = env['ir.module.module'].search([
|
||||
('name', 'like', 'l10n%'),
|
||||
|
||||
try_loading = type(env['account.chart.template']).try_loading
|
||||
|
||||
def try_loading_patch(self, template_code, company, install_demo=True, force_create=True):
|
||||
self = self.with_context(l10n_check_fields_complete=True)
|
||||
return try_loading(self, template_code, company, install_demo, force_create)
|
||||
|
||||
|
||||
# Ensure the presence of demo data, to see if they can be correctly installed
|
||||
if not env.ref('base.module_account').demo:
|
||||
force_demo(env)
|
||||
|
||||
# Install prerequisite modules
|
||||
_logger.info('Installing prerequisite modules')
|
||||
pre_mods = env['ir.module.module'].search([
|
||||
('name', 'in', (
|
||||
'stock_account',
|
||||
'mrp_accountant',
|
||||
)),
|
||||
('state', '=', 'uninstalled'),
|
||||
])
|
||||
l10n_mods.button_immediate_install()
|
||||
env.reset() # clear the set of environments
|
||||
env = env() # get an environment that refers to the new registry
|
||||
pre_mods.button_immediate_install()
|
||||
|
||||
coas = env['account.chart.template'].search([
|
||||
('id', 'not in', env['res.company'].search([]).chart_template_id.ids)
|
||||
# Install the requirements
|
||||
_logger.info('Installing all l10n modules')
|
||||
l10n_mods = env['ir.module.module'].search([
|
||||
'|',
|
||||
('name', '=like', 'l10n_%'),
|
||||
('name', '=like', 'test_l10n_%'),
|
||||
('state', '=', 'uninstalled'),
|
||||
])
|
||||
for coa in coas:
|
||||
cname = 'company_%s' % str(coa.id)
|
||||
company = env['res.company'].create({
|
||||
'name': cname,
|
||||
'country_id': coa.country_id.id,
|
||||
})
|
||||
with patch.object(AccountChartTemplate, 'try_loading', try_loading_patch),\
|
||||
patch.object(TranslationImporter, 'load_file', _load_file):
|
||||
l10n_mods.button_immediate_install()
|
||||
|
||||
# In all_l10n tests we need to verify demo data
|
||||
demo_failures = env['ir.demo_failure'].search([])
|
||||
if demo_failures:
|
||||
_logger.warning("Error while testing demo data for all_l10n tests.")
|
||||
for failure in demo_failures:
|
||||
_logger.warning("Demo data of module %s has failed: %s",
|
||||
failure.module_id.name, failure.error)
|
||||
|
||||
env.transaction.reset() # clear the set of environments
|
||||
idxs = []
|
||||
for model in env.registry.values():
|
||||
if not model._auto:
|
||||
continue
|
||||
|
||||
for field in model._fields.values():
|
||||
# TODO: handle non-orm indexes where the account field is alone or first
|
||||
if not field.store or field.index \
|
||||
or field.type != 'many2one' \
|
||||
or field.comodel_name != 'account.account':
|
||||
continue
|
||||
|
||||
idxname = make_index_name(model._table, field.name)
|
||||
env.cr.execute(SQL(
|
||||
"CREATE INDEX IF NOT EXISTS %s ON %s (%s)%s",
|
||||
SQL.identifier(idxname),
|
||||
SQL.identifier(model._table),
|
||||
SQL.identifier(field.name),
|
||||
SQL("") if field.required else SQL(" WHERE %s IS NOT NULL", SQL.identifier(field.name)),
|
||||
))
|
||||
idxs.append(idxname)
|
||||
|
||||
# Install Charts of Accounts
|
||||
_logger.info('Loading chart of account')
|
||||
already_loaded_codes = set(env['res.company'].search([]).mapped('chart_template'))
|
||||
not_loaded_codes = [
|
||||
(template_code, template)
|
||||
for template_code, template in env['account.chart.template']._get_chart_template_mapping().items()
|
||||
if template_code not in already_loaded_codes
|
||||
# We can't make it disappear from the list, but we raise a UserError if it's not already the COA
|
||||
and template_code not in ('syscohada', 'syscebnl')
|
||||
]
|
||||
companies = env['res.company'].create([
|
||||
{
|
||||
'name': f'company_coa_{template_code}',
|
||||
'country_id': template['country_id'],
|
||||
}
|
||||
for template_code, template in not_loaded_codes
|
||||
])
|
||||
env.cr.commit()
|
||||
|
||||
# Install the CoAs
|
||||
start = time.time()
|
||||
env.cr.execute('ANALYZE')
|
||||
logger = logging.getLogger('odoo.loading')
|
||||
logger.runbot('ANALYZE took %s seconds', time.time() - start) # not sure this one is useful
|
||||
for (template_code, _template), company in zip(not_loaded_codes, companies):
|
||||
env.user.company_ids += company
|
||||
env.user.company_id = company
|
||||
_logger.info('Testing COA: %s (company: %s)' % (coa.name, cname))
|
||||
_logger.info('Testing COA: %s (company: %s)', template_code, company.name)
|
||||
try:
|
||||
with env.cr.savepoint():
|
||||
coa.try_loading()
|
||||
env['account.chart.template'].with_context(l10n_check_fields_complete=True).try_loading(template_code, company, install_demo=True)
|
||||
env.cr.commit()
|
||||
if company.fiscal_position_ids and not company.domestic_fiscal_position_id:
|
||||
_logger.warning("No domestic fiscal position found in fiscal data for %s %s.", company.country_id.name, template_code)
|
||||
elif company.fiscal_position_ids:
|
||||
potential_domestic_fps = company.fiscal_position_ids.filtered_domain(
|
||||
Domain('country_id', '=', company.country_id.id)
|
||||
| Domain([
|
||||
('country_id', '=', False),
|
||||
('country_group_id', 'in', company.country_id.country_group_ids.ids),
|
||||
]),
|
||||
)
|
||||
if len(potential_domestic_fps) > 1:
|
||||
potential_domestic_fps.sorted(lambda x: x.country_id.id or float('inf')).sorted('sequence')
|
||||
if ((potential_domestic_fps[0].country_id == potential_domestic_fps[1].country_id) and
|
||||
(potential_domestic_fps[0].sequence == potential_domestic_fps[1].sequence)):
|
||||
_logger.warning("Several fiscal positions fitting for being tagged as domestic were found in fiscal data for %s %s.", company.country_id.name, template_code)
|
||||
except Exception:
|
||||
_logger.error("Error when creating COA %s", coa.name, exc_info=True)
|
||||
_logger.error("Error when creating COA %s", template_code, exc_info=True)
|
||||
env.cr.rollback()
|
||||
|
||||
env.cr.execute(SQL("DROP INDEX %s", SQL(", ").join(map(SQL.identifier, idxs))))
|
||||
env.cr.commit()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo import fields, Command
|
||||
|
||||
|
|
@ -11,30 +10,20 @@ import base64
|
|||
class TestAccountBankStatementLine(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.currency_1 = cls.company_data['currency']
|
||||
# We need a third currency as you could have a company's currency != journal's currency !=
|
||||
cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
|
||||
'name': 'Dark Chocolate Coin',
|
||||
'symbol': '🍫',
|
||||
'currency_unit_label': 'Dark Choco',
|
||||
'currency_subunit_label': 'Dark Cacao Powder',
|
||||
}, rate2016=6.0, rate2017=4.0)
|
||||
cls.currency_data_3 = cls.setup_multi_currency_data(default_values={
|
||||
'name': 'Black Chocolate Coin',
|
||||
'symbol': '🍫',
|
||||
'currency_unit_label': 'Black Choco',
|
||||
'currency_subunit_label': 'Black Cacao Powder',
|
||||
}, rate2016=12.0, rate2017=8.0)
|
||||
cls.currency_2 = cls.setup_other_currency('EUR')
|
||||
cls.currency_3 = cls.setup_other_currency('CAD', rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)])
|
||||
cls.currency_4 = cls.setup_other_currency('GBP', rates=[('2016-01-01', 12.0), ('2017-01-01', 8.0)])
|
||||
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
cls.bank_journal_1 = cls.company_data['default_journal_bank']
|
||||
cls.bank_journal_2 = cls.bank_journal_1.copy()
|
||||
cls.bank_journal_3 = cls.bank_journal_2.copy()
|
||||
cls.currency_1 = cls.company_data['currency']
|
||||
cls.currency_2 = cls.currency_data['currency']
|
||||
cls.currency_3 = cls.currency_data_2['currency']
|
||||
cls.currency_4 = cls.currency_data_3['currency']
|
||||
|
||||
cls.statement = cls.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
|
|
@ -191,7 +180,7 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
|
|||
}])
|
||||
|
||||
# Check the account.bank.statement.line is still correct after editing the account.move.
|
||||
statement_line.move_id.write({'line_ids': [
|
||||
statement_line.move_id.with_context(skip_readonly_check=True).write({'line_ids': [
|
||||
(1, liquidity_lines.id, {
|
||||
'debit': expected_liquidity_values.get('debit', 0.0),
|
||||
'credit': expected_liquidity_values.get('credit', 0.0),
|
||||
|
|
@ -355,7 +344,7 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
|
|||
|
||||
def test_constraints(self):
|
||||
def assertStatementLineConstraint(statement_line_vals):
|
||||
with self.assertRaises(Exception), self.cr.savepoint():
|
||||
with self.assertRaises(Exception):
|
||||
self.env['account.bank.statement.line'].create(statement_line_vals)
|
||||
|
||||
statement_line_vals = {
|
||||
|
|
@ -401,12 +390,12 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
|
|||
'move_id': st_line.move_id.id,
|
||||
},
|
||||
]
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
with self.assertRaises(UserError):
|
||||
st_line.move_id.write({
|
||||
'line_ids': [(0, 0, vals) for vals in addition_lines_to_create]
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
with self.assertRaises(UserError):
|
||||
st_line.line_ids.create(addition_lines_to_create)
|
||||
|
||||
def test_statement_line_move_onchange_1(self):
|
||||
|
|
@ -703,6 +692,18 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
|
|||
'date': fields.Date.from_string('2020-01-13')},
|
||||
])
|
||||
|
||||
# computing validity of non-consecutive statement shouldn't affect validity
|
||||
line5 = self.create_bank_transaction(-10, '2020-01-13')
|
||||
statement4 = self.env['account.bank.statement'].create({
|
||||
'line_ids': [Command.set(line5.ids)],
|
||||
'balance_start': -15,
|
||||
})
|
||||
(statement1 + statement4).invalidate_recordset(['is_valid'])
|
||||
self.assertRecordValues(statement1 + statement4, [
|
||||
{'is_valid': True},
|
||||
{'is_valid': True},
|
||||
])
|
||||
|
||||
# adding a statement to the first line should make statement1 invalid
|
||||
line1.statement_id = statement2
|
||||
statement2.flush_model()
|
||||
|
|
@ -1455,10 +1456,19 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
|
|||
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=move.ids).create({
|
||||
'date': fields.Date.from_string('2021-02-01'),
|
||||
'refund_method': 'cancel',
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
reversed_move = self.env['account.move'].browse(reversal['res_id'])
|
||||
|
||||
self.assertEqual(reversed_move.partner_id, partner)
|
||||
|
||||
def test_bank_transaction_creation_with_default_journal_entry_date(self):
|
||||
invoice_date_field = self.env['ir.model.fields'].search([('model', '=', 'account.move'), ('name', '=', 'invoice_date')], limit=1)
|
||||
self.env['ir.default'].create({
|
||||
'field_id': invoice_date_field.id,
|
||||
'json_value': '"2023-10-16"',
|
||||
})
|
||||
|
||||
transaction = self.create_bank_transaction(1, '2020-01-10', journal=self.bank_journal_1)
|
||||
assert transaction.date == transaction.move_id.date == fields.Date.from_string('2020-01-10')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,478 @@
|
|||
from odoo import Command, fields
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountBillPartialDeductibility(AccountTestInvoicingCommon):
|
||||
|
||||
def test_simple_bill_partial_deductibility(self):
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.today(),
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 25.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 3.75, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 11.25, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -115.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
bill.invoice_line_ids[0].quantity = 2
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 200.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -50.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 7.5, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 22.5, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -230.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 50.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
bill.invoice_line_ids[0].price_unit = 50.0
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 3.75, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 11.25, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -115.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 25.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
bill.invoice_line_ids[0].deductible_amount = 100.0
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 15.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -115.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
bill.invoice_line_ids[0].deductible_amount = 75.0
|
||||
bill.action_post()
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 11.25, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -115.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': bill.name + ' - private part', 'balance': 25.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': bill.name + ' - private part (taxes)', 'balance': 3.75, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_bill_partial_deductibility_with_identical_lines(self):
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 50.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 7.5, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 22.5, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -230.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_bill_partial_deductibility_with_several_invoice_lines(self):
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item 1',
|
||||
'price_unit': 100,
|
||||
'quantity': 3,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Partial item 2',
|
||||
'price_unit': 150,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 80.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Full item',
|
||||
'price_unit': 200,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 100.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Full item', 'balance': 200.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'product', 'name': 'Partial item 1', 'balance': 300.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'product', 'name': 'Partial item 2', 'balance': 150.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item 1', 'balance': -75.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item 2', 'balance': -30.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 105.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 15.75, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 81.75, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -747.5, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_bill_partial_deductibility_with_different_taxes(self):
|
||||
tax_21 = self.tax_purchase_a.copy({
|
||||
'name': '21%',
|
||||
'amount': 21,
|
||||
})
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [
|
||||
Command.set((self.tax_purchase_a + tax_21).ids),
|
||||
],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id, tax_21.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id, tax_21.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 25.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 9.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 11.25, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '21%', 'balance': 15.75, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -136.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_bill_partial_deductibility_with_several_lines_with_different_taxes(self):
|
||||
tax_21 = self.tax_purchase_a.copy({
|
||||
'name': '21%',
|
||||
'amount': 21,
|
||||
})
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item 1',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [
|
||||
Command.set(self.tax_purchase_a.ids),
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Partial item 2',
|
||||
'price_unit': 120,
|
||||
'quantity': 2,
|
||||
'deductible_amount': 50.00,
|
||||
'tax_ids': [
|
||||
Command.set(tax_21.ids),
|
||||
],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item 1', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'product', 'name': 'Partial item 2', 'balance': 240.0, 'tax_ids': [tax_21.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item 1', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item 2', 'balance': -120.0, 'tax_ids': [tax_21.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 145.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 28.95, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 11.25, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '21%', 'balance': 25.2, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -405.4, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_bill_partial_deductibility_with_discounts(self):
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'discount': 50,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 50.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -12.5, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 12.5, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 1.88, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 5.63, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -57.51, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_bill_partial_deductibility_with_cash_rounding(self):
|
||||
cash_rounding = self.env['account.cash.rounding'].create({
|
||||
'name': 'Rounding 10',
|
||||
'rounding_method': 'HALF-UP',
|
||||
'rounding': 10,
|
||||
'profit_account_id': self.cash_rounding_a.profit_account_id.id,
|
||||
'loss_account_id': self.cash_rounding_a.loss_account_id.id,
|
||||
})
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_cash_rounding_id': cash_rounding.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': 100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': -25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 25.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 3.75, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': 11.25, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'rounding', 'name': 'Rounding 10', 'balance' : 5.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -120.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_simple_refund_partial_deductibility(self):
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'Partial item', 'balance': -100.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product', 'name': 'Partial item', 'balance': 25.0, 'tax_ids': [self.tax_purchase_a.id]}, # noqa: E241
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': -25.0, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': -3.75, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'tax', 'name': '15%', 'balance': -11.25, 'tax_ids': []}, # noqa: E241
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': 115.0, 'tax_ids': []}, # noqa: E241
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
||||
def test_bill_non_deductible_tax_in_tax_totals(self):
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2024-01-01',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
expected_values = {
|
||||
'same_tax_base': False,
|
||||
'currency_id': self.env.company.currency_id.id,
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 15.0,
|
||||
'total_amount_currency': 115.0,
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 15.0,
|
||||
'tax_groups': [
|
||||
{
|
||||
'id': self.tax_purchase_a.tax_group_id.id,
|
||||
'non_deductible_tax_amount': -3.75,
|
||||
'non_deductible_tax_amount_currency': -3.75,
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 15.0,
|
||||
'display_base_amount_currency': 75.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
self._assert_tax_totals_summary(bill.tax_totals, expected_values)
|
||||
|
||||
def test_refund_non_deductible_tax_in_tax_totals(self):
|
||||
refund = self.env['account.move'].create({
|
||||
'move_type': 'in_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2024-01-01',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'Partial item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 75.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
expected_values = {
|
||||
'same_tax_base': False,
|
||||
'currency_id': self.env.company.currency_id.id,
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 15.0,
|
||||
'total_amount_currency': 115.0,
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 15.0,
|
||||
'tax_groups': [
|
||||
{
|
||||
'id': self.tax_purchase_a.tax_group_id.id,
|
||||
'non_deductible_tax_amount': -3.75,
|
||||
'non_deductible_tax_amount_currency': -3.75,
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 15.0,
|
||||
'display_base_amount_currency': 75.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
self._assert_tax_totals_summary(refund.tax_totals, expected_values)
|
||||
|
||||
def test_bill_partial_deductibility_with_reverse_charge(self):
|
||||
tax_reverse_charge = self.env['account.tax'].create({
|
||||
'name': 'Reverse Charge 20%',
|
||||
'amount_type': 'percent',
|
||||
'amount': 20.0,
|
||||
'type_tax_use': 'purchase',
|
||||
'invoice_repartition_line_ids': [
|
||||
Command.create({'repartition_type': 'base'}),
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'account_id': self.company_data['default_account_tax_purchase'].id}),
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'account_id': self.company_data['default_account_tax_purchase'].id}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
Command.create({'repartition_type': 'base'}),
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'account_id': self.company_data['default_account_tax_purchase'].id}),
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'account_id': self.company_data['default_account_tax_purchase'].id}),
|
||||
],
|
||||
})
|
||||
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.today(),
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'RC Partial Item',
|
||||
'price_unit': 100,
|
||||
'quantity': 1,
|
||||
'deductible_amount': 60.00,
|
||||
'tax_ids': [Command.set(tax_reverse_charge.ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertInvoiceValues(
|
||||
bill,
|
||||
[
|
||||
{'display_type': 'product', 'name': 'RC Partial Item', 'balance': 100.0, 'tax_ids': [tax_reverse_charge.id]},
|
||||
{'display_type': 'non_deductible_product', 'name': 'RC Partial Item', 'balance': -40.0, 'tax_ids': [tax_reverse_charge.id]},
|
||||
{'display_type': 'non_deductible_product_total', 'name': 'private part', 'balance': 40.0, 'tax_ids': []},
|
||||
{'display_type': 'non_deductible_tax', 'name': 'private part (taxes)', 'balance': 8.0, 'tax_ids': []},
|
||||
{'display_type': 'tax', 'name': 'Reverse Charge 20%', 'balance': -20.0, 'tax_ids': []},
|
||||
{'display_type': 'tax', 'name': 'Reverse Charge 20%', 'balance': 12.0, 'tax_ids': []},
|
||||
{'display_type': 'payment_term', 'name': False, 'balance': -100.0, 'tax_ids': []},
|
||||
],
|
||||
{}
|
||||
)
|
||||
|
|
@ -1,34 +1,86 @@
|
|||
from contextlib import closing
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.models import Model
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
from odoo import fields
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo import fields, Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import format_date
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMoveInalterableHash(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
def _init_and_post(self, vals, hash_version=False, secure_sequence=None):
|
||||
moves = self.env['account.move']
|
||||
for val in vals:
|
||||
move = self.init_invoice("out_invoice", val['partner'], val['date'], amounts=val['amounts'], journal=val.get('journal'), post=False)
|
||||
if secure_sequence: # Simulate old behavior (pre hash v4)
|
||||
move.secure_sequence_number = secure_sequence.next_by_id()
|
||||
if hash_version:
|
||||
move = move.with_context(hash_version=hash_version)
|
||||
move.action_post()
|
||||
moves |= move
|
||||
return moves
|
||||
|
||||
def _skip_hash_moves(self):
|
||||
def _do_not_hash_moves(self, **kwargs):
|
||||
pass
|
||||
return patch('odoo.addons.account.models.account_move.AccountMove._hash_moves', new=_do_not_hash_moves)
|
||||
|
||||
def _reverse_move(self, move):
|
||||
reversal = move._reverse_moves()
|
||||
reversal.action_post()
|
||||
return reversal
|
||||
|
||||
def _get_secure_sequence(self):
|
||||
"""Before hash v4, we had a secure_sequence on hashed journals.
|
||||
We removed it starting v4, however, to test previous versions, we need to create it
|
||||
to mock the old behavior."""
|
||||
return self.env['ir.sequence'].create({
|
||||
'name': 'SECURE_SEQUENCE',
|
||||
'code': 'SECURE_SEQUENCE',
|
||||
'implementation': 'no_gap',
|
||||
'prefix': '',
|
||||
'suffix': '',
|
||||
'padding': 0,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
|
||||
def _verify_integrity(self, moves, expected_msg_cover, expected_first_move=None, expected_last_move=None, prefix=None):
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results']
|
||||
name = prefix or moves[0].sequence_prefix
|
||||
integrity_check = next(filter(lambda r: name in r.get('journal_name'), integrity_check))
|
||||
self.assertRegex(integrity_check['msg_cover'], expected_msg_cover)
|
||||
if expected_first_move and expected_last_move:
|
||||
self.assertEqual(integrity_check['first_move_name'], expected_first_move.name)
|
||||
self.assertEqual(integrity_check['last_move_name'], expected_last_move.name)
|
||||
|
||||
def test_account_move_inalterable_hash(self):
|
||||
"""Test that we cannot alter a field used for the computation of the inalterable hash"""
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
move = self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True)
|
||||
self.company_data['default_journal_purchase'].restrict_mode_hash_table = True
|
||||
move = self._init_and_post([{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [1000]}])
|
||||
in_invoice = self.init_invoice("in_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True)
|
||||
# in_invoice and out_invoice should both be hashed on post
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
self.assertNotEqual(in_invoice.inalterable_hash, False)
|
||||
|
||||
with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
|
||||
common = "This document is protected by a hash. Therefore, you cannot edit the following fields:"
|
||||
with self.assertRaisesRegex(UserError, f"{common}.*Inalterability Hash."):
|
||||
move.inalterable_hash = 'fake_hash'
|
||||
with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
|
||||
move.secure_sequence_number = 666
|
||||
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
|
||||
with self.assertRaisesRegex(UserError, f"{common}.*Inalterability Hash."):
|
||||
in_invoice.inalterable_hash = "fake_hash"
|
||||
with self.assertRaisesRegex(UserError, f"{common}.*Number."):
|
||||
move.name = "fake name"
|
||||
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
|
||||
with self.assertRaisesRegex(UserError, f"{common}.*Date."):
|
||||
move.date = fields.Date.from_string('2023-01-02')
|
||||
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
|
||||
with self.assertRaisesRegex(UserError, f"{common}.*Company."):
|
||||
move.company_id = 666
|
||||
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
|
||||
with self.assertRaisesRegex(UserError, f"{common}(.*Company.*Date.)|(.*Date.*Company.)"):
|
||||
move.write({
|
||||
'company_id': 666,
|
||||
'date': fields.Date.from_string('2023-01-03')
|
||||
|
|
@ -45,211 +97,201 @@ class TestAccountMoveInalterableHash(AccountTestInvoicingCommon):
|
|||
|
||||
def test_account_move_hash_integrity_report(self):
|
||||
"""Test the hash integrity report"""
|
||||
moves = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
|
||||
)
|
||||
moves.action_post()
|
||||
moves = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-02', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-01', 'amounts': [1000, 2000]},
|
||||
])
|
||||
|
||||
# No records to be hashed because the restrict mode is not activated yet
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0] # First journal
|
||||
self.assertEqual(integrity_check['msg_cover'], 'This journal is not in strict mode.')
|
||||
for result in moves.company_id._check_hash_integrity()['results']:
|
||||
self.assertEqual(result['status'], 'no_data')
|
||||
|
||||
# No records to be hashed even if the restrict mode is activated because the hashing is not retroactive
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], 'There isn\'t any journal entry flagged for data inalterability yet for this journal.')
|
||||
|
||||
# Everything should be correctly hashed and verified
|
||||
new_moves = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-03", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_a, "2023-01-05", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-06", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_a, "2023-01-07", amounts=[1000, 2000])
|
||||
# First sequence
|
||||
first_chain_moves = moves | self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-05', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_a, 'date': '2023-01-04', 'amounts': [1000, 2000]}, # We don't care about the date order, just the sequence_prefix and sequence_number
|
||||
{'partner': self.partner_b, 'date': '2023-01-06', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_a, 'date': '2023-01-07', 'amounts': [1000, 2000]},
|
||||
])
|
||||
moves = first_chain_moves
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1])
|
||||
|
||||
# Second sequence
|
||||
second_chain_moves_first_move = self.init_invoice("out_invoice", self.partner_a, "2023-01-08", amounts=[1000, 2000])
|
||||
second_chain_moves_first_move.name = "XYZ/1"
|
||||
second_chain_moves_first_move.action_post()
|
||||
second_chain_moves = (
|
||||
second_chain_moves_first_move
|
||||
| self._init_and_post([
|
||||
{'partner': self.partner_b, 'date': '2023-01-09', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_a, 'date': '2023-01-12', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-11', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_a, 'date': '2023-01-12', 'amounts': [1000, 2000]},
|
||||
])
|
||||
)
|
||||
new_moves.action_post()
|
||||
moves |= new_moves
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*')
|
||||
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[2].date)))
|
||||
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
|
||||
|
||||
# First sequence again
|
||||
first_chain_moves_new_move = self.init_invoice("out_invoice", self.partner_a, "2023-01-08", amounts=[1000, 2000])
|
||||
first_chain_moves_new_move.name = first_chain_moves[-1].name[:-1] + str(int(first_chain_moves[-1].name[-1]) + 1)
|
||||
first_chain_moves_new_move.action_post()
|
||||
first_chain_moves |= first_chain_moves_new_move
|
||||
|
||||
# Verification of the two chains.
|
||||
moves = first_chain_moves | second_chain_moves
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", first_chain_moves[0], first_chain_moves[-1], 'INV/')
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", second_chain_moves[0], second_chain_moves[-1], 'XYZ/')
|
||||
|
||||
# Let's change one of the fields used by the hash. It should be detected by the integrity report.
|
||||
# We need to bypass the write method of account.move to do so.
|
||||
Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')
|
||||
date_hashed = first_chain_moves[3].date
|
||||
Model.write(first_chain_moves[3], {'date': fields.Date.from_string('2023-02-07')})
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {first_chain_moves[3].id}.*')
|
||||
|
||||
# Revert the previous change
|
||||
Model.write(moves[4], {'date': fields.Date.from_string("2023-01-05")})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*')
|
||||
Model.write(first_chain_moves[3], {'date': date_hashed})
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", first_chain_moves[0], first_chain_moves[-1], 'INV/')
|
||||
|
||||
# Let's try with the one of the subfields
|
||||
Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_b.id})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')
|
||||
Model.write(second_chain_moves[-1].line_ids[0], {'partner_id': self.partner_b.id})
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {second_chain_moves[-1].id}.*', prefix='XYZ/')
|
||||
|
||||
# Let's try with the inalterable_hash field itself
|
||||
Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_a.id}) # Revert the previous change
|
||||
Model.write(moves[-1], {'inalterable_hash': 'fake_hash'})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')
|
||||
Model.write(first_chain_moves[-1].line_ids[0], {'partner_id': self.partner_a.id}) # Revert the previous change
|
||||
Model.write(first_chain_moves[-1], {'inalterable_hash': 'fake_hash'})
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {first_chain_moves[-1].id}.*', prefix='INV/')
|
||||
|
||||
def test_account_move_hash_versioning_1(self):
|
||||
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
|
||||
This test focuses on the case where the user has only moves with the old hash algorithm."""
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
moves = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-02", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
|
||||
)
|
||||
moves.with_context(hash_version=1).action_post()
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
|
||||
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
|
||||
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
|
||||
secure_sequence = self._get_secure_sequence()
|
||||
moves = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-02', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-04', 'amounts': [1000, 2000]},
|
||||
], hash_version=1, secure_sequence=secure_sequence)
|
||||
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], prefix=moves[0].sequence_prefix)
|
||||
|
||||
# Let's change one of the fields used by the hash. It should be detected by the integrity report
|
||||
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
|
||||
# We need to bypass the write method of account.move to do so.
|
||||
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {moves[1].id}.*', prefix=moves[0].sequence_prefix)
|
||||
|
||||
def test_account_move_hash_versioning_2(self):
|
||||
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
|
||||
This test focuses on the case where the user has only moves with the new hash algorithm."""
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
moves = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
|
||||
)
|
||||
moves.action_post()
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
|
||||
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
|
||||
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
|
||||
secure_sequence = self._get_secure_sequence()
|
||||
moves = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-02', 'amounts': [1000, 2000]},
|
||||
], hash_version=2, secure_sequence=secure_sequence)
|
||||
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], prefix=moves[0].sequence_prefix)
|
||||
|
||||
# Let's change one of the fields used by the hash. It should be detected by the integrity report
|
||||
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
|
||||
# We need to bypass the write method of account.move to do so.
|
||||
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {moves[1].id}.*', prefix=moves[0].sequence_prefix)
|
||||
|
||||
def test_account_move_hash_versioning_v1_to_v2(self):
|
||||
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
|
||||
This test focuses on the case where the user has moves with both hash algorithms."""
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
moves_v1 = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
|
||||
)
|
||||
moves_v1.with_context(hash_version=1).action_post()
|
||||
secure_sequence = self._get_secure_sequence()
|
||||
moves_v1 = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-02', 'amounts': [1000, 2000]},
|
||||
], hash_version=1, secure_sequence=secure_sequence)
|
||||
|
||||
fields_v1 = moves_v1.with_context(hash_version=1)._get_integrity_hash_fields()
|
||||
moves_v2 = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
|
||||
)
|
||||
moves_v2.with_context(hash_version=2).action_post()
|
||||
moves_v2 = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-02', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-01', 'amounts': [1000, 2000]},
|
||||
], hash_version=2, secure_sequence=secure_sequence)
|
||||
|
||||
fields_v2 = moves_v2._get_integrity_hash_fields()
|
||||
self.assertNotEqual(fields_v1, fields_v2) # Make sure two different hash algorithms were used
|
||||
|
||||
moves = moves_v1 | moves_v2
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
|
||||
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
|
||||
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], prefix=moves[0].sequence_prefix)
|
||||
|
||||
# Let's change one of the fields used by the hash. It should be detected by the integrity report
|
||||
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
|
||||
# We need to bypass the write method of account.move to do so.
|
||||
date_hashed = moves[4].date
|
||||
Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {moves[4].id}.*', prefix=moves[0].sequence_prefix)
|
||||
|
||||
# Let's revert the change and make sure that we cannot use the v1 after the v2.
|
||||
# This means we don't simply check whether the move is correctly hashed with either algorithms,
|
||||
# but that we can only use v2 after v1 and not go back to v1 afterwards.
|
||||
Model.write(moves[4], {'date': fields.Date.from_string("2023-01-02")}) # Revert the previous change
|
||||
moves_v1_bis = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-10", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-11", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-12", amounts=[1000, 2000])
|
||||
)
|
||||
moves_v1_bis.with_context(hash_version=1).action_post()
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves_v1_bis[0].id}.')
|
||||
Model.write(moves[4], {'date': date_hashed}) # Revert the previous change
|
||||
moves_v1_bis = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-10', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-11', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-12', 'amounts': [1000, 2000]},
|
||||
], hash_version=1, secure_sequence=secure_sequence)
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {moves_v1_bis[0].id}.*', prefix=moves[0].sequence_prefix)
|
||||
|
||||
def test_account_move_hash_versioning_3(self):
|
||||
"""
|
||||
Version 2 does not take into account floating point representation issues.
|
||||
Test that version 3 covers correctly this case
|
||||
"""
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
|
||||
post=True) # Not hashed
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
moves_v3 = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[30*0.17, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
|
||||
)
|
||||
moves_v3.action_post()
|
||||
secure_sequence = self._get_secure_sequence()
|
||||
moves = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [30 * 0.17, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-02', 'amounts': [1000, 2000]},
|
||||
], hash_version=3, secure_sequence=secure_sequence)
|
||||
|
||||
# invalidate cache
|
||||
moves_v3[0].line_ids[0].invalidate_recordset()
|
||||
moves[0].line_ids[0].invalidate_recordset()
|
||||
|
||||
integrity_check_v3 = moves_v3.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertRegex(integrity_check_v3['msg_cover'], f'Entries are hashed from {moves_v3[0].name}.*')
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], prefix=moves[0].sequence_prefix)
|
||||
|
||||
def test_account_move_hash_versioning_v2_to_v3(self):
|
||||
"""
|
||||
We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
|
||||
This test focuses on the case with version 2 and version 3.
|
||||
"""
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
|
||||
post=True) # Not hashed
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
moves_v2 = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
|
||||
)
|
||||
moves_v2.with_context(hash_version=2).action_post()
|
||||
secure_sequence = self._get_secure_sequence()
|
||||
moves_v2 = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
], hash_version=2, secure_sequence=secure_sequence)
|
||||
|
||||
moves_v3 = (
|
||||
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
|
||||
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
|
||||
)
|
||||
moves_v3.with_context(hash_version=3).action_post()
|
||||
moves_v3 = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2023-01-02', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-01', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2023-01-03', 'amounts': [1000, 2000]},
|
||||
], hash_version=3, secure_sequence=secure_sequence)
|
||||
|
||||
moves = moves_v2 | moves_v3
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
|
||||
self.assertEqual(integrity_check['first_move_date'],
|
||||
format_date(self.env, fields.Date.to_string(moves[0].date)))
|
||||
self.assertEqual(integrity_check['last_move_date'],
|
||||
format_date(self.env, fields.Date.to_string(moves[-1].date)))
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], prefix=moves[0].sequence_prefix)
|
||||
|
||||
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
|
||||
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
|
||||
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {moves[1].id}.*', prefix=moves[0].sequence_prefix)
|
||||
|
||||
def test_account_move_hash_with_cash_rounding(self):
|
||||
# Enable inalterable hash
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
# Required for `invoice_cash_rounding_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('account.group_cash_rounding')
|
||||
self.env.user.group_ids += self.env.ref('account.group_cash_rounding')
|
||||
# Test 'add_invoice_line' rounding
|
||||
invoice = self.init_invoice('out_invoice', products=self.product_a+self.product_b)
|
||||
move_form = Form(invoice)
|
||||
|
|
@ -261,9 +303,538 @@ class TestAccountMoveInalterableHash(AccountTestInvoicingCommon):
|
|||
|
||||
# Should not raise
|
||||
invoice.action_post()
|
||||
invoice._generate_and_send(allow_fallback_pdf=False)
|
||||
|
||||
self.assertEqual(invoice.amount_total, 1410.0)
|
||||
self.assertEqual(invoice.amount_untaxed, 1200.0)
|
||||
self.assertEqual(invoice.amount_tax, 210)
|
||||
self.assertEqual(len(invoice.invoice_line_ids), 2)
|
||||
self.assertEqual(len(invoice.line_ids), 6)
|
||||
|
||||
def test_retroactive_hashing(self):
|
||||
"""The hash should be retroactive even to moves that were created before the restrict mode was activated."""
|
||||
move1 = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
self.assertFalse(move1.inalterable_hash)
|
||||
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
|
||||
move2 = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
|
||||
self.assertNotEqual(move2.inalterable_hash, False)
|
||||
self.assertNotEqual(move1.inalterable_hash, False)
|
||||
self._verify_integrity(move1 | move2, "Entries are correctly hashed", move1, move2)
|
||||
|
||||
def test_retroactive_hashing_backwards_compatibility(self):
|
||||
"""
|
||||
Simulate old version where the hash was not retroactive
|
||||
We should not consider these moves now either
|
||||
We should hash after the last moved hashed
|
||||
"""
|
||||
move1 = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
|
||||
# Posting a new move also posts the previous move (move1)
|
||||
self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
|
||||
Model.write(move1, {'inalterable_hash': False, 'secure_sequence_number': 0})
|
||||
|
||||
# The following should only compute the hash for move3, not move1 (move2 is already hashed)
|
||||
move3 = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
|
||||
self.assertNotEqual(move3.inalterable_hash, False)
|
||||
self.assertFalse(move1.inalterable_hash)
|
||||
|
||||
def test_no_hash_if_hole_in_sequence(self):
|
||||
"""If there is a hole in the sequence, we should not hash the moves"""
|
||||
move1 = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
move2 = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
move3 = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
|
||||
move2.button_draft() # Create hole in the middle of unhashed chain [move1, move2, move3]
|
||||
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
|
||||
with self.assertRaisesRegex(UserError, "An error occurred when computing the inalterability. A gap has been detected in the sequence."):
|
||||
move3.button_hash()
|
||||
|
||||
move1.button_hash() # Afterwards move2 is a hole at the beginning of the unhashed part of the chain [move1 (hashed), move2, move3]
|
||||
with self.assertRaisesRegex(UserError, "An error occurred when computing the inalterability. A gap has been detected in the sequence."):
|
||||
move3.button_hash()
|
||||
|
||||
with self._skip_hash_moves():
|
||||
move2.action_post()
|
||||
move3.button_hash() # Shouldn't raise
|
||||
|
||||
for move in (move1, move2, move3):
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
|
||||
with self._skip_hash_moves():
|
||||
move4 = self._reverse_move(move1)
|
||||
move5 = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=False)
|
||||
move5.action_post()
|
||||
|
||||
move6 = self._reverse_move(move2)
|
||||
move7 = self._reverse_move(move3)
|
||||
|
||||
self._verify_integrity(move1, "Entries are correctly hashed", move1, move3)
|
||||
|
||||
for move in (move4, move6, move7):
|
||||
self.assertFalse(move.inalterable_hash)
|
||||
|
||||
move7.button_hash() # Shouldn't raise, no sequence hole if we have a mix of invoices and credit notes
|
||||
self.assertFalse(move5.inalterable_hash) # move5 has another sequence_prefix, so not hashed here
|
||||
for move in (move4, move6, move7):
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
|
||||
move8 = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
for move in (move5 | move8):
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
|
||||
moves = (move1 | move2 | move3 | move4 | move5 | move6 | move7 | move8)
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", move1, move8, prefix='INV/')
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", move4, move7, prefix='RINV/')
|
||||
|
||||
def test_retroactive_hash_vendor_bills(self):
|
||||
"""The hash should be retroactive even to vendor bills that were created before the restrict mode was activated."""
|
||||
move1 = self.init_invoice("in_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
|
||||
self.company_data['default_journal_purchase'].restrict_mode_hash_table = True
|
||||
|
||||
move2 = self.init_invoice("in_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
|
||||
# We should hash vendor bills on post
|
||||
self.assertNotEqual(move1.inalterable_hash, False)
|
||||
self.assertNotEqual(move2.inalterable_hash, False)
|
||||
self._verify_integrity(move1 | move2, "Entries are correctly hashed", move1, move2)
|
||||
|
||||
def test_retroactive_hash_multiple_journals(self):
|
||||
"""If we have a recordset of moves in different journals, all of them should be hashed
|
||||
in a way that respects the journal to which they belong"""
|
||||
journal_sale2 = self.env['account.journal'].create({
|
||||
'name': 'Sale Journal 2',
|
||||
'type': 'sale',
|
||||
'code': 'SJ2',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
move1 = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'journal_id': journal_sale2.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'date': '2024-01-01',
|
||||
'invoice_line_ids': [Command.create({
|
||||
'name': 'test',
|
||||
'quantity': 1,
|
||||
'price_unit': 1000,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
})],
|
||||
})
|
||||
move1.action_post()
|
||||
self.assertFalse(move1.inalterable_hash)
|
||||
|
||||
move2 = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
self.assertFalse(move2.inalterable_hash)
|
||||
|
||||
move3 = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'journal_id': journal_sale2.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'date': '2024-01-01',
|
||||
'invoice_line_ids': [Command.create({
|
||||
'name': 'test',
|
||||
'quantity': 1,
|
||||
'price_unit': 1000,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
})],
|
||||
})
|
||||
move3.action_post()
|
||||
self.assertFalse(move3.inalterable_hash)
|
||||
|
||||
move4 = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
self.assertFalse(move4.inalterable_hash)
|
||||
|
||||
moves = move1 | move2 | move3 | move4
|
||||
journal_sale2.restrict_mode_hash_table = True
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
moves.button_hash()
|
||||
|
||||
for move in moves:
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", move1, move3, prefix=move1.sequence_prefix)
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", move2, move4, prefix=move2.sequence_prefix)
|
||||
|
||||
def test_hash_multiyear(self):
|
||||
"""Test that we can hash entries from different fiscal years"""
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
move1 = self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True)
|
||||
move2 = self.init_invoice("out_invoice", self.partner_a, "2023-01-03", amounts=[1000], post=True)
|
||||
move3 = self.init_invoice("out_invoice", self.partner_a, "2023-01-02", amounts=[1000], post=True)
|
||||
|
||||
move4 = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
move5 = self.init_invoice("out_invoice", self.partner_a, "2024-01-03", amounts=[1000], post=True)
|
||||
move6 = self.init_invoice("out_invoice", self.partner_a, "2024-01-04", amounts=[1000], post=True)
|
||||
|
||||
moves = move1 | move2 | move3 | move4 | move5 | move6
|
||||
for move in moves:
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", move1, move3, prefix=move1.sequence_prefix)
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", move4, move6, prefix=move4.sequence_prefix)
|
||||
|
||||
def test_hash_on_lock_date(self):
|
||||
"""
|
||||
The lock date and hashing should not interfere with each other.
|
||||
* We should be able to hash moves protected by a lock date.
|
||||
* We should be able to lock a period containing unhashed moves.
|
||||
"""
|
||||
for lock_date_field in [
|
||||
'hard_lock_date',
|
||||
'fiscalyear_lock_date',
|
||||
'sale_lock_date',
|
||||
]:
|
||||
with self.subTest(lock_date_field=lock_date_field), closing(self.env.cr.savepoint()):
|
||||
move1 = self.init_invoice('out_invoice', self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
move2 = self.init_invoice('out_invoice', self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
move3 = self.init_invoice('out_invoice', self.partner_a, "2024-01-03", amounts=[1000], post=True)
|
||||
move4 = self.init_invoice('out_invoice', self.partner_a, "2024-02-01", amounts=[1000], post=True)
|
||||
move5 = self.init_invoice('out_invoice', self.partner_a, "2024-02-01", amounts=[1000], post=True)
|
||||
|
||||
for move in (move1, move2, move3, move4, move5):
|
||||
self.assertFalse(move.inalterable_hash)
|
||||
|
||||
# Shouldn't raise (case no moves have ever been hashed)
|
||||
self.company_data['company'][lock_date_field] = fields.Date.to_date('2024-01-31')
|
||||
|
||||
# Let's has just one and revert the lock date
|
||||
if lock_date_field == 'hard_lock_date':
|
||||
def _validate_locks(*args, **kwargs):
|
||||
pass
|
||||
|
||||
with patch('odoo.addons.account.models.company.ResCompany._validate_locks', new=_validate_locks):
|
||||
self.company_data['company'][lock_date_field] = False
|
||||
else:
|
||||
self.company_data['company'][lock_date_field] = False
|
||||
move1.button_hash()
|
||||
|
||||
# We should be able to set the lock date (case there are hashed moves)
|
||||
self.company_data['company'][lock_date_field] = fields.Date.to_date('2024-01-31')
|
||||
|
||||
for move in (move2, move3, move4, move5):
|
||||
self.assertFalse(move.inalterable_hash)
|
||||
|
||||
# We should be able to hash the moves despite the lock date
|
||||
move5.button_hash()
|
||||
for move in (move1, move2, move3, move4, move5):
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True # to run integrity check
|
||||
self._verify_integrity(move5, "Entries are correctly hashed", move1, move5)
|
||||
|
||||
def test_retroactive_hashing_before_current(self):
|
||||
"""Test that we hash entries before the current recordset of moves, not the ones after"""
|
||||
move1 = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000], post=True)
|
||||
move2 = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000], post=True)
|
||||
move3 = self.init_invoice("out_invoice", self.partner_a, "2024-01-03", amounts=[1000], post=True)
|
||||
|
||||
move4 = self.init_invoice("out_invoice", self.partner_a, "2024-01-04", amounts=[1000], post=True)
|
||||
move5 = self.init_invoice("out_invoice", self.partner_a, "2024-01-05", amounts=[1000], post=True)
|
||||
move6 = self.init_invoice("out_invoice", self.partner_a, "2024-01-06", amounts=[1000], post=True)
|
||||
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
move3.button_hash()
|
||||
|
||||
for move in (move1, move2, move3):
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
for move in (move4, move5, move6):
|
||||
self.assertFalse(move.inalterable_hash)
|
||||
|
||||
self._verify_integrity(move1, "Entries are correctly hashed", move1, move3)
|
||||
move6.button_hash()
|
||||
self._verify_integrity(move1, "Entries are correctly hashed", move1, move6)
|
||||
|
||||
def test_account_move_hash_versioning_v3_to_v4(self):
|
||||
"""
|
||||
We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
|
||||
This test focuses on the case with version 3 and version 4.
|
||||
"""
|
||||
# Let's simulate v3 where the hash was on post and not retroactive
|
||||
# First let's create some moves that shouldn't be hashed (before restrict mode)
|
||||
secure_sequence = self._get_secure_sequence()
|
||||
moves_v3_pre_restrict_mode = self.env['account.move']
|
||||
for _ in range(3):
|
||||
moves_v3_pre_restrict_mode |= self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000, 2000], post=True)
|
||||
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
|
||||
# Now create some moves in v3 that should be hashed on post and have a secure_sequence_id
|
||||
moves_v3_post_restrict_mode = self.env['account.move']
|
||||
last_hash = ""
|
||||
for _ in range(3):
|
||||
move = self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000, 2000], post=False)
|
||||
with self._skip_hash_moves():
|
||||
move.action_post()
|
||||
move.inalterable_hash = move.with_context(hash_version=3)._calculate_hashes(last_hash)[move]
|
||||
last_hash = move.inalterable_hash
|
||||
move.secure_sequence_number = secure_sequence.next_by_id()
|
||||
moves_v3_post_restrict_mode |= move
|
||||
|
||||
moves_v3 = moves_v3_pre_restrict_mode | moves_v3_post_restrict_mode
|
||||
|
||||
# Use v4 now
|
||||
moves_v4 = self._init_and_post([
|
||||
{'partner': self.partner_a, 'date': '2024-01-02', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2024-01-01', 'amounts': [1000, 2000]},
|
||||
{'partner': self.partner_b, 'date': '2024-01-03', 'amounts': [1000, 2000]},
|
||||
]) # Default hash version is 4
|
||||
|
||||
# Don't ever allow to hash moves_v3_pre_restrict_mode
|
||||
moves_v3_pre_restrict_mode[-1]._hash_moves(raise_if_no_document=False) # Shouldn't raise
|
||||
self.assertFalse(moves_v3_pre_restrict_mode[-1].inalterable_hash)
|
||||
|
||||
with self.assertRaisesRegex(UserError, "This move could not be locked either because.*"):
|
||||
moves_v3_pre_restrict_mode.button_hash()
|
||||
|
||||
# Test that we allow holes that are not in the moves_to_hash and
|
||||
moves_v3_pre_restrict_mode[2].button_draft() # Create hole in sequence
|
||||
moves_v4 |= self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000, 2000], post=True)
|
||||
with self._skip_hash_moves():
|
||||
moves_v3_pre_restrict_mode[2].action_post() # Revert
|
||||
|
||||
# Check lock date, shouldn't raise even if there are no documents to hash
|
||||
self.company_data['company'].fiscalyear_lock_date = fields.Date.to_date('2024-01-31')
|
||||
|
||||
# We should have something like (mix of v3 and v4):
|
||||
# Name | Secure Sequence Number | Inalterable Hash
|
||||
# --------------|---------------------------|------------------
|
||||
# ### V3: Restricted mode not activated yet
|
||||
# INV/2024/1 | 0 | False
|
||||
# INV/2024/2 | 0 | False
|
||||
# INV/2024/3 | 0 | False
|
||||
# ### V3: Restricted mode activated + hash on post
|
||||
# INV/2024/4 | 1 | 87ba4c8...
|
||||
# INV/2024/5 | 2 | 09al8if...
|
||||
# INV/2024/6 | 3 | 0a9f8a9...
|
||||
# ### V4: No secure_sequence_number, hash retroactively on send&print (17.2 to 17.4) or post (17.5+)
|
||||
# INV/2024/7 | 0 | $4$aj98na1...
|
||||
# INV/2024/8 | 0 | $4$9177iai...
|
||||
# INV/2024/9 | 0 | $4$nwy7ao9
|
||||
# ### INV/2024/1, INV/2024/2, INV/2024/3 should not be hashed
|
||||
|
||||
for move in moves_v3_pre_restrict_mode:
|
||||
self.assertFalse(move.inalterable_hash)
|
||||
self.assertFalse(move.secure_sequence_number)
|
||||
|
||||
for move in moves_v3_post_restrict_mode:
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
self.assertNotEqual(move.secure_sequence_number, False)
|
||||
|
||||
for move in moves_v4:
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
self.assertFalse(move.secure_sequence_number)
|
||||
|
||||
moves = moves_v3 | moves_v4
|
||||
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves_v3_post_restrict_mode[0], moves[-1], prefix=moves[0].sequence_prefix)
|
||||
|
||||
Model.write(moves_v3_post_restrict_mode[1], {'date': fields.Date.from_string('2024-11-07')})
|
||||
self._verify_integrity(moves, f'Corrupted data on journal entry with id {moves_v3_post_restrict_mode[1].id}.*', prefix=moves[0].sequence_prefix)
|
||||
|
||||
def test_inalterable_hash_verification_by_batches(self):
|
||||
"""Test that the integrity report can handle a large amount of entries by
|
||||
verifying the integrity by batches."""
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
moves = self.env['account.move']
|
||||
for _ in range(10):
|
||||
moves |= self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000, 2000], post=True)
|
||||
|
||||
for move in moves:
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
|
||||
with patch('odoo.addons.account.models.company.INTEGRITY_HASH_BATCH_SIZE', 3):
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], moves[0].journal_id.name)
|
||||
|
||||
with patch('odoo.addons.account.models.company.INTEGRITY_HASH_BATCH_SIZE', 5):
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], moves[0].journal_id.name)
|
||||
|
||||
with patch('odoo.addons.account.models.company.INTEGRITY_HASH_BATCH_SIZE', 10):
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], moves[0].journal_id.name)
|
||||
|
||||
with patch('odoo.addons.account.models.company.INTEGRITY_HASH_BATCH_SIZE', 12):
|
||||
self._verify_integrity(moves, "Entries are correctly hashed", moves[0], moves[-1], moves[0].journal_id.name)
|
||||
|
||||
def test_error_on_unreconciled_bank_statement_lines(self):
|
||||
"""
|
||||
Check that an error is raised when we try to hash entries with unreconciled bank statement lines.
|
||||
"""
|
||||
unreconciled_bank_statement_line = self.env['account.bank.statement.line'].create({
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'date': '2017-01-01',
|
||||
'payment_ref': '2017_unreconciled',
|
||||
'amount': 10.0,
|
||||
})
|
||||
unreconciled_move = unreconciled_bank_statement_line.move_id
|
||||
unreconciled_move.journal_id.restrict_mode_hash_table = True
|
||||
with self.assertRaisesRegex(UserError, "An error occurred when computing the inalterability. All entries have to be reconciled."):
|
||||
unreconciled_move.button_hash()
|
||||
|
||||
def test_account_move_unhashed_entries(self):
|
||||
"""
|
||||
Test that when _get_chain_info is called with early_stop=True (e.g., when checking if a journal has unhashed
|
||||
entries), no error is raised and the right value is returned based on whether there are unhashed documents.
|
||||
"""
|
||||
sales_journal = self.company_data['default_journal_sale']
|
||||
# Create a move before the journal is set to 'Hash on post', allowing to test if the journal has unhashed entries.
|
||||
self._init_and_post([{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [1000]}])
|
||||
sales_journal.restrict_mode_hash_table = True
|
||||
# There should be unhashed entries in the sales journal until another move is posted
|
||||
self.assertTrue(sales_journal._get_moves_to_hash(include_pre_last_hash=False, early_stop=True))
|
||||
self._init_and_post([{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [1000]}])
|
||||
# After posting one entry, sales journal shouldn't have unhashed entries
|
||||
self.assertFalse(sales_journal._get_moves_to_hash(include_pre_last_hash=False, early_stop=True))
|
||||
|
||||
def test_account_group_account_secured(self):
|
||||
"""
|
||||
Test that user is not granted the group account secured if only entries from a journal without 'Hash on Post' is
|
||||
secured. Once entries from a journal without 'Hash on Post' are secured, the user is granted the access rights.
|
||||
"""
|
||||
group_account_secured = self.env.ref('account.group_account_secured')
|
||||
# `group_account_secured` can be by default in user groups (e.g. l10n_de)
|
||||
group_account_secured_in_user_groups = group_account_secured in self.env.user.group_ids.all_implied_ids
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
move = self._init_and_post([{'partner': self.partner_a, 'date': '2023-01-01', 'amounts': [1000]}])
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
# Unless `group_account_secured` was by default in user groups, user shouldn't be granted access rights since
|
||||
# only moves from a journal with 'Hash on Post' have been secured
|
||||
if not group_account_secured_in_user_groups:
|
||||
self.assertFalse(group_account_secured in self.env.user.group_ids.all_implied_ids)
|
||||
|
||||
# Once moves from a journal without 'Hash on Post' is secured, user should be granted secured group access rights
|
||||
in_invoice = self.init_invoice("in_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True)
|
||||
wizard = self.env['account.secure.entries.wizard'].create({'hash_date': '2023-01-02'})
|
||||
wizard.action_secure_entries()
|
||||
self.assertNotEqual(in_invoice.inalterable_hash, False)
|
||||
self.assertTrue(group_account_secured in self.env.user.group_ids.all_implied_ids)
|
||||
|
||||
def test_wizard_hashes_all_journals(self):
|
||||
"""
|
||||
Test that the wizard hashes all journals.
|
||||
* Regardless of the `restrict_mode_hash_table` setting on the journal.
|
||||
* Regardless of the lock date
|
||||
"""
|
||||
moves = self.env['account.move'].create([
|
||||
{
|
||||
'date': '2023-01-02',
|
||||
'journal_id': self.env['account.journal'].create({
|
||||
'code': f'wiz{idx}',
|
||||
'name': f'Wizard {journal_type}',
|
||||
'type': journal_type,
|
||||
}).id,
|
||||
'line_ids': [Command.create({
|
||||
'name': 'test',
|
||||
'quantity': 1,
|
||||
'balance': 0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
})],
|
||||
} for idx, journal_type in enumerate(('sale', 'purchase', 'cash', 'bank', 'credit', 'general'))
|
||||
])
|
||||
moves.action_post()
|
||||
self.company_data['company'].hard_lock_date = '2023-01-02'
|
||||
wizard = self.env['account.secure.entries.wizard'].create({'hash_date': '2023-01-02'})
|
||||
wizard.action_secure_entries()
|
||||
self.assertTrue(False not in moves.mapped('inalterable_hash'))
|
||||
|
||||
def test_wizard_ignores_sequence_prefixes_with_unreconciled_entries(self):
|
||||
"""
|
||||
Test that the wizard does not try to hash sequence prefixes containing unreconciled bank statement lines.
|
||||
But it should still hash the remaining sequence prefixes from the same journal.
|
||||
"""
|
||||
# Create 2 reconciled moves from different sequences
|
||||
reconciled_bank_statement_line_2016 = self.env['account.bank.statement.line'].create({
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'date': '2016-01-01',
|
||||
'payment_ref': '2016_reconciled',
|
||||
'amount': 0.0, # reconciled
|
||||
})
|
||||
reconciled_bank_statement_line_2017 = self.env['account.bank.statement.line'].create({
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'date': '2017-01-01',
|
||||
'payment_ref': '2017_reconciled',
|
||||
'amount': 0.0, # reconciled
|
||||
})
|
||||
|
||||
wizard = self.env['account.secure.entries.wizard'].create({'hash_date': '2017-01-01'})
|
||||
reconciled_bank_statement_lines = reconciled_bank_statement_line_2016 | reconciled_bank_statement_line_2017
|
||||
self.assertFalse(wizard.unreconciled_bank_statement_line_ids)
|
||||
self.assertEqual(wizard.move_to_hash_ids, reconciled_bank_statement_lines.move_id)
|
||||
|
||||
# Create an unreconciled move for the 2017 prefix
|
||||
unreconciled_bank_statement_line_2017 = self.env['account.bank.statement.line'].create({
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'date': '2017-01-01',
|
||||
'payment_ref': '2017_unreconciled',
|
||||
'amount': 10.0,
|
||||
})
|
||||
|
||||
wizard = self.env['account.secure.entries.wizard'].create({'hash_date': '2017-01-01'})
|
||||
self.assertEqual(wizard.unreconciled_bank_statement_line_ids, unreconciled_bank_statement_line_2017)
|
||||
self.assertEqual(wizard.move_to_hash_ids, reconciled_bank_statement_line_2016.move_id)
|
||||
|
||||
def test_wizard_backwards_compatibility(self):
|
||||
"""
|
||||
The wizard was introduced in odoo 17.5 when the hash version was 4.
|
||||
We check that:
|
||||
* We do not hash unhashed moves before the start of the hash sequence
|
||||
* The wizard displays information about the date of the first unhashed move:
|
||||
This excludes moves before the hard lock date.
|
||||
"""
|
||||
# Let's simulate v3 where the hash was on post and not retroactive
|
||||
# First let's create some moves that shouldn't be hashed (before restrict mode)
|
||||
secure_sequence = self._get_secure_sequence()
|
||||
moves_v3_pre_restrict_mode = self.env['account.move']
|
||||
for _ in range(3):
|
||||
moves_v3_pre_restrict_mode |= self.init_invoice("out_invoice", self.partner_a, "2024-01-01", amounts=[1000, 2000], post=True)
|
||||
|
||||
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
|
||||
# Now create some moves in v3 that should be hashed on post and have a secure_sequence_id
|
||||
moves_v3_post_restrict_mode = self.env['account.move']
|
||||
last_hash = ""
|
||||
for _ in range(3):
|
||||
move = self.init_invoice("out_invoice", self.partner_a, "2024-01-02", amounts=[1000, 2000], post=False)
|
||||
with self._skip_hash_moves():
|
||||
move.action_post()
|
||||
move.inalterable_hash = move.with_context(hash_version=3)._calculate_hashes(last_hash)[move]
|
||||
last_hash = move.inalterable_hash
|
||||
move.secure_sequence_number = secure_sequence.next_by_id()
|
||||
moves_v3_post_restrict_mode |= move
|
||||
|
||||
moves_v4 = self.init_invoice("out_invoice", self.partner_a, "2024-01-03", amounts=[1000], post=False)
|
||||
moves_v4 |= self.init_invoice("out_invoice", self.partner_a, "2024-01-03", amounts=[1000], post=False)
|
||||
moves_v4 |= self.init_invoice("out_invoice", self.partner_a, "2024-01-03", amounts=[1000], post=False)
|
||||
with self._skip_hash_moves():
|
||||
moves_v4.action_post()
|
||||
|
||||
for move in moves_v3_pre_restrict_mode | moves_v4:
|
||||
self.assertFalse(move.inalterable_hash)
|
||||
|
||||
# We cannot hash the moves_v3_pre_restrict_mode because the moves_v3_post_restrict_mode are hashed
|
||||
wizard = self.env['account.secure.entries.wizard'].create({'hash_date': '2024-01-03'})
|
||||
self.assertEqual(wizard.not_hashable_unlocked_move_ids, moves_v3_pre_restrict_mode)
|
||||
self.assertEqual(wizard.move_to_hash_ids, moves_v4)
|
||||
|
||||
# We can still hash the remaining moves
|
||||
with self.subTest(msg="Hash the remaining moves"), closing(self.env.cr.savepoint()):
|
||||
wizard.action_secure_entries()
|
||||
for move in moves_v3_pre_restrict_mode:
|
||||
self.assertFalse(move.inalterable_hash)
|
||||
for move in moves_v4:
|
||||
self.assertNotEqual(move.inalterable_hash, False)
|
||||
|
||||
# We can ignore the moves by setting the hard lock date:
|
||||
self.assertEqual(wizard.max_hash_date, fields.Date.from_string("2023-12-31"))
|
||||
self.company_data['company'].hard_lock_date = "2024-01-01"
|
||||
# There is nothing to hash
|
||||
wizard = self.env['account.secure.entries.wizard'].create({'hash_date': '2024-01-03'})
|
||||
self.assertEqual(wizard.max_hash_date, fields.Date.from_string("2024-01-02"))
|
||||
self.assertFalse(wizard.not_hashable_unlocked_move_ids)
|
||||
self.assertEqual(wizard.move_to_hash_ids, moves_v4)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,29 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo import fields
|
||||
from odoo import Command, fields
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
pack_of_six = cls.env['uom.uom'].search([('name', '=', 'Pack of 6')])
|
||||
cls.invoices = cls.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2016-01-01'),
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'currency_id': cls.other_currency.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 3,
|
||||
'price_unit': 750,
|
||||
'price_unit': 4500,
|
||||
'product_uom_id': pack_of_six.id,
|
||||
}),
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 3000,
|
||||
|
|
@ -33,9 +36,9 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
|||
{
|
||||
'move_type': 'out_receipt',
|
||||
'invoice_date': fields.Date.from_string('2016-01-01'),
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'currency_id': cls.other_currency.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 6000,
|
||||
|
|
@ -46,22 +49,28 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
|||
'move_type': 'out_refund',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2017-01-01'),
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'currency_id': cls.other_currency.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 1200,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 3,
|
||||
'price_unit': 4500,
|
||||
'product_uom_id': pack_of_six.id,
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2016-01-01'),
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'currency_id': cls.other_currency.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 60,
|
||||
|
|
@ -72,9 +81,9 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
|||
'move_type': 'in_receipt',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2016-01-01'),
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'currency_id': cls.other_currency.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 60,
|
||||
|
|
@ -85,15 +94,28 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
|||
'move_type': 'in_refund',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2017-01-01'),
|
||||
'currency_id': cls.currency_data['currency'].id,
|
||||
'currency_id': cls.other_currency.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 12,
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
'move_type': 'out_refund',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2017-01-01'),
|
||||
'currency_id': cls.other_currency.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 2400,
|
||||
}),
|
||||
]
|
||||
},
|
||||
])
|
||||
|
||||
def assertInvoiceReportValues(self, expected_values_list):
|
||||
|
|
@ -102,20 +124,66 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
|||
'price_average': vals[0],
|
||||
'price_subtotal': vals[1],
|
||||
'quantity': vals[2],
|
||||
'price_margin': vals[3],
|
||||
'inventory_value': vals[4],
|
||||
} for vals in expected_values_list]
|
||||
|
||||
self.assertRecordValues(reports, expected_values_dict)
|
||||
|
||||
def test_invoice_report_multiple_types(self):
|
||||
"""
|
||||
Each line represent an invoice line
|
||||
First and last lines use Packagings. Quantity and price from the invoice are adapted
|
||||
to the standard UoM of the product.
|
||||
|
||||
quantity is quantity in product_uom
|
||||
price_subtotal = Price_unit * Number_of_packages / currency_rate
|
||||
price_average = price_subtotal / quantity
|
||||
inventory_value = quantity * standard_price * (-1 OR 1 depending of move_type)
|
||||
price_margin = (price_average - standard_price) * quantity
|
||||
|
||||
E.g. first line:
|
||||
quantity : 6 * 3 = 18
|
||||
price_subtotal = 4500 * 3 / 3 = 4500
|
||||
price_average = 4500 / 18 = 250
|
||||
inventory_value = 800*18*-1 = -14400
|
||||
price_margin = (250 - 800) * 18 = -9900
|
||||
"""
|
||||
self.assertInvoiceReportValues([
|
||||
#price_average price_subtotal quantity
|
||||
[2000, 2000, 1],
|
||||
[1000, 1000, 1],
|
||||
[250, 750, 3],
|
||||
[6, 6, 1],
|
||||
[20, -20, -1],
|
||||
[20, -20, -1],
|
||||
[600, -600, -1],
|
||||
# pylint: disable=bad-whitespace
|
||||
# price_average, price_subtotal, quantity, price_margin, inventory_value
|
||||
[ 250, 4500, 18, -9900, -14400], # price_unit = 4500, currency.rate = 3.0
|
||||
[ 2000, 2000, 1, 1200, -800], # price_unit = 6000, currency.rate = 3.0
|
||||
[ 1000, 1000, 1, 200, -800], # price_unit = 3000, currency.rate = 3.0
|
||||
[ 6, 6, 1, 0, -800], # price_unit = 12, currency.rate = 2.0
|
||||
[ 20, -20, -1, 0, 800], # price_unit = 60, currency.rate = 3.0
|
||||
[ 20, -20, -1, 0, 800], # price_unit = 60, currency.rate = 3.0
|
||||
[ 600, -600, -1, 200, 800], # price_unit = 1200, currency.rate = 2.0
|
||||
[ 1200, -1200, -1, -400, 800], # price_unit = 2400, currency.rate = 2.0
|
||||
[ 375, -6750, -18, 7650, 14400], # price_unit = 4500, currency.rate = 2.0
|
||||
])
|
||||
|
||||
def test_invoice_report_multicompany_product_cost(self):
|
||||
"""
|
||||
In a multicompany environment, if you define one product with different standard price per company
|
||||
the invoice analysis report should only display the product from the company
|
||||
Standard Price in Company A: 800 (default setup)
|
||||
Standard Price in Company B: 700
|
||||
-> invoice report for Company A should remain the same
|
||||
"""
|
||||
self.product_a.with_company(self.company_data_2.get('company')).write({'standard_price': 700.0})
|
||||
self.assertInvoiceReportValues([
|
||||
# pylint: disable=bad-whitespace
|
||||
# price_average, price_subtotal, quantity, price_margin, inventory_value
|
||||
[ 250, 4500, 18, -9900, -14400], # price_unit = 4500, currency.rate = 3.0
|
||||
[ 2000, 2000, 1, 1200, -800], # price_unit = 6000, currency.rate = 3.0
|
||||
[ 1000, 1000, 1, 200, -800], # price_unit = 3000, currency.rate = 3.0
|
||||
[ 6, 6, 1, 0, -800], # price_unit = 12, currency.rate = 2.0
|
||||
[ 20, -20, -1, 0, 800], # price_unit = 60, currency.rate = 3.0
|
||||
[ 20, -20, -1, 0, 800], # price_unit = 60, currency.rate = 3.0
|
||||
[ 600, -600, -1, 200, 800], # price_unit = 1200, currency.rate = 2.0
|
||||
[ 1200, -1200, -1, -400, 800], # price_unit = 2400, currency.rate = 2.0
|
||||
[ 375, -6750, -18, 7650, 14400], # price_unit = 4500, currency.rate = 2.0
|
||||
])
|
||||
|
||||
def test_avg_price_calculation(self):
|
||||
|
|
@ -155,19 +223,125 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
|||
})
|
||||
invoice.action_post()
|
||||
|
||||
report = self.env['account.invoice.report'].read_group(
|
||||
report = self.env['account.invoice.report'].formatted_read_group(
|
||||
[('product_id', '=', product.id)],
|
||||
[],
|
||||
['price_subtotal:sum', 'quantity:sum', 'price_average:avg'],
|
||||
[],
|
||||
)
|
||||
self.assertEqual(report[0]['quantity'], 35)
|
||||
self.assertEqual(report[0]['price_subtotal'], 165)
|
||||
self.assertEqual(round(report[0]['price_average'], 2), 4.71)
|
||||
self.assertEqual(report[0]['quantity:sum'], 35)
|
||||
self.assertEqual(report[0]['price_subtotal:sum'], 165)
|
||||
self.assertEqual(round(report[0]['price_average:avg'], 2), 4.71)
|
||||
|
||||
# ensure that it works with only 'price_average:avg' in fields
|
||||
report = self.env['account.invoice.report'].read_group(
|
||||
# ensure that it works with only 'price_average:avg' in aggregates
|
||||
report = self.env['account.invoice.report'].formatted_read_group(
|
||||
[('product_id', '=', product.id)],
|
||||
['price_average:avg'],
|
||||
[],
|
||||
['price_average:avg'],
|
||||
)
|
||||
self.assertEqual(round(report[0]['price_average'], 2), 4.71)
|
||||
self.assertEqual(round(report[0]['price_average:avg'], 2), 4.71)
|
||||
|
||||
def test_avg_price_group_by_month(self):
|
||||
"""
|
||||
Check that the average is correctly calculated based on the total price and quantity
|
||||
with multiple invoices and group by month:
|
||||
Invoice 1:
|
||||
2 lines:
|
||||
- 10 units * 10$
|
||||
- 5 units * 5$
|
||||
Total quantity: 15
|
||||
Total price: 125$
|
||||
Average: 125 / 15 = 8.33
|
||||
Invoice 2:
|
||||
1 line:
|
||||
- 0 units * 5$
|
||||
Total quantity: 0
|
||||
Total price: 0$
|
||||
Average: 0.00
|
||||
"""
|
||||
self.env['account.move'].search([]).unlink()
|
||||
invoices = self.env["account.move"].create([
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2025-01-01'),
|
||||
'currency_id': self.env.company.currency_id.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 10,
|
||||
'price_unit': 10,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 5,
|
||||
'price_unit': 5,
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2025-02-01'),
|
||||
'currency_id': self.env.company.currency_id.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 0,
|
||||
'price_unit': 5,
|
||||
}),
|
||||
]
|
||||
},
|
||||
])
|
||||
invoices.action_post()
|
||||
|
||||
report = self.env['account.invoice.report'].formatted_read_group(
|
||||
[('product_id', '=', self.product_a.id)],
|
||||
['invoice_date:month'],
|
||||
['__count', 'price_subtotal:sum', 'quantity:sum', 'price_average:avg'],
|
||||
)
|
||||
|
||||
self.assertEqual(report[0]['__count'], 2)
|
||||
self.assertEqual(report[0]['quantity:sum'], 15.0)
|
||||
self.assertEqual(report[0]['price_subtotal:sum'], 125.0)
|
||||
self.assertEqual(round(report[0]['price_average:avg'], 2), 8.33)
|
||||
|
||||
self.assertEqual(report[1]['__count'], 1)
|
||||
self.assertEqual(report[1]['quantity:sum'], 0.0)
|
||||
self.assertEqual(report[1]['price_subtotal:sum'], 0.0)
|
||||
self.assertEqual(report[1]['price_average:avg'], 0.00)
|
||||
|
||||
def test_inventory_margin_currency(self):
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 750,
|
||||
}),
|
||||
],
|
||||
})
|
||||
egy_company = self.env['res.company'].create({
|
||||
'name': 'Egyptian Company',
|
||||
'currency_id': self.env.ref('base.EGP').id,
|
||||
'user_ids': [Command.set(self.env.user.ids)],
|
||||
})
|
||||
orig_company = self.env.company
|
||||
report = self.env['account.invoice.report'].search(
|
||||
[('move_id', '=', invoice.id)],
|
||||
)
|
||||
self.assertEqual(report.inventory_value, -800)
|
||||
self.assertEqual(report.price_margin, -50)
|
||||
self.env.user.company_id = egy_company
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2017-11-03',
|
||||
'rate': 0.5,
|
||||
'currency_id': orig_company.currency_id.id,
|
||||
})
|
||||
self.env.flush_all()
|
||||
self.env['account.invoice.report'].invalidate_model()
|
||||
report = self.env['account.invoice.report'].search(
|
||||
[('move_id', '=', invoice.id)],
|
||||
)
|
||||
self.assertEqual(report.inventory_value, -1600)
|
||||
self.assertEqual(report.price_margin, -100)
|
||||
|
|
|
|||
|
|
@ -1,91 +1,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ast import literal_eval
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import http
|
||||
from odoo.tools import hash_sign
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
|
||||
from odoo.tests import tagged
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import Form, tagged, HttpCase, new_test_user
|
||||
from odoo.addons.test_mail.data.test_mail_data import MAIL_EML_ATTACHMENT
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo import fields, Command
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountJournal(AccountTestInvoicingCommon):
|
||||
class TestAccountJournal(AccountTestInvoicingCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
def test_constraint_currency_consistency_with_accounts(self):
|
||||
''' The accounts linked to a bank/cash journal must share the same foreign currency
|
||||
if specified.
|
||||
'''
|
||||
journal_bank = self.company_data['default_journal_bank']
|
||||
journal_bank.currency_id = self.currency_data['currency']
|
||||
journal_bank.currency_id = self.other_currency
|
||||
|
||||
# Try to set a different currency on the 'debit' account.
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(ValidationError):
|
||||
journal_bank.default_account_id.currency_id = self.company_data['currency']
|
||||
|
||||
def test_euro_payment_reference_generation(self):
|
||||
"""
|
||||
Test the generation of European (ISO 11649) payment references to ensure
|
||||
it correctly handles various journal short codes.
|
||||
"""
|
||||
journal = self.company_data['default_journal_sale']
|
||||
journal.invoice_reference_model = 'euro'
|
||||
|
||||
# Case 1: Code contains alphanumeric value.
|
||||
journal.code = 'INV'
|
||||
invoice_valid = self.init_invoice("out_invoice", products=self.product_a)
|
||||
invoice_valid.journal_id = journal
|
||||
invoice_valid.action_post()
|
||||
self.assertTrue(invoice_valid.payment_reference, "A payment reference should be generated.")
|
||||
self.assertIn('INV', invoice_valid.payment_reference, "The reference should be based on the journal code.")
|
||||
|
||||
# Case 2: Code contains a hyphen.
|
||||
journal.code = 'INV-'
|
||||
invoice_invalid = self.init_invoice("out_invoice", products=self.product_a)
|
||||
invoice_invalid.journal_id = journal
|
||||
invoice_invalid.action_post()
|
||||
self.assertTrue(invoice_invalid.payment_reference, "A payment reference should be generated.")
|
||||
self.assertIn(str(journal.id), invoice_invalid.payment_reference, "The reference should fall back to using the journal ID.")
|
||||
|
||||
# Case 3: Code is non-ASCII but alphanumeric (e.g., Greek letter 'INVα'). # noqa: RUF003
|
||||
journal.code = 'INVα'
|
||||
invoice_unicode = self.init_invoice("out_invoice", products=self.product_a)
|
||||
invoice_unicode.journal_id = journal
|
||||
invoice_unicode.action_post()
|
||||
self.assertTrue(invoice_unicode.payment_reference, "A payment reference should be generated.")
|
||||
self.assertIn(str(journal.id), invoice_unicode.payment_reference, "The reference should fall back to using the journal ID for non-ASCII codes.")
|
||||
|
||||
def test_changing_journal_company(self):
|
||||
''' Ensure you can't change the company of an account.journal if there are some journal entries '''
|
||||
|
||||
self.company_data['default_journal_sale'].code = "DIFFERENT"
|
||||
self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.company_data['default_journal_sale'].id,
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
with self.assertRaisesRegex(UserError, "entries linked to it"):
|
||||
self.company_data['default_journal_sale'].company_id = self.company_data_2['company']
|
||||
|
||||
def test_account_control_create_journal_entry(self):
|
||||
move_vals = {
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': 'debit',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'credit',
|
||||
'account_id': self.company_data['default_account_expense'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 100.0,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
# Should fail because 'default_account_expense' is not allowed.
|
||||
self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_revenue']
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
self.env['account.move'].create(move_vals)
|
||||
|
||||
# Should be allowed because both accounts are accepted.
|
||||
self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_expense']
|
||||
self.env['account.move'].create(move_vals)
|
||||
|
||||
def test_account_control_existing_journal_entry(self):
|
||||
self.env['account.move'].create({
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': 'debit',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'credit',
|
||||
'account_id': self.company_data['default_account_expense'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 100.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
# There is already an other line using the 'default_account_expense' account.
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_revenue']
|
||||
|
||||
# Assigning both should be allowed
|
||||
self.company_data['default_journal_misc'].account_control_ids = \
|
||||
self.company_data['default_account_revenue'] + self.company_data['default_account_expense']
|
||||
|
||||
def test_account_journal_add_new_payment_method_multi(self):
|
||||
"""
|
||||
Test the automatic creation of payment method lines with the mode set to multi
|
||||
|
|
@ -94,7 +87,7 @@ class TestAccountJournal(AccountTestInvoicingCommon):
|
|||
|
||||
def _get_payment_method_information(self):
|
||||
res = Method_get_payment_method_information(self)
|
||||
res['multi'] = {'mode': 'multi', 'domain': [('type', '=', 'bank')]}
|
||||
res['multi'] = {'mode': 'multi', 'type': ('bank',)}
|
||||
return res
|
||||
|
||||
with patch.object(AccountPaymentMethod, '_get_payment_method_information', _get_payment_method_information):
|
||||
|
|
@ -104,10 +97,11 @@ class TestAccountJournal(AccountTestInvoicingCommon):
|
|||
'payment_type': 'inbound'
|
||||
})
|
||||
|
||||
journals = self.env['account.journal'].search([('inbound_payment_method_line_ids.code', '=', 'multi')])
|
||||
bank_journals_count = self.env['account.journal'].search_count([('type', '=', 'bank')])
|
||||
edited_journals_count = self.env['account.journal'].search_count([('inbound_payment_method_line_ids.code', '=', 'multi')])
|
||||
|
||||
# The two bank journals have been set
|
||||
self.assertEqual(len(journals), 2)
|
||||
# The bank journals have been set
|
||||
self.assertEqual(bank_journals_count, edited_journals_count)
|
||||
|
||||
def test_remove_payment_method_lines(self):
|
||||
"""
|
||||
|
|
@ -136,35 +130,13 @@ class TestAccountJournal(AccountTestInvoicingCommon):
|
|||
|
||||
self.assertFalse(second_method.exists())
|
||||
|
||||
def test_account_journal_alias_name(self):
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
self.assertEqual(journal.alias_name, 'vendor-bills-company_1_data')
|
||||
journal.name = 'ぁ'
|
||||
journal.alias_name = False
|
||||
self.assertEqual(journal.alias_name, 'bill-company_1_data')
|
||||
journal.code = 'ぁ'
|
||||
journal.alias_name = False
|
||||
self.assertEqual(journal.alias_name, 'purchase-company_1_data')
|
||||
|
||||
company_2_id = str(self.company_data_2['company'].id)
|
||||
journal_2 = self.company_data_2['default_journal_sale']
|
||||
self.company_data_2['company'].name = 'ぁ'
|
||||
journal_2.alias_name = False
|
||||
self.assertEqual(journal_2.alias_name, 'customer-invoices-' + company_2_id)
|
||||
journal_2.name = 'ぁ'
|
||||
journal_2.alias_name = False
|
||||
self.assertEqual(journal_2.alias_name, 'inv-' + company_2_id)
|
||||
journal_2.code = 'ぁ'
|
||||
journal_2.alias_name = False
|
||||
self.assertEqual(journal_2.alias_name, 'sale-' + company_2_id)
|
||||
|
||||
def test_account_journal_duplicates(self):
|
||||
new_journals = self.env["account.journal"].with_context(import_file=True).create([
|
||||
{"name": "OD_BLABLA"},
|
||||
{"name": "OD_BLABLU"},
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(new_journals.mapped("code")), ["GEN1", "OD_BL"], "The journals should be set correctly")
|
||||
self.assertEqual(sorted(new_journals.mapped("code")), ["MISC1", "OD_BL"], "The journals should be set correctly")
|
||||
|
||||
def test_archive_used_journal(self):
|
||||
journal = self.env['account.journal'].create({
|
||||
|
|
@ -205,3 +177,397 @@ class TestAccountJournal(AccountTestInvoicingCommon):
|
|||
journals.action_unarchive()
|
||||
self.assertTrue(journals[0].active)
|
||||
self.assertTrue(journals[1].active)
|
||||
|
||||
def test_journal_notifications_unsubscribe(self):
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
journal.incoming_einvoice_notification_email = 'test@example.com'
|
||||
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
res = self.url_open(
|
||||
f'/my/journal/{journal.id}/unsubscribe',
|
||||
data={'csrf_token': http.Request.csrf_token(self)},
|
||||
method='POST',
|
||||
)
|
||||
res.raise_for_status()
|
||||
|
||||
self.assertFalse(journal.incoming_einvoice_notification_email)
|
||||
|
||||
def test_journal_notifications_unsubscribe_success(self):
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
email = 'test@example.com'
|
||||
journal.incoming_einvoice_notification_email = email
|
||||
|
||||
self.authenticate(None, None)
|
||||
token = hash_sign(
|
||||
self.env,
|
||||
journal._get_journal_notification_unsubscribe_scope(),
|
||||
{'email_to_unsubscribe': email, 'journal_id': journal.id},
|
||||
)
|
||||
|
||||
res = self.url_open(
|
||||
f'/my/journal/{journal.id}/unsubscribe?token={token}',
|
||||
data={'csrf_token': http.Request.csrf_token(self)},
|
||||
method='POST',
|
||||
)
|
||||
res.raise_for_status()
|
||||
|
||||
self.assertFalse(journal.incoming_einvoice_notification_email)
|
||||
|
||||
def test_journal_notifications_unsubscribe_errors(self):
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
email = 'test@example.com'
|
||||
self.authenticate(None, None)
|
||||
valid_token = hash_sign(
|
||||
self.env(su=True),
|
||||
journal._get_journal_notification_unsubscribe_scope(),
|
||||
{'email_to_unsubscribe': email, 'journal_id': journal.id},
|
||||
)
|
||||
|
||||
def _get_token():
|
||||
return
|
||||
|
||||
def _unsubscribe(token, journal_id=journal.id):
|
||||
return self.url_open(
|
||||
f'/my/journal/{journal_id}/unsubscribe?token={token}',
|
||||
data={'csrf_token': http.Request.csrf_token(self)},
|
||||
method='POST',
|
||||
)
|
||||
|
||||
with self.subTest('invalid_token'):
|
||||
journal.incoming_einvoice_notification_email = email
|
||||
res = _unsubscribe('invalid_token')
|
||||
self.assertEqual(res.status_code, 403)
|
||||
self.assertEqual(journal.incoming_einvoice_notification_email, email)
|
||||
|
||||
with self.subTest('already_unsubscribed'):
|
||||
journal.incoming_einvoice_notification_email = email
|
||||
first_unsubscribe = _unsubscribe(valid_token)
|
||||
first_unsubscribe.raise_for_status()
|
||||
self.assertFalse(journal.incoming_einvoice_notification_email)
|
||||
second_unsubscribe = _unsubscribe(valid_token)
|
||||
self.assertEqual(second_unsubscribe.status_code, 404)
|
||||
|
||||
with self.subTest('wrong_journal_id'):
|
||||
journal.incoming_einvoice_notification_email = email
|
||||
res = _unsubscribe(valid_token, journal_id=journal.id + 1)
|
||||
self.assertEqual(res.status_code, 403)
|
||||
self.assertEqual(journal.incoming_einvoice_notification_email, email)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'mail_alias')
|
||||
class TestAccountJournalAlias(AccountTestInvoicingCommon, MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
def test_alias_name_creation(self):
|
||||
""" Test alias creation, notably avoid raising constraints due to ascii
|
||||
characters removal. See odoo/odoo@339cdffb68f91eb1455d447d1bdd7133c68723bd """
|
||||
# check base test data
|
||||
journal1 = self.company_data['default_journal_purchase']
|
||||
company1 = journal1.company_id
|
||||
journal2 = self.company_data_2['default_journal_sale']
|
||||
company2 = journal2.company_id
|
||||
# have a non ascii company name
|
||||
company2.name = 'ぁ'
|
||||
|
||||
for (aname, jname, jcode, jtype, jcompany), expected_alias_name in zip(
|
||||
[
|
||||
('youpie', 'Journal Name', 'NEW1', 'purchase', company1),
|
||||
(False, 'Journal Other Name', 'NEW2', 'purchase', company1),
|
||||
(False, 'ぁ', 'NEW3', 'purchase', company1),
|
||||
(False, 'ぁ', 'ぁ', 'purchase', company1),
|
||||
('youpie', 'Journal Name', 'NEW1', 'purchase', company2),
|
||||
(False, 'Journal Other Name', 'NEW2', 'purchase', company2),
|
||||
(False, 'ぁ', 'NEW3', 'purchase', company2),
|
||||
(False, 'ぁ', 'ぁ', 'purchase', company2),
|
||||
],
|
||||
[
|
||||
f'youpie-{company1.name}',
|
||||
f'journal-other-name-{company1.name}',
|
||||
f'new3-{company1.name}',
|
||||
f'purchase-{company1.name}',
|
||||
f'youpie-{company2.id}',
|
||||
f'journal-other-name-{company2.id}',
|
||||
f'new3-{company2.id}',
|
||||
f'purchase-{company2.id}',
|
||||
]
|
||||
):
|
||||
with self.subTest(aname=aname, jname=jname, jcode=jcode, jtype=jtype, jcompany=jcompany):
|
||||
new_journal = self.env['account.journal'].create({
|
||||
'code': jcode,
|
||||
'company_id': jcompany.id,
|
||||
'name': jname,
|
||||
'type': jtype,
|
||||
# force alias_name only if given, to check default value otherwise
|
||||
**({'alias_name': aname} if aname else {}),
|
||||
})
|
||||
self.assertEqual(new_journal.alias_name, expected_alias_name)
|
||||
|
||||
# other types: no mail support by default
|
||||
journals = self.env['account.journal'].create([{
|
||||
'code': f'NEW{jtype}',
|
||||
'name': f'Type {jtype}',
|
||||
'type': jtype}
|
||||
for jtype in ('general', 'cash', 'bank')
|
||||
])
|
||||
self.assertFalse(journals.alias_id, 'Do not create useless aliases')
|
||||
self.assertFalse(list(filter(None, journals.mapped('alias_name'))))
|
||||
|
||||
def test_alias_name_form(self):
|
||||
""" Test alias name update using Form tool (onchange) """
|
||||
journal = Form(self.env['account.journal'])
|
||||
journal.name = 'Test With Form'
|
||||
self.assertFalse(journal.alias_name)
|
||||
journal.type = 'sale'
|
||||
self.assertEqual(journal.alias_name, f'test-with-form-{self.env.company.name}')
|
||||
journal.type = 'cash'
|
||||
self.assertFalse(journal.alias_name)
|
||||
|
||||
def test_alias_from_type(self):
|
||||
""" Test alias behavior on journal, especially alias_name management as
|
||||
well as defaults update, see odoo/odoo@400b6860271a11b9914166ff7e42939c4c6192dc """
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
|
||||
# assert base test data
|
||||
company_name = 'company_1_data'
|
||||
journal_code = 'BILL'
|
||||
journal_name = 'Purchases'
|
||||
journal_alias = journal.alias_id
|
||||
self.assertEqual(journal.code, journal_code)
|
||||
self.assertEqual(journal.company_id.name, company_name)
|
||||
self.assertEqual(journal.name, journal_name)
|
||||
self.assertEqual(journal.type, 'purchase')
|
||||
|
||||
# assert default creation data
|
||||
self.assertEqual(journal_alias.alias_contact, 'everyone')
|
||||
self.assertDictEqual(
|
||||
dict(literal_eval(journal_alias.alias_defaults)),
|
||||
{
|
||||
'move_type': 'in_invoice',
|
||||
'company_id': journal.company_id.id,
|
||||
'journal_id': journal.id,
|
||||
}
|
||||
)
|
||||
self.assertFalse(journal_alias.alias_force_thread_id, 'Journal alias should create new moves')
|
||||
self.assertEqual(journal_alias.alias_model_id, self.env['ir.model']._get('account.move'),
|
||||
'Journal alias targets moves')
|
||||
self.assertEqual(journal_alias.alias_name, f'purchases-{company_name}')
|
||||
self.assertEqual(journal_alias.alias_parent_model_id, self.env['ir.model']._get('account.journal'),
|
||||
'Journal alias owned by journal itself')
|
||||
self.assertEqual(journal_alias.alias_parent_thread_id, journal.id,
|
||||
'Journal alias owned by journal itself')
|
||||
|
||||
# update alias_name, ensure a fallback on a real name when not explicit reset
|
||||
for alias_name, expected in [
|
||||
(False, False),
|
||||
('', False),
|
||||
(' ', f'purchases-{company_name}'), # error recuperation
|
||||
('.', f'purchases-{company_name}'), # error recuperation
|
||||
('😊', f'purchases-{company_name}'), # resets, unicode not supported
|
||||
('ぁ', f'purchases-{company_name}'), # resets, non ascii not supported
|
||||
('Youpie Boum', 'youpie-boum'),
|
||||
]:
|
||||
with self.subTest(alias_name=alias_name):
|
||||
journal.write({'alias_name': alias_name})
|
||||
self.assertEqual(journal.alias_name, expected)
|
||||
self.assertEqual(journal_alias.alias_name, expected)
|
||||
|
||||
# changing type should void if not purchase or sale
|
||||
for jtype in ('general', 'cash', 'bank'):
|
||||
journal.write({'type': jtype})
|
||||
self.assertEqual(journal.alias_id, journal_alias,
|
||||
'Dà not unlink aliases, just reset their value')
|
||||
self.assertFalse(journal.alias_name)
|
||||
self.assertFalse(journal_alias.alias_name)
|
||||
|
||||
# changing type should reset if sale or purchase
|
||||
journal.company_id.write({'name': 'New Company Name'})
|
||||
journal.write({'name': 'Reset Journal', 'type': 'sale'})
|
||||
journal_alias_2 = journal.alias_id
|
||||
self.assertEqual(journal_alias_2.alias_contact, 'everyone')
|
||||
self.assertDictEqual(
|
||||
dict(literal_eval(journal_alias_2.alias_defaults)),
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'company_id': journal.company_id.id,
|
||||
'journal_id': journal.id,
|
||||
}
|
||||
)
|
||||
self.assertFalse(journal_alias_2.alias_force_thread_id, 'Journal alias should create new moves')
|
||||
self.assertEqual(journal_alias_2.alias_model_id, self.env['ir.model']._get('account.move'),
|
||||
'Journal alias targets moves')
|
||||
self.assertEqual(journal_alias_2.alias_name, 'reset-journal-new-company-name')
|
||||
self.assertEqual(journal_alias_2.alias_parent_model_id, self.env['ir.model']._get('account.journal'),
|
||||
'Journal alias owned by journal itself')
|
||||
self.assertEqual(journal_alias_2.alias_parent_thread_id, journal.id,
|
||||
'Journal alias owned by journal itself')
|
||||
|
||||
def test_alias_create_unique(self):
|
||||
""" Make auto-generated alias_name unique when needed """
|
||||
company_name = self.company_data['company'].name
|
||||
journal = self.env['account.journal'].create({
|
||||
'name': 'Test Journal',
|
||||
'type': 'sale',
|
||||
'code': 'A',
|
||||
})
|
||||
journal2 = self.env['account.journal'].create({
|
||||
'name': 'Test Journal',
|
||||
'type': 'sale',
|
||||
'code': 'B',
|
||||
})
|
||||
self.assertEqual(journal.alias_name, f'test-journal-{company_name}')
|
||||
self.assertEqual(journal2.alias_name, f'test-journal-{company_name}-b')
|
||||
|
||||
def test_non_latin_journal_code_payment_reference(self):
|
||||
""" Ensure non-Latin journal codes do not cause errors and payment references are valid """
|
||||
non_latin_code = 'TΠY'
|
||||
latin_code = 'TPY'
|
||||
|
||||
journal_non_latin = self.env['account.journal'].create({
|
||||
'name': 'Test Journal',
|
||||
'type': 'sale',
|
||||
'code': non_latin_code,
|
||||
'invoice_reference_model': 'euro'
|
||||
})
|
||||
journal_latin = self.env['account.journal'].create({
|
||||
'name': 'Test Journal',
|
||||
'type': 'sale',
|
||||
'code': latin_code,
|
||||
'invoice_reference_model': 'euro'
|
||||
})
|
||||
|
||||
invoice_non_latin = self.init_invoice(
|
||||
move_type='out_invoice',
|
||||
partner=self.partner_a,
|
||||
invoice_date=fields.Date.today(),
|
||||
post=True,
|
||||
products=[self.product_a],
|
||||
journal=journal_non_latin,
|
||||
)
|
||||
invoice_latin = self.init_invoice(
|
||||
move_type='out_invoice',
|
||||
partner=self.partner_a,
|
||||
invoice_date=fields.Date.today(),
|
||||
post=True,
|
||||
products=[self.product_a],
|
||||
journal=journal_latin,
|
||||
)
|
||||
|
||||
expected_id = str(invoice_non_latin.journal_id.id)
|
||||
ref_parts_non_latin = invoice_non_latin.payment_reference.split()
|
||||
self.assertEqual(ref_parts_non_latin[1][:len(expected_id)], expected_id, "The reference should start with " + expected_id)
|
||||
|
||||
ref_parts_latin = invoice_latin.payment_reference.split()
|
||||
self.assertIn(ref_parts_latin[1][:3], latin_code, f"Expected journal code '{latin_code}' in second part of reference")
|
||||
|
||||
def test_use_default_account_from_journal(self):
|
||||
"""
|
||||
Test that the autobalance uses the default account id of the journal
|
||||
"""
|
||||
autobalance_account = self.env['account.account'].create({
|
||||
'name': 'Autobalance Account',
|
||||
'account_type': 'income',
|
||||
'code': 'A',
|
||||
})
|
||||
journal = self.env['account.journal'].create({
|
||||
'name': 'Test Journal',
|
||||
'type': 'general',
|
||||
'code': 'B',
|
||||
'default_account_id': autobalance_account.id,
|
||||
})
|
||||
|
||||
entry = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'journal_id': journal.id,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
'tax_ids': (self.company_data['default_tax_sale']),
|
||||
'account_id': self.company_data['default_account_revenue'].id
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
entry.action_post()
|
||||
self.assertRecordValues(entry.line_ids, [
|
||||
{'balance': 100.0, 'account_id': self.company_data['default_account_revenue'].id},
|
||||
{'balance': 15.0, 'account_id': self.company_data['default_account_tax_sale'].id},
|
||||
{'balance': -115.0, 'account_id': autobalance_account.id},
|
||||
])
|
||||
|
||||
def test_send_email_to_alias_from_other_company(self):
|
||||
user_company_2 = new_test_user(
|
||||
self.env,
|
||||
name='company 2 user',
|
||||
login='company_2_user',
|
||||
password='company_2_user',
|
||||
email='company_2_user@test.com',
|
||||
company_id=self.company_data_2['company'].id
|
||||
)
|
||||
self.format_and_process(
|
||||
MAIL_EML_ATTACHMENT,
|
||||
user_company_2.email,
|
||||
self.company_data['default_journal_purchase'].alias_email,
|
||||
subject='purchase test mail',
|
||||
target_model='account.move',
|
||||
msg_id='<test-account-move-alias-id>',
|
||||
)
|
||||
self.assertTrue(self.env['account.move'].search([('invoice_source_email', '=', 'company_2_user@test.com')]))
|
||||
|
||||
def test_alias_uniqueness_without_domain(self):
|
||||
"""Ensure alias_name is unique even if alias_domain is not defined."""
|
||||
default_account = self.env['account.account'].search(
|
||||
domain=[('account_type', 'in', ('income', 'income_other'))],
|
||||
limit=1,
|
||||
)
|
||||
with Form(self.env['account.journal']) as journal_form:
|
||||
journal_form.type = 'sale'
|
||||
journal_form.code = 'A'
|
||||
journal_form.name = 'Test Journal 1'
|
||||
journal_form.default_account_id = default_account
|
||||
journal_1 = journal_form.save()
|
||||
with Form(self.env['account.journal']) as journal_form:
|
||||
journal_form.type = 'sale'
|
||||
journal_form.code = 'B'
|
||||
journal_form.name = 'Test Journal 2'
|
||||
journal_form.default_account_id = default_account
|
||||
journal_2 = journal_form.save()
|
||||
self.assertNotEqual(journal_1.alias_id.alias_name, journal_2.alias_id.alias_name)
|
||||
|
||||
def test_payment_method_line_accounts_on_recompute(self):
|
||||
"""
|
||||
Test that outstanding payments/receipts accounts are not removed during the computation of the payment method lines
|
||||
"""
|
||||
bank_journal = self.company_data['default_journal_bank']
|
||||
outstanding_receipt_account = self.env['account.chart.template'].ref('account_journal_payment_debit_account_id')
|
||||
outstanding_payment_account = self.env['account.chart.template'].ref('account_journal_payment_credit_account_id')
|
||||
|
||||
inbound_method_lines = bank_journal.inbound_payment_method_line_ids
|
||||
inbound_method_lines_names = inbound_method_lines.mapped('name')
|
||||
inbound_method_lines[0].payment_account_id = outstanding_receipt_account
|
||||
|
||||
outbound_method_lines = bank_journal.outbound_payment_method_line_ids
|
||||
outbound_method_lines_names = outbound_method_lines.mapped('name')
|
||||
outbound_method_lines[0].payment_account_id = outstanding_payment_account
|
||||
new_outbound_payment_line = outbound_method_lines[0].copy({'payment_account_id': self.company_data['default_account_deferred_expense'].id})
|
||||
bank_journal.outbound_payment_method_line_ids = [Command.link(new_outbound_payment_line.id)]
|
||||
|
||||
# Set currency_id to trigger the compute of {in,out}bound_payment_method_line_ids
|
||||
bank_journal.currency_id = self.company_data['currency']
|
||||
|
||||
self.assertRecordValues(bank_journal.inbound_payment_method_line_ids, [
|
||||
{
|
||||
'name': name,
|
||||
'payment_account_id': outstanding_receipt_account.id if index == 0 else False,
|
||||
} for index, name in enumerate(inbound_method_lines_names)
|
||||
])
|
||||
self.assertRecordValues(bank_journal.outbound_payment_method_line_ids, [
|
||||
{
|
||||
'name': name,
|
||||
'payment_account_id': outstanding_payment_account.id if index == 0 else False,
|
||||
} for index, name in enumerate(outbound_method_lines_names)
|
||||
])
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@ from dateutil.relativedelta import relativedelta
|
|||
from freezegun import freeze_time
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.account.tests.test_account_journal_dashboard_common import TestAccountJournalDashboardCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools.misc import format_amount
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountJournalDashboard(AccountTestInvoicingCommon):
|
||||
class TestAccountJournalDashboard(TestAccountJournalDashboardCommon):
|
||||
|
||||
@freeze_time("2019-01-22")
|
||||
def test_customer_invoice_dashboard(self):
|
||||
# This test is defined in the account_3way_match module with different values, so we skip it when the module is installed
|
||||
if self.env['ir.module.module'].search([('name', '=', 'account_3way_match')]).state == 'installed':
|
||||
self.skipTest("This test won't work if account_3way_match is installed")
|
||||
|
||||
journal = self.company_data['default_journal_sale']
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
|
|
@ -44,7 +48,7 @@ class TestAccountJournalDashboard(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
# Check Draft
|
||||
dashboard_data = journal.get_journal_dashboard_datas()
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
|
||||
self.assertEqual(dashboard_data['number_draft'], 2)
|
||||
self.assertIn('68.42', dashboard_data['sum_draft'])
|
||||
|
|
@ -55,24 +59,42 @@ class TestAccountJournalDashboard(AccountTestInvoicingCommon):
|
|||
# Check Both
|
||||
invoice.action_post()
|
||||
|
||||
dashboard_data = journal.get_journal_dashboard_datas()
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['number_draft'], 1)
|
||||
self.assertIn('-\N{ZERO WIDTH NO-BREAK SPACE}13.30', dashboard_data['sum_draft'])
|
||||
|
||||
self.assertEqual(dashboard_data['number_waiting'], 1)
|
||||
self.assertIn('81.72', dashboard_data['sum_waiting'])
|
||||
|
||||
# Check partial on invoice
|
||||
partial_payment = self.env['account.payment'].create({
|
||||
'amount': 13.3,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
partial_payment.action_post()
|
||||
|
||||
(invoice + partial_payment.move_id).line_ids.filtered(lambda line: line.account_type == 'asset_receivable').reconcile()
|
||||
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['number_draft'], 1)
|
||||
self.assertIn('13.3', dashboard_data['sum_draft'])
|
||||
|
||||
self.assertEqual(dashboard_data['number_waiting'], 1)
|
||||
self.assertIn('68.42', dashboard_data['sum_waiting'])
|
||||
|
||||
# Check waiting payment
|
||||
refund.action_post()
|
||||
|
||||
dashboard_data = journal.get_journal_dashboard_datas()
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['number_draft'], 0)
|
||||
self.assertIn('0.00', dashboard_data['sum_draft'])
|
||||
|
||||
self.assertEqual(dashboard_data['number_waiting'], 2)
|
||||
self.assertIn('68.42', dashboard_data['sum_waiting'])
|
||||
self.assertIn('55.12', dashboard_data['sum_waiting'])
|
||||
|
||||
# Check partial
|
||||
# Check partial on refund
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 10.0,
|
||||
'payment_type': 'outbound',
|
||||
|
|
@ -85,53 +107,68 @@ class TestAccountJournalDashboard(AccountTestInvoicingCommon):
|
|||
.filtered(lambda line: line.account_type == 'asset_receivable')\
|
||||
.reconcile()
|
||||
|
||||
dashboard_data = journal.get_journal_dashboard_datas()
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['number_draft'], 0)
|
||||
self.assertIn('0.00', dashboard_data['sum_draft'])
|
||||
|
||||
self.assertEqual(dashboard_data['number_waiting'], 2)
|
||||
self.assertIn('78.42', dashboard_data['sum_waiting'])
|
||||
self.assertIn('65.12', dashboard_data['sum_waiting'])
|
||||
|
||||
dashboard_data = journal.get_journal_dashboard_datas()
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['number_late'], 2)
|
||||
self.assertIn('78.42', dashboard_data['sum_late'])
|
||||
self.assertIn('65.12', dashboard_data['sum_late'])
|
||||
|
||||
def test_sale_purchase_journal_for_multi_currency_purchase(self):
|
||||
currency = self.currency_data['currency']
|
||||
def test_sale_purchase_journal_for_purchase(self):
|
||||
"""
|
||||
Test different purchase journal setups with or without multicurrency:
|
||||
1) Journal with no currency, bills in foreign currency -> dashboard data should be displayed in company currency
|
||||
2) Journal in foreign currency, bills in foreign currency -> dashboard data should be displayed in foreign currency
|
||||
3) Journal in foreign currency, bills in company currency -> dashboard data should be displayed in foreign currency
|
||||
4) Journal in company currency, bills in company currency -> dashboard data should be displayed in company currency
|
||||
5) Journal in company currency, bills in foreign currency -> dashboard data should be displayed in company currency
|
||||
"""
|
||||
# This test is defined in the account_3way_match module with different values, so we skip it when the module is installed
|
||||
if self.env['ir.module.module'].search([('name', '=', 'account_3way_match')]).state == 'installed':
|
||||
self.skipTest("This test won't work if account_3way_match is installed")
|
||||
|
||||
foreign_currency = self.other_currency
|
||||
company_currency = self.company_data['currency']
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'invoice_date': '2017-01-01',
|
||||
'date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': currency.id,
|
||||
'invoice_line_ids': [
|
||||
(0, 0, {'name': 'test', 'price_unit': 200})
|
||||
],
|
||||
})
|
||||
invoice.action_post()
|
||||
setup_values = [
|
||||
[self.company_data['default_journal_purchase'], foreign_currency],
|
||||
[self.company_data['default_journal_purchase'].copy({'currency_id': foreign_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), foreign_currency],
|
||||
[self.company_data['default_journal_purchase'].copy({'currency_id': foreign_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), company_currency],
|
||||
[self.company_data['default_journal_purchase'].copy({'currency_id': company_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), company_currency],
|
||||
[self.company_data['default_journal_purchase'].copy({'currency_id': company_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), foreign_currency],
|
||||
]
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 90.0,
|
||||
'date': '2016-01-01',
|
||||
'payment_type': 'outbound',
|
||||
'partner_type': 'supplier',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': currency.id,
|
||||
})
|
||||
payment.action_post()
|
||||
expected_vals_list = [
|
||||
# number_draft, sum_draft, number_waiting, sum_waiting, number_late, sum_late, currency
|
||||
[ 1, 100, 1, 55, 1, 55, company_currency],
|
||||
[ 1, 200, 1, 110, 1, 110, foreign_currency],
|
||||
[ 1, 400, 1, 220, 1, 220, foreign_currency],
|
||||
[ 1, 200, 1, 110, 1, 110, company_currency],
|
||||
[ 1, 100, 1, 55, 1, 55, company_currency],
|
||||
]
|
||||
|
||||
(invoice + payment.move_id).line_ids.filtered_domain([
|
||||
('account_id', '=', self.company_data['default_account_payable'].id)
|
||||
]).reconcile()
|
||||
for (purchase_journal, bill_currency), expected_vals in zip(setup_values, expected_vals_list):
|
||||
with self.subTest(purchase_journal_currency=purchase_journal.currency_id, bill_currency=bill_currency, expected_vals=expected_vals):
|
||||
bill = self.init_invoice('in_invoice', invoice_date='2017-01-01', post=True, amounts=[200], currency=bill_currency, journal=purchase_journal)
|
||||
_draft_bill = self.init_invoice('in_invoice', invoice_date='2017-01-01', post=False, amounts=[200], currency=bill_currency, journal=purchase_journal)
|
||||
|
||||
dashboard_data = self.company_data['default_journal_purchase'].get_journal_dashboard_datas()
|
||||
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_waiting'])
|
||||
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_late'])
|
||||
payment = self.init_payment(-90, post=True, date='2017-01-01', currency=bill_currency)
|
||||
(bill + payment.move_id).line_ids.filtered_domain([
|
||||
('account_id', '=', self.company_data['default_account_payable'].id)
|
||||
]).reconcile()
|
||||
|
||||
self.assertDashboardPurchaseSaleData(purchase_journal, *expected_vals)
|
||||
|
||||
def test_sale_purchase_journal_for_multi_currency_sale(self):
|
||||
currency = self.currency_data['currency']
|
||||
# This test is defined in the account_3way_match module with different values, so we skip it when the module is installed
|
||||
if self.env['ir.module.module'].search([('name', '=', 'account_3way_match')]).state == 'installed':
|
||||
self.skipTest("This test won't work if account_3way_match is installed")
|
||||
|
||||
currency = self.other_currency
|
||||
company_currency = self.company_data['currency']
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
|
|
@ -160,9 +197,28 @@ class TestAccountJournalDashboard(AccountTestInvoicingCommon):
|
|||
('account_id', '=', self.company_data['default_account_receivable'].id)
|
||||
]).reconcile()
|
||||
|
||||
dashboard_data = self.company_data['default_journal_sale'].get_journal_dashboard_datas()
|
||||
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_waiting'])
|
||||
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_late'])
|
||||
default_journal_sale = self.company_data['default_journal_sale']
|
||||
dashboard_data = default_journal_sale._get_journal_dashboard_data_batched()[default_journal_sale.id]
|
||||
self.assertEqual(format_amount(self.env, 55, company_currency), dashboard_data['sum_waiting'])
|
||||
self.assertEqual(format_amount(self.env, 55, company_currency), dashboard_data['sum_late'])
|
||||
|
||||
@freeze_time("2023-03-15")
|
||||
def test_purchase_journal_numbers_and_sums(self):
|
||||
# This test is defined in the account_3way_match module with different values, so we skip it when the module is installed
|
||||
if self.env['ir.module.module'].search([('name', '=', 'account_3way_match')]).state == 'installed':
|
||||
self.skipTest("This test won't work if account_3way_match is installed")
|
||||
|
||||
company_currency = self.company_data['currency']
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
|
||||
self._create_test_vendor_bills(journal)
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
# Expected behavior is to have three moves waiting for payment for a total amount of 4440$ one of which would be late
|
||||
# for a total amount of 40$ (second move has one of three lines late but that's not enough to make the move late)
|
||||
self.assertEqual(3, dashboard_data['number_waiting'])
|
||||
self.assertEqual(format_amount(self.env, 4440, company_currency), dashboard_data['sum_waiting'])
|
||||
self.assertEqual(1, dashboard_data['number_late'])
|
||||
self.assertEqual(format_amount(self.env, 40, company_currency), dashboard_data['sum_late'])
|
||||
|
||||
def test_gap_in_sequence_warning(self):
|
||||
journal = self.company_data['default_journal_sale']
|
||||
|
|
@ -187,9 +243,9 @@ class TestAccountJournalDashboard(AccountTestInvoicingCommon):
|
|||
self.assertFalse(journal._query_has_sequence_holes()) # no gap, no gap warning, and draft moves shouldn't trigger the warning
|
||||
|
||||
moves[2:4].button_draft()
|
||||
self.assertFalse(journal._query_has_sequence_holes()) # no gap (with draft moves using sequence numbers), no gap warning
|
||||
self.assertTrue(journal._query_has_sequence_holes()) # gap due to draft moves using sequence numbers, gap warning
|
||||
moves[3].unlink()
|
||||
self.assertTrue(journal.has_sequence_holes) # gap due to missing sequence, gap warning
|
||||
self.assertTrue(journal._query_has_sequence_holes()) # gap due to missing sequence, gap warning
|
||||
|
||||
moves[2].action_post()
|
||||
self.company_data['company'].write({'fiscalyear_lock_date': gap_date + relativedelta(days=1)})
|
||||
|
|
@ -198,3 +254,153 @@ class TestAccountJournalDashboard(AccountTestInvoicingCommon):
|
|||
moves[6].button_draft()
|
||||
moves[6].button_cancel()
|
||||
self.assertTrue(journal._query_has_sequence_holes()) # gap due to canceled move using a sequence, gap warning
|
||||
|
||||
def test_bank_journal_with_default_account_as_outstanding_account_payments(self):
|
||||
"""
|
||||
Test that payments are excluded from the miscellaneaous operations and are included in the balance
|
||||
when having the default_account_id set as outstanding account on the journal
|
||||
"""
|
||||
bank_journal = self.company_data['default_journal_bank'].copy()
|
||||
bank_journal.outbound_payment_method_line_ids[0].payment_account_id = bank_journal.default_account_id
|
||||
bank_journal.inbound_payment_method_line_ids[0].payment_account_id = bank_journal.default_account_id
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 100,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'journal_id': bank_journal.id,
|
||||
})
|
||||
payment.action_post()
|
||||
|
||||
dashboard_data = bank_journal._get_journal_dashboard_data_batched()[bank_journal.id]
|
||||
self.assertEqual(dashboard_data['nb_misc_operations'], 0)
|
||||
self.assertEqual(dashboard_data['account_balance'], (bank_journal.currency_id or self.env.company.currency_id).format(100))
|
||||
|
||||
def test_bank_journal_different_currency(self):
|
||||
"""Test that the misc operations amount on the dashboard is correct
|
||||
for a bank account in another currency."""
|
||||
foreign_currency = self.other_currency
|
||||
bank_journal = self.company_data['default_journal_bank'].copy({'currency_id': foreign_currency.id})
|
||||
|
||||
self.assertNotEqual(bank_journal.currency_id, bank_journal.company_id.currency_id)
|
||||
|
||||
move = self.env['account.move'].create({
|
||||
'journal_id': self.company_data['default_journal_misc'].id,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'account_id': bank_journal.default_account_id.id,
|
||||
'currency_id': foreign_currency.id,
|
||||
'amount_currency': 100,
|
||||
}),
|
||||
Command.create({
|
||||
'account_id': self.company_data['default_account_assets'].id,
|
||||
'currency_id': foreign_currency.id,
|
||||
'amount_currency': -100,
|
||||
})
|
||||
]
|
||||
})
|
||||
move.action_post()
|
||||
|
||||
dashboard_data = bank_journal._get_journal_dashboard_data_batched()[bank_journal.id]
|
||||
self.assertEqual(dashboard_data.get('misc_operations_balance', 0), foreign_currency.format(100))
|
||||
|
||||
bank_journal.default_account_id.currency_id = False # not a normal case
|
||||
company_currency_move = self.env['account.move'].create({
|
||||
'journal_id': self.company_data['default_journal_misc'].id,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'account_id': bank_journal.default_account_id.id,
|
||||
'debit': 100,
|
||||
}),
|
||||
Command.create({
|
||||
'account_id': self.company_data['default_account_assets'].id,
|
||||
'credit': 100,
|
||||
})
|
||||
]
|
||||
})
|
||||
company_currency_move.action_post()
|
||||
dashboard_data = bank_journal._get_journal_dashboard_data_batched()[bank_journal.id]
|
||||
|
||||
self.assertEqual(dashboard_data.get('misc_operations_balance', 0), None)
|
||||
self.assertEqual(dashboard_data.get('misc_class', ''), 'text-warning')
|
||||
|
||||
def test_to_check_posted(self):
|
||||
"""We want to only have the information on posted moves"""
|
||||
journal = self.env['account.journal'].create({
|
||||
'name': 'Test Foreign Currency Journal',
|
||||
'type': 'sale',
|
||||
'code': 'TEST',
|
||||
'currency_id': self.currency.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'journal_id': journal.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'checked': False,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['to_check_balance'], journal.currency_id.format(0))
|
||||
|
||||
move.action_post()
|
||||
move.checked = False
|
||||
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['to_check_balance'], journal.currency_id.format(100))
|
||||
|
||||
def test_to_check_amount_different_currency(self):
|
||||
"""
|
||||
We want the to_check amount to be displayed in the journal currency
|
||||
Company currency = $
|
||||
Journal's currency = €
|
||||
Inv01 of 100 EUR; rate: 2€/1$
|
||||
Inv02 of 100 CHF; rate: 4CHF/1$
|
||||
|
||||
=> to check = 150 €
|
||||
"""
|
||||
self.env.ref('base.CHF').write({'active': True})
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.env.ref('base.EUR').id,
|
||||
'name': '2024-12-01',
|
||||
'rate': 2.0,
|
||||
})
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.env.ref('base.CHF').id,
|
||||
'name': '2024-12-01',
|
||||
'rate': 4.0,
|
||||
})
|
||||
journal = self.env['account.journal'].create({
|
||||
'name': 'Test Foreign Currency Journal',
|
||||
'type': 'sale',
|
||||
'code': 'TEST',
|
||||
'currency_id': self.env.ref('base.EUR').id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
moves = self.env['account.move'].create([{
|
||||
'move_type': 'out_invoice',
|
||||
'journal_id': journal.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': currency.id,
|
||||
'checked': False,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [],
|
||||
})
|
||||
]
|
||||
} for currency in (self.env.ref('base.EUR'), self.env.ref('base.CHF'))])
|
||||
moves.action_post()
|
||||
moves.checked = False
|
||||
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertEqual(dashboard_data['to_check_balance'], journal.currency_id.format(150))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools.misc import format_amount
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountJournalDashboardCommon(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
|
||||
def _create_test_vendor_bills(self, journal):
|
||||
# Setup multiple payments term
|
||||
twentyfive_now_term = self.env['account.payment.term'].create({
|
||||
'name': '25% now, rest in 30 days',
|
||||
'note': 'Pay 25% on invoice date and 75% 30 days later',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 25.00,
|
||||
'delay_type': 'days_after',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 75.00,
|
||||
'delay_type': 'days_after',
|
||||
'nb_days': 30,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'journal_id': journal.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2023-04-01',
|
||||
'date': '2023-03-15',
|
||||
'invoice_payment_term_id': twentyfive_now_term.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'name': 'product test 1',
|
||||
'price_unit': 4000,
|
||||
'tax_ids': [],
|
||||
})]
|
||||
}).action_post()
|
||||
# This bill has two residual amls. One of 1000$ and one of 3000$. Both are waiting for payment and due in 16 and 46 days.
|
||||
# number_waiting += 1, sum_waiting += -4000$, number_late += 0, sum_late += 0$
|
||||
|
||||
self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'journal_id': journal.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2023-03-01',
|
||||
'date': '2023-03-15',
|
||||
'invoice_payment_term_id': twentyfive_now_term.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'name': 'product test 1',
|
||||
'price_unit': 400,
|
||||
'tax_ids': [],
|
||||
})]
|
||||
}).action_post()
|
||||
# This bill has two residual amls. One of 100$ and one of 300$. One is late and due 14 days prior and one which is waiting for payment and due in 15 days.
|
||||
# Even though one entry is late, the entire move isn't considered late since all entries are not.
|
||||
# number_waiting += 1, sum_waiting += -400$, number_late += 0, sum_late += 0$
|
||||
|
||||
self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'journal_id': journal.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2023-02-01',
|
||||
'date': '2023-03-15',
|
||||
'invoice_payment_term_id': twentyfive_now_term.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'name': 'product test 1',
|
||||
'price_unit': 40,
|
||||
'tax_ids': [],
|
||||
})]
|
||||
}).action_post()
|
||||
# This bill has two residual amls. One of 10$ and one of 30$. Both of them are late and due 45 and 15 days prior.
|
||||
# number_waiting += 1, sum_waiting += -40$, number_late += 1, sum_late += -40$
|
||||
|
||||
def assertDashboardPurchaseSaleData(self, journal, number_draft, sum_draft, number_waiting, sum_waiting, number_late, sum_late, currency, **kwargs):
|
||||
expected_values = {
|
||||
'number_draft': number_draft,
|
||||
'sum_draft': format_amount(self.env, sum_draft, currency),
|
||||
'number_waiting': number_waiting,
|
||||
'sum_waiting': format_amount(self.env, sum_waiting, currency),
|
||||
'number_late': number_late,
|
||||
'sum_late': format_amount(self.env, sum_late, currency),
|
||||
**kwargs
|
||||
}
|
||||
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||
self.assertDictEqual({**dashboard_data, **expected_values}, dashboard_data)
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
from contextlib import closing
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.addons.account.models.company import SOFT_LOCK_DATE_FIELDS
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import new_test_user, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountLockException(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.fakenow = cls.env.cr.now()
|
||||
cls.startClassPatcher(freeze_time(cls.fakenow))
|
||||
|
||||
cls.other_user = new_test_user(
|
||||
cls.env,
|
||||
name='Other User',
|
||||
login='other_user',
|
||||
password='password',
|
||||
email='other_user@example.com',
|
||||
group_ids=cls.get_default_groups().ids,
|
||||
company_id=cls.env.company.id,
|
||||
)
|
||||
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
cls.soft_lock_date_info = [
|
||||
('fiscalyear_lock_date', 'out_invoice'),
|
||||
('tax_lock_date', 'out_invoice'),
|
||||
('sale_lock_date', 'out_invoice'),
|
||||
('purchase_lock_date', 'in_invoice'),
|
||||
]
|
||||
|
||||
def test_user_exception_move_edit_multi_user(self):
|
||||
"""
|
||||
Test that an exception for a specific user only works for that user.
|
||||
"""
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an exception to make the move editable (for the current user)
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_move_edit_multi_user',
|
||||
})
|
||||
move.button_draft()
|
||||
move.action_post()
|
||||
|
||||
# Check that the exception does not apply to other users
|
||||
with self.assertRaises(UserError):
|
||||
move.with_user(self.other_user).button_draft()
|
||||
|
||||
def test_global_exception_move_edit_multi_user(self):
|
||||
"""
|
||||
Test that an exception without a specified user works for any user.
|
||||
"""
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add a global exception to make the move editable for everyone
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': False,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_global_exception_move_edit_multi_user',
|
||||
})
|
||||
|
||||
move.button_draft()
|
||||
move.action_post()
|
||||
|
||||
move.with_user(self.other_user).button_draft()
|
||||
|
||||
|
||||
def test_user_exception_branch(self):
|
||||
"""
|
||||
Test that the locking and exception mechanism works correctly in company hierarchies.
|
||||
* A lock in the branch does not lock the parent.
|
||||
* A lock in the parent also locks the branch.
|
||||
* An exception in the branch does not matter for the lock in the parent.
|
||||
* Let both parent and branch be locked.
|
||||
To make changes in the locked period in the brranch we need exceptions in both companies.
|
||||
"""
|
||||
|
||||
root_company = self.company_data['company']
|
||||
root_company.write({'child_ids': [Command.create({'name': 'branch'})]})
|
||||
self.cr.precommit.run() # load the CoA
|
||||
branch = root_company.child_ids
|
||||
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
# Create a move in the branch
|
||||
branch_move = self.init_invoice(
|
||||
move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a, company=branch,
|
||||
)
|
||||
|
||||
# Create a move in the parent company
|
||||
root_move = self.init_invoice(
|
||||
move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a, company=root_company,
|
||||
)
|
||||
|
||||
# Lock the branch
|
||||
branch[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
|
||||
# The branch_move is locked while the root_move is not
|
||||
with self.assertRaises(UserError):
|
||||
branch_move.button_draft()
|
||||
root_move.button_draft()
|
||||
root_move.action_post()
|
||||
|
||||
# Add an exception in the branch to make the branch_move editable (for the current user)
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': branch.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_branch branch exception',
|
||||
})
|
||||
branch_move.button_draft()
|
||||
branch_move.action_post()
|
||||
|
||||
# Lock the parent company
|
||||
root_company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
|
||||
# Check that both moves are locked now (the branch exception alone is insufficient)
|
||||
for move in [branch_move, root_move]:
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an exception in the parent company to make both moves editable (for the current user)
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': root_company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_branch root_company exception',
|
||||
})
|
||||
for move in [branch_move, root_move]:
|
||||
move.button_draft()
|
||||
move.action_post()
|
||||
|
||||
|
||||
def test_user_exception_wrong_company(self):
|
||||
"""
|
||||
Test that an exception only works for the specified company.
|
||||
"""
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an exception for another company
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': self.company_data_2['company'].id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_move_edit_multi_user',
|
||||
})
|
||||
|
||||
# Check that the exception is insufficient
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
|
||||
def test_user_exception_insufficient(self):
|
||||
"""
|
||||
Test that the exception only works if the specified lock date is actually before the accounting date.
|
||||
"""
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an exception before the lock date but not before the date of the test_invoice
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2016-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_move_edit_multi_user',
|
||||
})
|
||||
|
||||
# Check that the exception is insufficient
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
|
||||
def test_expired_exception(self):
|
||||
"""
|
||||
Test that the exception does not work if we are past the `end_datetime` of the exception.
|
||||
"""
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an expired exception
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'create_date': self.fakenow - timedelta(hours=24),
|
||||
'end_datetime': self.fakenow - timedelta(seconds=1),
|
||||
'reason': 'test_expired_exception',
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
|
||||
def test_revoked_exception(self):
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an exception to make the move editable (for the current user)
|
||||
exception = self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_move_edit_multi_user',
|
||||
})
|
||||
move.button_draft()
|
||||
move.action_post()
|
||||
|
||||
exception.action_revoke()
|
||||
|
||||
# Check that the exception does not work anymore
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
|
||||
def test_user_exception_wrong_field(self):
|
||||
for lock_date_field, move_type, exception_lock_date_field in [
|
||||
('fiscalyear_lock_date', 'out_invoice', 'tax_lock_date'),
|
||||
('tax_lock_date', 'out_invoice', 'fiscalyear_lock_date'),
|
||||
('sale_lock_date', 'out_invoice', 'purchase_lock_date'),
|
||||
('purchase_lock_date', 'in_invoice', 'sale_lock_date'),
|
||||
]:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an exception for a different lock date field
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': self.company_data_2['company'].id,
|
||||
'user_id': self.env.user.id,
|
||||
exception_lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_wrong_field',
|
||||
})
|
||||
|
||||
# Check that the exception is insufficient
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
|
||||
def test_hard_lock_date(self):
|
||||
"""
|
||||
Test that
|
||||
* exceptions (for other lock date fields) do not allow bypassing the hard lock date
|
||||
* the hard lock date cannot be decreased or removed
|
||||
"""
|
||||
in_move = self.init_invoice('in_invoice', invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
out_move = self.init_invoice('out_invoice', invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
|
||||
self.company.hard_lock_date = fields.Date.to_date('2020-01-01')
|
||||
|
||||
# Check that we cannot remove the hard lock date.
|
||||
with self.assertRaises(UserError):
|
||||
self.company.hard_lock_date = False
|
||||
|
||||
# Check that we cannot decrease the hard lock date.
|
||||
with self.assertRaises(UserError):
|
||||
self.company.hard_lock_date = fields.Date.to_date('2019-01-01')
|
||||
|
||||
# Create exceptions for all lock date fields except the hard lock date
|
||||
self.env['account.lock_exception'].create([
|
||||
{
|
||||
'company_id': self.company_data_2['company'].id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': f'test_hard_lock_ignores_exceptions {lock_date_field}',
|
||||
}
|
||||
for lock_date_field in SOFT_LOCK_DATE_FIELDS
|
||||
])
|
||||
|
||||
# Check that the exceptions are insufficient
|
||||
for move in [in_move, out_move]:
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
def test_company_lock_date(self):
|
||||
"""
|
||||
Test the `company_lock_date` field is set corretly on exception creation.
|
||||
Test the behavior when a company lock date is changed.
|
||||
* Every active exception gets revoked and recreated with the new company lock date
|
||||
* Non-active exceptions are not affected
|
||||
"""
|
||||
self.env['account.lock_exception'].search([]).sudo().unlink()
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
|
||||
revoked_exception = self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_exception_recreated_on_lock_date_change revoked',
|
||||
})
|
||||
revoked_exception.action_revoke()
|
||||
active_exception = self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_exception_recreated_on_lock_date_change active',
|
||||
})
|
||||
|
||||
# Check that the company lock date field was set correcyly on exception creation
|
||||
self.assertEqual(revoked_exception.company_lock_date, fields.Date.to_date('2020-01-01'))
|
||||
self.assertEqual(active_exception.company_lock_date, fields.Date.to_date('2020-01-01'))
|
||||
|
||||
# The lock date change should trigger the "recreation" proces
|
||||
self.company[lock_date_field] = fields.Date.to_date('2021-01-01')
|
||||
|
||||
self.assertEqual(revoked_exception.company_lock_date, fields.Date.to_date('2020-01-01'))
|
||||
|
||||
self.assertEqual(active_exception.state, 'revoked')
|
||||
|
||||
exceptions = self.env['account.lock_exception'].with_context(active_test=False).search([])
|
||||
self.assertEqual(len(exceptions), 3)
|
||||
new_exception = exceptions - revoked_exception - active_exception
|
||||
# Check that the new exception is a "recreation" of the `active_exception`
|
||||
self.assertRecordValues(new_exception, [{
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: fields.Date.to_date('2010-01-01'),
|
||||
'company_lock_date': fields.Date.to_date('2021-01-01'),
|
||||
'end_datetime': self.env.cr.now() + timedelta(hours=24),
|
||||
'reason': 'test_exception_recreated_on_lock_date_change active',
|
||||
}])
|
||||
|
||||
|
||||
def test_user_exception_remove_lock_date(self):
|
||||
"""
|
||||
Test that an exception removing a lock date (instead of just decreasing it) works.
|
||||
"""
|
||||
for lock_date_field, move_type in self.soft_lock_date_info:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type), closing(self.cr.savepoint()):
|
||||
move = self.init_invoice(move_type, invoice_date='2016-01-01', post=True, amounts=[1000.0], taxes=self.tax_sale_a)
|
||||
|
||||
# Lock the move
|
||||
self.company[lock_date_field] = fields.Date.to_date('2020-01-01')
|
||||
with self.assertRaises(UserError):
|
||||
move.button_draft()
|
||||
|
||||
# Add an exception removing the lock date
|
||||
self.env['account.lock_exception'].create({
|
||||
'company_id': self.company.id,
|
||||
'user_id': self.env.user.id,
|
||||
lock_date_field: False,
|
||||
'end_datetime': self.fakenow + timedelta(hours=24),
|
||||
'reason': 'test_user_exception_move_edit_multi_user',
|
||||
})
|
||||
move.button_draft()
|
||||
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import TestAccountMergeCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMergeWizard(TestAccountMergeCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
cls.company_1 = cls.company_data['company']
|
||||
cls.company_2 = cls.company_data_2['company']
|
||||
cls.accounts = cls.env['account.account']._load_records([
|
||||
{
|
||||
'xml_id': f'account.{cls.company_1.id}_test_account_1',
|
||||
'values': {
|
||||
'name': 'My First Account',
|
||||
'code': '100234',
|
||||
'account_type': 'asset_receivable',
|
||||
'company_ids': [Command.link(cls.company_1.id)],
|
||||
'tax_ids': [Command.link(cls.company_data['default_tax_sale'].id)],
|
||||
'tag_ids': [Command.link(cls.env.ref('account.account_tag_operating').id)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'xml_id': f'account.{cls.company_1.id}_test_account_2',
|
||||
'values': {
|
||||
'name': 'My Second Account',
|
||||
'code': '100235',
|
||||
'account_type': 'liability_payable',
|
||||
'company_ids': [Command.link(cls.company_1.id)],
|
||||
'tax_ids': [Command.link(cls.company_data['default_tax_sale'].id)],
|
||||
'tag_ids': [Command.link(cls.env.ref('account.account_tag_operating').id)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'xml_id': f'account.{cls.company_2.id}_test_account_3',
|
||||
'values': {
|
||||
'name': 'My Third Account',
|
||||
'code': '100236',
|
||||
'account_type': 'asset_receivable',
|
||||
'company_ids': [Command.link(cls.company_2.id)],
|
||||
'tax_ids': [Command.link(cls.company_data_2['default_tax_sale'].id)],
|
||||
'tag_ids': [Command.link(cls.env.ref('account.account_tag_investing').id)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'xml_id': f'account.{cls.company_2.id}_test_account_4',
|
||||
'values': {
|
||||
'name': 'My Fourth Account',
|
||||
'code': '100237',
|
||||
'account_type': 'liability_payable',
|
||||
'company_ids': [Command.link(cls.company_2.id)],
|
||||
'tax_ids': [Command.link(cls.company_data_2['default_tax_sale'].id)],
|
||||
'tag_ids': [Command.link(cls.env.ref('account.account_tag_investing').id)],
|
||||
},
|
||||
}
|
||||
])
|
||||
|
||||
def _create_hashed_move(self, account, company_data):
|
||||
hashed_move = self.env['account.move'].create([
|
||||
{
|
||||
'journal_id': company_data['default_journal_sale'].id,
|
||||
'date': '2024-07-20',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'account_id': account.id,
|
||||
'balance': 10.0,
|
||||
}),
|
||||
Command.create({
|
||||
'account_id': company_data['default_account_receivable'].id,
|
||||
'balance': -10.0,
|
||||
})
|
||||
]
|
||||
},
|
||||
])
|
||||
hashed_move.action_post()
|
||||
company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||||
hashed_move.button_hash()
|
||||
|
||||
def test_merge(self):
|
||||
""" Check that you can merge accounts. """
|
||||
# 1. Set-up various fields pointing to the accounts to merge
|
||||
referencing_records = {
|
||||
account: self._create_references_to_account(account)
|
||||
for account in self.accounts
|
||||
}
|
||||
|
||||
# Also set up different names for the accounts in various languages
|
||||
self.env['res.lang']._activate_lang('fr_FR')
|
||||
self.env['res.lang']._activate_lang('nl_NL')
|
||||
self.accounts[0].with_context({'lang': 'fr_FR'}).name = "Mon premier compte"
|
||||
self.accounts[2].with_context({'lang': 'fr_FR'}).name = "Mon troisième compte"
|
||||
self.accounts[2].with_context({'lang': 'nl_NL'}).name = "Mijn derde conto"
|
||||
|
||||
# 2. Check that the merge wizard groups accounts 1 and 3 together, and accounts 2 and 4 together.
|
||||
wizard = self._create_account_merge_wizard(self.accounts)
|
||||
expected_wizard_line_vals = [
|
||||
{
|
||||
'display_type': 'line_section',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': 'Trade Receivable (Reconcilable)',
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': False,
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[2].id,
|
||||
'info': False,
|
||||
},
|
||||
{
|
||||
'display_type': 'line_section',
|
||||
'account_id': self.accounts[1].id,
|
||||
'info': 'Trade Payable (Reconcilable)',
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[1].id,
|
||||
'info': False,
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[3].id,
|
||||
'info': False,
|
||||
},
|
||||
]
|
||||
|
||||
self.assertRecordValues(wizard.wizard_line_ids, expected_wizard_line_vals)
|
||||
|
||||
# 3. Perform the merge
|
||||
wizard.action_merge()
|
||||
|
||||
# 4. Check that the accounts other than the ones to merge into are deleted.
|
||||
self.assertFalse(self.accounts[2:].exists())
|
||||
|
||||
# 5. Check that the company_ids and codes are correctly merged.
|
||||
self.assertRecordValues(
|
||||
self.accounts[:2],
|
||||
[
|
||||
{
|
||||
'company_ids': [self.company_1.id, self.company_2.id],
|
||||
'name': 'My First Account',
|
||||
'code': '100234',
|
||||
'tax_ids': [self.company_data['default_tax_sale'].id, self.company_data_2['default_tax_sale'].id],
|
||||
'tag_ids': [self.env.ref('account.account_tag_operating').id, self.env.ref('account.account_tag_investing').id],
|
||||
},
|
||||
{
|
||||
'company_ids': [self.company_1.id, self.company_2.id],
|
||||
'name': 'My Second Account',
|
||||
'code': '100235',
|
||||
'tax_ids': [self.company_data['default_tax_sale'].id, self.company_data_2['default_tax_sale'].id],
|
||||
'tag_ids': [self.env.ref('account.account_tag_operating').id, self.env.ref('account.account_tag_investing').id],
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertRecordValues(
|
||||
self.accounts[:2].with_company(self.company_2),
|
||||
[{'code': '100236'}, {'code': '100237'}]
|
||||
)
|
||||
|
||||
# 6. Check that references to the accounts are merged correctly
|
||||
merged_account_by_account = {
|
||||
self.accounts[0]: self.accounts[0],
|
||||
self.accounts[1]: self.accounts[1],
|
||||
self.accounts[2]: self.accounts[0],
|
||||
self.accounts[3]: self.accounts[1],
|
||||
}
|
||||
for account, referencing_records_for_account in referencing_records.items():
|
||||
expected_account = merged_account_by_account[account]
|
||||
for referencing_record, fname in referencing_records_for_account.items():
|
||||
expected_field_value = expected_account.ids if referencing_record._fields[fname].type == 'many2many' else expected_account.id
|
||||
self.assertRecordValues(referencing_record, [{fname: expected_field_value}])
|
||||
|
||||
# 7. Check that the xmlids are preserved
|
||||
self.assertEqual(self.env['account.chart.template'].ref('test_account_1'), self.accounts[0])
|
||||
self.assertEqual(self.env['account.chart.template'].ref('test_account_2'), self.accounts[1])
|
||||
self.assertEqual(self.env['account.chart.template'].with_company(self.company_2).ref('test_account_3'), self.accounts[0])
|
||||
self.assertEqual(self.env['account.chart.template'].with_company(self.company_2).ref('test_account_4'), self.accounts[1])
|
||||
|
||||
# 8. Check that the name translations are merged correctly
|
||||
self.assertRecordValues(self.accounts[0].with_context({'lang': 'fr_FR'}), [{'name': "Mon premier compte"}])
|
||||
self.assertRecordValues(self.accounts[0].with_context({'lang': 'nl_NL'}), [{'name': "Mijn derde conto"}])
|
||||
|
||||
def test_cannot_merge_same_company(self):
|
||||
""" Check that you cannot merge two accounts belonging to the same company. """
|
||||
self.accounts[1].account_type = 'asset_receivable'
|
||||
|
||||
wizard = self._create_account_merge_wizard(self.accounts[:2])
|
||||
|
||||
expected_wizard_line_vals = [
|
||||
{
|
||||
'display_type': 'line_section',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': 'Trade Receivable (Reconcilable)',
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': False,
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[1].id,
|
||||
'info': "Belongs to the same company as 100234 My First Account.",
|
||||
},
|
||||
]
|
||||
|
||||
self.assertRecordValues(wizard.wizard_line_ids, expected_wizard_line_vals)
|
||||
|
||||
def test_can_merge_accounts_if_one_is_hashed(self):
|
||||
""" Check that you can merge two accounts if only one is hashed, but that the hashed account's ID is preserved. """
|
||||
|
||||
# 1. Create hashed move and check that the wizard has no errors
|
||||
self._create_hashed_move(self.accounts[2], self.company_data_2)
|
||||
wizard = self._create_account_merge_wizard(self.accounts[0] | self.accounts[2])
|
||||
|
||||
expected_wizard_line_vals = [
|
||||
{
|
||||
'display_type': 'line_section',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': 'Trade Receivable (Reconcilable)',
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': False,
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[2].id,
|
||||
'info': False,
|
||||
},
|
||||
]
|
||||
|
||||
self.assertRecordValues(wizard.wizard_line_ids, expected_wizard_line_vals)
|
||||
|
||||
# 2. Perform the merge
|
||||
wizard.action_merge()
|
||||
|
||||
# 3. Check that the non-hashed account is deleted.
|
||||
self.assertFalse(self.accounts[0].exists())
|
||||
|
||||
def test_cannot_merge_two_hashed_accounts(self):
|
||||
""" Check that you cannot merge two accounts if both are hashed. """
|
||||
self._create_hashed_move(self.accounts[0], self.company_data)
|
||||
self._create_hashed_move(self.accounts[2], self.company_data_2)
|
||||
|
||||
wizard = self._create_account_merge_wizard(self.accounts[0] | self.accounts[2])
|
||||
|
||||
expected_wizard_line_vals = [
|
||||
{
|
||||
'display_type': 'line_section',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': 'Trade Receivable (Reconcilable)',
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[0].id,
|
||||
'info': False,
|
||||
},
|
||||
{
|
||||
'display_type': 'account',
|
||||
'account_id': self.accounts[2].id,
|
||||
'info': "Contains hashed entries, but 100234 My First Account also has hashed entries.",
|
||||
},
|
||||
]
|
||||
|
||||
self.assertRecordValues(wizard.wizard_line_ids, expected_wizard_line_vals)
|
||||
|
||||
def test_merge_accounts_company_dependent_related(self):
|
||||
payable_accounts = self.env['account.account'].search([('name', '=', 'Account Payable')])
|
||||
self.assertEqual(len(payable_accounts), 2)
|
||||
wizard = self._create_account_merge_wizard(payable_accounts)
|
||||
wizard.action_merge()
|
||||
payable_accounts = self.env['account.account'].search([('name', '=', 'Account Payable')])
|
||||
self.assertEqual(len(payable_accounts), 1)
|
||||
for company in self.env.companies:
|
||||
partner_payable_account = self.partner_a.with_company(company).property_account_payable_id.exists()
|
||||
self.assertEqual(partner_payable_account, payable_accounts)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from odoo import http
|
||||
from odoo.tests import tagged, HttpCase
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestAccountMoveAttachment(HttpCase):
|
||||
|
||||
def test_preserving_manually_added_attachments(self):
|
||||
""" Preserve attachments manually added (not coming from emails) to an invoice """
|
||||
self.authenticate("admin", "admin")
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
})
|
||||
self.assertFalse(invoice.attachment_ids)
|
||||
response = self.url_open("/mail/attachment/upload",
|
||||
{
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
"thread_id": invoice.id,
|
||||
"thread_model": "account.move",
|
||||
},
|
||||
files={'ufile': ('salut.txt', b"Salut !\n", 'text/plain')},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(invoice.attachment_ids)
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from contextlib import closing
|
||||
|
||||
import freezegun
|
||||
|
||||
from odoo import fields, Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
import freezegun
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPERS
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
@ -32,17 +39,18 @@ class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
def _create_payment(self, date, **kwargs):
|
||||
return self.env['account.payment'].create({
|
||||
payment = self.env['account.payment'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
**kwargs,
|
||||
'date': date,
|
||||
})
|
||||
payment.action_post()
|
||||
return payment
|
||||
|
||||
def _set_lock_date(self, lock_date, period_lock_date=None):
|
||||
def _set_lock_date(self, lock_date):
|
||||
self.env.company.fiscalyear_lock_date = fields.Date.from_string(lock_date)
|
||||
self.env.company.period_lock_date = fields.Date.from_string(period_lock_date)
|
||||
|
||||
def _reverse_invoice(self, invoice):
|
||||
move_reversal = self.env['account.move.reversal']\
|
||||
|
|
@ -50,9 +58,8 @@ class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
|
|||
.create({
|
||||
'journal_id': invoice.journal_id.id,
|
||||
'reason': "no reason",
|
||||
'refund_method': 'cancel',
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
reversal = move_reversal.refund_moves()
|
||||
return self.env['account.move'].browse(reversal['res_id'])
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
@ -147,31 +154,30 @@ class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
|
|||
|
||||
@freezegun.freeze_time('2017-02-12')
|
||||
def test_reconcile_with_lock_date(self):
|
||||
invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.currency_data['currency'].id)
|
||||
refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.currency_data['currency'].id)
|
||||
invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.other_currency.id)
|
||||
refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.other_currency.id)
|
||||
(invoice + refund).action_post()
|
||||
self._set_lock_date('2017-01-31')
|
||||
|
||||
res = (invoice + refund).line_ids\
|
||||
.filtered(lambda x: x.account_id.account_type == 'asset_receivable')\
|
||||
.reconcile()
|
||||
exchange_move = res['partials'].exchange_move_id
|
||||
amls = (invoice + refund).line_ids.filtered(lambda x: x.account_id.account_type == 'asset_receivable')
|
||||
amls.reconcile()
|
||||
exchange_move = amls.matched_debit_ids.exchange_move_id
|
||||
|
||||
self.assertRecordValues(exchange_move, [{
|
||||
'date': fields.Date.from_string('2017-02-01'),
|
||||
'date': fields.Date.from_string('2017-02-12'),
|
||||
'amount_total_signed': 200.0,
|
||||
}])
|
||||
|
||||
@freezegun.freeze_time('2017-02-12')
|
||||
def test_unreconcile_with_lock_date(self):
|
||||
invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.currency_data['currency'].id)
|
||||
refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.currency_data['currency'].id)
|
||||
invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.other_currency.id)
|
||||
refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.other_currency.id)
|
||||
(invoice + refund).action_post()
|
||||
|
||||
res = (invoice + refund).line_ids\
|
||||
.filtered(lambda x: x.account_id.account_type == 'asset_receivable')\
|
||||
.reconcile()
|
||||
exchange_move = res['partials'].exchange_move_id
|
||||
amls = (invoice + refund).line_ids.filtered(lambda x: x.account_id.account_type == 'asset_receivable')
|
||||
amls.reconcile()
|
||||
exchange_move = amls.matched_debit_ids.exchange_move_id
|
||||
self.assertEqual(exchange_move.state, 'posted')
|
||||
|
||||
self._set_lock_date('2017-01-31')
|
||||
(invoice + refund).line_ids.remove_move_reconcile()
|
||||
|
|
@ -201,11 +207,11 @@ class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
|
|||
|
||||
invoice = self._create_invoice(
|
||||
'out_invoice', '2016-01-01',
|
||||
currency_id=self.currency_data['currency'].id,
|
||||
currency_id=self.other_currency.id,
|
||||
invoice_line_ids=[{'tax_ids': [Command.set(tax.ids)]}],
|
||||
)
|
||||
payment = self._create_payment('2016-02-01', amount=invoice.amount_total)
|
||||
(invoice + payment.move_id).action_post()
|
||||
invoice.action_post()
|
||||
|
||||
self._set_lock_date('2017-01-03')
|
||||
|
||||
|
|
@ -236,9 +242,9 @@ class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
|
|||
@freezegun.freeze_time('2023-05-01')
|
||||
def test_caba_with_different_lock_dates(self):
|
||||
"""
|
||||
Test the date of the CABA move when reconciling a payment with an invoice
|
||||
with date before fiscalyear_period but after period_lock_date either when
|
||||
having accountant rights or not.
|
||||
Test the date of the CABA move when reconciling a payment in case the lock dates
|
||||
are different between post and reconciliation time (caba move creation time).
|
||||
Ensure that user groups (accountant rights) do not matter.
|
||||
"""
|
||||
self.env.company.tax_exigibility = True
|
||||
|
||||
|
|
@ -256,30 +262,54 @@ class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
|
|||
'cash_basis_transition_account_id': tax_waiting_account.id,
|
||||
})
|
||||
|
||||
self._set_lock_date('2023-01-01', '2023-02-01')
|
||||
|
||||
for group, expected_date in (
|
||||
('account.group_account_manager', '2023-01-30'),
|
||||
('account.group_account_invoice', '2023-05-01'),
|
||||
# User groups do not matter
|
||||
for group in (
|
||||
'account.group_account_manager',
|
||||
'account.group_account_invoice',
|
||||
):
|
||||
with self.subTest(group=group, expected_date=expected_date):
|
||||
self.env.user.groups_id = [Command.set(self.env.ref(group).ids)]
|
||||
with self.subTest(group=group), closing(self.cr.savepoint()):
|
||||
self.env.user.group_ids = [Command.set(self.env.ref(group).ids)]
|
||||
|
||||
self.assertTrue(self.env.user.user_has_groups(group))
|
||||
self.assertTrue(self.env.user.has_group(group))
|
||||
|
||||
invoice = self._create_invoice(
|
||||
'out_invoice', '2023-01-02',
|
||||
invoice_line_ids=[{'tax_ids': [Command.set(tax.ids)]}],
|
||||
)
|
||||
payment = self._create_payment('2023-01-30', amount=invoice.amount_total)
|
||||
(invoice + payment.move_id).action_post()
|
||||
|
||||
self.env.company.sudo().sale_lock_date = fields.Date.to_date('2023-02-01')
|
||||
invoice.action_post()
|
||||
self.assertEqual(invoice.date.isoformat(), '2023-02-28')
|
||||
self.assertEqual(payment.move_id.date.isoformat(), '2023-01-30')
|
||||
|
||||
self.env.company.sudo().sale_lock_date = fields.Date.to_date('2023-03-01')
|
||||
(invoice + payment.move_id).line_ids\
|
||||
.filtered(lambda x: x.account_id.account_type == 'asset_receivable')\
|
||||
.reconcile()
|
||||
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)])
|
||||
|
||||
self.assertRecordValues(caba_move, [{
|
||||
'date': fields.Date.from_string(expected_date),
|
||||
}])
|
||||
# The sale lock date does not matter for the caba move, since it is not in a sale journal
|
||||
self.assertEqual(caba_move.journal_id.type, 'general')
|
||||
self.assertEqual(caba_move.date.isoformat(), '2023-02-28')
|
||||
|
||||
@freezegun.freeze_time('2024-08-05')
|
||||
def test_lock_date_exceptions(self):
|
||||
for lock_date_field, move_type in [
|
||||
('fiscalyear_lock_date', 'out_invoice'),
|
||||
('tax_lock_date', 'out_invoice'),
|
||||
('sale_lock_date', 'out_invoice'),
|
||||
('purchase_lock_date', 'in_invoice'),
|
||||
]:
|
||||
with self.subTest(lock_date_field=lock_date_field, move_type=move_type):
|
||||
self.env.company[lock_date_field] = '2024-07-31'
|
||||
self.env['account.lock_exception'].create({
|
||||
lock_date_field: fields.Date.to_date('2024-01-01'),
|
||||
'end_datetime': False,
|
||||
})
|
||||
move = self.init_invoice(
|
||||
move_type, amounts=[100], taxes=self.env.company.account_sale_tax_id,
|
||||
invoice_date='2024-07-01', post=True
|
||||
)
|
||||
self.assertEqual(move.date, fields.Date.to_date('2024-07-01'))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMoveDuplicate(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.invoice = cls.init_invoice('in_invoice', products=cls.product_a + cls.product_b)
|
||||
|
||||
def test_in_invoice_single_duplicate_reference(self):
|
||||
""" Ensure duplicated ref are computed correctly in this simple case (in_invoice)"""
|
||||
bill_1 = self.invoice
|
||||
bill_1.ref = 'a unique supplier reference that will be copied'
|
||||
bill_2 = bill_1.copy(default={'invoice_date': bill_1.invoice_date})
|
||||
# ensure no Error is raised
|
||||
bill_2.ref = bill_1.ref
|
||||
self.assertRecordValues(bill_2, [{'duplicated_ref_ids': bill_1.ids}])
|
||||
|
||||
def test_out_invoice_single_duplicate_reference(self):
|
||||
"""
|
||||
Ensure duplicated move are computed correctly in this simple case (out_invoice).
|
||||
For it to be a duplicate, the partner, the invoice date and the amount total must be the same.
|
||||
"""
|
||||
invoice_1 = self.init_invoice(
|
||||
move_type='out_invoice',
|
||||
products=self.product_a,
|
||||
invoice_date='2023-01-01'
|
||||
)
|
||||
invoice_2 = invoice_1.copy(default={'invoice_date': invoice_1.invoice_date})
|
||||
self.assertRecordValues(invoice_2, [{'duplicated_ref_ids': invoice_1.ids}])
|
||||
|
||||
# Different date but same product and same partner, no duplicate
|
||||
invoice_3 = invoice_1.copy(default={'invoice_date': '2023-12-31'})
|
||||
self.assertRecordValues(invoice_3, [{'duplicated_ref_ids': []}])
|
||||
|
||||
# Different product and same partner and same date, no duplicate
|
||||
invoice_4 = invoice_1 = self.init_invoice(
|
||||
move_type='out_invoice',
|
||||
products=self.product_b,
|
||||
invoice_date='2023-01-01'
|
||||
)
|
||||
self.assertRecordValues(invoice_4, [{'duplicated_ref_ids': []}])
|
||||
|
||||
def test_in_invoice_single_duplicate_reference_with_form(self):
|
||||
""" Ensure duplicated ref are computed correctly with UI's NEW_ID"""
|
||||
invoice_1 = self.invoice
|
||||
invoice_1.ref = 'a unique supplier reference that will be copied'
|
||||
move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
|
||||
move_form.partner_id = self.partner_a
|
||||
move_form.invoice_date = invoice_1.invoice_date
|
||||
move_form.ref = invoice_1.ref
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.product_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.product_b
|
||||
invoice_2 = move_form.save()
|
||||
self.assertRecordValues(invoice_2, [{'duplicated_ref_ids': invoice_1.ids}])
|
||||
|
||||
def test_in_invoice_multiple_duplicate_reference_batch(self):
|
||||
""" Ensure duplicated ref are computed correctly even when updated in batch"""
|
||||
invoice_1 = self.invoice
|
||||
invoice_1.ref = 'a unique supplier reference that will be copied'
|
||||
invoice_2 = invoice_1.copy(default={'invoice_date': invoice_1.invoice_date})
|
||||
invoice_3 = invoice_1.copy(default={'invoice_date': invoice_1.invoice_date})
|
||||
|
||||
# reassign to trigger the compute method
|
||||
invoices = invoice_1 + invoice_2 + invoice_3
|
||||
invoices.ref = invoice_1.ref
|
||||
self.assertRecordValues(invoices, [
|
||||
{'duplicated_ref_ids': (invoice_2 + invoice_3).ids},
|
||||
{'duplicated_ref_ids': (invoice_1 + invoice_3).ids},
|
||||
{'duplicated_ref_ids': (invoice_1 + invoice_2).ids},
|
||||
])
|
||||
|
||||
def test_in_invoice_multiple_duplicate_reference_batch_in_edit_mode(self):
|
||||
"""
|
||||
Ensure duplicated ref are computed correctly even when updated in batch
|
||||
when they are in edit mode
|
||||
"""
|
||||
invoice_1 = self.invoice
|
||||
invoice_1.ref = 'a unique supplier reference that will be copied'
|
||||
invoice_2 = invoice_1.copy(default={'invoice_date': invoice_1.invoice_date})
|
||||
invoices_new = self.env['account.move'].browse([
|
||||
self.env['account.move'].new(origin=inv).id for inv in (invoice_1, invoice_2)
|
||||
])
|
||||
# reassign to trigger the compute method
|
||||
invoices_new.ref = invoice_1.ref
|
||||
self.assertRecordValues(invoices_new, [
|
||||
{'duplicated_ref_ids': (invoices_new[1]).ids},
|
||||
{'duplicated_ref_ids': (invoices_new[0]).ids},
|
||||
])
|
||||
|
||||
def test_in_invoice_single_duplicate_reference_diff_date(self):
|
||||
""" Ensure duplicated ref are computed correctly for different dates"""
|
||||
bill1 = self.invoice.copy({'invoice_date': self.invoice.invoice_date})
|
||||
bill1.ref = 'bill1'
|
||||
|
||||
# Same ref but different year -> Not duplicated
|
||||
bill2 = bill1.copy({'invoice_date': '2020-01-01'})
|
||||
bill2.ref = bill1.ref
|
||||
self.assertNotIn(bill1, bill2.duplicated_ref_ids)
|
||||
self.assertNotIn(bill2, bill1.duplicated_ref_ids)
|
||||
|
||||
# Same ref and same year -> Duplicated
|
||||
bill3 = bill1.copy({'invoice_date': f"{bill1.invoice_date.year}-04-11"})
|
||||
bill3.ref = bill1.ref
|
||||
self.assertEqual(bill3.duplicated_ref_ids, bill1)
|
||||
|
||||
# Even after posting
|
||||
bill3.action_post()
|
||||
self.assertEqual(bill3.duplicated_ref_ids, bill1)
|
||||
|
||||
# Same ref and no invoice date -> Duplicated
|
||||
bill4 = self.invoice.copy()
|
||||
bill4.ref = "bill4"
|
||||
bill5 = bill4.copy()
|
||||
bill5.ref = bill4.ref
|
||||
self.assertEqual(bill5.duplicated_ref_ids, bill4)
|
||||
|
||||
def test_in_invoice_single_duplicate_no_reference(self):
|
||||
""" Ensure duplicated bills are recognized with or without
|
||||
a reference when the amount, date, partner are equal"""
|
||||
bill1 = self.invoice.copy({'invoice_date': '2020-01-01'})
|
||||
bill2 = bill1.copy()
|
||||
|
||||
# trigger compute method
|
||||
all_bills = bill1 + bill2
|
||||
all_bills.invoice_date = self.invoice.invoice_date
|
||||
|
||||
# Assert duplicates when there is no ref
|
||||
self.assertIn(bill1, bill2.duplicated_ref_ids)
|
||||
self.assertIn(bill2, bill1.duplicated_ref_ids)
|
||||
|
||||
# Assert duplicates when there is one ref
|
||||
bill1.update({'ref': "bill1 ref"})
|
||||
bill2.update({'ref': bill2.ref})
|
||||
self.assertIn(bill1, bill2.duplicated_ref_ids)
|
||||
self.assertIn(bill2, bill1.duplicated_ref_ids)
|
||||
|
||||
# Assert duplicated when different refs
|
||||
bill1.update({'ref': bill1.ref})
|
||||
bill2.update({'ref': "bill2 ref"})
|
||||
self.assertIn(bill1, bill2.duplicated_ref_ids)
|
||||
self.assertIn(bill2, bill1.duplicated_ref_ids)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,30 @@
|
|||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAccountMoveImportTemplate(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.AccountMove = self.env['account.move']
|
||||
|
||||
def fetch_template_for_type(self, move_type):
|
||||
return self.AccountMove.with_context(default_move_type=move_type).get_import_templates()
|
||||
|
||||
def test_import_template(self):
|
||||
|
||||
def test_template(move_type, file_name):
|
||||
template = self.fetch_template_for_type(move_type)
|
||||
self.assertEqual(len(template), 1)
|
||||
self.assertEqual(template[0].get('template'), file_name)
|
||||
|
||||
test_template('entry', '/account/static/xls/misc_operations_import_template.xlsx')
|
||||
test_template('out_invoice', '/account/static/xls/customer_invoices_credit_notes_import_template.xlsx')
|
||||
test_template('out_refund', '/account/static/xls/customer_invoices_credit_notes_import_template.xlsx')
|
||||
test_template('in_invoice', '/account/static/xls/vendor_bills_refunds_import_template.xlsx')
|
||||
test_template('in_refund', '/account/static/xls/vendor_bills_refunds_import_template.xlsx')
|
||||
|
||||
template = self.fetch_template_for_type('unknown_type')
|
||||
self.assertEqual(template, [])
|
||||
template = self.fetch_template_for_type(None)
|
||||
self.assertEqual(template, [])
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,8 +3,7 @@
|
|||
from lxml import etree
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo import fields, Command
|
||||
|
||||
from collections import defaultdict
|
||||
|
|
@ -14,13 +13,14 @@ from collections import defaultdict
|
|||
class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
|
||||
cls.invoice = cls.init_invoice('in_refund', products=cls.product_a+cls.product_b)
|
||||
|
||||
cls.product_line_vals_1 = {
|
||||
'name': cls.product_a.name,
|
||||
'product_id': cls.product_a.id,
|
||||
'account_id': cls.product_a.property_account_expense_id.id,
|
||||
'partner_id': cls.partner_a.id,
|
||||
|
|
@ -39,7 +39,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'date_maturity': False,
|
||||
}
|
||||
cls.product_line_vals_2 = {
|
||||
'name': cls.product_b.name,
|
||||
'name': 'product_b',
|
||||
'product_id': cls.product_b.id,
|
||||
'account_id': cls.product_b.property_account_expense_id.id,
|
||||
'partner_id': cls.partner_a.id,
|
||||
|
|
@ -96,7 +96,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'date_maturity': False,
|
||||
}
|
||||
cls.term_line_vals_1 = {
|
||||
'name': '',
|
||||
'name': False,
|
||||
'product_id': False,
|
||||
'account_id': cls.company_data['default_account_payable'].id,
|
||||
'partner_id': cls.partner_a.id,
|
||||
|
|
@ -120,13 +120,16 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'journal_id': cls.company_data['default_journal_purchase'].id,
|
||||
'invoice_date': fields.Date.from_string('2019-01-01'),
|
||||
'fiscal_position_id': False,
|
||||
'payment_reference': '',
|
||||
'payment_reference': False,
|
||||
'invoice_payment_term_id': cls.pay_terms_a.id,
|
||||
'amount_untaxed': 960.0,
|
||||
'amount_tax': 168.0,
|
||||
'amount_total': 1128.0,
|
||||
}
|
||||
(cls.tax_armageddon + cls.tax_armageddon.children_tax_ids).write({'type_tax_use': 'purchase'})
|
||||
|
||||
@classmethod
|
||||
def setup_armageddon_tax(cls, tax_name, company_data, **kwargs):
|
||||
return super().setup_armageddon_tax(tax_name, company_data, type_tax_use='purchase', **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
super(TestAccountMoveInRefundOnchanges, self).setUp()
|
||||
|
|
@ -147,7 +150,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(self.invoice, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'name': self.product_b.name,
|
||||
'name': 'product_b',
|
||||
'product_id': self.product_b.id,
|
||||
'product_uom_id': self.product_b.uom_id.id,
|
||||
'account_id': self.product_b.property_account_expense_id.id,
|
||||
|
|
@ -266,7 +269,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'name': 'turlututu installment #1',
|
||||
'partner_id': self.partner_b.id,
|
||||
'account_id': self.partner_b.property_account_payable_id.id,
|
||||
'amount_currency': 338.4,
|
||||
|
|
@ -274,7 +277,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'name': 'turlututu installment #2',
|
||||
'partner_id': self.partner_b.id,
|
||||
'account_id': self.partner_b.property_account_payable_id.id,
|
||||
'amount_currency': 789.6,
|
||||
|
|
@ -323,7 +326,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'name': 'turlututu installment #1',
|
||||
'account_id': self.partner_b.property_account_payable_id.id,
|
||||
'partner_id': self.partner_b.id,
|
||||
'amount_currency': 331.2,
|
||||
|
|
@ -331,7 +334,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'name': 'turlututu installment #2',
|
||||
'account_id': self.partner_b.property_account_payable_id.id,
|
||||
'partner_id': self.partner_b.id,
|
||||
'amount_currency': 772.8,
|
||||
|
|
@ -444,7 +447,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
|
||||
def test_in_refund_line_onchange_cash_rounding_1(self):
|
||||
# Required for `invoice_cash_rounding_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('account.group_cash_rounding')
|
||||
self.env.user.group_ids += self.env.ref('account.group_cash_rounding')
|
||||
# Test 'add_invoice_line' rounding
|
||||
move_form = Form(self.invoice)
|
||||
# Add a cash rounding having 'add_invoice_line'.
|
||||
|
|
@ -602,44 +605,46 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
def test_in_refund_line_onchange_currency_1(self):
|
||||
self.other_currency.rounding = 0.001
|
||||
|
||||
move_form = Form(self.invoice)
|
||||
move_form.currency_id = self.currency_data['currency']
|
||||
move_form.currency_id = self.other_currency
|
||||
move_form.save()
|
||||
|
||||
self.assertInvoiceValues(self.invoice, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -800.0,
|
||||
'credit': 400.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -160.0,
|
||||
'credit': 80.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -144.0,
|
||||
'credit': 72.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -24.0,
|
||||
'credit': 12.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1128.0,
|
||||
'debit': 564.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
# Change the date to get another rate: 1/3 instead of 1/2.
|
||||
|
|
@ -650,38 +655,38 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(self.invoice, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -800.0,
|
||||
'credit': 266.67,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -160.0,
|
||||
'credit': 53.33,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -144.0,
|
||||
'credit': 48.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -24.0,
|
||||
'credit': 8.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1128.0,
|
||||
'debit': 376.0,
|
||||
'date_maturity': fields.Date.from_string('2016-01-01'),
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'invoice_date': fields.Date.from_string('2016-01-01'),
|
||||
})
|
||||
|
|
@ -698,41 +703,41 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
{
|
||||
**self.product_line_vals_1,
|
||||
'quantity': 0.1,
|
||||
'price_unit': 0.05,
|
||||
'price_unit': 0.045,
|
||||
'price_subtotal': 0.005,
|
||||
'price_total': 0.006,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -0.005,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -160.0,
|
||||
'credit': 53.33,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -24.001,
|
||||
'credit': 8.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -24.0,
|
||||
'credit': 8.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 208.006,
|
||||
'debit': 69.33,
|
||||
'date_maturity': fields.Date.from_string('2016-01-01'),
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'invoice_date': fields.Date.from_string('2016-01-01'),
|
||||
'amount_untaxed': 160.005,
|
||||
|
|
@ -749,11 +754,11 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
{
|
||||
**self.product_line_vals_1,
|
||||
'quantity': 0.1,
|
||||
'price_unit': 0.05,
|
||||
'price_subtotal': 0.01,
|
||||
'price_total': 0.01,
|
||||
'amount_currency': -0.01,
|
||||
'credit': 0.01,
|
||||
'price_unit': 0.045,
|
||||
'price_subtotal': 0.0,
|
||||
'price_total': 0.0,
|
||||
'amount_currency': -0.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
self.product_line_vals_2,
|
||||
{
|
||||
|
|
@ -764,8 +769,8 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.tax_line_vals_2,
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'amount_currency': 208.01,
|
||||
'debit': 208.01,
|
||||
'amount_currency': 208.0,
|
||||
'debit': 208.0,
|
||||
'date_maturity': fields.Date.from_string('2016-01-01'),
|
||||
},
|
||||
], {
|
||||
|
|
@ -773,9 +778,9 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'currency_id': self.company_data['currency'].id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'invoice_date': fields.Date.from_string('2016-01-01'),
|
||||
'amount_untaxed': 160.01,
|
||||
'amount_untaxed': 160.0,
|
||||
'amount_tax': 48.0,
|
||||
'amount_total': 208.01,
|
||||
'amount_total': 208.0,
|
||||
})
|
||||
|
||||
def test_in_refund_onchange_past_invoice_1(self):
|
||||
|
|
@ -784,15 +789,15 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
# `purchase` adds a view which makes `invoice_vendor_bill_id` invisible
|
||||
# for purchase users
|
||||
# https://github.com/odoo/odoo/blob/385884afd31f25d61e99d139ecd4c574d99a1863/addons/purchase/views/account_move_views.xml#L26
|
||||
self.env.user.groups_id -= self.env.ref('purchase.group_purchase_manager')
|
||||
self.env.user.groups_id -= self.env.ref('purchase.group_purchase_user')
|
||||
# 'invisible': ['|', ('state', '!=', 'draft'), ('move_type', '!=', 'in_invoice')]
|
||||
self.env.user.group_ids -= self.env.ref('purchase.group_purchase_manager')
|
||||
self.env.user.group_ids -= self.env.ref('purchase.group_purchase_user')
|
||||
# invisible="state != 'draft' or move_type != 'in_invoice'"
|
||||
# This is an in_refund invoice, `invoice_vendor_bill_id` is not supposed to be visible
|
||||
# and therefore not supposed to be changed.
|
||||
view = self.env.ref('account.view_move_form')
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="invoice_vendor_bill_id"]'):
|
||||
del node.attrib['attrs']
|
||||
del node.attrib['invisible']
|
||||
view.arch = etree.tostring(tree)
|
||||
|
||||
move_form = Form(self.invoice)
|
||||
|
|
@ -815,7 +820,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'move_type': 'in_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2019-01-01'),
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_payment_term_id': self.pay_terms_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
|
|
@ -836,37 +841,37 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -800.0,
|
||||
'credit': 400.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -160.0,
|
||||
'credit': 80.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -144.0,
|
||||
'credit': 72.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -24.0,
|
||||
'credit': 12.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1128.0,
|
||||
'debit': 564.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
def test_in_refund_write_1(self):
|
||||
|
|
@ -875,7 +880,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'move_type': 'in_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2019-01-01'),
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_payment_term_id': self.pay_terms_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
|
|
@ -900,37 +905,37 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -800.0,
|
||||
'credit': 400.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -160.0,
|
||||
'credit': 80.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -144.0,
|
||||
'credit': 72.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -24.0,
|
||||
'credit': 12.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1128.0,
|
||||
'debit': 564.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
def test_in_refund_create_storno(self):
|
||||
|
|
@ -942,7 +947,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'move_type': 'in_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2019-01-01'),
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_payment_term_id': self.pay_terms_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
|
|
@ -963,7 +968,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -800.0,
|
||||
'balance': -400.0,
|
||||
'debit': -400.0,
|
||||
|
|
@ -971,7 +976,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -160.0,
|
||||
'balance': -80.0,
|
||||
'debit': -80.0,
|
||||
|
|
@ -979,7 +984,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -144.0,
|
||||
'balance': -72.0,
|
||||
'debit': -72.0,
|
||||
|
|
@ -987,7 +992,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -24.0,
|
||||
'balance': -12.0,
|
||||
'debit': -12.0,
|
||||
|
|
@ -995,7 +1000,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1128.0,
|
||||
'balance': 564.0,
|
||||
'debit': 0.0,
|
||||
|
|
@ -1003,7 +1008,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
def test_in_refund_reverse_caba(self):
|
||||
|
|
@ -1012,19 +1017,16 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'code': 'TWAIT',
|
||||
'account_type': 'liability_current',
|
||||
'reconcile': True,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
tax_final_account = self.env['account.account'].create({
|
||||
'name': 'TAX_TO_DEDUCT',
|
||||
'code': 'TDEDUCT',
|
||||
'account_type': 'asset_current',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
tax_base_amount_account = self.env['account.account'].create({
|
||||
'name': 'TAX_BASE',
|
||||
'code': 'TBASE',
|
||||
'account_type': 'asset_current',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
self.env.company.account_cash_basis_base_account_id = tax_base_amount_account
|
||||
self.env.company.tax_exigibility = True
|
||||
|
|
@ -1138,20 +1140,17 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'code': 'TWAIT',
|
||||
'account_type': 'liability_current',
|
||||
'reconcile': True,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
tax_final_account = self.env['account.account'].create({
|
||||
'name': 'TAX_TO_DEDUCT',
|
||||
'code': 'TDEDUCT',
|
||||
'account_type': 'asset_current',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
default_expense_account = self.company_data['default_account_expense']
|
||||
not_default_expense_account = self.env['account.account'].create({
|
||||
'name': 'NOT_DEFAULT_EXPENSE',
|
||||
'code': 'NDE',
|
||||
'account_type': 'expense',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
self.env.company.tax_exigibility = True
|
||||
tax_tags = defaultdict(dict)
|
||||
|
|
@ -1218,7 +1217,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
# check caba move
|
||||
partial_rec = invoice.mapped('line_ids.matched_credit_ids')
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
|
||||
# all amls with tax_tag should all have tax_tag_invert at True since the caba move comes from a vendor refund
|
||||
expected_values = [
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1228,7 +1226,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': not_default_expense_account.id,
|
||||
'debit': 800.0,
|
||||
'credit': 0.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1238,7 +1235,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': not_default_expense_account.id,
|
||||
'debit': 0.0,
|
||||
'credit': 800.0,
|
||||
'tax_tag_invert': True,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1248,7 +1244,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': default_expense_account.id,
|
||||
'debit': 0.0,
|
||||
'credit': 300.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1258,7 +1253,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': default_expense_account.id,
|
||||
'debit': 300.0,
|
||||
'credit': 0.0,
|
||||
'tax_tag_invert': True,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1268,7 +1262,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': tax_waiting_account.id,
|
||||
'debit': 50.0,
|
||||
'credit': 0.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': tax.id,
|
||||
|
|
@ -1278,7 +1271,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': tax_final_account.id,
|
||||
'debit': 0.0,
|
||||
'credit': 50.0,
|
||||
'tax_tag_invert': True,
|
||||
},
|
||||
]
|
||||
self.assertRecordValues(caba_move.line_ids, expected_values)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ from odoo import Command
|
|||
@tagged('post_install', '-at_install')
|
||||
class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.other_currency = cls.setup_other_currency('EUR', rounding=0.001)
|
||||
|
||||
def _dispatch_move_lines(self, moves):
|
||||
base_lines = moves.line_ids\
|
||||
.filtered(lambda x: x.tax_ids and not x.tax_line_id)\
|
||||
|
|
@ -19,9 +25,9 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
|
||||
def _get_tax_details(self, fallback=False, extra_domain=None):
|
||||
domain = [('company_id', '=', self.env.company.id)] + (extra_domain or [])
|
||||
tax_details_query, tax_details_params = self.env['account.move.line']._get_query_tax_details_from_domain(domain, fallback=fallback)
|
||||
tax_details_query = self.env['account.move.line']._get_query_tax_details_from_domain(domain, fallback=fallback)
|
||||
self.env['account.move.line'].flush_model()
|
||||
self.cr.execute(tax_details_query, tax_details_params)
|
||||
self.cr.execute(tax_details_query)
|
||||
tax_details_res = self.cr.dictfetchall()
|
||||
return sorted(tax_details_res, key=lambda x: (x['base_line_id'], abs(x['base_amount']), abs(x['tax_amount'])))
|
||||
|
||||
|
|
@ -293,7 +299,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
'base_line_id': base_lines[0].id,
|
||||
'tax_line_id': tax_lines[0].id,
|
||||
'tax_line_id': tax_lines[1].id,
|
||||
'base_amount': -1000.0,
|
||||
'tax_amount': -100.0,
|
||||
},
|
||||
|
|
@ -323,7 +329,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
'base_line_id': base_lines[1].id,
|
||||
'tax_line_id': tax_lines[1].id,
|
||||
'tax_line_id': tax_lines[0].id,
|
||||
'base_amount': -1000.0,
|
||||
'tax_amount': -100.0,
|
||||
},
|
||||
|
|
@ -901,7 +907,8 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
Command.create({
|
||||
'name': 'line3',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'price_unit': -400.0,
|
||||
'price_unit': 400.0,
|
||||
'quantity': -1,
|
||||
'tax_ids': [Command.set(fixed_tax.ids)],
|
||||
}),
|
||||
]
|
||||
|
|
@ -1037,7 +1044,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
'credit': 0.0,
|
||||
'amount_currency': 2400.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'tax_ids': [Command.set(percent_tax.ids)],
|
||||
}),
|
||||
Command.create({
|
||||
|
|
@ -1046,7 +1053,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
'credit': 0.0,
|
||||
'amount_currency': 6000.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'tax_ids': [Command.set(percent_tax.ids)],
|
||||
}),
|
||||
# Tax lines
|
||||
|
|
@ -1056,7 +1063,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
'credit': 0.0,
|
||||
'amount_currency': 360.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'tax_repartition_line_id': tax_rep.id,
|
||||
}),
|
||||
Command.create({
|
||||
|
|
@ -1065,7 +1072,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
'credit': 0.0,
|
||||
'amount_currency': 200.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'tax_repartition_line_id': tax_rep.id,
|
||||
}),
|
||||
# Balance
|
||||
|
|
@ -1212,6 +1219,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
self.assertTotalAmounts(invoice, tax_details)
|
||||
|
||||
def test_tax_on_payment(self):
|
||||
self.company_data['default_account_assets'].reconcile = True
|
||||
percent_tax = self.env['account.tax'].create({
|
||||
'name': "percent_tax",
|
||||
'amount_type': 'percent',
|
||||
|
|
@ -1252,7 +1260,7 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
def test_amounts_sign(self):
|
||||
for tax_sign in (1, -1):
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': "tax",
|
||||
'name': f"tax {tax_sign}",
|
||||
'amount_type': 'percent',
|
||||
'amount': tax_sign * 10.0,
|
||||
})
|
||||
|
|
@ -1292,3 +1300,98 @@ class TestAccountTaxDetailsReport(AccountTestInvoicingCommon):
|
|||
for amount in amounts],
|
||||
)
|
||||
self.assertTotalAmounts(invoice, tax_details)
|
||||
|
||||
def test_multiple_same_tax_lines_with_analytic(self):
|
||||
""" One Invoice line with analytic_distribution and another without analytic_distribution with same group of tax"""
|
||||
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan with Tax details'})
|
||||
analytic_account = self.env['account.analytic.account'].create({
|
||||
'name': 'Analytic account with Tax details',
|
||||
'plan_id': analytic_plan.id,
|
||||
'company_id': False,
|
||||
})
|
||||
# Don't set analytic to False here. allowed ORM to do it becosue it's set SQL Null
|
||||
child1_tax = self.env['account.tax'].create({
|
||||
'name': "child1_tax",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'invoice_repartition_line_ids': [
|
||||
Command.create({
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': self.company_data['default_account_tax_sale'].id,
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
Command.create({
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': self.company_data['default_account_tax_sale'].id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
child2_tax = child1_tax.copy({'name': 'child2_tax', 'amount': 5.0})
|
||||
tax_group = self.env['account.tax'].create({
|
||||
'name': "tax_group",
|
||||
'amount_type': 'group',
|
||||
'children_tax_ids': [Command.set((child1_tax + child2_tax).ids)],
|
||||
})
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2019-01-01',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'line1',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'price_unit': 1000.0,
|
||||
'tax_ids': [Command.set(tax_group.ids)],
|
||||
'analytic_distribution': {
|
||||
analytic_account.id: 100,
|
||||
},
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'line2',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [Command.set(tax_group.ids)],
|
||||
}),
|
||||
]
|
||||
})
|
||||
base_lines, tax_lines = self._dispatch_move_lines(invoice)
|
||||
tax_details = self._get_tax_details()
|
||||
self.assertTaxDetailsValues(
|
||||
tax_details,
|
||||
[
|
||||
{
|
||||
'base_line_id': base_lines[0].id,
|
||||
'tax_line_id': tax_lines[1].id,
|
||||
'base_amount': -1000.0,
|
||||
'tax_amount': -50.0,
|
||||
},
|
||||
{
|
||||
'base_line_id': base_lines[0].id,
|
||||
'tax_line_id': tax_lines[0].id,
|
||||
'base_amount': -1000.0,
|
||||
'tax_amount': -100.0,
|
||||
},
|
||||
{
|
||||
'base_line_id': base_lines[1].id,
|
||||
'tax_line_id': tax_lines[1].id,
|
||||
'base_amount': -100.0,
|
||||
'tax_amount': -5.0,
|
||||
},
|
||||
{
|
||||
'base_line_id': base_lines[1].id,
|
||||
'tax_line_id': tax_lines[0].id,
|
||||
'base_amount': -100.0,
|
||||
'tax_amount': -10.0,
|
||||
},
|
||||
],
|
||||
)
|
||||
self.assertTotalAmounts(invoice, tax_details)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=bad-whitespace
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo import fields, Command
|
||||
|
||||
from collections import defaultdict
|
||||
|
|
@ -11,13 +10,15 @@ from collections import defaultdict
|
|||
class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.other_currency = cls.setup_other_currency('HRK')
|
||||
|
||||
cls.invoice = cls.init_invoice('out_refund', products=cls.product_a+cls.product_b)
|
||||
|
||||
cls.product_line_vals_1 = {
|
||||
'name': cls.product_a.name,
|
||||
'name': 'product_a',
|
||||
'product_id': cls.product_a.id,
|
||||
'account_id': cls.product_a.property_account_income_id.id,
|
||||
'partner_id': cls.partner_a.id,
|
||||
|
|
@ -36,7 +37,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'date_maturity': False,
|
||||
}
|
||||
cls.product_line_vals_2 = {
|
||||
'name': cls.product_b.name,
|
||||
'name': 'product_b',
|
||||
'product_id': cls.product_b.id,
|
||||
'account_id': cls.product_b.property_account_income_id.id,
|
||||
'partner_id': cls.partner_a.id,
|
||||
|
|
@ -93,7 +94,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'date_maturity': False,
|
||||
}
|
||||
cls.term_line_vals_1 = {
|
||||
'name': '',
|
||||
'name': False,
|
||||
'product_id': False,
|
||||
'account_id': cls.company_data['default_account_receivable'].id,
|
||||
'partner_id': cls.partner_a.id,
|
||||
|
|
@ -117,7 +118,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'journal_id': cls.company_data['default_journal_sale'].id,
|
||||
'date': fields.Date.from_string('2019-01-01'),
|
||||
'fiscal_position_id': False,
|
||||
'payment_reference': '',
|
||||
'payment_reference': False,
|
||||
'invoice_payment_term_id': cls.pay_terms_a.id,
|
||||
'amount_untaxed': 1200.0,
|
||||
'amount_tax': 210.0,
|
||||
|
|
@ -143,7 +144,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(self.invoice, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'name': self.product_b.name,
|
||||
'name': 'product_b',
|
||||
'product_id': self.product_b.id,
|
||||
'product_uom_id': self.product_b.uom_id.id,
|
||||
'account_id': self.product_b.property_account_income_id.id,
|
||||
|
|
@ -240,7 +241,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
def test_out_refund_line_onchange_partner_1(self):
|
||||
move_form = Form(self.invoice)
|
||||
move_form.partner_id = self.partner_b
|
||||
move_form.payment_reference = 'turlututu'
|
||||
move_form.save()
|
||||
|
||||
self.assertInvoiceValues(self.invoice, [
|
||||
|
|
@ -262,25 +262,24 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'name': 'installment #1',
|
||||
'partner_id': self.partner_b.id,
|
||||
'account_id': self.partner_b.property_account_receivable_id.id,
|
||||
'amount_currency': -423.0,
|
||||
'credit': 423.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'installment #2',
|
||||
'partner_id': self.partner_b.id,
|
||||
'account_id': self.partner_b.property_account_receivable_id.id,
|
||||
'amount_currency': -987.0,
|
||||
'credit': 987.0,
|
||||
'date_maturity': fields.Date.from_string('2019-02-28'),
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'partner_id': self.partner_b.id,
|
||||
'account_id': self.partner_b.property_account_receivable_id.id,
|
||||
'amount_currency': -423.0,
|
||||
'credit': 423.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'partner_id': self.partner_b.id,
|
||||
'payment_reference': 'turlututu',
|
||||
'fiscal_position_id': self.fiscal_pos_a.id,
|
||||
'invoice_payment_term_id': self.pay_terms_b.id,
|
||||
'amount_untaxed': 1200.0,
|
||||
|
|
@ -319,25 +318,24 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'name': 'installment #1',
|
||||
'account_id': self.partner_b.property_account_receivable_id.id,
|
||||
'partner_id': self.partner_b.id,
|
||||
'amount_currency': -414.0,
|
||||
'credit': 414.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'installment #2',
|
||||
'account_id': self.partner_b.property_account_receivable_id.id,
|
||||
'partner_id': self.partner_b.id,
|
||||
'amount_currency': -966.0,
|
||||
'credit': 966.0,
|
||||
'date_maturity': fields.Date.from_string('2019-02-28'),
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': 'turlututu',
|
||||
'account_id': self.partner_b.property_account_receivable_id.id,
|
||||
'partner_id': self.partner_b.id,
|
||||
'amount_currency': -414.0,
|
||||
'credit': 414.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'partner_id': self.partner_b.id,
|
||||
'payment_reference': 'turlututu',
|
||||
'fiscal_position_id': self.fiscal_pos_a.id,
|
||||
'invoice_payment_term_id': self.pay_terms_b.id,
|
||||
'amount_untaxed': 1200.0,
|
||||
|
|
@ -437,7 +435,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
|
||||
def test_out_refund_line_onchange_cash_rounding_1(self):
|
||||
# Required for `invoice_cash_rounding_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('account.group_cash_rounding')
|
||||
self.env.user.group_ids += self.env.ref('account.group_cash_rounding')
|
||||
# Test 'add_invoice_line' rounding
|
||||
move_form = Form(self.invoice)
|
||||
# Add a cash rounding having 'add_invoice_line'.
|
||||
|
|
@ -592,44 +590,46 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
def test_out_refund_line_onchange_currency_1(self):
|
||||
self.other_currency.rounding = 0.001
|
||||
|
||||
move_form = Form(self.invoice)
|
||||
move_form.currency_id = self.currency_data['currency']
|
||||
move_form.currency_id = self.other_currency
|
||||
move_form.save()
|
||||
|
||||
self.assertInvoiceValues(self.invoice, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1000.0,
|
||||
'debit': 500.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 200.0,
|
||||
'debit': 100.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 180.0,
|
||||
'debit': 90.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 30.0,
|
||||
'debit': 15.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -1410.0,
|
||||
'credit': 705.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
# Change the date to get another rate: 1/3 instead of 1/2.
|
||||
|
|
@ -639,38 +639,38 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(self.invoice, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1000.0,
|
||||
'debit': 333.33,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 200.0,
|
||||
'debit': 66.67,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 180.0,
|
||||
'debit': 60.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 30.0,
|
||||
'debit': 10.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -1410.0,
|
||||
'credit': 470.0,
|
||||
'date_maturity': fields.Date.from_string('2016-01-01'),
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
})
|
||||
|
||||
|
|
@ -686,41 +686,41 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
{
|
||||
**self.product_line_vals_1,
|
||||
'quantity': 0.1,
|
||||
'price_unit': 0.05,
|
||||
'price_unit': 0.045,
|
||||
'price_subtotal': 0.005,
|
||||
'price_total': 0.006,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 0.005,
|
||||
'debit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 200.0,
|
||||
'debit': 66.67,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 30.001,
|
||||
'debit': 10.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 30.0,
|
||||
'debit': 10.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -260.006,
|
||||
'credit': 86.67,
|
||||
'date_maturity': fields.Date.from_string('2016-01-01'),
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'amount_untaxed': 200.005,
|
||||
'amount_tax': 60.001,
|
||||
|
|
@ -736,11 +736,11 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
{
|
||||
**self.product_line_vals_1,
|
||||
'quantity': 0.1,
|
||||
'price_unit': 0.05,
|
||||
'price_subtotal': 0.01,
|
||||
'price_total': 0.01,
|
||||
'amount_currency': 0.01,
|
||||
'debit': 0.01,
|
||||
'price_unit': 0.045,
|
||||
'price_subtotal': 0.0,
|
||||
'price_total': 0.0,
|
||||
'amount_currency': 0.0,
|
||||
'debit': 0.0,
|
||||
},
|
||||
self.product_line_vals_2,
|
||||
{
|
||||
|
|
@ -751,17 +751,17 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.tax_line_vals_2,
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'amount_currency': -260.01,
|
||||
'credit': 260.01,
|
||||
'amount_currency': -260.0,
|
||||
'credit': 260.0,
|
||||
'date_maturity': fields.Date.from_string('2016-01-01'),
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'amount_untaxed': 200.01,
|
||||
'amount_untaxed': 200.0,
|
||||
'amount_tax': 60.0,
|
||||
'amount_total': 260.01,
|
||||
'amount_total': 260.0,
|
||||
})
|
||||
|
||||
def test_out_refund_create_1(self):
|
||||
|
|
@ -770,7 +770,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'move_type': 'out_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2019-01-01'),
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_payment_term_id': self.pay_terms_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
|
|
@ -791,37 +791,37 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1000.0,
|
||||
'debit': 500.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 200.0,
|
||||
'debit': 100.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 180.0,
|
||||
'debit': 90.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 30.0,
|
||||
'debit': 15.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -1410.0,
|
||||
'credit': 705.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
def test_out_refund_write_1(self):
|
||||
|
|
@ -830,7 +830,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'move_type': 'out_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2019-01-01'),
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_payment_term_id': self.pay_terms_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
|
|
@ -855,37 +855,37 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1000.0,
|
||||
'debit': 500.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 200.0,
|
||||
'debit': 100.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 180.0,
|
||||
'debit': 90.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 30.0,
|
||||
'debit': 15.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -1410.0,
|
||||
'credit': 705.0,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
def test_out_refund_create_storno(self):
|
||||
|
|
@ -897,7 +897,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'move_type': 'out_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': fields.Date.from_string('2019-01-01'),
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_payment_term_id': self.pay_terms_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
|
|
@ -918,7 +918,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
self.assertInvoiceValues(move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 1000.0,
|
||||
'balance': 500.0,
|
||||
'debit': 0.0,
|
||||
|
|
@ -926,7 +926,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 200.0,
|
||||
'balance': 100.0,
|
||||
'debit': 0.0,
|
||||
|
|
@ -934,7 +934,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 180.0,
|
||||
'balance': 90.0,
|
||||
'debit': 0.0,
|
||||
|
|
@ -942,7 +942,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': 30.0,
|
||||
'balance': 15.0,
|
||||
'debit': 0.0,
|
||||
|
|
@ -950,7 +950,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'amount_currency': -1410.0,
|
||||
'balance': -705.0,
|
||||
'debit': -705.0,
|
||||
|
|
@ -958,7 +958,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
})
|
||||
|
||||
def test_out_refund_reverse_caba(self):
|
||||
|
|
@ -967,19 +967,16 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'code': 'TWAIT',
|
||||
'account_type': 'liability_current',
|
||||
'reconcile': True,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
tax_final_account = self.env['account.account'].create({
|
||||
'name': 'TAX_TO_DEDUCT',
|
||||
'code': 'TDEDUCT',
|
||||
'account_type': 'asset_current',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
tax_base_amount_account = self.env['account.account'].create({
|
||||
'name': 'TAX_BASE',
|
||||
'code': 'TBASE',
|
||||
'account_type': 'asset_current',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
self.env.company.account_cash_basis_base_account_id = tax_base_amount_account
|
||||
self.env.company.tax_exigibility = True
|
||||
|
|
@ -1093,20 +1090,17 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'code': 'TWAIT',
|
||||
'account_type': 'liability_current',
|
||||
'reconcile': True,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
tax_final_account = self.env['account.account'].create({
|
||||
'name': 'TAX_TO_DEDUCT',
|
||||
'code': 'TDEDUCT',
|
||||
'account_type': 'asset_current',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
default_income_account = self.company_data['default_account_revenue']
|
||||
not_default_income_account = self.env['account.account'].create({
|
||||
'name': 'NOT_DEFAULT_INCOME',
|
||||
'code': 'NDI',
|
||||
'account_type': 'income',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
self.env.company.tax_exigibility = True
|
||||
tax_tags = defaultdict(dict)
|
||||
|
|
@ -1173,7 +1167,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
# check caba move
|
||||
partial_rec = invoice.mapped('line_ids.matched_debit_ids')
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
|
||||
# all amls with tax_tag should all have tax_tag_invert at False since the caba move comes from an invoice refund
|
||||
expected_values = [
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1183,7 +1176,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': not_default_income_account.id,
|
||||
'debit': 0.0,
|
||||
'credit': 1000.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1193,7 +1185,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': not_default_income_account.id,
|
||||
'debit': 1000.0,
|
||||
'credit': 0.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1203,7 +1194,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': default_income_account.id,
|
||||
'debit': 300.0,
|
||||
'credit': 0.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1213,7 +1203,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': default_income_account.id,
|
||||
'debit': 0.0,
|
||||
'credit': 300.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': False,
|
||||
|
|
@ -1223,7 +1212,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': tax_waiting_account.id,
|
||||
'debit': 0.0,
|
||||
'credit': 70.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
{
|
||||
'tax_line_id': tax.id,
|
||||
|
|
@ -1233,7 +1221,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
|
|||
'account_id': tax_final_account.id,
|
||||
'debit': 70.0,
|
||||
'credit': 0.0,
|
||||
'tax_tag_invert': False,
|
||||
},
|
||||
]
|
||||
self.assertRecordValues(caba_move.line_ids, expected_values)
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMovePartnerCount(AccountTestInvoicingCommon):
|
||||
|
||||
def test_account_move_count(self):
|
||||
self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
|
||||
},
|
||||
{
|
||||
'move_type': 'in_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
|
||||
},
|
||||
]).action_post()
|
||||
|
||||
self.assertEqual(self.partner_a.supplier_rank, 1)
|
||||
self.assertEqual(self.partner_a.customer_rank, 1)
|
||||
|
|
@ -8,22 +8,15 @@ from odoo import Command
|
|||
class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.receivable_account = cls.company_data['default_account_receivable']
|
||||
cls.payable_account = cls.company_data['default_account_payable']
|
||||
|
||||
cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
|
||||
'name': 'Stars',
|
||||
'symbol': '☆',
|
||||
'currency_unit_label': 'Stars',
|
||||
'currency_subunit_label': 'Little Stars',
|
||||
}, rate2016=6.0, rate2017=4.0)
|
||||
|
||||
cls.curr_1 = cls.company_data['currency']
|
||||
cls.curr_2 = cls.currency_data['currency']
|
||||
cls.curr_3 = cls.currency_data_2['currency']
|
||||
cls.curr_2 = cls.setup_other_currency('EUR')
|
||||
cls.curr_3 = cls.setup_other_currency('CAD', rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)])
|
||||
|
||||
cls.payment_2016_curr_1 = cls.env['account.move'].create({
|
||||
'date': '2016-01-01',
|
||||
|
|
@ -157,7 +150,7 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
|||
'date': '2016-01-01',
|
||||
'invoice_date': '2016-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.curr_2.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 300,
|
||||
|
|
@ -189,7 +182,7 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
|||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.curr_2.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 300,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,31 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMoveRounding(AccountTestInvoicingCommon):
|
||||
|
||||
def test_move_line_rounding(self):
|
||||
"""Whatever arguments we give to the creation of an account move,
|
||||
in every case the amounts should be properly rounded to the currency's precision.
|
||||
In other words, we don't fall victim of the limitation introduced by 9d87d15db6dd40
|
||||
|
||||
Here the rounding should be done according to company_currency_id, which is a related
|
||||
on move_id.company_id.currency_id.
|
||||
In principle, it should not be necessary to add it to the create values,
|
||||
since it is supposed to be computed by the ORM...
|
||||
"""
|
||||
move = self.env['account.move'].create({
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 100.0 / 3, 'account_id': self.company_data['default_account_revenue'].id}),
|
||||
(0, 0, {'credit': 100.0 / 3, 'account_id': self.company_data['default_account_revenue'].id}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
[(33.33, 0.0), (0.0, 33.33)],
|
||||
move.line_ids.mapped(lambda x: (x.debit, x.credit)),
|
||||
"Quantities should have been rounded according to the currency."
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,42 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTourRenderInvoiceReport(AccountTestInvoicingHttpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.env.user.write({
|
||||
'groups_id': [
|
||||
(6, 0, (cls.env.ref('account.group_account_manager') + cls.env.ref('base.group_system')).ids),
|
||||
],
|
||||
})
|
||||
|
||||
cls.out_invoice = cls.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_date': '2019-05-01',
|
||||
'date': '2019-05-01',
|
||||
'invoice_line_ids': [
|
||||
(0, 0, {'name': 'line1', 'price_unit': 100.0}),
|
||||
],
|
||||
})
|
||||
cls.out_invoice.action_post()
|
||||
|
||||
report_layout = cls.env.ref('web.report_layout_standard')
|
||||
|
||||
cls.company_data['company'].write({
|
||||
'primary_color': '#123456',
|
||||
'secondary_color': '#789101',
|
||||
'external_report_layout_id': report_layout.view_id.id,
|
||||
})
|
||||
|
||||
cls.env.ref('account.account_invoices_without_payment').report_type = 'qweb-html'
|
||||
|
||||
def test_render_account_document_layout(self):
|
||||
self.start_tour('/web', 'account_render_report', login=self.env.user.login, timeout=200)
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountPartner(AccountTestInvoicingCommon):
|
||||
|
||||
@freeze_time("2023-05-31")
|
||||
def test_days_sales_outstanding(self):
|
||||
partner = self.env['res.partner'].create({'name': 'MyCustomer'})
|
||||
self.assertEqual(partner.days_sales_outstanding, 0.0)
|
||||
move_1 = self.init_invoice("out_invoice", partner, invoice_date="2023-01-01", amounts=[3000], taxes=self.tax_sale_a)
|
||||
self.assertEqual(partner.days_sales_outstanding, 0.0)
|
||||
move_1.action_post()
|
||||
self.env.invalidate_all() #needed to force the update of partner.credit
|
||||
self.assertEqual(partner.days_sales_outstanding, 150) #DSO = number of days since move_1
|
||||
self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move_1.ids).create({
|
||||
'amount': move_1.amount_total,
|
||||
'partner_id': partner.id,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
})._create_payments()
|
||||
self.env.invalidate_all()
|
||||
self.assertEqual(partner.days_sales_outstanding, 0.0)
|
||||
self.init_invoice("out_invoice", partner, "2023-05-15", amounts=[1500], taxes=self.tax_sale_a, post=True)
|
||||
self.env.invalidate_all()
|
||||
self.assertEqual(partner.days_sales_outstanding, 50)
|
||||
|
||||
def test_account_move_count(self):
|
||||
self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
|
||||
},
|
||||
{
|
||||
'move_type': 'in_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
|
||||
},
|
||||
]).action_post()
|
||||
|
||||
# rank updates are updated in the post-commit phase
|
||||
with self.enter_registry_test_mode():
|
||||
self.env.cr.postcommit.run()
|
||||
self.assertEqual(self.partner_a.supplier_rank, 1)
|
||||
self.assertEqual(self.partner_a.customer_rank, 1)
|
||||
|
||||
# a second move is updated in postcommit
|
||||
self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-02',
|
||||
'invoice_date': '2017-01-02',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
|
||||
},
|
||||
]).action_post()
|
||||
# rank updates are updated in the post-commit phase
|
||||
with self.enter_registry_test_mode():
|
||||
self.env.cr.postcommit.run()
|
||||
self.assertEqual(self.partner_a.customer_rank, 2)
|
||||
|
||||
def test_manually_write_partner_id(self):
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': '2025-04-29',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'quantity': 1,
|
||||
'price_unit': 500.0,
|
||||
'tax_ids': [Command.link(self.tax_sale_a.id)],
|
||||
})],
|
||||
})
|
||||
move.action_post()
|
||||
reversal = move._reverse_moves(cancel=True)
|
||||
|
||||
receivable_lines = (move + reversal).line_ids.filtered(lambda l: l.display_type == 'payment_term')
|
||||
|
||||
# Changing the partner should be possible despite being in locked periods as long as the VAT is the same
|
||||
move.company_id.fiscalyear_lock_date = '9999-12-31'
|
||||
move.company_id.tax_lock_date = '9999-12-31'
|
||||
|
||||
# Initially, move's commercial partner should be partner_a
|
||||
self.assertEqual(move.commercial_partner_id, self.partner_a)
|
||||
self.assertEqual(receivable_lines.mapped('reconciled'), [True, True])
|
||||
|
||||
self.partner_a.parent_id = self.partner_b
|
||||
|
||||
# Assert accounting move and move lines now use new commercial partner
|
||||
self.assertEqual(move.commercial_partner_id, self.partner_b)
|
||||
self.assertTrue(
|
||||
all(line.partner_id == self.partner_b for line in move.line_ids),
|
||||
"All move lines should be reassigned to the new commercial partner."
|
||||
)
|
||||
self.assertEqual(receivable_lines.mapped('reconciled'), [True, True])
|
||||
|
||||
def test_manually_write_partner_id_different_vat(self):
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': '2025-04-29',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'quantity': 1,
|
||||
'price_unit': 500.0,
|
||||
})],
|
||||
})
|
||||
move.action_post()
|
||||
self.partner_a.vat = 'SOMETHING'
|
||||
self.partner_b.vat = 'DIFFERENT'
|
||||
with self.assertRaisesRegex(UserError, "different Tax ID"):
|
||||
self.partner_a.parent_id = self.partner_b
|
||||
|
||||
def test_manually_write_partner_id_empty_string_vs_False(self):
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': '2025-04-29',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'quantity': 1,
|
||||
'price_unit': 500.0,
|
||||
})],
|
||||
})
|
||||
move.action_post()
|
||||
self.partner_a.vat = ''
|
||||
self.partner_b.vat = False
|
||||
|
||||
self.partner_a.parent_id = self.partner_b
|
||||
|
||||
def test_res_partner_bank(self):
|
||||
self.env.user.group_ids = (
|
||||
self.env.ref('base.group_partner_manager')
|
||||
+ self.env.ref('account.group_account_user')
|
||||
+ self.env.ref('account.group_validate_bank_account')
|
||||
)
|
||||
|
||||
partner = self.env['res.partner'].create({'name': 'MyCustomer'})
|
||||
account = self.env['res.partner.bank'].create({
|
||||
'acc_number': '123456789',
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
account.allow_out_payment = True
|
||||
|
||||
with self.assertRaisesRegex(UserError, "has been trusted"), self.cr.savepoint():
|
||||
account.write({'acc_number': '1234567890999'})
|
||||
with self.assertRaisesRegex(UserError, "has been trusted"), self.cr.savepoint():
|
||||
account.write({'sanitized_acc_number': '1234567890999'})
|
||||
with self.assertRaisesRegex(UserError, "has been trusted"), self.cr.savepoint():
|
||||
account.write({'partner_id': self.env['res.partner'].create({'name': 'MyCustomer 2'}).id})
|
||||
|
||||
account.allow_out_payment = False
|
||||
account.write({'acc_number': '1234567890999000'})
|
||||
|
||||
self.env.user.group_ids -= self.env.ref('account.group_validate_bank_account')
|
||||
with self.assertRaisesRegex(UserError, "You do not have the rights to trust"), self.cr.savepoint():
|
||||
account.write({'allow_out_payment': True})
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,168 @@
|
|||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountPaymentDuplicateMoves(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.company = cls.company_data['company']
|
||||
cls.receivable = cls.company_data['default_account_receivable']
|
||||
cls.payable = cls.company_data['default_account_payable']
|
||||
cls.bank_journal = cls.company_data['default_journal_bank']
|
||||
cls.comp_curr = cls.company_data['currency']
|
||||
|
||||
cls.payment_in = cls.env['account.payment'].create({
|
||||
'amount': 50.0,
|
||||
'payment_type': 'inbound',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'destination_account_id': cls.receivable.id,
|
||||
})
|
||||
cls.payment_out = cls.env['account.payment'].create({
|
||||
'amount': 50.0,
|
||||
'payment_type': 'outbound',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'destination_account_id': cls.payable.id,
|
||||
})
|
||||
|
||||
cls.out_invoice_1 = cls.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 50.0, 'tax_ids': []})],
|
||||
})
|
||||
cls.in_invoice_1 = cls.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 50.0, 'tax_ids': []})],
|
||||
})
|
||||
cls.out_invoice_2 = cls.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 20.0, 'tax_ids': []})],
|
||||
})
|
||||
(cls.out_invoice_1 + cls.out_invoice_2 + cls.in_invoice_1).action_post()
|
||||
|
||||
def test_duplicate_payments(self):
|
||||
""" Ensure duplicated payments are computed correctly for both inbound and outbound payments.
|
||||
For it to be a duplicate, the partner, the date and the amount must be the same.
|
||||
"""
|
||||
payment_in_1 = self.payment_in
|
||||
payment_out_1 = self.payment_out
|
||||
|
||||
# Different type but same partner, amount and date, no duplicate
|
||||
self.assertRecordValues(payment_in_1, [{'duplicate_payment_ids': []}])
|
||||
|
||||
# Create duplicate payments
|
||||
payment_in_2 = payment_in_1.copy(default={'date': payment_in_1.date})
|
||||
payment_out_2 = payment_out_1.copy(default={'date': payment_out_1.date})
|
||||
# Inbound payment finds duplicate inbound payment, not the outbound payment with same information
|
||||
self.assertRecordValues(payment_in_2, [{
|
||||
'duplicate_payment_ids': [payment_in_1.id],
|
||||
}])
|
||||
# Outbound payment finds duplicate outbound duplicate, not the inbound payment with same information
|
||||
self.assertRecordValues(payment_out_2, [{
|
||||
'duplicate_payment_ids': [payment_out_1.id],
|
||||
}])
|
||||
# Different date but same amount and same partner, no duplicate
|
||||
payment_out_3 = payment_out_1.copy(default={'date': '2023-12-31'})
|
||||
self.assertRecordValues(payment_out_3, [{'duplicate_payment_ids': []}])
|
||||
|
||||
# Different amount but same partner and same date, no duplicate
|
||||
payment_out_4 = self.env['account.payment'].create({
|
||||
'amount': 60.0,
|
||||
'payment_type': 'outbound',
|
||||
'partner_id': self.partner_a.id,
|
||||
'destination_account_id': self.payable.id,
|
||||
})
|
||||
self.assertRecordValues(payment_out_4, [{'duplicate_payment_ids': []}])
|
||||
|
||||
# Different partner but same amount and same date, no duplicate
|
||||
payment_out_5 = self.env['account.payment'].create({
|
||||
'amount': 50.0,
|
||||
'payment_type': 'outbound',
|
||||
'partner_id': self.partner_b.id,
|
||||
'destination_account_id': self.payable.id,
|
||||
})
|
||||
self.assertRecordValues(payment_out_5, [{'duplicate_payment_ids': []}])
|
||||
|
||||
def test_in_payment_multiple_duplicate_inbound_batch(self):
|
||||
""" Ensure duplicated payments are computed correctly when updated in batch,
|
||||
where payments are all of a single payment type
|
||||
"""
|
||||
payment_1 = self.payment_in
|
||||
payment_2 = payment_1.copy(default={'date': payment_1.date})
|
||||
payment_3 = payment_1.copy(default={'date': payment_1.date})
|
||||
|
||||
payments = payment_1 + payment_2 + payment_3
|
||||
|
||||
self.assertRecordValues(payments, [
|
||||
{'duplicate_payment_ids': (payment_2 + payment_3).ids},
|
||||
{'duplicate_payment_ids': (payment_1 + payment_3).ids},
|
||||
{'duplicate_payment_ids': (payment_1 + payment_2).ids},
|
||||
])
|
||||
|
||||
def test_in_payment_multiple_duplicate_multiple_journals(self):
|
||||
""" Ensure duplicated payments are computed correctly when updated in batch,
|
||||
with inbound and outbound payments with different journals
|
||||
"""
|
||||
payment_in_1 = self.payment_in
|
||||
payment_out_1 = self.payment_out
|
||||
# Create a different journals with a different outstanding account
|
||||
bank_journal_B = self.bank_journal.copy()
|
||||
bank_journal_B.inbound_payment_method_line_ids.payment_account_id = self.env['account.account'].create({
|
||||
'name': 'Outstanding Payment Account B',
|
||||
'code': 'OPAB',
|
||||
'account_type': 'asset_current',
|
||||
'reconcile': True,
|
||||
})
|
||||
# Create new payments in the second journal
|
||||
payment_in_2 = payment_in_1.copy(default={'date': payment_in_1.date})
|
||||
payment_in_2.journal_id = bank_journal_B
|
||||
payment_out_2 = payment_out_1.copy(default={'date': payment_out_1.date})
|
||||
payment_out_2.journal_id = bank_journal_B
|
||||
|
||||
payments = payment_in_1 + payment_out_1 + payment_in_2 + payment_out_2
|
||||
|
||||
self.assertRecordValues(payments, [
|
||||
{'duplicate_payment_ids': [payment_in_2.id]},
|
||||
{'duplicate_payment_ids': [payment_out_2.id]},
|
||||
{'duplicate_payment_ids': [payment_in_1.id]},
|
||||
{'duplicate_payment_ids': [payment_out_1.id]},
|
||||
])
|
||||
|
||||
def test_register_payment_different_payment_types(self):
|
||||
""" Test that payment wizard correctly calculates duplicate_payment_ids """
|
||||
payment_1 = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=self.out_invoice_1.ids).create({'payment_date': self.payment_in.date})
|
||||
payment_2 = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=self.in_invoice_1.ids).create({'payment_date': self.payment_out.date})
|
||||
existing_payment_in = self.payment_in
|
||||
existing_payment_out = self.payment_out
|
||||
|
||||
# Payment wizards flag unreconciled existing payments of the same payment type only
|
||||
self.assertRecordValues(payment_1, [{'duplicate_payment_ids': [existing_payment_in.id]}])
|
||||
self.assertRecordValues(payment_2, [{'duplicate_payment_ids': [existing_payment_out.id]}])
|
||||
|
||||
def test_register_payment_single_batch_duplicate_payments(self):
|
||||
""" Test that duplicate_payment_ids is correctly calculated for single batches """
|
||||
payment_1 = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=self.out_invoice_1.ids).create({'payment_date': self.payment_in.date})
|
||||
payment_2 = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=self.out_invoice_2.ids).create({'payment_date': self.out_invoice_2.date})
|
||||
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
|
||||
combined_payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
|
||||
'amount': 50.0, # amount can be changed manually
|
||||
'group_payment': True,
|
||||
'payment_difference_handling': 'open',
|
||||
'payment_method_line_id': self.inbound_payment_method_line.id,
|
||||
})
|
||||
existing_payment = self.payment_in
|
||||
|
||||
self.assertRecordValues(payment_1, [{'duplicate_payment_ids': [existing_payment.id]}])
|
||||
self.assertRecordValues(payment_2, [{'duplicate_payment_ids': []}]) # different amount, not a duplicate
|
||||
# Combined payments does not show payment_1 as duplicate because payment_1 is reconciled
|
||||
self.assertRecordValues(combined_payments, [{'duplicate_payment_ids': [existing_payment.id]}])
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from datetime import date, datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountPaymentItems(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.bill = cls.create_bill(due_date='2023-03-20')
|
||||
cls.late_bill = cls.create_bill(due_date='2023-03-01')
|
||||
cls.discount_bill = cls.create_bill(due_date='2023-03-20', discount_days=19)
|
||||
cls.late_discount_bill = cls.create_bill(due_date='2023-04-20', discount_days=9)
|
||||
|
||||
@classmethod
|
||||
def create_bill(cls, due_date, discount_days=None):
|
||||
payment_term = cls.create_payment_term(due_date, discount_days)
|
||||
bill = cls.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'journal_id': cls.company_data['default_journal_purchase'].id,
|
||||
'partner_id': cls.partner_a.id,
|
||||
'date': '2023-03-15',
|
||||
'invoice_date': '2023-03-01',
|
||||
'invoice_date_due': due_date,
|
||||
'invoice_payment_term_id': payment_term.id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 1,
|
||||
'name': 'product_a',
|
||||
'discount': 10.00,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [],
|
||||
'discount_date': date(2023, 3, 1) + timedelta(days=discount_days) if discount_days else False,
|
||||
'date_maturity': due_date,
|
||||
})]
|
||||
})
|
||||
bill.action_post()
|
||||
return bill
|
||||
|
||||
@classmethod
|
||||
def create_payment_term(cls, due_date, discount_days=None):
|
||||
due_days = (datetime.strptime(due_date, '%Y-%m-%d').date() - date(2023, 3, 1)).days
|
||||
payment_term = cls.env['account.payment.term'].create({
|
||||
'name': 'Payment Term For Testing',
|
||||
'early_discount': bool(discount_days),
|
||||
'discount_days': discount_days if discount_days else False,
|
||||
'discount_percentage': 5,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'delay_type': 'days_after',
|
||||
'nb_days': due_days,
|
||||
}),
|
||||
],
|
||||
})
|
||||
return payment_term
|
||||
|
||||
@freeze_time("2023-03-15")
|
||||
def test_payment_date(self):
|
||||
self.assertEqual(str(self.bill.line_ids[0].payment_date), '2023-03-20')
|
||||
self.assertEqual(str(self.late_bill.line_ids[0].payment_date), '2023-03-01')
|
||||
self.assertEqual(str(self.discount_bill.line_ids[0].payment_date), '2023-03-20')
|
||||
self.assertEqual(str(self.late_discount_bill.line_ids[0].payment_date), '2023-04-20')
|
||||
|
||||
def test_search_payment_date(self):
|
||||
for today, search, expected in [
|
||||
('2023-03-05', '2023-03-01', self.late_bill),
|
||||
('2023-03-05', '2023-03-30', self.bill + self.late_bill + self.discount_bill + self.late_discount_bill),
|
||||
('2023-03-15', '2023-03-01', self.late_bill),
|
||||
('2023-03-15', '2023-03-30', self.bill + self.late_bill + self.discount_bill),
|
||||
('2023-03-25', '2023-03-01', self.late_bill),
|
||||
('2023-03-25', '2023-03-30', self.bill + self.late_bill + self.discount_bill),
|
||||
('2023-03-25', '2023-06-30', self.bill + self.late_bill + self.discount_bill + self.late_discount_bill),
|
||||
]:
|
||||
with freeze_time(today):
|
||||
self.assertEqual(self.env['account.move.line'].search([
|
||||
('payment_date', '=', search),
|
||||
('partner_id', '=', self.partner_a.id),
|
||||
]).move_id, expected)
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged, Form
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountPaymentMethodLine(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.bank_journal_1 = cls.company_data['default_journal_bank']
|
||||
cls.bank_journal_2 = cls.company_data['default_journal_bank'].copy()
|
||||
|
||||
cls.inbound_payment_method_line_1 = cls.env['account.payment.method.line'].create({
|
||||
'name': 'new inbound payment method line 1',
|
||||
'payment_method_id': cls.bank_journal_1.available_payment_method_ids[0].id,
|
||||
'payment_type': 'inbound',
|
||||
'journal_id': cls.bank_journal_1.id,
|
||||
})
|
||||
cls.inbound_payment_method_line_2 = cls.env['account.payment.method.line'].create({
|
||||
'name': 'new inbound payment method line 2',
|
||||
'payment_method_id': cls.bank_journal_1.available_payment_method_ids[0].id,
|
||||
'payment_type': 'inbound',
|
||||
'journal_id': cls.bank_journal_1.id,
|
||||
})
|
||||
cls.inbound_payment_method_line_other_journal = cls.env['account.payment.method.line'].create({
|
||||
'name': 'new inbound payment method line other journal',
|
||||
'payment_method_id': cls.bank_journal_2.available_payment_method_ids[0].id,
|
||||
'payment_type': 'inbound',
|
||||
'journal_id': cls.bank_journal_2.id,
|
||||
})
|
||||
cls.partner_c = cls.partner_a.copy()
|
||||
cls.partner_a.property_inbound_payment_method_line_id = cls.inbound_payment_method_line_1
|
||||
cls.partner_b.property_inbound_payment_method_line_id = cls.inbound_payment_method_line_2
|
||||
cls.partner_c.property_inbound_payment_method_line_id = cls.inbound_payment_method_line_other_journal
|
||||
|
||||
cls.move_partner_a = cls.init_invoice(move_type='out_invoice', partner=cls.partner_a, products=cls.product_a, post=True)
|
||||
cls.move_partner_b = cls.init_invoice(move_type='out_invoice', partner=cls.partner_b, products=cls.product_a, post=True)
|
||||
cls.move_partner_c = cls.init_invoice(move_type='out_invoice', partner=cls.partner_c, products=cls.product_a, post=True)
|
||||
|
||||
def assertRegisterPayment(self, expected_journal, expected_payment_method, move_partner, payment_method_line=False):
|
||||
if payment_method_line and expected_payment_method:
|
||||
move_partner.preferred_payment_method_line_id = expected_payment_method
|
||||
|
||||
payment = self.env['account.payment.register'].with_context(
|
||||
active_model='account.move',
|
||||
active_ids=move_partner.ids,
|
||||
).create({})
|
||||
|
||||
if not expected_payment_method:
|
||||
expected_payment_method = payment.journal_id._get_available_payment_method_lines(payment.payment_type)[0]._origin
|
||||
|
||||
self.assertRecordValues(payment, [{
|
||||
'journal_id': expected_journal.id,
|
||||
'payment_method_line_id': expected_payment_method.id,
|
||||
}])
|
||||
|
||||
def test_move_register_payment_wizard(self):
|
||||
"""
|
||||
This test will do a basic flow where we do a register payment from an invoice by using the register payment
|
||||
wizard. If we have a payment method set on the partner, the preferred payment method will be the one from
|
||||
the partner and so the wizard will have the payment method line from the partner. However, we can modify the
|
||||
preferred payment line on the move and so the payment method line and journal of the wizard will be changed.
|
||||
"""
|
||||
# The preferred payment method will be the one set on the partner
|
||||
self.assertRegisterPayment(
|
||||
self.bank_journal_1,
|
||||
self.inbound_payment_method_line_1,
|
||||
self.move_partner_a,
|
||||
)
|
||||
# We then modify it from the move and check if that still works
|
||||
self.assertRegisterPayment(
|
||||
self.bank_journal_1,
|
||||
self.inbound_payment_method_line_2,
|
||||
self.move_partner_a,
|
||||
True,
|
||||
)
|
||||
self.assertRegisterPayment(
|
||||
self.bank_journal_2,
|
||||
self.inbound_payment_method_line_other_journal,
|
||||
self.move_partner_a,
|
||||
True,
|
||||
)
|
||||
|
||||
def test_multiple_moves_register_payment(self):
|
||||
"""
|
||||
This will test the register payment wizard when selecting multiple move with different partner to see if the
|
||||
payment method lines are set correctly.
|
||||
"""
|
||||
# Test with two moves with same payment method lines and same partners
|
||||
move_partner_a_copy = self.move_partner_a.copy()
|
||||
move_partner_a_copy.action_post()
|
||||
self.assertRegisterPayment(
|
||||
self.bank_journal_1,
|
||||
self.inbound_payment_method_line_1,
|
||||
self.move_partner_a + move_partner_a_copy,
|
||||
)
|
||||
|
||||
# Test with two moves with same payment method lines but different partners
|
||||
self.partner_d = self.partner_a.copy()
|
||||
move_partner_d = self.init_invoice(move_type='out_invoice', partner=self.partner_d, products=self.product_a, post=True)
|
||||
self.assertRegisterPayment(
|
||||
self.bank_journal_1,
|
||||
self.inbound_payment_method_line_1,
|
||||
self.move_partner_a + move_partner_d,
|
||||
)
|
||||
|
||||
# Test with two moves with different partners and different payment method lines
|
||||
self.assertRegisterPayment(
|
||||
self.bank_journal_1,
|
||||
None, # We will get in the assertRegisterPayment the first payment method line of the journal
|
||||
self.move_partner_a + self.move_partner_b,
|
||||
)
|
||||
|
||||
def test_move_register_payment_view(self):
|
||||
"""
|
||||
This test will check the payment method line on a payment from the account payment view.
|
||||
When setting a partner the payment method must change and the journal if the payment method line is from
|
||||
another journal that the one that has been set.
|
||||
"""
|
||||
with Form(self.env['account.payment'].with_context(default_partner_id=self.partner_a)) as pay_form:
|
||||
self.assertEqual(pay_form.journal_id.id, self.bank_journal_1.id)
|
||||
self.assertEqual(pay_form.payment_method_line_id.id, self.inbound_payment_method_line_1.id)
|
||||
|
||||
pay_form.partner_id = self.partner_b
|
||||
self.assertEqual(pay_form.payment_method_line_id.id, self.inbound_payment_method_line_2.id)
|
||||
|
||||
pay_form.partner_id = self.partner_c
|
||||
self.assertEqual(pay_form.journal_id.id, self.bank_journal_2.id)
|
||||
self.assertEqual(pay_form.payment_method_line_id.id, self.inbound_payment_method_line_other_journal.id)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,60 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountReport(AccountTestInvoicingCommon):
|
||||
|
||||
def test_copy_report(self):
|
||||
""" Ensure that copying a report correctly adjust codes, formulas and subformulas. """
|
||||
report = self.env['account.report'].create({
|
||||
'name': "Report To Copy",
|
||||
'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'name': "test_line_1",
|
||||
'code': "test_line_1",
|
||||
'sequence': 1,
|
||||
'expression_ids': [
|
||||
Command.create({
|
||||
'date_scope': 'strict_range',
|
||||
'engine': 'external',
|
||||
'formula': 'sum',
|
||||
'label': 'balance',
|
||||
}),
|
||||
]
|
||||
}),
|
||||
Command.create({
|
||||
'name': "test_line_2",
|
||||
'code': "test_line_2",
|
||||
'sequence': 2,
|
||||
'expression_ids': [
|
||||
Command.create({
|
||||
'date_scope': 'strict_range',
|
||||
'engine': 'aggregation',
|
||||
'formula': 'test_line_1.balance',
|
||||
'subformula': 'if_other_expr_above(test_line_1.balance, USD(0))',
|
||||
'label': 'balance',
|
||||
})
|
||||
],
|
||||
})
|
||||
]
|
||||
})
|
||||
copy = report.copy()
|
||||
# Ensure that the two line codes are updated.
|
||||
self.assertEqual(copy.line_ids[0].code, 'test_line_1_COPY')
|
||||
self.assertEqual(copy.line_ids[1].code, 'test_line_2_COPY')
|
||||
# Ensure that the line 2 expression formula and subformula point to the correct code.
|
||||
expression = copy.line_ids[1].expression_ids
|
||||
self.assertEqual(expression.formula, 'test_line_1_COPY.balance')
|
||||
self.assertEqual(expression.subformula, 'if_other_expr_above(test_line_1_COPY.balance, USD(0))')
|
||||
|
||||
with self.assertRaisesRegex(ValidationError, "Invalid formula for expression 'balance' of line 'test_line_2'"):
|
||||
expression.write({
|
||||
'engine': 'account_codes',
|
||||
'formula': 'test(12)',
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountSectionAndSubsection(AccountTestInvoicingCommon):
|
||||
|
||||
def test_get_child_lines_with_one_taxes(self):
|
||||
move = self.init_invoice('out_invoice')
|
||||
move.invoice_line_ids = [
|
||||
Command.create({
|
||||
'name': "Section 1",
|
||||
'display_type': 'line_section',
|
||||
'collapse_prices': True,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': self.tax_sale_a.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'name': "Subsection 1.1",
|
||||
'display_type': 'line_subsection',
|
||||
'collapse_composition': True,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 200,
|
||||
'tax_ids': self.tax_sale_a.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_b.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': self.tax_sale_a.ids,
|
||||
}),
|
||||
]
|
||||
section_lines = move.invoice_line_ids[0]._get_child_lines()
|
||||
expected_values = [
|
||||
{'display_type': 'line_section', 'name': 'Section 1', 'price_subtotal': 400.0, 'taxes': ['15%']},
|
||||
{'display_type': 'product', 'name': 'product_a', 'price_subtotal': 100.0, 'taxes': []},
|
||||
{'display_type': 'product', 'name': 'Subsection 1.1', 'price_subtotal': 300.0, 'taxes': ['15%']},
|
||||
]
|
||||
for expected_value, line_value in zip(expected_values, section_lines):
|
||||
for key, value in expected_value.items():
|
||||
self.assertEqual(line_value[key], value)
|
||||
|
||||
def test_get_child_lines_with_multiple_taxes(self):
|
||||
move = self.init_invoice('out_invoice')
|
||||
move.invoice_line_ids = [
|
||||
Command.create({
|
||||
'name': "Section 1",
|
||||
'display_type': 'line_section',
|
||||
'collapse_prices': True,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': self.tax_sale_a.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': self.tax_sale_b.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'name': "Subsection 1.1",
|
||||
'display_type': 'line_subsection',
|
||||
'collapse_composition': True,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_b.id,
|
||||
'price_unit': 200,
|
||||
'tax_ids': self.tax_sale_a.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product_b.id,
|
||||
'price_unit': 200,
|
||||
'tax_ids': self.tax_sale_b.ids,
|
||||
}),
|
||||
]
|
||||
section_lines = move.invoice_line_ids[0]._get_child_lines()
|
||||
expected_values = [
|
||||
{'display_type': 'line_section', 'name': 'Section 1', 'price_subtotal': 600.0, 'taxes': ['15%', '15% (copy)']},
|
||||
{'display_type': 'product', 'name': 'product_a', 'price_subtotal': 100.0, 'taxes': []},
|
||||
{'display_type': 'product', 'name': 'product_a', 'price_subtotal': 100.0, 'taxes': []},
|
||||
{'display_type': 'product', 'name': 'Subsection 1.1', 'price_subtotal': 200.0, 'taxes': ['15%']},
|
||||
{'display_type': 'product', 'name': 'Subsection 1.1', 'price_subtotal': 200.0, 'taxes': ['15% (copy)']},
|
||||
]
|
||||
for expected_value, line_value in zip(expected_values, section_lines):
|
||||
for key, value in expected_value.items():
|
||||
self.assertEqual(line_value[key], value)
|
||||
|
||||
def test_get_child_lines_with_products_in_subsections(self):
|
||||
move = self._create_invoice(
|
||||
invoice_line_ids=[Command.create(vals) for vals in [
|
||||
{
|
||||
'name': "Section 1",
|
||||
'display_type': 'line_section',
|
||||
'collapse_prices': True,
|
||||
},
|
||||
{
|
||||
'name': "Subsection 1.1",
|
||||
'display_type': 'line_subsection',
|
||||
'collapse_composition': False,
|
||||
},
|
||||
{
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 200,
|
||||
'tax_ids': self.tax_sale_a.ids,
|
||||
},
|
||||
{
|
||||
'name': "Subsection 1.2",
|
||||
'display_type': 'line_subsection',
|
||||
'collapse_composition': False,
|
||||
},
|
||||
{
|
||||
'product_id': self.product_b.id,
|
||||
'price_unit': 200,
|
||||
'tax_ids': self.tax_sale_b.ids,
|
||||
},
|
||||
]]
|
||||
)
|
||||
section_lines = move.invoice_line_ids[0]._get_child_lines()
|
||||
expected_values = [
|
||||
{'display_type': 'line_section', 'name': 'Section 1', 'price_subtotal': 400.0, 'taxes': ['15%', '15% (copy)']},
|
||||
{'display_type': 'line_subsection', 'name': 'Subsection 1.1', 'price_subtotal': 200.0, 'taxes': ['15%']},
|
||||
{'display_type': 'product', 'name': 'product_a', 'price_subtotal': 200.0, 'taxes': []},
|
||||
{'display_type': 'line_subsection', 'name': 'Subsection 1.2', 'price_subtotal': 200.0, 'taxes': ['15% (copy)']},
|
||||
{'display_type': 'product', 'name': 'product_b', 'price_subtotal': 200.0, 'taxes': []},
|
||||
]
|
||||
for expected_value, line_value in zip(expected_values, section_lines):
|
||||
for key, value in expected_value.items():
|
||||
self.assertEqual(line_value[key], value)
|
||||
|
|
@ -1,12 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountTax(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
@classmethod
|
||||
def default_env_context(cls):
|
||||
# OVERRIDE
|
||||
return {}
|
||||
|
||||
def set_up_and_use_tax(self):
|
||||
|
||||
self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2023-01-01',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'invoice_line',
|
||||
'quantity': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
# Create two lines after creating the move so that those lines are not used in the move
|
||||
self.company_data['default_tax_sale'].write({
|
||||
'invoice_repartition_line_ids': [
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': 0.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': 0.0}),
|
||||
],
|
||||
})
|
||||
|
||||
self.flush_tracking()
|
||||
self.assertTrue(self.company_data['default_tax_sale'].is_used)
|
||||
|
||||
def flush_tracking(self):
|
||||
""" Force the creation of tracking values. """
|
||||
self.env.flush_all()
|
||||
self.cr.flush()
|
||||
|
||||
def test_changing_tax_company(self):
|
||||
''' Ensure you can't change the company of an account.tax if there are some journal entries '''
|
||||
|
||||
|
|
@ -26,5 +71,269 @@ class TestAccountTax(AccountTestInvoicingCommon):
|
|||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
with self.assertRaises(UserError):
|
||||
self.company_data['default_tax_sale'].company_id = self.company_data_2['company']
|
||||
|
||||
def test_logging_of_tax_update_when_tax_is_used(self):
|
||||
""" Modifications of a used tax should be logged. """
|
||||
|
||||
self.set_up_and_use_tax()
|
||||
|
||||
self.company_data['default_tax_sale'].write({
|
||||
'name': self.company_data['default_tax_sale'].name + ' MODIFIED',
|
||||
'amount': 21,
|
||||
'amount_type': 'fixed',
|
||||
'type_tax_use': 'purchase',
|
||||
'price_include_override': 'tax_included',
|
||||
'include_base_amount': True,
|
||||
'is_base_affected': False,
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.company_data['default_tax_sale'].message_ids), 1,
|
||||
"Only 1 message should have been created when updating all the values.")
|
||||
# There are 7 tracked values in account.tax and we update each of them, each on should be included in the message
|
||||
self.assertEqual(len(self.company_data['default_tax_sale'].message_ids.tracking_value_ids), 7,
|
||||
"The number of updated value should be 7.")
|
||||
|
||||
def test_logging_of_repartition_lines_addition_when_tax_is_used(self):
|
||||
""" Adding repartition lines in a used tax should be logged. """
|
||||
|
||||
self.set_up_and_use_tax()
|
||||
|
||||
self.company_data['default_tax_sale'].write({
|
||||
'invoice_repartition_line_ids': [
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': -100.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': -100.0}),
|
||||
],
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
previews = self.company_data['default_tax_sale'].message_ids.mapped('preview')
|
||||
self.assertIn(
|
||||
"New Invoice repartition line 4: -100.0 (Factor Percent) None (Account) None (Tax Grids) False (Use in tax closing)",
|
||||
previews
|
||||
)
|
||||
self.assertIn(
|
||||
"New Refund repartition line 4: -100.0 (Factor Percent) None (Account) None (Tax Grids) False (Use in tax closing)",
|
||||
previews
|
||||
)
|
||||
|
||||
def test_logging_of_repartition_lines_update_when_tax_is_used(self):
|
||||
""" Updating repartition lines in a used tax should be logged. """
|
||||
|
||||
self.set_up_and_use_tax()
|
||||
|
||||
last_invoice_rep_line = self.company_data['default_tax_sale'].invoice_repartition_line_ids\
|
||||
.filtered(lambda tax_rep: not tax_rep.factor_percent)
|
||||
last_refund_rep_line = self.company_data['default_tax_sale'].refund_repartition_line_ids\
|
||||
.filtered(lambda tax_rep: not tax_rep.factor_percent)
|
||||
|
||||
self.company_data['default_tax_sale'].write({
|
||||
"invoice_repartition_line_ids": [
|
||||
Command.update(last_invoice_rep_line.id, {
|
||||
'factor_percent': -100,
|
||||
'tag_ids': [Command.create({'name': 'TaxTag12345'})]
|
||||
}),
|
||||
],
|
||||
"refund_repartition_line_ids": [
|
||||
Command.update(last_refund_rep_line.id, {
|
||||
'factor_percent': -100,
|
||||
'account_id': self.company_data['default_account_tax_purchase'].id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
previews = self.company_data['default_tax_sale'].message_ids.mapped('preview')
|
||||
self.assertIn("Invoice repartition line 3: 0.0 -100.0 (Factor Percent) None ['TaxTag12345'] (Tax Grids)", previews)
|
||||
self.assertIn("Refund repartition line 3: 0.0 -100.0 (Factor Percent) None 131000 Tax Paid (Account) False True (Use in tax closing)", previews)
|
||||
|
||||
def test_logging_of_repartition_lines_reordering_when_tax_is_used(self):
|
||||
""" Reordering repartition lines in a used tax should be logged. """
|
||||
|
||||
self.set_up_and_use_tax()
|
||||
|
||||
last_invoice_rep_line = self.company_data['default_tax_sale'].invoice_repartition_line_ids\
|
||||
.filtered(lambda tax_rep: not tax_rep.factor_percent)
|
||||
last_refund_rep_line = self.company_data['default_tax_sale'].refund_repartition_line_ids\
|
||||
.filtered(lambda tax_rep: not tax_rep.factor_percent)
|
||||
|
||||
self.company_data['default_tax_sale'].write({
|
||||
"invoice_repartition_line_ids": [
|
||||
Command.update(last_invoice_rep_line.id, {'sequence': 0}),
|
||||
],
|
||||
"refund_repartition_line_ids": [
|
||||
Command.update(last_refund_rep_line.id, {'sequence': 0}),
|
||||
],
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
previews = self.company_data['default_tax_sale'].message_ids.mapped('preview')
|
||||
self.assertIn("Invoice repartition line 1: 100.0 0.0 (Factor Percent)", previews)
|
||||
self.assertIn("Invoice repartition line 3: 0.0 100.0 (Factor Percent) None 251000 Tax Received (Account) False True (Use in tax closing)", previews)
|
||||
|
||||
def test_logging_of_repartition_lines_removal_when_tax_is_used(self):
|
||||
""" Deleting repartition lines in a used tax should be logged. """
|
||||
|
||||
self.set_up_and_use_tax()
|
||||
|
||||
last_invoice_rep_line = self.company_data['default_tax_sale'].invoice_repartition_line_ids.sorted(key=lambda r: r.sequence)[-1]
|
||||
last_refund_rep_line = self.company_data['default_tax_sale'].refund_repartition_line_ids.sorted(key=lambda r: r.sequence)[-1]
|
||||
|
||||
self.company_data['default_tax_sale'].write({
|
||||
"invoice_repartition_line_ids": [
|
||||
Command.delete(last_invoice_rep_line.id),
|
||||
],
|
||||
"refund_repartition_line_ids": [
|
||||
Command.delete(last_refund_rep_line.id),
|
||||
],
|
||||
})
|
||||
self.flush_tracking()
|
||||
|
||||
previews = self.company_data['default_tax_sale'].message_ids.mapped('preview')
|
||||
self.assertIn(
|
||||
"Removed Invoice repartition line 3: 0.0 (Factor Percent) None (Account) None (Tax Grids) False (Use in tax closing)",
|
||||
previews
|
||||
)
|
||||
self.assertIn(
|
||||
"Removed Refund repartition line 3: 0.0 (Factor Percent) None (Account) None (Tax Grids) False (Use in tax closing)",
|
||||
previews
|
||||
)
|
||||
|
||||
def test_tax_is_used_when_in_transactions(self):
|
||||
''' Ensures that a tax is set to used when it is part of some transactions '''
|
||||
|
||||
# Account.move is one type of transaction
|
||||
tax_invoice = self.env['account.tax'].create({
|
||||
'name': 'test_is_used_invoice',
|
||||
'amount': '100',
|
||||
})
|
||||
|
||||
self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2023-01-01',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'invoice_line',
|
||||
'quantity': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [Command.set(tax_invoice.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
tax_invoice.invalidate_model(fnames=['is_used'])
|
||||
self.assertTrue(tax_invoice.is_used)
|
||||
|
||||
# Account.reconcile is another of transaction
|
||||
tax_reconciliation = self.env['account.tax'].create({
|
||||
'name': 'test_is_used_reconcilition',
|
||||
'amount': '100',
|
||||
})
|
||||
self.env['account.reconcile.model'].create({
|
||||
'name': "test_tax_is_used",
|
||||
'line_ids': [Command.create({
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'tax_ids': [Command.set(tax_reconciliation.ids)],
|
||||
})],
|
||||
})
|
||||
tax_reconciliation.invalidate_model(fnames=['is_used'])
|
||||
self.assertTrue(tax_reconciliation.is_used)
|
||||
|
||||
def test_tax_no_duplicate_in_repartition_line(self):
|
||||
""" Test that whenever a tax generate a second tax line
|
||||
the same tax is not applied to the tax line.
|
||||
"""
|
||||
|
||||
account_1 = self.company_data['default_account_tax_sale'].copy()
|
||||
account_2 = self.company_data['default_account_tax_sale'].copy()
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': "tax",
|
||||
'amount': 15.0,
|
||||
'include_base_amount': True,
|
||||
'invoice_repartition_line_ids': [
|
||||
Command.create({
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': account_1.id,
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': -100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': account_2.id,
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
Command.create({
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': account_1.id,
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': -100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': account_2.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2019-01-01',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'invoice_line',
|
||||
'quantity': 1.0,
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [Command.set(tax.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertRecordValues(invoice, [{
|
||||
'amount_untaxed': 100.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 100.0,
|
||||
}])
|
||||
self.assertRecordValues(invoice.line_ids, [
|
||||
{'display_type': 'product', 'tax_ids': tax.ids, 'balance': -100.0, 'account_id': self.company_data['default_account_revenue'].id},
|
||||
{'display_type': 'tax', 'tax_ids': [], 'balance': -15.0, 'account_id': account_1.id},
|
||||
{'display_type': 'tax', 'tax_ids': [], 'balance': 15.0, 'account_id': account_2.id},
|
||||
{'display_type': 'payment_term', 'tax_ids': [], 'balance': 100.0, 'account_id': self.company_data['default_account_receivable'].id},
|
||||
])
|
||||
|
||||
def test_negative_factor_percent(self):
|
||||
account_1 = self.company_data['default_account_tax_sale'].copy()
|
||||
with self.assertRaisesRegex(ValidationError, r"Invoice and credit note distribution should have a total factor \(\+\) equals to 100\."):
|
||||
self.env['account.tax'].create({
|
||||
'name': "tax",
|
||||
'amount': 15.0,
|
||||
'include_base_amount': True,
|
||||
'invoice_repartition_line_ids': [
|
||||
Command.create({
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': -100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': account_1.id,
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
Command.create({
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
Command.create({
|
||||
'factor_percent': -100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': account_1.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
from dateutil.relativedelta import relativedelta
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestCheckAccountMoves(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.simple_accountman.group_ids = cls.env.ref('account.group_account_invoice')
|
||||
cls.bank_journal = cls.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', cls.company.id)], limit=1)
|
||||
|
||||
def test_try_check_move_with_invoicing_user(self):
|
||||
if 'accountant' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest('accountant is not installed')
|
||||
invoice = self._create_invoice(checked=True)
|
||||
invoice.action_post()
|
||||
with self.assertRaisesRegex(ValidationError, 'Validated entries can only be changed by your accountant.'):
|
||||
invoice.with_user(self.simple_accountman).button_draft()
|
||||
|
||||
invoice.button_draft()
|
||||
self.assertEqual(invoice.state, 'draft')
|
||||
|
||||
invoice.action_post()
|
||||
invoice.checked = False
|
||||
invoice.with_user(self.simple_accountman).button_draft()
|
||||
self.assertEqual(invoice.state, 'draft')
|
||||
|
||||
def test_post_move_auto_check(self):
|
||||
if 'accountant' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest('accountant is not installed')
|
||||
invoice_admin = self._create_invoice()
|
||||
invoice_admin.action_post()
|
||||
# As the user has admin right, the move should be auto checked
|
||||
self.assertTrue(invoice_admin.checked)
|
||||
|
||||
invoice_invoicing = self._create_invoice(user_id=self.simple_accountman.id)
|
||||
invoice_invoicing.with_user(self.simple_accountman).action_post()
|
||||
# As the user has only invoicing right, the move shouldn't be checked
|
||||
self.assertFalse(invoice_invoicing.checked)
|
||||
|
||||
def test_post_move_auto_check_with_auto_post(self):
|
||||
if 'accountant' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest('accountant is not installed')
|
||||
invoice = self._create_invoice(auto_post='at_date', date=fields.Date.today())
|
||||
self.assertFalse(invoice.checked)
|
||||
with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode():
|
||||
self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger()
|
||||
self.assertTrue(invoice.checked)
|
||||
|
||||
def test_create_statement_line_auto_check(self):
|
||||
"""Test if a user changes the reconciliation on a st_line, it marks the bank move as 'To Review'"""
|
||||
if 'accountant' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest('accountant is not installed')
|
||||
payment = self.env['account.payment'].create({
|
||||
'payment_type': 'inbound',
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
||||
'partner_type': 'customer',
|
||||
'amount': 100,
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'memo': 'INV/2025/00001',
|
||||
})
|
||||
payment.action_post()
|
||||
|
||||
bank_line_1 = self.env['account.bank.statement.line'].create([{
|
||||
'journal_id': self.bank_journal.id,
|
||||
'date': '2025-01-01',
|
||||
'payment_ref': "INV/2025/00001",
|
||||
'amount': -100,
|
||||
}])
|
||||
bank_line_1._try_auto_reconcile_statement_lines()
|
||||
self.assertTrue(bank_line_1.move_id.checked)
|
||||
with self.assertRaisesRegex(ValidationError, 'Validated entries can only be changed by your accountant.'):
|
||||
bank_line_1.with_user(self.simple_accountman).delete_reconciled_line(payment.move_id.line_ids[0].id)
|
||||
271
odoo-bringout-oca-ocb-account/account/tests/test_audit_trail.py
Normal file
271
odoo-bringout-oca-ocb-account/account/tests/test_audit_trail.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import logging
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon, AccountTestInvoicingHttpCommon
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged, new_test_user
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAuditTrail(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env['base'].with_context(
|
||||
tracking_disable=False,
|
||||
mail_create_nolog=False,
|
||||
mail_notrack=False,
|
||||
).env
|
||||
cls.env.company.restrictive_audit_trail = False
|
||||
cls.move = cls.create_move()
|
||||
|
||||
@classmethod
|
||||
def create_move(cls):
|
||||
return cls.env['account.move'].create({
|
||||
'date': '2021-04-01',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'balance': 100,
|
||||
'account_id': cls.company_data['default_account_revenue'].id
|
||||
}),
|
||||
Command.create({
|
||||
'balance': -100,
|
||||
'account_id': cls.company_data['default_account_revenue'].id
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def get_trail(self, record):
|
||||
self.env.cr.precommit.run()
|
||||
return self.env['mail.message'].search([
|
||||
('model', '=', record._name),
|
||||
('res_id', '=', record.id),
|
||||
])
|
||||
|
||||
def assertTrail(self, trail, expected):
|
||||
self.assertEqual(len(trail), len(expected))
|
||||
for message, expected_needle in zip(trail, expected[::-1]):
|
||||
self.assertIn(expected_needle, message.account_audit_log_preview)
|
||||
|
||||
def test_can_unlink_draft(self):
|
||||
self.env.company.restrictive_audit_trail = True
|
||||
self.move.unlink()
|
||||
|
||||
def test_cant_unlink_posted(self):
|
||||
self.env.company.restrictive_audit_trail = True
|
||||
self.move.action_post()
|
||||
self.move.button_draft()
|
||||
with self.assertRaisesRegex(UserError, "remove parts of a restricted audit trail"):
|
||||
self.move.unlink()
|
||||
|
||||
def test_cant_unlink_message(self):
|
||||
self.env.company.restrictive_audit_trail = True
|
||||
self.move.action_post()
|
||||
self.env.cr.flush()
|
||||
audit_trail = self.get_trail(self.move)
|
||||
with self.assertRaisesRegex(UserError, "remove parts of a restricted audit trail"):
|
||||
audit_trail.unlink()
|
||||
|
||||
def test_cant_unown_message(self):
|
||||
self.env.company.restrictive_audit_trail = True
|
||||
self.move.action_post()
|
||||
self.env.cr.flush()
|
||||
audit_trail = self.get_trail(self.move)
|
||||
with self.assertRaisesRegex(UserError, "remove parts of a restricted audit trail"):
|
||||
audit_trail.res_id = 0
|
||||
|
||||
def test_cant_unlink_tracking_value(self):
|
||||
self.env.company.restrictive_audit_trail = True
|
||||
self.move.action_post()
|
||||
self.env.cr.precommit.run()
|
||||
self.move.name = 'track this!'
|
||||
audit_trail = self.get_trail(self.move)
|
||||
trackings = audit_trail.tracking_value_ids.sudo()
|
||||
self.assertTrue(trackings)
|
||||
with self.assertRaisesRegex(UserError, "remove parts of a restricted audit trail"):
|
||||
trackings.unlink()
|
||||
|
||||
def test_content(self):
|
||||
messages = ["Journal Entry created"]
|
||||
self.assertTrail(self.get_trail(self.move), messages)
|
||||
|
||||
self.move.action_post()
|
||||
messages.append("Updated\nFalse ⇨ True (Reviewed)\nFalse ⇨ MISC/2021/04/0001 (Number)\nDraft ⇨ Posted (Status)")
|
||||
self.assertTrail(self.get_trail(self.move), messages)
|
||||
|
||||
self.move.button_draft()
|
||||
messages.append("Updated\nTrue ⇨ False (Reviewed)\nPosted ⇨ Draft (Status)")
|
||||
self.assertTrail(self.get_trail(self.move), messages)
|
||||
|
||||
self.move.name = "nawak"
|
||||
messages.append("Updated\nMISC/2021/04/0001 ⇨ nawak (Number)")
|
||||
self.assertTrail(self.get_trail(self.move), messages)
|
||||
|
||||
self.move.line_ids = [
|
||||
Command.update(self.move.line_ids[0].id, {'balance': 300}),
|
||||
Command.update(self.move.line_ids[1].id, {'credit': 200}), # writing on debit/credit or balance both log
|
||||
Command.create({
|
||||
'balance': -100,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
})
|
||||
]
|
||||
messages.extend([
|
||||
"updated\n100.0 ⇨ 300.0",
|
||||
"updated\n-100.0 ⇨ -200.0",
|
||||
"created\n ⇨ 400000 Product Sales (Account)\n0.0 ⇨ -100.0 (Balance)",
|
||||
])
|
||||
self.assertTrail(self.get_trail(self.move), messages)
|
||||
|
||||
self.move.line_ids[0].tax_ids = self.env.company.account_purchase_tax_id
|
||||
suspense_account_code = self.env.company.account_journal_suspense_account_id.code
|
||||
messages.extend([
|
||||
"updated\n ⇨ 15% (Taxes)",
|
||||
"created\n ⇨ 131000 Tax Paid (Account)\n0.0 ⇨ 45.0 (Balance)\nFalse ⇨ 15% (Label)",
|
||||
f"created\n ⇨ {suspense_account_code} Bank Suspense Account (Account)\n0.0 ⇨ -45.0 (Balance)\nFalse ⇨ Automatic Balancing Line (Label)",
|
||||
])
|
||||
self.assertTrail(self.get_trail(self.move), messages)
|
||||
self.move.with_context(dynamic_unlink=True).line_ids.unlink()
|
||||
messages.extend([
|
||||
"deleted\n400000 Product Sales ⇨ (Account)\n300.0 ⇨ 0.0 (Balance)\n15% ⇨ (Taxes)",
|
||||
"deleted\n400000 Product Sales ⇨ (Account)\n-200.0 ⇨ 0.0 (Balance)",
|
||||
"deleted\n400000 Product Sales ⇨ (Account)\n-100.0 ⇨ 0.0 (Balance)",
|
||||
"deleted\n131000 Tax Paid ⇨ (Account)\n45.0 ⇨ 0.0 (Balance)\n15% ⇨ False (Label)",
|
||||
f"deleted\n{suspense_account_code} Bank Suspense Account ⇨ (Account)\n-45.0 ⇨ 0.0 (Balance)\nAutomatic Balancing Line ⇨ False (Label)",
|
||||
])
|
||||
self.assertTrail(self.get_trail(self.move), messages)
|
||||
|
||||
self.env.company.restrictive_audit_trail = True
|
||||
messages_company = ["Updated\nFalse ⇨ True (Restrictive Audit Trail)"]
|
||||
self.assertTrail(self.get_trail(self.company), messages_company)
|
||||
|
||||
def test_partner_notif(self):
|
||||
"""Audit trail should not block partner notification."""
|
||||
user = new_test_user(
|
||||
self.env, 'test-user-notif', groups="base.group_portal",
|
||||
notification_type='email',
|
||||
)
|
||||
# identify that user as being a customer
|
||||
user.partner_id.sudo().customer_rank += 1
|
||||
self.assertGreater(user.partner_id.customer_rank, 0)
|
||||
user.partner_id.message_post(body='Test', partner_ids=user.partner_id.ids)
|
||||
|
||||
def test_partner_unlink(self):
|
||||
"""Audit trail should not block partner unlink if they didn't create moves"""
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Test',
|
||||
'customer_rank': 1,
|
||||
})
|
||||
partner.unlink()
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAuditTrailAttachment(AccountTestInvoicingHttpCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.company.restrictive_audit_trail = True
|
||||
cls.document_installed = 'documents_account' in cls.env['ir.module.module']._installed()
|
||||
if cls.document_installed:
|
||||
folder_test = cls.env['documents.document'].create({
|
||||
'name': 'folder_test',
|
||||
'type': 'folder',
|
||||
})
|
||||
existing_setting = cls.env['documents.account.folder.setting'].sudo().search(
|
||||
[('journal_id', '=', cls.company_data['default_journal_sale'].id)])
|
||||
if existing_setting:
|
||||
existing_setting.folder_id = folder_test
|
||||
else:
|
||||
cls.env['documents.account.folder.setting'].sudo().create({
|
||||
'folder_id': folder_test.id,
|
||||
'journal_id': cls.company_data['default_journal_sale'].id,
|
||||
})
|
||||
|
||||
def _send_and_print(self, invoice):
|
||||
return self.env['account.move.send'].with_context(
|
||||
force_report_rendering=True,
|
||||
)._generate_and_send_invoices(invoice)
|
||||
|
||||
def test_audit_trail_attachment(self):
|
||||
invoice = self.env['account.move'].create([{
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'name': 'product',
|
||||
'quantity': 1,
|
||||
'price_unit': 100,
|
||||
})],
|
||||
}])
|
||||
invoice.action_post()
|
||||
self.assertFalse(invoice.message_main_attachment_id)
|
||||
|
||||
# Print the invoice for the first time
|
||||
first_attachment = self._send_and_print(invoice)
|
||||
self.assertTrue(first_attachment)
|
||||
|
||||
# Remove the attachment, it should only archive it instead of deleting it
|
||||
first_attachment.unlink()
|
||||
self.assertTrue(first_attachment.exists())
|
||||
# But we cannot entirely remove it
|
||||
with self.assertRaisesRegex(UserError, "remove parts of a restricted audit trail."):
|
||||
first_attachment.unlink()
|
||||
|
||||
# Print a second time the invoice, it generates a new attachment
|
||||
invoice.invalidate_recordset()
|
||||
second_attachment = self._send_and_print(invoice)
|
||||
self.assertNotEqual(first_attachment, second_attachment)
|
||||
|
||||
# Make sure we can browse all the attachments in the UI (as it changes the main attachment)
|
||||
first_attachment.register_as_main_attachment()
|
||||
self.assertEqual(invoice.message_main_attachment_id, first_attachment)
|
||||
second_attachment.register_as_main_attachment()
|
||||
self.assertEqual(invoice.message_main_attachment_id, second_attachment)
|
||||
|
||||
if self.document_installed:
|
||||
# Make sure we can change the version history of the document
|
||||
document = self.env['documents.document'].search([
|
||||
('res_model', '=', 'account.move'),
|
||||
('res_id', '=', invoice.id),
|
||||
('name', '=ilike', '%.pdf'),
|
||||
])
|
||||
self.assertTrue(document)
|
||||
document.attachment_id = first_attachment
|
||||
document.attachment_id = second_attachment
|
||||
else:
|
||||
_logger.runbot("Documents module is not installed, skipping part of the test")
|
||||
|
||||
def test_audit_trail_write_attachment(self):
|
||||
invoice = self.env['account.move'].create([{
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({
|
||||
'name': 'product',
|
||||
'quantity': 1,
|
||||
'price_unit': 100,
|
||||
})],
|
||||
}])
|
||||
invoice.action_post()
|
||||
self.assertFalse(invoice.message_main_attachment_id)
|
||||
|
||||
# Print the invoice for the first time
|
||||
self._send_and_print(invoice)
|
||||
attachment = invoice.message_main_attachment_id
|
||||
|
||||
with self.assertRaisesRegex(UserError, "remove parts of a restricted audit trail."):
|
||||
attachment.write({
|
||||
'res_id': self.env.user.id,
|
||||
'res_model': self.env.user._name,
|
||||
})
|
||||
|
||||
with self.assertRaisesRegex(UserError, "remove parts of a restricted audit trail."):
|
||||
attachment.datas = b'new data'
|
||||
|
||||
# Adding an attachment to the log notes should be allowed
|
||||
another_attachment = self.env['ir.attachment'].create({
|
||||
'name': 'doc.pdf',
|
||||
'res_model': 'mail.compose.message',
|
||||
# Ensures a bytes-like object with guessed mimetype = 'application/pdf' (checked in _except_audit_trail())
|
||||
'datas': attachment.datas,
|
||||
})
|
||||
invoice.message_post(message_type='comment', attachment_ids=another_attachment.ids)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,314 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from contextlib import nullcontext, closing
|
||||
from freezegun import freeze_time
|
||||
from functools import partial
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests import tagged, Form
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestCompanyBranch(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
|
||||
cls.company_data['company'].write({
|
||||
'child_ids': [
|
||||
Command.create({'name': 'Branch A'}),
|
||||
Command.create({'name': 'Branch B'}),
|
||||
],
|
||||
})
|
||||
cls.cr.precommit.run() # load the CoA
|
||||
|
||||
cls.root_company = cls.company_data['company']
|
||||
cls.branch_a, cls.branch_b = cls.root_company.child_ids
|
||||
|
||||
def test_chart_template_loading(self):
|
||||
# Some company params have to be the same
|
||||
self.assertEqual(self.root_company.currency_id, self.branch_a.currency_id)
|
||||
self.assertEqual(self.root_company.fiscalyear_last_day, self.branch_a.fiscalyear_last_day)
|
||||
self.assertEqual(self.root_company.fiscalyear_last_month, self.branch_a.fiscalyear_last_month)
|
||||
|
||||
# The accounts are shared
|
||||
root_accounts = self.env['account.account'].search([('company_ids', 'parent_of', self.root_company.id)])
|
||||
branch_a_accounts = self.env['account.account'].search([('company_ids', 'parent_of', self.branch_a.id)])
|
||||
self.assertTrue(root_accounts)
|
||||
self.assertEqual(root_accounts, branch_a_accounts)
|
||||
|
||||
# The journals are shared
|
||||
root_journals = self.env['account.journal'].search([('company_id', 'parent_of', self.root_company.id)])
|
||||
branch_a_journals = self.env['account.journal'].search([('company_id', 'parent_of', self.branch_a.id)])
|
||||
self.assertTrue(root_journals)
|
||||
self.assertEqual(root_journals, branch_a_journals)
|
||||
|
||||
def test_reconciliation(self):
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': '2016-01-01',
|
||||
'company_id': self.branch_a.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'product',
|
||||
'price_unit': 1000,
|
||||
})
|
||||
],
|
||||
})
|
||||
invoice.action_post()
|
||||
refund = self.env['account.move'].create({
|
||||
'move_type': 'out_refund',
|
||||
'invoice_date': '2017-01-01',
|
||||
'company_id': self.root_company.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'product',
|
||||
'price_unit': 1000,
|
||||
})
|
||||
],
|
||||
})
|
||||
refund.action_post()
|
||||
|
||||
payment_lines = (invoice + refund).line_ids.filtered(lambda l: l.display_type == 'payment_term')
|
||||
payment_lines.reconcile()
|
||||
self.assertEqual(payment_lines.mapped('amount_residual'), [0, 0])
|
||||
self.assertFalse(payment_lines.matched_debit_ids.exchange_move_id)
|
||||
|
||||
# Can still open the invoice with only it's branch accessible
|
||||
self.env.invalidate_all()
|
||||
with Form(invoice.with_context(allowed_company_ids=self.branch_a.ids)):
|
||||
pass
|
||||
|
||||
def test_reconciliation_foreign_currency(self):
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': '2016-01-01',
|
||||
'company_id': self.branch_a.id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'product',
|
||||
'price_unit': 1000,
|
||||
})
|
||||
],
|
||||
})
|
||||
invoice.action_post()
|
||||
refund = self.env['account.move'].create({
|
||||
'move_type': 'out_refund',
|
||||
'invoice_date': '2017-01-01',
|
||||
'company_id': self.root_company.id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'product',
|
||||
'price_unit': 1000,
|
||||
})
|
||||
],
|
||||
})
|
||||
refund.action_post()
|
||||
|
||||
payment_lines = (invoice + refund).line_ids.filtered(lambda l: l.display_type == 'payment_term')
|
||||
payment_lines.reconcile()
|
||||
self.assertEqual(payment_lines.mapped('amount_residual'), [0, 0])
|
||||
self.assertTrue(payment_lines.matched_debit_ids.exchange_move_id)
|
||||
self.assertTrue(payment_lines.matched_debit_ids.exchange_move_id.journal_id.company_id, invoice.company_id)
|
||||
|
||||
# Can still open the invoice with only it's branch accessible
|
||||
self.env.invalidate_all()
|
||||
with Form(invoice.with_context(allowed_company_ids=self.branch_a.ids)):
|
||||
pass
|
||||
|
||||
def test_lock_dates(self):
|
||||
moves = self.env['account.move'].search([])
|
||||
moves.filtered(lambda x: x.state != 'draft').button_draft()
|
||||
moves.unlink()
|
||||
for lock, lock_sale, lock_purchase in [
|
||||
('hard_lock_date', True, True),
|
||||
('fiscalyear_lock_date', True, True),
|
||||
('tax_lock_date', True, True),
|
||||
('sale_lock_date', True, False),
|
||||
('purchase_lock_date', False, True),
|
||||
]:
|
||||
for root_lock, branch_lock, invoice_date, company, move_type, failure_expected in (
|
||||
# before both locks
|
||||
('3021-01-01', '3022-01-01', '3020-01-01', self.root_company, 'in_invoice', lock_purchase),
|
||||
('3021-01-01', '3022-01-01', '3020-01-01', self.root_company, 'out_invoice', lock_sale),
|
||||
('3021-01-01', '3022-01-01', '3020-01-01', self.branch_a, 'in_invoice', lock_purchase),
|
||||
('3021-01-01', '3022-01-01', '3020-01-01', self.branch_a, 'out_invoice', lock_sale),
|
||||
# between root and branch lock
|
||||
('3020-01-01', '3022-01-01', '3021-01-01', self.root_company, 'in_invoice', False),
|
||||
('3020-01-01', '3022-01-01', '3021-01-01', self.root_company, 'out_invoice', False),
|
||||
('3020-01-01', '3022-01-01', '3021-01-01', self.branch_a, 'in_invoice', lock_purchase),
|
||||
('3020-01-01', '3022-01-01', '3021-01-01', self.branch_a, 'out_invoice', lock_sale),
|
||||
# between branch and root lock
|
||||
('3022-01-01', '3020-01-01', '3021-01-01', self.root_company, 'in_invoice', lock_purchase),
|
||||
('3022-01-01', '3020-01-01', '3021-01-01', self.root_company, 'out_invoice', lock_sale),
|
||||
('3022-01-01', '3020-01-01', '3021-01-01', self.branch_a, 'in_invoice', lock_purchase),
|
||||
('3022-01-01', '3020-01-01', '3021-01-01', self.branch_a, 'out_invoice', lock_sale),
|
||||
# after both locks
|
||||
('3020-01-01', '3021-01-01', '3022-01-01', self.root_company, 'in_invoice', False),
|
||||
('3020-01-01', '3021-01-01', '3022-01-01', self.root_company, 'out_invoice', False),
|
||||
('3020-01-01', '3021-01-01', '3022-01-01', self.branch_a, 'in_invoice', False),
|
||||
('3020-01-01', '3021-01-01', '3022-01-01', self.branch_a, 'out_invoice', False),
|
||||
):
|
||||
with self.subTest(
|
||||
lock=lock,
|
||||
root_lock=root_lock,
|
||||
branch_lock=branch_lock,
|
||||
invoice_date=invoice_date,
|
||||
move_type=move_type,
|
||||
company=company.name,
|
||||
), closing(self.env.cr.savepoint()):
|
||||
check = partial(self.assertRaises, UserError) if failure_expected else nullcontext
|
||||
move = self.init_invoice(
|
||||
move_type, amounts=[100], taxes=self.root_company.account_sale_tax_id,
|
||||
invoice_date=invoice_date, post=True, company=company,
|
||||
)
|
||||
self.assertEqual(move.date, fields.Date.to_date(invoice_date))
|
||||
with freeze_time('4000-01-01'): # ensure we don't lock in the future
|
||||
self.root_company[lock] = root_lock
|
||||
self.branch_a[lock] = branch_lock
|
||||
with check():
|
||||
move.button_draft()
|
||||
|
||||
def test_change_record_company(self):
|
||||
account = self.env['account.account'].create({
|
||||
'name': 'volatile',
|
||||
'code': 'vola',
|
||||
'account_type': 'income',
|
||||
'company_ids': [Command.link(self.branch_a.id)]
|
||||
})
|
||||
account_lines = [Command.create({
|
||||
'account_id': account.id,
|
||||
'name': 'name',
|
||||
})]
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'volatile',
|
||||
})
|
||||
tax_lines = [Command.create({
|
||||
'account_id': self.root_company.account_journal_suspense_account_id.id,
|
||||
'tax_ids': [Command.set(tax.ids)],
|
||||
'name': 'name',
|
||||
})]
|
||||
for record, lines, company_field in (
|
||||
(account, account_lines, 'company_ids'),
|
||||
(tax, tax_lines, 'company_id'),
|
||||
):
|
||||
with self.subTest(model=record._name):
|
||||
self.env['account.move'].create({'company_id': self.branch_a.id, 'line_ids': lines})
|
||||
# Can switch to main
|
||||
record[company_field] = self.root_company
|
||||
|
||||
# Can switch back
|
||||
record[company_field] = self.branch_a
|
||||
|
||||
# Can't use in main if owned by a branch
|
||||
with self.assertRaisesRegex(UserError, 'belongs to another company'):
|
||||
self.env['account.move'].create({'company_id': self.root_company.id, 'line_ids': lines})
|
||||
|
||||
# Can still switch to main
|
||||
record[company_field] = self.root_company
|
||||
|
||||
# Can use in main now
|
||||
self.env['account.move'].create({'company_id': self.root_company.id, 'line_ids': lines})
|
||||
|
||||
# Can't switch back to branch if used in main
|
||||
with self.assertRaisesRegex(UserError, 'journal items linked'):
|
||||
record[company_field] = self.branch_a
|
||||
|
||||
def test_branch_should_keep_parent_company_currency(self):
|
||||
test_country = self.env['res.country'].create({
|
||||
'name': 'Gold Country',
|
||||
'code': 'zz',
|
||||
'currency_id': self.other_currency.id
|
||||
})
|
||||
root_company = self.env['res.company'].create({
|
||||
'name': 'Gold Company',
|
||||
'country_id': test_country.id,
|
||||
})
|
||||
# with the generic_coa, try_loading forces currency_id to USD and account_fiscal_country_id to United States
|
||||
self.env['account.chart.template'].try_loading('generic_coa', company=root_company, install_demo=False)
|
||||
# So we write these values after try_loading
|
||||
root_company.write({
|
||||
'currency_id': test_country.currency_id.id,
|
||||
'account_fiscal_country_id': test_country.id,
|
||||
})
|
||||
|
||||
root_company.write({
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'Gold Branch',
|
||||
'country_id': test_country.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.env['account.chart.template'].try_loading('generic_coa', company=root_company.child_ids[0], install_demo=False)
|
||||
self.assertEqual(root_company.currency_id, root_company.child_ids[0].currency_id)
|
||||
|
||||
def test_switch_company_currency(self):
|
||||
"""
|
||||
A user should not be able to switch the currency of another company
|
||||
when that company already has posted account move lines.
|
||||
"""
|
||||
# Create company A (user's company)
|
||||
company_a = self.env['res.company'].create({
|
||||
'name': "Company A",
|
||||
})
|
||||
|
||||
# Get company B from test setup
|
||||
company_b = self.company_data['company']
|
||||
|
||||
# Create a purchase journal for company B
|
||||
journal = self.env['account.journal'].create({
|
||||
'name': "Vendor Bills Journal",
|
||||
'code': "VEND",
|
||||
'type': "purchase",
|
||||
'company_id': company_b.id,
|
||||
'currency_id': company_b.currency_id.id,
|
||||
})
|
||||
|
||||
# Create an invoice for company B
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': "in_invoice",
|
||||
'company_id': company_b.id,
|
||||
'journal_id': journal.id,
|
||||
})
|
||||
invoice.currency_id = self.env.ref('base.USD').id
|
||||
|
||||
# Add a line to the invoice using an expense account
|
||||
self.env['account.move.line'].create({
|
||||
'move_id': invoice.id,
|
||||
'account_id': self.company_data["default_account_expense"].id,
|
||||
'name': "Test Invoice Line",
|
||||
'company_id': company_b.id,
|
||||
})
|
||||
|
||||
# Create a user that only belongs to company A
|
||||
user = self.env['res.users'].create({
|
||||
'name': "User A",
|
||||
'login': "user_a@example.com",
|
||||
'email': "user_a@example.com",
|
||||
'company_id': company_a.id,
|
||||
'company_ids': [Command.set([company_a.id])],
|
||||
})
|
||||
|
||||
# Try to change company B's currency as user A (should raise UserError)
|
||||
user_env = self.env(user=user)
|
||||
with self.assertRaises(UserError):
|
||||
user_env['res.company'].browse(company_b.id).write({
|
||||
'currency_id': self.env.ref('base.EUR').id,
|
||||
})
|
||||
|
||||
def test_set_fiscalyear_last_day_to_negative_value(self):
|
||||
"""Test that ensure that fiscalyear_last_day raises ValidationError when set
|
||||
to negative value."""
|
||||
with self.assertRaises(ValidationError):
|
||||
self.root_company.fiscalyear_last_day = -1
|
||||
300
odoo-bringout-oca-ocb-account/account/tests/test_dict_to_xml.py
Normal file
300
odoo-bringout-oca-ocb-account/account/tests/test_dict_to_xml.py
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
from lxml import etree
|
||||
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
from odoo.addons.account.tools import dict_to_xml
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestDictToXml(TransactionCase):
|
||||
def assertXmlEqual(self, element1, element2):
|
||||
self.assertEqual(etree.tostring(element1), etree.tostring(element2))
|
||||
|
||||
def test_10_empty_node(self):
|
||||
element = dict_to_xml(node={}, tag='Node')
|
||||
self.assertIsNone(element)
|
||||
|
||||
def test_11_render_empty_node(self):
|
||||
element = dict_to_xml(node={}, tag='Node', render_empty_nodes=True)
|
||||
self.assertXmlEqual(element, etree.fromstring('<Node/>'))
|
||||
|
||||
def test_21_simple_node(self):
|
||||
node = {
|
||||
'_tag': 'Node',
|
||||
'_text': 'content',
|
||||
'_comment': 'comment',
|
||||
'attribute1': 'value1',
|
||||
'attribute2': None,
|
||||
}
|
||||
element = dict_to_xml(node)
|
||||
self.assertXmlEqual(element, etree.fromstring('<Node attribute1="value1">content</Node>'))
|
||||
|
||||
def test_22_simple_node_with_nsmap(self):
|
||||
node = {
|
||||
'_tag': 'ns1:Node',
|
||||
'_text': 'content',
|
||||
'_comment': 'comment',
|
||||
'attribute1': 'value1',
|
||||
'ns2:attribute2': 'value2',
|
||||
'ns2:attribute3': None,
|
||||
}
|
||||
nsmap = {
|
||||
None: 'urn:ns0',
|
||||
'ns1': 'urn:ns1',
|
||||
'ns2': 'urn:ns2',
|
||||
}
|
||||
element = dict_to_xml(node, nsmap=nsmap)
|
||||
self.assertXmlEqual(element, etree.fromstring(
|
||||
'<ns1:Node xmlns="urn:ns0" xmlns:ns1="urn:ns1" xmlns:ns2="urn:ns2" attribute1="value1" ns2:attribute2="value2">'
|
||||
'content</ns1:Node>'
|
||||
))
|
||||
|
||||
def test_31_compound_node(self):
|
||||
node = {
|
||||
'_tag': 'Parent',
|
||||
'Child1': {
|
||||
'_text': 'content 1',
|
||||
'attribute1': 'value1',
|
||||
},
|
||||
'Child2': {
|
||||
'_text': None,
|
||||
'attribute2': None,
|
||||
},
|
||||
'Child3': [
|
||||
{
|
||||
'attribute3': 'value3',
|
||||
},
|
||||
{
|
||||
'attribute4': 'value4',
|
||||
},
|
||||
],
|
||||
}
|
||||
element = dict_to_xml(node)
|
||||
self.assertXmlEqual(element, etree.fromstring(
|
||||
'<Parent><Child1 attribute1="value1">content 1</Child1>'
|
||||
'<Child3 attribute3="value3"/><Child3 attribute4="value4"/>'
|
||||
'</Parent>'
|
||||
))
|
||||
|
||||
def test_32_compound_node_render_empty_nodes(self):
|
||||
node = {
|
||||
'_tag': 'Parent',
|
||||
'Child1': {
|
||||
'_text': 'content 1',
|
||||
'attribute1': 'value1',
|
||||
},
|
||||
'Child2': {
|
||||
'_text': None,
|
||||
'attribute2': None,
|
||||
},
|
||||
'Child3': [
|
||||
{
|
||||
'attribute3': 'value3',
|
||||
},
|
||||
{
|
||||
'attribute4': None,
|
||||
},
|
||||
None,
|
||||
],
|
||||
}
|
||||
element = dict_to_xml(node, render_empty_nodes=True)
|
||||
self.assertXmlEqual(element, etree.fromstring(
|
||||
'<Parent><Child1 attribute1="value1">content 1</Child1>'
|
||||
'<Child2/>'
|
||||
'<Child3 attribute3="value3"/>'
|
||||
'<Child3/>'
|
||||
'</Parent>'
|
||||
))
|
||||
|
||||
def test_33_compound_node_with_template(self):
|
||||
node = {
|
||||
'_tag': 'Parent',
|
||||
'Child3': [
|
||||
{
|
||||
'attribute3': 'value3',
|
||||
},
|
||||
{
|
||||
'attribute4': 'value4',
|
||||
},
|
||||
None,
|
||||
],
|
||||
'Child2': {
|
||||
'_text': None,
|
||||
'attribute2': None,
|
||||
},
|
||||
'Child1': {
|
||||
'_text': 'content 1',
|
||||
'attribute1': 'value1',
|
||||
},
|
||||
}
|
||||
template = {
|
||||
'Child1': {},
|
||||
'Child3': {},
|
||||
}
|
||||
element = dict_to_xml(node, template=template)
|
||||
self.assertXmlEqual(element, etree.fromstring(
|
||||
'<Parent><Child1 attribute1="value1">content 1</Child1>'
|
||||
'<Child3 attribute3="value3"/>'
|
||||
'<Child3 attribute4="value4"/>'
|
||||
'</Parent>'
|
||||
))
|
||||
|
||||
def test_34_compound_node_with_template_raises(self):
|
||||
node = {
|
||||
'_tag': 'Parent',
|
||||
'Child3': [
|
||||
{
|
||||
'attribute3': 'value3',
|
||||
},
|
||||
{
|
||||
'attribute4': 'value4',
|
||||
},
|
||||
None,
|
||||
],
|
||||
'UnknownChild': {
|
||||
'_text': 'something',
|
||||
},
|
||||
'Child1': {
|
||||
'_text': 'content 1',
|
||||
'attribute1': 'value1',
|
||||
},
|
||||
}
|
||||
template = {
|
||||
'Child1': {},
|
||||
'Child3': {},
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
dict_to_xml(node, template=template)
|
||||
|
||||
def test_35_compound_node_with_template_and_nsmap(self):
|
||||
node = {
|
||||
'_tag': 'Parent',
|
||||
'ns3:Child3': [
|
||||
{
|
||||
'attribute3': 'value3',
|
||||
},
|
||||
{
|
||||
'attribute4': 'value4',
|
||||
},
|
||||
None,
|
||||
],
|
||||
'ns2:Child2': {
|
||||
'_text': None,
|
||||
'attribute2': None,
|
||||
},
|
||||
'ns1:Child1': {
|
||||
'_text': 'content 1',
|
||||
'attribute1': 'value1',
|
||||
},
|
||||
}
|
||||
template = {
|
||||
'ns1:Child1': {},
|
||||
'ns3:Child3': {},
|
||||
}
|
||||
nsmap = {
|
||||
None: 'urn:ns0',
|
||||
'ns1': 'urn:ns1',
|
||||
'ns2': 'urn:ns2',
|
||||
'ns3': 'urn:ns3',
|
||||
}
|
||||
element = dict_to_xml(node, template=template, nsmap=nsmap)
|
||||
self.assertXmlEqual(element, etree.fromstring(
|
||||
'<Parent xmlns="urn:ns0" xmlns:ns1="urn:ns1" xmlns:ns2="urn:ns2" xmlns:ns3="urn:ns3">'
|
||||
'<ns1:Child1 attribute1="value1">content 1</ns1:Child1>'
|
||||
'<ns3:Child3 attribute3="value3"/>'
|
||||
'<ns3:Child3 attribute4="value4"/>'
|
||||
'</Parent>'
|
||||
))
|
||||
|
||||
def test_40_complex_example(self):
|
||||
node = {
|
||||
'_tag': 'Parent',
|
||||
'_comment': 'comment',
|
||||
'ns1:Child1': [
|
||||
{
|
||||
'ns2:attribute1': 'value1',
|
||||
'ns3:SubChild1': {
|
||||
'_text': 'content 1',
|
||||
'attribute2': 'value2',
|
||||
},
|
||||
'ns3:SubChild2': {
|
||||
'_text': None,
|
||||
},
|
||||
},
|
||||
{
|
||||
'ns2:attribute2': None,
|
||||
'ns3:Subchild3': {
|
||||
'_text': None
|
||||
},
|
||||
},
|
||||
None,
|
||||
],
|
||||
}
|
||||
template = {
|
||||
'ns1:Child1': {
|
||||
'ns3:SubChild1': {},
|
||||
},
|
||||
}
|
||||
nsmap = {
|
||||
None: 'urn:ns0',
|
||||
'ns1': 'urn:ns1',
|
||||
'ns2': 'urn:ns2',
|
||||
'ns3': 'urn:ns3',
|
||||
}
|
||||
element = dict_to_xml(node, template=template, nsmap=nsmap)
|
||||
self.assertXmlEqual(element, etree.fromstring(
|
||||
'<Parent xmlns="urn:ns0" xmlns:ns1="urn:ns1" xmlns:ns2="urn:ns2" xmlns:ns3="urn:ns3">'
|
||||
'<ns1:Child1 ns2:attribute1="value1">'
|
||||
'<ns3:SubChild1 attribute2="value2">content 1</ns3:SubChild1>'
|
||||
'</ns1:Child1>'
|
||||
'</Parent>'
|
||||
))
|
||||
|
||||
def test_41_complex_example_render_empty_nodes(self):
|
||||
node = {
|
||||
'_tag': 'Parent',
|
||||
'_comment': 'comment',
|
||||
'ns1:Child1': [
|
||||
{
|
||||
'ns2:attribute1': 'value1',
|
||||
'ns3:SubChild1': {
|
||||
'_text': 'content 1',
|
||||
'attribute2': 'value2',
|
||||
},
|
||||
'ns3:SubChild2': {
|
||||
'_text': None,
|
||||
},
|
||||
},
|
||||
{
|
||||
'ns2:attribute2': None,
|
||||
'ns3:Subchild3': {
|
||||
'_text': None
|
||||
},
|
||||
},
|
||||
None,
|
||||
],
|
||||
}
|
||||
template = {
|
||||
'ns1:Child1': {
|
||||
'ns3:SubChild1': {},
|
||||
'ns3:SubChild2': {},
|
||||
'ns3:Subchild3': {},
|
||||
},
|
||||
}
|
||||
nsmap = {
|
||||
None: 'urn:ns0',
|
||||
'ns1': 'urn:ns1',
|
||||
'ns2': 'urn:ns2',
|
||||
'ns3': 'urn:ns3',
|
||||
}
|
||||
element = dict_to_xml(node, template=template, render_empty_nodes=True, nsmap=nsmap)
|
||||
self.assertXmlEqual(element, etree.fromstring(
|
||||
'<Parent xmlns="urn:ns0" xmlns:ns1="urn:ns1" xmlns:ns2="urn:ns2" xmlns:ns3="urn:ns3">'
|
||||
'<ns1:Child1 ns2:attribute1="value1">'
|
||||
'<ns3:SubChild1 attribute2="value2">content 1</ns3:SubChild1>'
|
||||
'<ns3:SubChild2/>'
|
||||
'</ns1:Child1>'
|
||||
'<ns1:Child1>'
|
||||
'<ns3:Subchild3/>'
|
||||
'</ns1:Child1>'
|
||||
'</Parent>'
|
||||
))
|
||||
66
odoo-bringout-oca-ocb-account/account/tests/test_digest.py
Normal file
66
odoo-bringout-oca-ocb-account/account/tests/test_digest.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.digest.tests.common import TestDigestCommon
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountDigest(TestDigestCommon):
|
||||
|
||||
@classmethod
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
account1 = cls.env['account.account'].search([('internal_group', '=', 'income'), ('company_ids', '=', cls.company_1.id)], limit=1)
|
||||
account2 = cls.env['account.account'].search([('internal_group', '=', 'expense'), ('company_ids', '=', cls.company_1.id)], limit=1)
|
||||
cls.env['account.journal'].with_company(cls.company_2).create({
|
||||
'name': 'Test Journal',
|
||||
'code': 'code',
|
||||
'type': 'general',
|
||||
})
|
||||
|
||||
comp2_account, comp2_account2 = cls.env['account.account'].create([{
|
||||
'name': 'Account 1 Company 2',
|
||||
'account_type': 'expense_depreciation',
|
||||
'code': 'aaaaaa',
|
||||
'company_ids': [Command.link(cls.company_2.id)],
|
||||
}, {
|
||||
'name': 'Account 2 Company 2',
|
||||
'account_type': 'income_other',
|
||||
'code': 'bbbbbb',
|
||||
'company_ids': [Command.link(cls.company_2.id)],
|
||||
}])
|
||||
|
||||
cls.env['account.move'].search([]).state = 'draft'
|
||||
|
||||
moves = cls.env['account.move'].create({
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 5, 'credit': 0, 'account_id': account1.id}),
|
||||
(0, 0, {'debit': 0, 'credit': 5, 'account_id': account2.id}),
|
||||
(0, 0, {'debit': 8, 'credit': 0, 'account_id': account1.id}),
|
||||
(0, 0, {'debit': 0, 'credit': 8, 'account_id': account2.id}),
|
||||
],
|
||||
})
|
||||
|
||||
moves |= cls.env['account.move'].with_company(cls.company_2).create({
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 0, 'credit': 2, 'account_id': comp2_account.id}),
|
||||
(0, 0, {'debit': 2, 'credit': 0, 'account_id': comp2_account2.id}),
|
||||
],
|
||||
})
|
||||
|
||||
moves.state = 'posted'
|
||||
|
||||
def test_kpi_account_total_revenue_value(self):
|
||||
self.assertEqual(int(self.digest_1.kpi_account_total_revenue_value), -13)
|
||||
self.assertEqual(int(self.digest_2.kpi_account_total_revenue_value), -2)
|
||||
self.assertEqual(int(self.digest_3.kpi_account_total_revenue_value), -13)
|
||||
|
||||
self.digest_3.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
int(self.digest_3.with_company(self.company_2).kpi_account_total_revenue_value),
|
||||
-2,
|
||||
msg='When no company is set, the KPI must be computed based on the current company',
|
||||
)
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestDownloadDocs(AccountTestInvoicingHttpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
invoice_1 = cls.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({'price_unit': 100})],
|
||||
'attachment_ids': [Command.create({'name': "Attachment", 'mimetype': 'text/plain', 'res_model': 'account.move', 'datas': "test"})],
|
||||
})
|
||||
invoice_2 = cls.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': cls.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({'price_unit': 200})]
|
||||
})
|
||||
cls.invoices = invoice_1 + invoice_2
|
||||
cls.invoices.action_post()
|
||||
cls.invoices._generate_and_send()
|
||||
assert invoice_1.invoice_pdf_report_id and invoice_2.invoice_pdf_report_id
|
||||
|
||||
def test_download_invoice_attachments_not_auth(self):
|
||||
url = f'/account/download_invoice_attachments/{",".join(map(str, self.invoices.invoice_pdf_report_id.ids))}'
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn(
|
||||
'oe_login_form',
|
||||
res.content.decode('utf-8'),
|
||||
'When not authenticated, the download is not possible.'
|
||||
)
|
||||
|
||||
def test_download_invoice_attachments_one(self):
|
||||
attachment = self.invoices[0].invoice_pdf_report_id
|
||||
url = f'/account/download_invoice_attachments/{attachment.id}'
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.content, attachment.raw)
|
||||
|
||||
def test_download_invoice_attachments_multiple(self):
|
||||
attachments = self.invoices.invoice_pdf_report_id
|
||||
url = f'/account/download_invoice_attachments/{",".join(map(str, attachments.ids))}'
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
with ZipFile(BytesIO(res.content)) as zip_file:
|
||||
self.assertEqual(
|
||||
zip_file.namelist(),
|
||||
self.invoices.invoice_pdf_report_id.mapped('name'),
|
||||
)
|
||||
|
||||
def test_download_invoice_documents_filetype_one(self):
|
||||
url = f'/account/download_invoice_documents/{self.invoices[0].id}/pdf'
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.content, self.invoices[0].invoice_pdf_report_id.raw)
|
||||
|
||||
def test_download_invoice_documents_filetype_multiple(self):
|
||||
url = f'/account/download_invoice_documents/{",".join(map(str, self.invoices.ids))}/pdf'
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
with ZipFile(BytesIO(res.content)) as zip_file:
|
||||
self.assertEqual(
|
||||
zip_file.namelist(),
|
||||
self.invoices.invoice_pdf_report_id.mapped('name'),
|
||||
)
|
||||
|
||||
def test_download_invoice_documents_filetype_all(self):
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
url = f'/account/download_invoice_documents/{",".join(map(str, self.invoices.ids))}/all'
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
with ZipFile(BytesIO(res.content)) as zip_file:
|
||||
file_names = zip_file.namelist()
|
||||
self.assertEqual(len(file_names), 2)
|
||||
self.assertTrue(self.invoices[0].invoice_pdf_report_id.name in file_names)
|
||||
self.assertTrue(self.invoices[1].invoice_pdf_report_id.name in file_names)
|
||||
|
||||
def test_download_moves_attachments(self):
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
url = f'/account/download_move_attachments/{",".join(map(str, self.invoices.ids))}'
|
||||
attachment_names = sorted([doc['filename'] for invoice in self.invoices for doc in invoice._get_invoice_legal_documents_all()])
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
with ZipFile(BytesIO(res.content)) as zip_file:
|
||||
file_names = sorted(zip_file.namelist())
|
||||
self.assertEqual(file_names, attachment_names)
|
||||
|
||||
def test_download_moves_attachments_with_bills(self):
|
||||
bill = self.init_invoice('in_invoice', products=self.product_a, post=True)
|
||||
bill.message_main_attachment_id = self.env['ir.attachment'].create({'name': "Attachment", 'mimetype': 'text/plain', 'res_model': 'account.move', 'datas': "test_bill"})
|
||||
attachment_names = [bill.message_main_attachment_id.name]
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
url = f'/account/download_move_attachments/{bill.id}'
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
with ZipFile(BytesIO(res.content)) as zip_file:
|
||||
file_names = sorted(zip_file.namelist())
|
||||
self.assertEqual(file_names, attachment_names)
|
||||
|
||||
def test_download_moves_attachments_with_duplicate_names(self):
|
||||
bill_1 = self.init_invoice('in_invoice', products=self.product_a, post=True)
|
||||
bill_2 = self.init_invoice('in_invoice', products=self.product_a, post=True)
|
||||
bill_3 = self.init_invoice('in_invoice', products=self.product_a, post=True)
|
||||
att_name = "Attachment"
|
||||
bill_1.message_main_attachment_id = self.env['ir.attachment'].create({'name': att_name, 'mimetype': 'text/plain', 'res_model': 'account.move', 'datas': "test_bill"})
|
||||
bill_2.message_main_attachment_id = self.env['ir.attachment'].create({'name': att_name, 'mimetype': 'text/plain', 'res_model': 'account.move', 'datas': "test_bill"})
|
||||
bill_3.message_main_attachment_id = self.env['ir.attachment'].create({'name': f"{att_name} (1)", 'mimetype': 'text/plain', 'res_model': 'account.move', 'datas': "test_bill"})
|
||||
attachment_names = [att_name, f"{att_name} (1)", f"{att_name} (1) (1)"]
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
|
||||
url = f'/account/download_move_attachments/{bill_1.id},{bill_2.id},{bill_3.id}'
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
with ZipFile(BytesIO(res.content)) as zip_file:
|
||||
file_names = sorted(zip_file.namelist())
|
||||
self.assertEqual(file_names, attachment_names)
|
||||
|
||||
att_name = "Attachment.ext"
|
||||
bill_1.message_main_attachment_id.name = att_name
|
||||
bill_2.message_main_attachment_id.name = att_name
|
||||
attachment_names = [f"{att_name.split('.')[0]} (1).{att_name.split('.')[1]}", att_name]
|
||||
|
||||
url = f'/account/download_move_attachments/{bill_1.id},{bill_2.id}'
|
||||
res = self.url_open(url)
|
||||
with ZipFile(BytesIO(res.content)) as zip_file:
|
||||
file_names = sorted(zip_file.namelist())
|
||||
self.assertEqual(file_names, attachment_names)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
@tagged('external_l10n', '-at_install', 'post_install', '-standard', 'external')
|
||||
class TestDownloadXsds(TransactionCase):
|
||||
def test_download_xsds(self):
|
||||
self.env['ir.attachment'].action_download_xsd_files()
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
|
||||
|
||||
|
||||
class TestDuplicatePartnerBank(SavepointCaseWithUserDemo):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.company_a = cls.env['res.company'].create({"name": "companyA"})
|
||||
cls.user_a = cls.env['res.users'].with_company(cls.company_a).create({"name": "userA", "login": "test@test.com", "group_ids": [(6, 0, [cls.env.ref("base.group_user").id, cls.env.ref("base.group_partner_manager").id])]})
|
||||
cls.partner_a = cls.env['res.partner'].with_user(cls.user_a).create({"name": "PartnerA", "company_id": cls.company_a.id})
|
||||
cls.partner_bank_a = cls.env['res.partner.bank'].with_user(cls.user_a).create({"acc_number": "12345", "partner_id": cls.partner_a.id})
|
||||
|
||||
cls.company_b = cls.env['res.company'].create({"name": "companyB"})
|
||||
cls.user_b = cls.env['res.users'].with_company(cls.company_b).create({"name": "userB", "login": "test1@test.com", "group_ids": [(6, 0, [cls.env.ref("base.group_user").id, cls.env.ref("base.group_partner_manager").id])]})
|
||||
cls.partner_b = cls.env['res.partner'].with_user(cls.user_b).create({"name": "PartnerB", "company_id": cls.company_b.id})
|
||||
cls.partner_bank_b = cls.env['res.partner.bank'].with_user(cls.user_b).create({"acc_number": "12345", "partner_id": cls.partner_b.id})
|
||||
|
||||
def test_duplicate_acc_number_different_company(self):
|
||||
self.assertFalse(self.partner_bank_b.duplicate_bank_partner_ids)
|
||||
|
||||
def test_duplicate_acc_number_no_company(self):
|
||||
self.partner_a.company_id = False
|
||||
self.partner_bank_a.company_id = False
|
||||
self.partner_b.company_id = False
|
||||
self.partner_bank_b.company_id = False
|
||||
self.assertTrue(self.partner_bank_a.duplicate_bank_partner_ids, self.partner_a)
|
||||
|
||||
def test_duplicate_acc_number_b_company(self):
|
||||
self.partner_a.company_id = False
|
||||
self.partner_bank_a.company_id = False
|
||||
self.assertTrue(self.partner_bank_b.duplicate_bank_partner_ids, self.partner_a)
|
||||
|
||||
def test_remove_bank_account_from_partner(self):
|
||||
bank = self.env['res.bank'].create({'name': 'SBI Bank'})
|
||||
partner = self.env['res.partner'].create({'name': 'Rich Cat'})
|
||||
self.partner_bank_a.write({
|
||||
'acc_number': '99999',
|
||||
'bank_id': bank.id,
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
|
||||
self.assertEqual(len(partner.bank_ids), 1)
|
||||
|
||||
with Form(partner) as partner_form:
|
||||
partner_form.bank_ids.remove(0)
|
||||
|
||||
self.assertEqual(len(partner.bank_ids), 0)
|
||||
|
||||
def test_duplicate_acc_number_inactive_bank_account(self):
|
||||
self.partner_bank_b.active = False
|
||||
self.assertFalse(self.partner_bank_a.duplicate_bank_partner_ids)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,146 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
|
||||
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
|
||||
<cbc:ID>F26/0045</cbc:ID>
|
||||
<cbc:IssueDate>2026-02-28</cbc:IssueDate>
|
||||
<cbc:DueDate>2026-02-01</cbc:DueDate>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:Note>Test Invoice Note</cbc:Note>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:OrderReference>
|
||||
<cbc:ID>F26/0045</cbc:ID>
|
||||
</cac:OrderReference>
|
||||
<cac:AdditionalDocumentReference>
|
||||
<cbc:ID>test_pdf.pdf</cbc:ID>
|
||||
<cac:Attachment>
|
||||
<cbc:EmbeddedDocumentBinaryObject mimeCode="application/pdf" filename="test_pdf.pdf">
|
||||
JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9Db3VudCAxCi9LaWRzIFszIDAgUl0KPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCAyIDAgUgovTWVkaWFCb3ggWzAgMCAzMDAgMTQ0XQovQ29udGVudHMgNCAwIFIKL1Jlc291cmNlcyA8PAovRm9udCA8PAovRjEgNSAwIFIKPj4KPj4KPj4KZW5kb2JqCjQgMCBvYmoKPDwKL0xlbmd0aCA0NAo+PgpzdHJlYW0KQlQgL0YxIDEyIFRmIDUwIDcwIFRkIChIZWxsbyBQREYpIFRqIEVUCmVuZHN0cmVhbQplbmRvYmoKNSAwIG9iago8PAovVHlwZSAvRm9udAovU3VidHlwZSAvVHlwZTEKL05hbWUgL0YxCi9CYXNlRm9udCAvSGVsdmV0aWNhCj4+CmVuZG9iagp4cmVmCjAgNgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTAgMDAwMDAgbiAKMDAwMDAwMDA2MSAwMDAwMCBuIAowMDAwMDAwMTE2IDAwMDAwIG4gCjAwMDAwMDAyMjAgMDAwMDAgbiAKMDAwMDAwMDMxNSAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDYKL1Jvb3QgMSAwIFIKPj4Kc3RhcnR4cmVmCjM5MAolJUVPRgo=
|
||||
</cbc:EmbeddedDocumentBinaryObject>
|
||||
</cac:Attachment>
|
||||
</cac:AdditionalDocumentReference>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="0208">0239843188</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Testing company</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Street</cbc:StreetName>
|
||||
<cbc:CityName>City</cbc:CityName>
|
||||
<cbc:PostalZone>5000</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>BE0239843188</cbc:CompanyID>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>
|
||||
<cac:PartyLegalEntity>
|
||||
<cbc:RegistrationName>Testing company</cbc:RegistrationName>
|
||||
<cbc:CompanyID>BE0239843188</cbc:CompanyID>
|
||||
</cac:PartyLegalEntity>
|
||||
<cac:Contact>
|
||||
<cbc:Name>Testing company</cbc:Name>
|
||||
<cbc:Telephone>+32 123 12 12 12</cbc:Telephone>
|
||||
<cbc:ElectronicMail>info@testingcompany.be</cbc:ElectronicMail>
|
||||
</cac:Contact>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cbc:EndpointID schemeID="0208">0239843189</cbc:EndpointID>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Odoo</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Street</cbc:StreetName>
|
||||
<cbc:CityName>City</cbc:CityName>
|
||||
<cbc:PostalZone>5001</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>BE0239843189</cbc:CompanyID>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>
|
||||
<cac:PartyLegalEntity>
|
||||
<cbc:RegistrationName>Odoo</cbc:RegistrationName>
|
||||
<cbc:CompanyID>BE0239843189</cbc:CompanyID>
|
||||
</cac:PartyLegalEntity>
|
||||
<cac:Contact>
|
||||
<cbc:Name>Odoo</cbc:Name>
|
||||
<cbc:Telephone>+1 123-123-1234</cbc:Telephone>
|
||||
<cbc:ElectronicMail>info@odoo.com</cbc:ElectronicMail>
|
||||
</cac:Contact>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:Delivery>
|
||||
<cac:DeliveryLocation>
|
||||
<cac:Address>
|
||||
<cbc:StreetName>Street</cbc:StreetName>
|
||||
<cbc:CityName>City</cbc:CityName>
|
||||
<cbc:PostalZone>5001</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:Address>
|
||||
</cac:DeliveryLocation>
|
||||
</cac:Delivery>
|
||||
<cac:PaymentMeans>
|
||||
<cbc:PaymentMeansCode name="credit transfer">30</cbc:PaymentMeansCode>
|
||||
<cbc:PaymentID>F26/0045</cbc:PaymentID>
|
||||
<cac:PayeeFinancialAccount>
|
||||
<cbc:ID>BE0239843189</cbc:ID>
|
||||
<cac:FinancialInstitutionBranch>
|
||||
<cbc:ID>REVOBEB2</cbc:ID>
|
||||
</cac:FinancialInstitutionBranch>
|
||||
</cac:PayeeFinancialAccount>
|
||||
</cac:PaymentMeans>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="EUR">713.14</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="EUR">3395.92</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="EUR">713.14</cbc:TaxAmount>
|
||||
<cac:TaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>21.0</cbc:Percent>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:TaxCategory>
|
||||
</cac:TaxSubtotal>
|
||||
</cac:TaxTotal>
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">3395.92</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="EUR">3395.92</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="EUR">4109.06</cbc:TaxInclusiveAmount>
|
||||
<cbc:PrepaidAmount currencyID="EUR">0.00</cbc:PrepaidAmount>
|
||||
<cbc:PayableAmount currencyID="EUR">4109.06</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">3395.92</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Description>PO1234</cbc:Description>
|
||||
<cbc:Name>Commission</cbc:Name>
|
||||
<cac:ClassifiedTaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>21.0</cbc:Percent>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:ClassifiedTaxCategory>
|
||||
</cac:Item>
|
||||
<cac:Price>
|
||||
<cbc:PriceAmount currencyID="EUR">3395.92</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from odoo.tests import common
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import Command
|
||||
|
||||
|
||||
class TestFiscalPosition(common.TransactionCase):
|
||||
|
|
@ -43,25 +44,23 @@ class TestFiscalPosition(common.TransactionCase):
|
|||
country_id=fr.id))
|
||||
cls.alberto = cls.res_partner.create(dict(
|
||||
name="Alberto",
|
||||
vat="BE0477472701",
|
||||
vat="ZUÑ920208KL4",
|
||||
country_id=mx.id))
|
||||
cls.be_nat = cls.fp.create(dict(
|
||||
name="BE-NAT",
|
||||
auto_apply=True,
|
||||
country_id=be.id,
|
||||
vat_required=False,
|
||||
sequence=10))
|
||||
cls.fr_b2c = cls.fp.create(dict(
|
||||
name="EU-VAT-FR-B2C",
|
||||
auto_apply=True,
|
||||
country_id=fr.id,
|
||||
vat_required=False,
|
||||
sequence=40))
|
||||
cls.fr_b2b = cls.fp.create(dict(
|
||||
name="EU-VAT-FR-B2B",
|
||||
auto_apply=True,
|
||||
country_id=fr.id,
|
||||
vat_required=True,
|
||||
sequence=40))
|
||||
cls.fr_b2c = cls.fp.create(dict(
|
||||
name="EU-VAT-FR-B2C",
|
||||
auto_apply=True,
|
||||
country_id=fr.id,
|
||||
sequence=50))
|
||||
|
||||
def test_10_fp_country(self):
|
||||
|
|
@ -71,90 +70,43 @@ class TestFiscalPosition(common.TransactionCase):
|
|||
expected_pos.id,
|
||||
message)
|
||||
|
||||
george, jc, ben, alberto = self.george, self.jc, self.ben, self.alberto
|
||||
|
||||
# B2B has precedence over B2C for same country even when sequence gives lower precedence
|
||||
self.assertGreater(self.fr_b2b.sequence, self.fr_b2c.sequence)
|
||||
assert_fp(george, self.fr_b2b, "FR-B2B should have precedence over FR-B2C")
|
||||
self.fr_b2b.auto_apply = False
|
||||
assert_fp(george, self.fr_b2c, "FR-B2C should match now")
|
||||
self.fr_b2b.auto_apply = True
|
||||
|
||||
# Create positions matching on Country Group and on NO country at all
|
||||
self.eu_intra_b2b = self.fp.create(dict(
|
||||
name="EU-INTRA B2B",
|
||||
auto_apply=True,
|
||||
country_group_id=self.eu.id,
|
||||
vat_required=True,
|
||||
sequence=20))
|
||||
self.world = self.fp.create(dict(
|
||||
name="WORLD-EXTRA",
|
||||
auto_apply=True,
|
||||
vat_required=False,
|
||||
sequence=30))
|
||||
|
||||
# Country match has higher precedence than group match or sequence
|
||||
self.assertGreater(self.fr_b2b.sequence, self.eu_intra_b2b.sequence)
|
||||
assert_fp(george, self.fr_b2b, "FR-B2B should have precedence over EU-INTRA B2B")
|
||||
|
||||
# B2B has precedence regardless of country or group match
|
||||
self.assertGreater(self.eu_intra_b2b.sequence, self.be_nat.sequence)
|
||||
assert_fp(jc, self.eu_intra_b2b, "EU-INTRA B2B should match before BE-NAT")
|
||||
|
||||
# Lower sequence = higher precedence if country/group and VAT matches
|
||||
self.assertFalse(ben.vat) # No VAT set
|
||||
assert_fp(ben, self.be_nat, "BE-NAT should match before EU-INTRA due to lower sequence")
|
||||
|
||||
# Remove BE from EU group, now BE-NAT should be the fallback match before the wildcard WORLD
|
||||
self.be.write({'country_group_ids': [(3, self.eu.id)]})
|
||||
self.assertTrue(jc.vat) # VAT set
|
||||
assert_fp(jc, self.be_nat, "BE-NAT should match as fallback even w/o VAT match")
|
||||
|
||||
# No country = wildcard match only if nothing else matches
|
||||
self.assertTrue(alberto.vat) # with VAT
|
||||
assert_fp(alberto, self.world, "WORLD-EXTRA should match anything else (1)")
|
||||
alberto.vat = False # or without
|
||||
assert_fp(alberto, self.world, "WORLD-EXTRA should match anything else (2)")
|
||||
self.assertFalse(self.ben.vat) # No VAT set
|
||||
assert_fp(self.ben, self.be_nat, "BE-NAT should match before EU-INTRA due to lower sequence")
|
||||
|
||||
# Zip range
|
||||
self.fr_b2b_zip100 = self.fr_b2b.copy(dict(zip_from=0, zip_to=5000, sequence=60))
|
||||
george.zip = 6000
|
||||
assert_fp(george, self.fr_b2b, "FR-B2B with wrong zip range should not match")
|
||||
george.zip = 3000
|
||||
assert_fp(george, self.fr_b2b_zip100, "FR-B2B with zip range should have precedence")
|
||||
fr_b2b_zip100 = self.fr_b2b.copy(dict(zip_from=0, zip_to=5000, sequence=1))
|
||||
self.george.zip = 6000
|
||||
assert_fp(self.george, self.fr_b2b, "FR-B2B with wrong zip range should not match")
|
||||
self.george.zip = 1234
|
||||
assert_fp(self.george, fr_b2b_zip100, "FR-B2B with ok zip range should match")
|
||||
self.george.zip = None
|
||||
|
||||
# States
|
||||
self.fr_b2b_state = self.fr_b2b.copy(dict(state_ids=[(4, self.state_fr.id)], sequence=70))
|
||||
george.state_id = self.state_fr
|
||||
assert_fp(george, self.fr_b2b_zip100, "FR-B2B with zip should have precedence over states")
|
||||
george.zip = False
|
||||
assert_fp(george, self.fr_b2b_state, "FR-B2B with states should have precedence")
|
||||
fr_b2b_state = self.fr_b2b.copy(dict(state_ids=[(4, self.state_fr.id)], sequence=1))
|
||||
assert_fp(self.george, self.fr_b2b, "FR-B2B with states should not match")
|
||||
self.george.state_id = self.state_fr
|
||||
assert_fp(self.george, fr_b2b_state, "FR-B2B with states should match")
|
||||
|
||||
# Dedicated position has max precedence
|
||||
george.property_account_position_id = self.be_nat
|
||||
assert_fp(george, self.be_nat, "Forced position has max precedence")
|
||||
self.george.property_account_position_id = self.be_nat
|
||||
assert_fp(self.george, self.be_nat, "Forced position has max precedence")
|
||||
|
||||
|
||||
def test_20_fp_one_tax_2m(self):
|
||||
self.env.company.country_id = self.env.ref('base.us')
|
||||
self.env['account.tax.group'].create(
|
||||
{'name': 'Test Tax Group', 'company_id': self.env.company.id}
|
||||
)
|
||||
|
||||
self.src_tax = self.env['account.tax'].create({'name': "SRC", 'amount': 0.0})
|
||||
self.dst1_tax = self.env['account.tax'].create({'name': "DST1", 'amount': 0.0})
|
||||
self.dst2_tax = self.env['account.tax'].create({'name': "DST2", 'amount': 0.0})
|
||||
|
||||
self.fp2m = self.fp.create({
|
||||
'name': "FP-TAX2TAXES",
|
||||
'tax_ids': [
|
||||
(0,0,{
|
||||
'tax_src_id': self.src_tax.id,
|
||||
'tax_dest_id': self.dst1_tax.id
|
||||
}),
|
||||
(0,0,{
|
||||
'tax_src_id': self.src_tax.id,
|
||||
'tax_dest_id': self.dst2_tax.id
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.dst1_tax = self.env['account.tax'].create({'name': "DST1", 'amount': 0.0, 'fiscal_position_ids': [Command.set(self.fp2m.ids)], 'original_tax_ids': [Command.set(self.src_tax.ids)], 'sequence': 10})
|
||||
self.dst2_tax = self.env['account.tax'].create({'name': "DST2", 'amount': 0.0, 'fiscal_position_ids': [Command.set(self.fp2m.ids)], 'original_tax_ids': [Command.set(self.src_tax.ids)], 'sequence': 5})
|
||||
mapped_taxes = self.fp2m.map_tax(self.src_tax)
|
||||
|
||||
self.assertEqual(mapped_taxes, self.dst1_tax | self.dst2_tax)
|
||||
|
|
@ -175,24 +127,22 @@ class TestFiscalPosition(common.TransactionCase):
|
|||
'vat_required': True,
|
||||
'sequence': 10,
|
||||
})
|
||||
fp_eu_priv = self.env['account.fiscal.position'].create({
|
||||
'name': 'EU privé',
|
||||
'auto_apply': True,
|
||||
'country_group_id': self.eu.id,
|
||||
'vat_required': False,
|
||||
'sequence': 20,
|
||||
})
|
||||
fp_eu_intra = self.env['account.fiscal.position'].create({
|
||||
'name': 'Régime Intra-Communautaire',
|
||||
'auto_apply': True,
|
||||
'country_group_id': self.eu.id,
|
||||
'vat_required': True,
|
||||
'sequence': 20,
|
||||
})
|
||||
fp_eu_priv = self.env['account.fiscal.position'].create({
|
||||
'name': 'EU privé',
|
||||
'auto_apply': True,
|
||||
'country_group_id': self.eu.id,
|
||||
'sequence': 30,
|
||||
})
|
||||
fp_eu_extra = self.env['account.fiscal.position'].create({
|
||||
'name': 'Régime Extra-Communautaire',
|
||||
'auto_apply': True,
|
||||
'vat_required': False,
|
||||
'sequence': 40,
|
||||
})
|
||||
|
||||
|
|
@ -270,6 +220,77 @@ class TestFiscalPosition(common.TransactionCase):
|
|||
fp_eu_extra
|
||||
)
|
||||
|
||||
def test_domestic_fp_map_self(self):
|
||||
self.env.company.country_id = self.us
|
||||
self.env['account.tax.group'].create(
|
||||
{'name': 'Test Tax Group', 'company_id': self.env.company.id}
|
||||
)
|
||||
fp = self.env['account.fiscal.position'].create({
|
||||
'name': 'FP Self',
|
||||
})
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'Source Dest Tax',
|
||||
'amount': 10,
|
||||
'fiscal_position_ids': [Command.link(fp.id)],
|
||||
})
|
||||
self.assertEqual(fp.map_tax(tax), tax)
|
||||
|
||||
def test_domestic_fp(self):
|
||||
"""
|
||||
check if the domestic fiscal position is well computed in different scenarios.
|
||||
"""
|
||||
country_group, a_country_group = self.env['res.country.group'].create([{
|
||||
'name': "One country group",
|
||||
}, {
|
||||
'name': "Alphabetically first country_group",
|
||||
}])
|
||||
my_country = self.env['res.country'].create({
|
||||
'name': "Neverland",
|
||||
'code': 'PP',
|
||||
'country_group_ids': [Command.set(country_group.ids + a_country_group.ids)],
|
||||
})
|
||||
self.env.company.country_id = my_country
|
||||
|
||||
# AT case - no sequence, one country_id
|
||||
fp_1, fp_2, fp_3 = self.env['account.fiscal.position'].create([{
|
||||
'name': 'FP First',
|
||||
'country_group_id': country_group.id,
|
||||
}, {
|
||||
'name': 'FP Second',
|
||||
'country_id': my_country.id,
|
||||
}, {
|
||||
'name': 'FP 3',
|
||||
'country_group_id': country_group.id,
|
||||
}])
|
||||
self.assertEqual(self.env.company.domestic_fiscal_position_id, fp_2)
|
||||
|
||||
# SA case - same sequence, one country_id
|
||||
(fp_1 + fp_2 + fp_3).write({'sequence': 10})
|
||||
fp_1.write({
|
||||
'country_id': my_country.id,
|
||||
'country_group_id': False,
|
||||
})
|
||||
fp_2.write({'country_id': False})
|
||||
self.assertEqual(self.env.company.domestic_fiscal_position_id, fp_1)
|
||||
|
||||
# NL case - different sequence, both country_group_id and country_id on a fp
|
||||
(fp_1 + fp_2).write({'country_group_id': country_group.id})
|
||||
fp_1.write({'country_id': False})
|
||||
fp_2.write({'country_id': my_country.id})
|
||||
fp_3.write({'country_group_id': a_country_group.id})
|
||||
self.assertEqual(self.env.company.domestic_fiscal_position_id, fp_2)
|
||||
|
||||
# Check that sequence is applied after the country
|
||||
fp_2.write({'sequence': 20})
|
||||
fp_3.write({'sequence': 15})
|
||||
self.assertEqual(self.env.company.domestic_fiscal_position_id, fp_1)
|
||||
|
||||
# CH/LI case - one fp with country_group_id only, nothing for others
|
||||
fp_1.write({'sequence': 30})
|
||||
fp_2.write({'country_id': False})
|
||||
fp_3.write({'country_group_id': False})
|
||||
self.assertEqual(self.env.company.domestic_fiscal_position_id, fp_2)
|
||||
|
||||
def test_fiscal_position_constraint(self):
|
||||
"""
|
||||
Test fiscal position constraint by updating the record
|
||||
|
|
@ -304,3 +325,21 @@ class TestFiscalPosition(common.TransactionCase):
|
|||
'zip_from': '123',
|
||||
'zip_to': '456',
|
||||
}])
|
||||
|
||||
def test_fiscal_position_different_vat_country(self):
|
||||
""" If the country is European, we need to be able to put the VAT of another country through the prefix"""
|
||||
fiscal_position = self.fp.create({
|
||||
'name': 'Special Delivery Case',
|
||||
'country_id': self.env.ref('base.fr').id,
|
||||
'foreign_vat': 'BE0477472701',
|
||||
})
|
||||
self.assertEqual(fiscal_position.foreign_vat, 'BE0477472701')
|
||||
|
||||
def test_get_first_fiscal_position(self):
|
||||
fiscal_positions = self.fp.create([{
|
||||
'name': f'fiscal_position_{sequence}',
|
||||
'auto_apply': True,
|
||||
'country_id': self.jc.country_id.id,
|
||||
'sequence': sequence
|
||||
} for sequence in range(1, 3)])
|
||||
self.assertEqual(self.fp._get_fiscal_position(self.jc), fiscal_positions[0])
|
||||
|
|
|
|||
|
|
@ -1,920 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxTotals(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.tax_group1 = cls.env['account.tax.group'].create({
|
||||
'name': '1',
|
||||
'sequence': 1
|
||||
})
|
||||
cls.tax_group2 = cls.env['account.tax.group'].create({
|
||||
'name': '2',
|
||||
'sequence': 2
|
||||
})
|
||||
cls.tax_group_sub1 = cls.env['account.tax.group'].create({
|
||||
'name': 'subtotals 1',
|
||||
'preceding_subtotal': "PRE GROUP 1",
|
||||
'sequence': 3
|
||||
})
|
||||
cls.tax_group_sub2 = cls.env['account.tax.group'].create({
|
||||
'name': 'subtotals 2',
|
||||
'preceding_subtotal': "PRE GROUP 2",
|
||||
'sequence': 4
|
||||
})
|
||||
cls.tax_group_sub3 = cls.env['account.tax.group'].create({
|
||||
'name': 'subtotals 3',
|
||||
'preceding_subtotal': "PRE GROUP 1", # same as sub1, on purpose
|
||||
'sequence': 5
|
||||
})
|
||||
|
||||
cls.tax_10 = cls.env['account.tax'].create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
})
|
||||
cls.tax_16 = cls.env['account.tax'].create({
|
||||
'name': "tax_16",
|
||||
'amount_type': 'percent',
|
||||
'amount': 16.0,
|
||||
})
|
||||
cls.tax_53 = cls.env['account.tax'].create({
|
||||
'name': "tax_53",
|
||||
'amount_type': 'percent',
|
||||
'amount': 53.0,
|
||||
})
|
||||
cls.tax_17a = cls.env['account.tax'].create({
|
||||
'name': "tax_17a",
|
||||
'amount_type': 'percent',
|
||||
'amount': 17.0,
|
||||
})
|
||||
cls.tax_17b = cls.tax_17a.copy({'name': "tax_17b"})
|
||||
|
||||
def assertTaxTotals(self, document, expected_values):
|
||||
main_keys_to_ignore = {'formatted_amount_total', 'formatted_amount_untaxed'}
|
||||
group_keys_to_ignore = {'group_key', 'formatted_tax_group_amount', 'formatted_tax_group_base_amount', 'hide_base_amount'}
|
||||
subtotals_keys_to_ignore = {'formatted_amount'}
|
||||
|
||||
to_compare = document.tax_totals
|
||||
|
||||
for key in main_keys_to_ignore:
|
||||
del to_compare[key]
|
||||
|
||||
for key in group_keys_to_ignore:
|
||||
for groups in to_compare['groups_by_subtotal'].values():
|
||||
for group in groups:
|
||||
del group[key]
|
||||
|
||||
for key in subtotals_keys_to_ignore:
|
||||
for subtotal in to_compare['subtotals']:
|
||||
del subtotal[key]
|
||||
|
||||
self.assertEqual(to_compare, expected_values)
|
||||
|
||||
def _create_document_for_tax_totals_test(self, lines_data):
|
||||
""" Creates and returns a new record of a model defining a tax_totals
|
||||
field and using the related widget.
|
||||
|
||||
By default, this function creates an invoice, but it is overridden in sale
|
||||
and purchase to create respectively a sale.order or a purchase.order. This way,
|
||||
we can test the invoice_tax_totals from both these models in the same way as
|
||||
account.move's.
|
||||
|
||||
:param lines_data: a list of tuple (amount, taxes), where amount is a base amount,
|
||||
and taxes a recordset of account.tax objects corresponding
|
||||
to the taxes to apply on this amount. Each element of the list
|
||||
corresponds to a line of the document (invoice line, PO line, SO line).
|
||||
"""
|
||||
invoice_lines_vals = [
|
||||
(0, 0, {
|
||||
'name': 'line',
|
||||
'display_type': 'product',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'price_unit': amount,
|
||||
'tax_ids': [(6, 0, taxes.ids)],
|
||||
})
|
||||
for amount, taxes in lines_data]
|
||||
|
||||
return self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2019-01-01',
|
||||
'invoice_line_ids': invoice_lines_vals,
|
||||
})
|
||||
|
||||
def test_multiple_tax_lines(self):
|
||||
tax_10 = self.env['account.tax'].create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
})
|
||||
|
||||
tax_20 = self.env['account.tax'].create({
|
||||
'name': "tax_20",
|
||||
'amount_type': 'percent',
|
||||
'amount': 20.0,
|
||||
'tax_group_id': self.tax_group2.id,
|
||||
})
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(1000, tax_10 + tax_20),
|
||||
(1000, tax_10),
|
||||
(1000, tax_20),
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 3600,
|
||||
'amount_untaxed': 3000,
|
||||
'display_tax_base': True,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 200,
|
||||
'tax_group_base_amount': 2000,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
|
||||
{
|
||||
'tax_group_name': self.tax_group2.name,
|
||||
'tax_group_amount': 400,
|
||||
'tax_group_base_amount': 2000,
|
||||
'tax_group_id': self.tax_group2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 3000,
|
||||
}
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount"],
|
||||
})
|
||||
|
||||
# Same but both are sharing the same tax group.
|
||||
|
||||
tax_20.tax_group_id = self.tax_group1
|
||||
document.invalidate_model(['tax_totals'])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 3600,
|
||||
'amount_untaxed': 3000,
|
||||
'display_tax_base': False,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 600,
|
||||
'tax_group_base_amount': 3000,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 3000,
|
||||
}
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount"],
|
||||
})
|
||||
|
||||
def test_zero_tax_lines(self):
|
||||
tax_0 = self.env['account.tax'].create({
|
||||
'name': "tax_0",
|
||||
'amount_type': 'percent',
|
||||
'amount': 0.0,
|
||||
})
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(1000, tax_0),
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 1000,
|
||||
'amount_untaxed': 1000,
|
||||
'display_tax_base': False,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': tax_0.tax_group_id.name,
|
||||
'tax_group_amount': 0,
|
||||
'tax_group_base_amount': 1000,
|
||||
'tax_group_id': tax_0.tax_group_id.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 1000,
|
||||
}
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount"],
|
||||
})
|
||||
|
||||
def test_tax_affect_base_1(self):
|
||||
tax_10 = self.env['account.tax'].create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
})
|
||||
|
||||
tax_20 = self.env['account.tax'].create({
|
||||
'name': "tax_20",
|
||||
'amount_type': 'percent',
|
||||
'amount': 20.0,
|
||||
'tax_group_id': self.tax_group2.id,
|
||||
})
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(1100, tax_10 + tax_20),
|
||||
(1100, tax_10),
|
||||
(1000, tax_20),
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 3620,
|
||||
'amount_untaxed': 3000,
|
||||
'display_tax_base': True,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 200,
|
||||
'tax_group_base_amount': 2000,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
|
||||
{
|
||||
'tax_group_name': self.tax_group2.name,
|
||||
'tax_group_amount': 420,
|
||||
'tax_group_base_amount': 2100,
|
||||
'tax_group_id': self.tax_group2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 3000,
|
||||
}
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount"],
|
||||
})
|
||||
|
||||
# Same but both are sharing the same tax group.
|
||||
|
||||
tax_20.tax_group_id = self.tax_group1
|
||||
document.invalidate_model(['tax_totals'])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 3620,
|
||||
'amount_untaxed': 3000,
|
||||
'display_tax_base': False,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 620,
|
||||
'tax_group_base_amount': 3000,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 3000,
|
||||
}
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount"],
|
||||
})
|
||||
|
||||
def test_tax_affect_base_2(self):
|
||||
tax_10 = self.env['account.tax'].create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
'include_base_amount': True,
|
||||
'sequence': 2,
|
||||
})
|
||||
|
||||
tax_20 = self.env['account.tax'].create({
|
||||
'name': "tax_20",
|
||||
'amount_type': 'percent',
|
||||
'amount': 20.0,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
'sequence': 2,
|
||||
})
|
||||
|
||||
tax_30 = self.env['account.tax'].create({
|
||||
'name': "tax_30",
|
||||
'amount_type': 'percent',
|
||||
'amount': 30.0,
|
||||
'tax_group_id': self.tax_group2.id,
|
||||
'include_base_amount': True,
|
||||
'sequence': 1,
|
||||
})
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(1000, tax_10 + tax_20),
|
||||
(1000, tax_30 + tax_10),
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 2750,
|
||||
'amount_untaxed': 2000,
|
||||
'display_tax_base': True,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 450,
|
||||
'tax_group_base_amount': 2300,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
|
||||
{
|
||||
'tax_group_name': self.tax_group2.name,
|
||||
'tax_group_amount': 300,
|
||||
'tax_group_base_amount': 1000,
|
||||
'tax_group_id': self.tax_group2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 2000,
|
||||
}
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount"],
|
||||
})
|
||||
|
||||
# Same but both are sharing the same tax group.
|
||||
|
||||
tax_30.tax_group_id = self.tax_group1
|
||||
document.invalidate_model(['tax_totals'])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 2750,
|
||||
'amount_untaxed': 2000,
|
||||
'display_tax_base': False,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 750,
|
||||
'tax_group_base_amount': 2000,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 2000,
|
||||
}
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount"],
|
||||
})
|
||||
|
||||
def test_subtotals_basic(self):
|
||||
tax_10 = self.env['account.tax'].create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
})
|
||||
|
||||
tax_25 = self.env['account.tax'].create({
|
||||
'name': "tax_25",
|
||||
'amount_type': 'percent',
|
||||
'amount': 25.0,
|
||||
'tax_group_id': self.tax_group_sub2.id,
|
||||
})
|
||||
|
||||
tax_42 = self.env['account.tax'].create({
|
||||
'name': "tax_42",
|
||||
'amount_type': 'percent',
|
||||
'amount': 42.0,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
})
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(1000, tax_10),
|
||||
(1000, tax_25),
|
||||
(100, tax_42),
|
||||
(200, tax_42 + tax_10 + tax_25),
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 2846,
|
||||
'amount_untaxed': 2300,
|
||||
'display_tax_base': True,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 126,
|
||||
'tax_group_base_amount': 300,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
],
|
||||
'PRE GROUP 1': [
|
||||
{
|
||||
'tax_group_name': self.tax_group_sub1.name,
|
||||
'tax_group_amount': 120,
|
||||
'tax_group_base_amount': 1200,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
},
|
||||
],
|
||||
'PRE GROUP 2': [
|
||||
{
|
||||
'tax_group_name': self.tax_group_sub2.name,
|
||||
'tax_group_amount': 300,
|
||||
'tax_group_base_amount': 1200,
|
||||
'tax_group_id': self.tax_group_sub2.id,
|
||||
},
|
||||
]
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 2300,
|
||||
},
|
||||
|
||||
{
|
||||
'name': "PRE GROUP 1",
|
||||
'amount': 2426,
|
||||
},
|
||||
|
||||
{
|
||||
'name': "PRE GROUP 2",
|
||||
'amount': 2546,
|
||||
},
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount", "PRE GROUP 1", "PRE GROUP 2"],
|
||||
})
|
||||
|
||||
def test_after_total_mix(self):
|
||||
tax_10 = self.env['account.tax'].create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'tax_group_id': self.tax_group_sub3.id,
|
||||
})
|
||||
|
||||
tax_25 = self.env['account.tax'].create({
|
||||
'name': "tax_25",
|
||||
'amount_type': 'percent',
|
||||
'amount': -25.0,
|
||||
'tax_group_id': self.tax_group_sub2.id,
|
||||
})
|
||||
|
||||
tax_42 = self.env['account.tax'].create({
|
||||
'name': "tax_42",
|
||||
'amount_type': 'percent',
|
||||
'amount': 42.0,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
})
|
||||
|
||||
tax_30 = self.env['account.tax'].create({
|
||||
'name': "tax_30",
|
||||
'amount_type': 'percent',
|
||||
'amount': 30.0,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
})
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(100, tax_10),
|
||||
(100, tax_25 + tax_42 + tax_30),
|
||||
(200, tax_10 + tax_25),
|
||||
(1000, tax_30),
|
||||
(100, tax_30 + tax_10)
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 1867,
|
||||
'amount_untaxed': 1500,
|
||||
'display_tax_base': True,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [
|
||||
{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 360,
|
||||
'tax_group_base_amount': 1200,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
},
|
||||
],
|
||||
|
||||
'PRE GROUP 1': [
|
||||
{
|
||||
'tax_group_name': self.tax_group_sub1.name,
|
||||
'tax_group_amount': 42,
|
||||
'tax_group_base_amount': 100,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
},
|
||||
|
||||
{
|
||||
'tax_group_name': self.tax_group_sub3.name,
|
||||
'tax_group_amount': 40,
|
||||
'tax_group_base_amount': 400,
|
||||
'tax_group_id': self.tax_group_sub3.id,
|
||||
},
|
||||
],
|
||||
|
||||
'PRE GROUP 2': [
|
||||
{
|
||||
'tax_group_name': self.tax_group_sub2.name,
|
||||
'tax_group_amount': -75,
|
||||
'tax_group_base_amount': 300,
|
||||
'tax_group_id': self.tax_group_sub2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 1500,
|
||||
},
|
||||
|
||||
{
|
||||
'name': "PRE GROUP 1",
|
||||
'amount': 1860,
|
||||
},
|
||||
|
||||
{
|
||||
'name': "PRE GROUP 2",
|
||||
'amount': 1942,
|
||||
},
|
||||
],
|
||||
'subtotals_order': ["Untaxed Amount", "PRE GROUP 1", "PRE GROUP 2"],
|
||||
})
|
||||
|
||||
def test_discounted_tax(self):
|
||||
tax_21_exempted = self.env['account.tax'].create({
|
||||
'name': "tax_21_exempted",
|
||||
'amount_type': 'group',
|
||||
'amount': 2.0,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
'children_tax_ids': [
|
||||
Command.create({
|
||||
'name': "tax_exempt",
|
||||
'amount_type': 'percent',
|
||||
'amount': -2.0,
|
||||
'include_base_amount': True,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
'sequence': 1,
|
||||
}),
|
||||
Command.create({
|
||||
'name': "tax_21",
|
||||
'amount_type': 'percent',
|
||||
'amount': 21.0,
|
||||
'tax_group_id': self.tax_group_sub2.id,
|
||||
'sequence': 2,
|
||||
}),
|
||||
Command.create({
|
||||
'name': "tax_reapply",
|
||||
'amount_type': 'percent',
|
||||
'amount': 2.0,
|
||||
'is_base_affected': False,
|
||||
'tax_group_id': self.tax_group_sub3.id,
|
||||
'sequence': 3,
|
||||
}),
|
||||
]
|
||||
})
|
||||
self.tax_group_sub1.preceding_subtotal = "Tax exemption"
|
||||
self.tax_group_sub2.preceding_subtotal = "Tax application"
|
||||
self.tax_group_sub3.preceding_subtotal = "Reapply amount"
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(1000 / 0.98, tax_21_exempted),
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 1230.41,
|
||||
'amount_untaxed': 1020.41,
|
||||
'display_tax_base': True,
|
||||
'groups_by_subtotal': {
|
||||
"Reapply amount": [{
|
||||
'tax_group_name': self.tax_group_sub3.name,
|
||||
'tax_group_amount': 20.41,
|
||||
'tax_group_base_amount': 1020.41,
|
||||
'tax_group_id': self.tax_group_sub3.id,
|
||||
}],
|
||||
"Tax application": [{
|
||||
'tax_group_name': self.tax_group_sub2.name,
|
||||
'tax_group_amount': 210.0,
|
||||
'tax_group_base_amount': 1000.0,
|
||||
'tax_group_id': self.tax_group_sub2.id,
|
||||
}],
|
||||
"Tax exemption": [{
|
||||
'tax_group_name': self.tax_group_sub1.name,
|
||||
'tax_group_amount': -20.41,
|
||||
'tax_group_base_amount': 1020.41,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
}],
|
||||
},
|
||||
'subtotals': [{
|
||||
'name': "Tax exemption",
|
||||
'amount': 1020.41,
|
||||
}, {
|
||||
'name': "Tax application",
|
||||
'amount': 1000.00,
|
||||
}, {
|
||||
'name': "Reapply amount",
|
||||
'amount': 1210.00,
|
||||
}],
|
||||
'subtotals_order': ["Tax exemption", "Tax application", "Reapply amount"],
|
||||
})
|
||||
|
||||
def test_invoice_grouped_taxes_with_tax_group(self):
|
||||
""" A tax of type group with a tax_group_id being the same as one of the children tax shouldn't affect the
|
||||
result of the _prepare_tax_totals.
|
||||
"""
|
||||
tax_10_withheld = self.env['account.tax'].create({
|
||||
'name': "tax_10_withheld",
|
||||
'amount_type': 'group',
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
'children_tax_ids': [
|
||||
Command.create({
|
||||
'name': "tax_withheld",
|
||||
'amount_type': 'percent',
|
||||
'amount': -47,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
'sequence': 1,
|
||||
}),
|
||||
Command.create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
'sequence': 2,
|
||||
}),
|
||||
]
|
||||
})
|
||||
self.tax_group_sub1.preceding_subtotal = "Tax withholding"
|
||||
|
||||
document = self._create_document_for_tax_totals_test([
|
||||
(100, tax_10_withheld),
|
||||
])
|
||||
|
||||
self.assertTaxTotals(document, {
|
||||
'amount_total': 63,
|
||||
'amount_untaxed': 100,
|
||||
'display_tax_base': True,
|
||||
'groups_by_subtotal': {
|
||||
'Untaxed Amount': [{
|
||||
'tax_group_name': self.tax_group1.name,
|
||||
'tax_group_amount': 10,
|
||||
'tax_group_base_amount': 100,
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
}],
|
||||
"Tax withholding": [{
|
||||
'tax_group_name': self.tax_group_sub1.name,
|
||||
'tax_group_amount': -47,
|
||||
'tax_group_base_amount': 100,
|
||||
'tax_group_id': self.tax_group_sub1.id,
|
||||
}],
|
||||
},
|
||||
'subtotals': [{
|
||||
'name': "Untaxed Amount",
|
||||
'amount': 100,
|
||||
}, {
|
||||
'name': "Tax withholding",
|
||||
'amount': 110,
|
||||
}],
|
||||
'subtotals_order': ["Untaxed Amount", "Tax withholding"],
|
||||
})
|
||||
|
||||
def test_taxtotals_with_different_tax_rounding_methods(self):
|
||||
|
||||
def run_case(rounding_line, lines, expected_tax_group_amounts):
|
||||
self.env.company.tax_calculation_rounding_method = rounding_line
|
||||
|
||||
document = self._create_document_for_tax_totals_test(lines)
|
||||
tax_amounts = document.tax_totals['groups_by_subtotal']['Untaxed Amount']
|
||||
|
||||
if len(expected_tax_group_amounts) != len(tax_amounts):
|
||||
self.fail("Wrong number of values to compare.")
|
||||
|
||||
for tax_amount, expected in zip(tax_amounts, expected_tax_group_amounts):
|
||||
actual = tax_amount['tax_group_amount']
|
||||
if document.currency_id.compare_amounts(actual, expected) != 0:
|
||||
self.fail(f'{document.currency_id.round(actual)} != {expected}')
|
||||
|
||||
# one line, two taxes
|
||||
lines = [
|
||||
(100.41, self.tax_16 + self.tax_53),
|
||||
]
|
||||
run_case('round_per_line', lines, [69.29])
|
||||
run_case('round_globally', lines, [69.29])
|
||||
|
||||
# two lines, different taxes
|
||||
lines = [
|
||||
(50.4, self.tax_17a),
|
||||
(47.21, self.tax_17b),
|
||||
]
|
||||
run_case('round_per_line', lines, [16.60])
|
||||
run_case('round_globally', lines, [16.60])
|
||||
|
||||
# two lines, same tax
|
||||
lines = [
|
||||
(50.4, self.tax_17a),
|
||||
(47.21, self.tax_17a),
|
||||
]
|
||||
run_case('round_per_line', lines, [16.60])
|
||||
run_case('round_globally', lines, [16.59])
|
||||
|
||||
lines = [
|
||||
(54.45, self.tax_10),
|
||||
(600, self.tax_10),
|
||||
(-500, self.tax_10),
|
||||
]
|
||||
run_case('round_per_line', lines, [15.45])
|
||||
run_case('round_globally', lines, [15.45])
|
||||
|
||||
def test_cash_rounding_amount_total_rounded(self):
|
||||
tax_15 = self.env['account.tax'].create({
|
||||
'name': "tax_15",
|
||||
'amount_type': 'percent',
|
||||
'tax_group_id': self.tax_group1.id,
|
||||
'amount': 15.0,
|
||||
})
|
||||
tax_10 = self.env['account.tax'].create({
|
||||
'name': "tax_10",
|
||||
'amount_type': 'percent',
|
||||
'tax_group_id': self.tax_group2.id,
|
||||
'amount': 10.0,
|
||||
})
|
||||
cash_rounding_biggest_tax = self.env['account.cash.rounding'].create({
|
||||
'name': 'biggest tax Rounding HALF-UP',
|
||||
'rounding': 1,
|
||||
'strategy': 'biggest_tax',
|
||||
'rounding_method': 'HALF-UP',
|
||||
})
|
||||
cash_rounding_add_invoice_line = self.env['account.cash.rounding'].create({
|
||||
'name': 'add invoice line Rounding HALF-UP',
|
||||
'rounding': 1,
|
||||
'strategy': 'add_invoice_line',
|
||||
'profit_account_id': self.company_data['default_account_revenue'].id,
|
||||
'loss_account_id': self.company_data['default_account_expense'].id,
|
||||
'rounding_method': 'HALF-UP',
|
||||
})
|
||||
|
||||
for move_type in ['out_invoice', 'in_invoice']:
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': move_type,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2019-01-01',
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'line',
|
||||
'display_type': 'product',
|
||||
'price_unit': 378,
|
||||
'tax_ids': [Command.set(tax_15.ids)],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'line',
|
||||
'display_type': 'product',
|
||||
'price_unit': 100,
|
||||
'tax_ids': [Command.set(tax_10.ids)],
|
||||
})
|
||||
],
|
||||
})
|
||||
|
||||
move.invoice_cash_rounding_id = cash_rounding_biggest_tax
|
||||
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][0]['tax_group_amount'], 57)
|
||||
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][1]['tax_group_amount'], 10)
|
||||
self.assertEqual(move.tax_totals['amount_total'], 545)
|
||||
|
||||
move.invoice_cash_rounding_id = cash_rounding_add_invoice_line
|
||||
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][0]['tax_group_amount'], 56.7)
|
||||
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][1]['tax_group_amount'], 10)
|
||||
self.assertEqual(move.tax_totals['rounding_amount'], 0.3)
|
||||
self.assertEqual(move.tax_totals['amount_total'], 544.7)
|
||||
self.assertEqual(move.tax_totals['amount_total_rounded'], 545)
|
||||
|
||||
def test_recompute_cash_rounding_lines(self):
|
||||
# if rounding_method is changed then rounding shouldn't be recomputed in posted invoices
|
||||
cash_rounding_add_invoice_line = self.env['account.cash.rounding'].create({
|
||||
'name': 'Add invoice line Rounding UP',
|
||||
'rounding': 1,
|
||||
'strategy': 'add_invoice_line',
|
||||
'profit_account_id': self.company_data['default_account_revenue'].id,
|
||||
'loss_account_id': self.company_data['default_account_expense'].id,
|
||||
'rounding_method': 'UP',
|
||||
})
|
||||
moves_rounding = {}
|
||||
moves = self.env['account.move']
|
||||
for move_type in ['out_invoice', 'in_invoice']:
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': move_type,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2019-01-01',
|
||||
'invoice_cash_rounding_id': cash_rounding_add_invoice_line.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'line',
|
||||
'display_type': 'product',
|
||||
'price_unit': 99.5,
|
||||
})
|
||||
],
|
||||
})
|
||||
moves_rounding[move] = sum(move.line_ids.filtered(lambda line: line.display_type == 'rounding').mapped('balance'))
|
||||
moves += move
|
||||
moves.action_post()
|
||||
cash_rounding_add_invoice_line.rounding_method = 'DOWN'
|
||||
# check if rounding is recomputed
|
||||
moves.to_check = True
|
||||
for move in moves_rounding:
|
||||
self.assertEqual(sum(move.line_ids.filtered(lambda line: line.display_type == 'rounding').mapped('balance')), moves_rounding[move])
|
||||
|
||||
def test_recompute_cash_rounding_lines_multi_company(self):
|
||||
"""
|
||||
Ensure that when a move is created with cash rounding that will add an invoice line, the cash rounding accounts
|
||||
will be that of the move's company and not the user's company.
|
||||
"""
|
||||
cash_rounding_tbl = self.env['account.cash.rounding'].with_company(self.company_data["company"])
|
||||
cash_rounding_add_invoice_line = cash_rounding_tbl.create({
|
||||
'name': 'Add invoice line Rounding UP',
|
||||
'rounding': 1,
|
||||
'rounding_method': 'UP',
|
||||
'strategy': 'add_invoice_line',
|
||||
'profit_account_id': self.company_data['default_account_revenue'].id,
|
||||
'loss_account_id': self.company_data['default_account_expense'].id,
|
||||
})
|
||||
|
||||
cash_rounding_add_invoice_line.with_company(self.company_data_2["company"]).write({
|
||||
'profit_account_id': self.company_data_2['default_account_revenue'].id,
|
||||
'loss_account_id': self.company_data_2['default_account_expense'].id,
|
||||
})
|
||||
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2019-01-01',
|
||||
'company_id': self.company_data_2['company'].id,
|
||||
'invoice_cash_rounding_id': cash_rounding_add_invoice_line.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'line',
|
||||
'display_type': 'product',
|
||||
'price_unit': 99.5,
|
||||
})
|
||||
],
|
||||
})
|
||||
|
||||
rounding_line_account = move.line_ids.filtered(lambda line: line.display_type == 'rounding').mapped('account_id')
|
||||
self.assertEqual(rounding_line_account, self.company_data_2['default_account_revenue'])
|
||||
|
||||
def test_cash_rounding_amount_total_rounded_foreign_currency(self):
|
||||
tax_15 = self.env['account.tax'].create({
|
||||
'name': "tax_15",
|
||||
'amount_type': 'percent',
|
||||
'amount': 15.0,
|
||||
})
|
||||
cash_rounding = self.env['account.cash.rounding'].create({
|
||||
'name': 'Rounding HALF-UP',
|
||||
'rounding': 10,
|
||||
'strategy': 'biggest_tax',
|
||||
'rounding_method': 'HALF-UP',
|
||||
})
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2023-01-01',
|
||||
'rate': 0.2,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
for move_type in ['out_invoice', 'in_invoice']:
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': move_type,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2023-01-01',
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'line',
|
||||
'display_type': 'product',
|
||||
'price_unit': 100,
|
||||
'tax_ids': [tax_15.id],
|
||||
})
|
||||
]
|
||||
})
|
||||
move.invoice_cash_rounding_id = cash_rounding
|
||||
self.assertEqual(move.tax_totals['amount_total'], 120)
|
||||
|
|
@ -9,10 +9,9 @@ from odoo.tests import tagged, Form
|
|||
class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.company_data['company'].country_id = cls.env.ref('base.us')
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('HRK')
|
||||
|
||||
cls.percent_tax_1 = cls.env['account.tax'].create({
|
||||
'name': '21%',
|
||||
|
|
@ -24,7 +23,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'name': '21% incl',
|
||||
'amount_type': 'percent',
|
||||
'amount': 21,
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
'include_base_amount': True,
|
||||
'sequence': 20,
|
||||
})
|
||||
|
|
@ -38,7 +37,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'name': '5% incl',
|
||||
'amount_type': 'percent',
|
||||
'amount': 5,
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
'include_base_amount': True,
|
||||
'sequence': 40,
|
||||
})
|
||||
|
|
@ -76,8 +75,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
}),
|
||||
],
|
||||
})
|
||||
tax_tags = tax_report_line.expression_ids._get_matching_tags()
|
||||
cls.tax_tag_pos, cls.tax_tag_neg = tax_tags.sorted('tax_negate')
|
||||
cls.tax_tag = tax_report_line.expression_ids._get_matching_tags()
|
||||
|
||||
base_report_line = cls.env['account.report.line'].create({
|
||||
'name': 'base_test_tax_report_line',
|
||||
|
|
@ -91,10 +89,9 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
}),
|
||||
],
|
||||
})
|
||||
base_tags = base_report_line.expression_ids._get_matching_tags()
|
||||
cls.base_tag_pos, cls.base_tag_neg = base_tags.sorted('tax_negate')
|
||||
cls.base_tag = base_report_line.expression_ids._get_matching_tags()
|
||||
|
||||
def _create_invoice(self, taxes_per_line, inv_type='out_invoice', currency_id=False, invoice_payment_term_id=False):
|
||||
def _create_invoice_taxes_per_line(self, taxes_per_line, inv_type='out_invoice', currency_id=False, invoice_payment_term_id=False):
|
||||
''' Create an invoice on the fly.
|
||||
|
||||
:param taxes_per_line: A list of tuple (price_unit, account.tax recordset)
|
||||
|
|
@ -125,10 +122,10 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
--------------------------------------------
|
||||
21% | / | 100 | 21
|
||||
'''
|
||||
invoice = self._create_invoice([(100, self.env['account.tax'])])
|
||||
invoice = self._create_invoice_taxes_per_line([(100, self.env['account.tax'])])
|
||||
invoice.invoice_line_ids[0].tax_ids = self.percent_tax_1
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [
|
||||
{'name': self.percent_tax_1.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': []},
|
||||
{'name': self.percent_tax_1.name, 'tax_base_amount': -100, 'balance': -21, 'tax_ids': []},
|
||||
])
|
||||
|
||||
def test_one_tax_per_line(self):
|
||||
|
|
@ -146,16 +143,16 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
21% incl | / | 100 | 21
|
||||
12% | / | 100 | 12
|
||||
'''
|
||||
invoice = self._create_invoice([
|
||||
invoice = self._create_invoice_taxes_per_line([
|
||||
(100, self.percent_tax_1),
|
||||
(121, self.percent_tax_1_incl),
|
||||
(100, self.percent_tax_2),
|
||||
])
|
||||
invoice.action_post()
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted(lambda x: x.name), [
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': 100, 'balance': -12, 'tax_ids': []},
|
||||
{'name': self.percent_tax_1.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': []},
|
||||
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': []},
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': -100, 'balance': -12, 'tax_ids': []},
|
||||
{'name': self.percent_tax_1.name, 'tax_base_amount': -100, 'balance': -21, 'tax_ids': []},
|
||||
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': -100, 'balance': -21, 'tax_ids': []},
|
||||
])
|
||||
|
||||
def test_affecting_base_amount(self):
|
||||
|
|
@ -172,14 +169,14 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
12% | / | 121 | 14.52
|
||||
12% | / | 100 | 12
|
||||
'''
|
||||
invoice = self._create_invoice([
|
||||
invoice = self._create_invoice_taxes_per_line([
|
||||
(121, self.percent_tax_1_incl + self.percent_tax_2),
|
||||
(100, self.percent_tax_2),
|
||||
])
|
||||
invoice.action_post()
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted(lambda x: -x.balance), [
|
||||
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': [self.percent_tax_2.id]},
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': 221, 'balance': -26.52, 'tax_ids': []},
|
||||
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': -100, 'balance': -21, 'tax_ids': [self.percent_tax_2.id]},
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': -221, 'balance': -26.52, 'tax_ids': []},
|
||||
])
|
||||
|
||||
def test_group_of_taxes(self):
|
||||
|
|
@ -196,15 +193,15 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
12% | 21% incl | 121 | 14.52
|
||||
12% | / | 100 | 12
|
||||
'''
|
||||
invoice = self._create_invoice([
|
||||
invoice = self._create_invoice_taxes_per_line([
|
||||
(121, self.group_tax),
|
||||
(100, self.percent_tax_2),
|
||||
])
|
||||
invoice.action_post()
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted('balance'), [
|
||||
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100.0, 'balance': -21.0, 'tax_ids': [self.percent_tax_2.id]},
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': 121.0, 'balance': -14.52, 'tax_ids': []},
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': 100.0, 'balance': -12.0, 'tax_ids': []},
|
||||
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': -100.0, 'balance': -21.0, 'tax_ids': [self.percent_tax_2.id]},
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': -121.0, 'balance': -14.52, 'tax_ids': []},
|
||||
{'name': self.percent_tax_2.name, 'tax_base_amount': -100.0, 'balance': -12.0, 'tax_ids': []},
|
||||
])
|
||||
|
||||
def _create_tax_tag(self, tag_name):
|
||||
|
|
@ -271,7 +268,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
# Test invoice repartition
|
||||
invoice = self._create_invoice([(100, tax)], inv_type='out_invoice')
|
||||
invoice = self._create_invoice_taxes_per_line([(100, tax)], inv_type='out_invoice')
|
||||
invoice.action_post()
|
||||
|
||||
self.assertEqual(len(invoice.line_ids), 4, "There should be 4 account move lines created for the invoice: payable, base and 2 tax lines")
|
||||
|
|
@ -287,7 +284,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
self.assertEqual(inv_tax_lines.filtered(lambda x: x.account_id == account_2).tax_tag_ids, inv_tax_tag_90, "Tax line on account 2 should have 90% tag")
|
||||
|
||||
# Test refund repartition
|
||||
refund = self._create_invoice([(100, tax)], inv_type='out_refund')
|
||||
refund = self._create_invoice_taxes_per_line([(100, tax)], inv_type='out_refund')
|
||||
refund.action_post()
|
||||
|
||||
self.assertEqual(len(refund.line_ids), 4, "There should be 4 account move lines created for the refund: payable, base and 2 tax lines")
|
||||
|
|
@ -319,10 +316,10 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'type_tax_use': 'sale',
|
||||
'amount_type': 'division',
|
||||
'amount': 100,
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
'include_base_amount': True,
|
||||
})
|
||||
invoice = self._create_invoice([(100, sale_tax)])
|
||||
invoice = self._create_invoice_taxes_per_line([(100, sale_tax)])
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
|
||||
'name': sale_tax.name,
|
||||
'tax_base_amount': 0.0,
|
||||
|
|
@ -348,21 +345,21 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, self.base_tag_pos.ids)],
|
||||
'tag_ids': [(6, 0, self.base_tag.ids)],
|
||||
}),
|
||||
(0, 0, {
|
||||
'repartition_type': 'tax',
|
||||
'tag_ids': [(6, 0, self.tax_tag_pos.ids)],
|
||||
'tag_ids': [(6, 0, self.tax_tag.ids)],
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, self.base_tag_neg.ids)],
|
||||
'tag_ids': [(6, 0, self.base_tag.ids)],
|
||||
}),
|
||||
(0, 0, {
|
||||
'repartition_type': 'tax',
|
||||
'tag_ids': [(6, 0, self.tax_tag_neg.ids)],
|
||||
'tag_ids': [(6, 0, self.tax_tag.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
@ -393,9 +390,9 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
move = move_form.save()
|
||||
|
||||
self.assertRecordValues(move.line_ids.sorted('balance'), [
|
||||
{'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': ref_tax_rep_ln.id, 'tax_tag_invert': False},
|
||||
{'balance': 1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
|
||||
{'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': ref_tax_rep_ln.id},
|
||||
{'balance': 1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
])
|
||||
|
||||
# === Tax in credit ===
|
||||
|
|
@ -420,9 +417,9 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
move = move_form.save()
|
||||
|
||||
self.assertRecordValues(move.line_ids.sorted('balance'), [
|
||||
{'balance': -1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': True},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': inv_tax_rep_ln.id, 'tax_tag_invert': True},
|
||||
{'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
|
||||
{'balance': -1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': -1000, 'tax_repartition_line_id': inv_tax_rep_ln.id},
|
||||
{'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
])
|
||||
|
||||
def test_misc_journal_entry_tax_tags_purchase(self):
|
||||
|
|
@ -434,21 +431,21 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, self.base_tag_pos.ids)],
|
||||
'tag_ids': [(6, 0, self.base_tag.ids)],
|
||||
}),
|
||||
(0, 0, {
|
||||
'repartition_type': 'tax',
|
||||
'tag_ids': [(6, 0, self.tax_tag_pos.ids)],
|
||||
'tag_ids': [(6, 0, self.tax_tag.ids)],
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, self.base_tag_neg.ids)],
|
||||
'tag_ids': [(6, 0, self.base_tag.ids)],
|
||||
}),
|
||||
(0, 0, {
|
||||
'repartition_type': 'tax',
|
||||
'tag_ids': [(6, 0, self.tax_tag_neg.ids)],
|
||||
'tag_ids': [(6, 0, self.tax_tag.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
@ -478,9 +475,9 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
move = move_form.save()
|
||||
|
||||
self.assertRecordValues(move.line_ids.sorted('balance'), [
|
||||
{'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': inv_tax_rep_ln.id, 'tax_tag_invert': False},
|
||||
{'balance': 1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
|
||||
{'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': inv_tax_rep_ln.id},
|
||||
{'balance': 1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
])
|
||||
|
||||
# === Tax in credit ===
|
||||
|
|
@ -505,9 +502,9 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
move = move_form.save()
|
||||
|
||||
self.assertRecordValues(move.line_ids.sorted('balance'), [
|
||||
{'balance': -1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': True},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': ref_tax_rep_ln.id, 'tax_tag_invert': True},
|
||||
{'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
|
||||
{'balance': -1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': -1000, 'tax_repartition_line_id': ref_tax_rep_ln.id},
|
||||
{'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False},
|
||||
])
|
||||
|
||||
def test_misc_entry_tax_group_signs(self):
|
||||
|
|
@ -525,11 +522,11 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, self.base_tag_pos.ids)],
|
||||
'tag_ids': [(6, 0, self.base_tag.ids)],
|
||||
}),
|
||||
(0, 0, {
|
||||
'repartition_type': 'tax',
|
||||
'tag_ids': [(6, 0, self.tax_tag_pos.ids)],
|
||||
'tag_ids': [(6, 0, self.tax_tag.ids)],
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
|
|
@ -550,11 +547,11 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'refund_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, self.base_tag_neg.ids)],
|
||||
'tag_ids': [(6, 0, self.base_tag.ids)],
|
||||
}),
|
||||
(0, 0, {
|
||||
'repartition_type': 'tax',
|
||||
'tag_ids': [(6, 0, self.tax_tag_neg.ids)],
|
||||
'tag_ids': [(6, 0, self.tax_tag.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
@ -588,37 +585,37 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
# Sale tax on debit: use refund repartition
|
||||
debit_sale_move = _create_misc_operation(sale_group, 'debit')
|
||||
self.assertRecordValues(debit_sale_move.line_ids.sorted('balance'), [
|
||||
{'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
{'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000},
|
||||
{'balance': 1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0},
|
||||
{'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
{'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': 1000},
|
||||
{'balance': 1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0},
|
||||
])
|
||||
|
||||
# Sale tax on credit: use invoice repartition
|
||||
credit_sale_move = _create_misc_operation(sale_group, 'credit')
|
||||
self.assertRecordValues(credit_sale_move.line_ids.sorted('balance'), [
|
||||
{'balance': -1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
|
||||
{'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000},
|
||||
{'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
{'balance': -1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': -1000},
|
||||
{'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': -1000},
|
||||
{'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
])
|
||||
|
||||
# Purchase tax on debit: use invoice repartition
|
||||
debit_purchase_move = _create_misc_operation(purchase_group, 'debit')
|
||||
self.assertRecordValues(debit_purchase_move.line_ids.sorted('balance'), [
|
||||
{'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
{'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
|
||||
{'balance': 1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0},
|
||||
{'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
{'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': 1000},
|
||||
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
|
||||
{'balance': 1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0},
|
||||
])
|
||||
|
||||
# Purchase tax on credit: use refund repartition
|
||||
credit_purchase_move = _create_misc_operation(purchase_group, 'credit')
|
||||
self.assertRecordValues(credit_purchase_move.line_ids.sorted('balance'), [
|
||||
{'balance': -1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000},
|
||||
{'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
|
||||
{'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
{'balance': -1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag.ids, 'tax_base_amount': 0},
|
||||
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag.ids, 'tax_base_amount': -1000},
|
||||
{'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': -1000},
|
||||
{'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
|
||||
])
|
||||
|
||||
def test_tax_calculation_foreign_currency_large_quantity(self):
|
||||
|
|
@ -631,15 +628,15 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
self.env['res.currency.rate'].create({
|
||||
'name': '2018-01-01',
|
||||
'rate': 1.1726,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.currency_data['currency'].rounding = 0.05
|
||||
self.other_currency.rounding = 0.05
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_date': '2018-01-01',
|
||||
'date': '2018-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
|
|
@ -651,7 +648,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
|
||||
'tax_base_amount': 48098.24, # 20000 * 2.82 / 1.1726
|
||||
'tax_base_amount': -48098.24, # 20000 * 2.82 / 1.1726
|
||||
'credit': 10100.63, # tax_base_amount * 0.21
|
||||
}])
|
||||
|
||||
|
|
@ -660,31 +657,31 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
self.env['res.currency.rate'].create({
|
||||
'name': '2018-01-01',
|
||||
'rate': 0.654065014,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.currency_data['currency'].rounding = 0.05
|
||||
self.other_currency.rounding = 0.05
|
||||
|
||||
invoice = self._create_invoice([
|
||||
invoice = self._create_invoice_taxes_per_line([
|
||||
(5, self.percent_tax_3_incl),
|
||||
(10, self.percent_tax_3_incl),
|
||||
(50, self.percent_tax_3_incl),
|
||||
], currency_id=self.currency_data['currency'], invoice_payment_term_id=self.pay_terms_a)
|
||||
], currency_id=self.other_currency, invoice_payment_term_id=self.pay_terms_a)
|
||||
invoice.action_post()
|
||||
|
||||
def test_tax_calculation_multi_currency(self):
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2018-01-01',
|
||||
'rate': 0.273748,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.currency_data['currency'].rounding = 0.01
|
||||
self.other_currency.rounding = 0.01
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_date': '2018-01-01',
|
||||
'date': '2018-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
|
|
@ -696,59 +693,24 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
|
||||
'tax_base_amount': 567.38, # 155.32 * 1 / (1 / 0.273748)
|
||||
'balance': -119.16, # tax_base_amount * 0.21
|
||||
'tax_base_amount': -567.38, # 155.32 * 1 / (1 / 0.273748)
|
||||
'balance': -119.15, # tax_base_amount * 0.21
|
||||
}])
|
||||
|
||||
self.assertRecordValues(invoice.line_ids.filtered(lambda l: not l.name), [{
|
||||
'balance': 686.54,
|
||||
'balance': 686.53,
|
||||
}])
|
||||
|
||||
with Form(invoice) as invoice_form:
|
||||
invoice_form.currency_id = self.currency_data['currency']
|
||||
invoice_form.currency_id = self.other_currency
|
||||
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
|
||||
'tax_base_amount': 567.38,
|
||||
'balance': -119.16,
|
||||
'tax_base_amount': -567.38,
|
||||
'balance': -119.15,
|
||||
}])
|
||||
|
||||
self.assertRecordValues(invoice.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable'), [{
|
||||
'balance': 686.54,
|
||||
}])
|
||||
|
||||
def test_tax_calculation_multi_currency_100_included_tax(self):
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2018-01-01',
|
||||
'rate': 0.273748,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.currency_data['currency'].rounding = 0.01
|
||||
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'tax_100',
|
||||
'amount_type': 'division',
|
||||
'amount': 100,
|
||||
'price_include': True,
|
||||
})
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'invoice_date': '2018-01-01',
|
||||
'date': '2018-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'name': 'xxxx',
|
||||
'quantity': 1,
|
||||
'price_unit': 100.00,
|
||||
'tax_ids': [(6, 0, tax.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
|
||||
'tax_base_amount': 0.0,
|
||||
'balance': -365.3, # 100 * (1 / 0.273748)
|
||||
'balance': 686.53,
|
||||
}])
|
||||
|
||||
def test_fixed_tax_with_zero_price(self):
|
||||
|
|
@ -757,7 +719,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'amount_type': 'fixed',
|
||||
'amount': 5,
|
||||
})
|
||||
invoice = self._create_invoice([
|
||||
invoice = self._create_invoice_taxes_per_line([
|
||||
(0, fixed_tax),
|
||||
])
|
||||
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
|
||||
|
|
@ -770,6 +732,64 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'debit': 0,
|
||||
}])
|
||||
|
||||
def test_tax_repartition_lines_dispatch_amount_1(self):
|
||||
""" Ensure the tax amount is well dispatched to the repartition lines and the rounding errors are well managed.
|
||||
The 5% tax is applied on 1 so the total tax amount should be 0.05.
|
||||
However, there are 10 tax repartition lines of 0.05 * 0.1 = 0.005 that will be rounded to 0.01.
|
||||
The test checks the tax amount doesn't become 10 * 0.01 = 0.1 instead of 0.05.
|
||||
"""
|
||||
base_tax_rep = Command.create({'repartition_type': 'base', 'factor_percent': 100.0})
|
||||
tax_reps = [Command.create({'repartition_type': 'tax', 'factor_percent': 10.0})] * 10
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': "test_tax_repartition_lines_dispatch_amount_1",
|
||||
'amount_type': 'percent',
|
||||
'amount': 5.0,
|
||||
'invoice_repartition_line_ids': [base_tax_rep] + tax_reps,
|
||||
'refund_repartition_line_ids': [base_tax_rep] + tax_reps,
|
||||
})
|
||||
invoice = self._create_invoice_taxes_per_line([(1, tax)])
|
||||
self.assertRecordValues(invoice, [{
|
||||
'amount_untaxed': 1.0,
|
||||
'amount_tax': 0.05,
|
||||
'amount_total': 1.05,
|
||||
}])
|
||||
self.assertRecordValues(
|
||||
invoice.line_ids.filtered('tax_line_id').sorted('balance'),
|
||||
[{'balance': -0.01}] * 5,
|
||||
)
|
||||
|
||||
def test_tax_repartition_lines_dispatch_amount_2(self):
|
||||
""" Ensure the tax amount is well dispatched to the repartition lines and the rounding errors are well managed.
|
||||
The 5% tax is applied on 1 so the total tax amount should be 0.05 but the distribution is 100 - 100 = 0%.
|
||||
So at the end, the tax amount is 0.
|
||||
However, there are 10 positive tax repartition lines of 0.05 * 0.1 = 0.005 that will be rounded to 0.01
|
||||
and one negative repartition line of 50% and 2 of 25% that will give respectively 0.025 ~= 0.03 and 0.0125 ~= 0.01.
|
||||
The test checks the tax amount is still zero at the end.
|
||||
"""
|
||||
base_tax_rep = Command.create({'repartition_type': 'base', 'factor_percent': 100.0})
|
||||
plus_tax_reps = [Command.create({'repartition_type': 'tax', 'factor_percent': 10.0})] * 10
|
||||
neg_tax_reps = [
|
||||
Command.create({'repartition_type': 'tax', 'factor_percent': percent})
|
||||
for percent in (-50, -25, -25)
|
||||
]
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': "test_tax_repartition_lines_dispatch_amount_2",
|
||||
'amount_type': 'percent',
|
||||
'amount': 5.0,
|
||||
'invoice_repartition_line_ids': [base_tax_rep] + plus_tax_reps + neg_tax_reps,
|
||||
'refund_repartition_line_ids': [base_tax_rep] + plus_tax_reps + neg_tax_reps,
|
||||
})
|
||||
invoice = self._create_invoice_taxes_per_line([(1, tax)], inv_type='out_refund')
|
||||
self.assertRecordValues(invoice, [{
|
||||
'amount_untaxed': 1.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 1.0,
|
||||
}])
|
||||
self.assertRecordValues(
|
||||
invoice.line_ids.filtered('tax_line_id').sorted('balance'),
|
||||
[{'balance': -0.03}] + [{'balance': -0.01}] * 2 + [{'balance': 0.01}] * 5,
|
||||
)
|
||||
|
||||
def test_tax_line_amount_currency_modification_auto_balancing(self):
|
||||
date = '2017-01-01'
|
||||
move = self.env['account.move'].create({
|
||||
|
|
@ -777,10 +797,10 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'date': date,
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': date,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'currency_id': self.other_currency.id,
|
||||
'invoice_payment_term_id': self.pay_terms_a.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'name': self.product_a.name,
|
||||
'product_id': self.product_a.id,
|
||||
'product_uom_id': self.product_a.uom_id.id,
|
||||
|
|
@ -788,7 +808,7 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
'price_unit': 1000,
|
||||
'tax_ids': self.product_a.taxes_id.ids,
|
||||
}),
|
||||
(0, None, {
|
||||
Command.create({
|
||||
'name': self.product_b.name,
|
||||
'product_id': self.product_b.id,
|
||||
'product_uom_id': self.product_b.uom_id.id,
|
||||
|
|
@ -816,3 +836,117 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
|||
self.assertRecordValues(receivable_line, [
|
||||
{'amount_currency': 1410.02, 'balance': 705.02},
|
||||
])
|
||||
|
||||
def test_product_account_tags(self):
|
||||
product_tag = self.env['account.account.tag'].create({
|
||||
'name': "Pikachu",
|
||||
'applicability': 'products',
|
||||
})
|
||||
self.product_a.account_tag_ids = [Command.set(product_tag.ids)]
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2025-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'test with product tag',
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 1000,
|
||||
'tax_ids': self.percent_tax_1.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'test with product tag',
|
||||
'product_id': self.product_b.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': self.percent_tax_1.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'test without product tag',
|
||||
'product_id': self.product_b.id,
|
||||
'price_unit': 200,
|
||||
'tax_ids': self.percent_tax_2.ids,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
invoice.action_post()
|
||||
|
||||
self.assertRecordValues(
|
||||
invoice.line_ids,
|
||||
[
|
||||
{'tax_ids': self.percent_tax_1.ids, 'tax_line_id': False, 'tax_tag_ids': product_tag.ids, 'credit': 1000, 'debit': 0},
|
||||
{'tax_ids': self.percent_tax_1.ids, 'tax_line_id': False, 'tax_tag_ids': [], 'credit': 100, 'debit': 0},
|
||||
{'tax_ids': self.percent_tax_2.ids, 'tax_line_id': False, 'tax_tag_ids': [], 'credit': 200, 'debit': 0},
|
||||
{'tax_ids': [], 'tax_line_id': self.percent_tax_1.id, 'tax_tag_ids': product_tag.ids, 'credit': 210, 'debit': 0},
|
||||
{'tax_ids': [], 'tax_line_id': self.percent_tax_1.id, 'tax_tag_ids': [], 'credit': 21, 'debit': 0},
|
||||
{'tax_ids': [], 'tax_line_id': self.percent_tax_2.id, 'tax_tag_ids': [], 'credit': 24, 'debit': 0},
|
||||
{'tax_ids': [], 'tax_line_id': False, 'tax_tag_ids': [], 'credit': 0, 'debit': 1555},
|
||||
],
|
||||
)
|
||||
|
||||
def test_fiscal_position_tax_mapping_with_inactive_tax(self):
|
||||
""" Test that inactive taxes are not mapped by fiscal positions """
|
||||
fp = self.env['account.fiscal.position'].create({'name': 'FP'})
|
||||
src_tax = self.company_data['default_tax_sale']
|
||||
active_tax = self.percent_tax_1.copy({
|
||||
'fiscal_position_ids': [Command.set(fp.ids)],
|
||||
'original_tax_ids': [Command.set(src_tax.ids)],
|
||||
})
|
||||
self.percent_tax_2.copy({
|
||||
'active': False,
|
||||
'fiscal_position_ids': [Command.set(fp.ids)],
|
||||
'original_tax_ids': [Command.set(src_tax.ids)],
|
||||
})
|
||||
self.partner_a.property_account_position_id = fp
|
||||
|
||||
move = self.init_invoice('out_invoice', self.partner_a, post=True, products=[self.product])
|
||||
line = move.invoice_line_ids
|
||||
self.assertEqual(line.tax_ids, active_tax, f"The tax should be {active_tax.name}, but is is currently {line.tax_ids.name}")
|
||||
|
||||
def test_multiple_onchange_product_and_price(self):
|
||||
"""
|
||||
This test checks that the totals are computed correctly when an onchange is executed
|
||||
with "price_unit" before "product_id" in the values.
|
||||
This test covers a UI issue where the totals were not updated when the price was changed,
|
||||
then the product and finally the price again.
|
||||
The issue was only occuring between the change of value and the next save.
|
||||
That's why the test is using the onchange method directly instead of using a Form.
|
||||
"""
|
||||
invoice = self.init_invoice('out_invoice', products=self.product_a)
|
||||
self.assertEqual(invoice.tax_totals['base_amount'], 1000.0)
|
||||
self.assertEqual(invoice.tax_totals['total_amount'], 1150.0)
|
||||
# The onchange is executed directly to simulate the following flow:
|
||||
# 1) unit price is changed to any value
|
||||
# 2) product is changed to "Product B"
|
||||
# 3) unit price is changed to 2000.0
|
||||
results = invoice.onchange(
|
||||
{'invoice_line_ids': [Command.update(invoice.invoice_line_ids[0].id, {'price_unit': 2000.0, 'product_id': self.product_b.id})]},
|
||||
['invoice_line_ids'],
|
||||
{"invoice_line_ids": {}, 'tax_totals': {}}
|
||||
)
|
||||
self.assertEqual(results['value']['tax_totals']['base_amount'], 2000.0)
|
||||
self.assertEqual(results['value']['tax_totals']['total_amount'], 2600.0)
|
||||
|
||||
def test_tax_mapping_with_tax_fiscal_position_set_to_all(self):
|
||||
"""
|
||||
A tax without any fiscal positions assigned should be applicable to
|
||||
any fiscal position.
|
||||
This test ensures that when a single fiscal position exists and a tax
|
||||
has no fiscal position restriction, the tax is correctly applied to
|
||||
invoice lines.
|
||||
"""
|
||||
self.env['account.fiscal.position'].search([]).action_archive()
|
||||
default_tax = self.company_data['default_tax_sale']
|
||||
self.env['account.fiscal.position'].create({
|
||||
'name': 'FP',
|
||||
'auto_apply': True,
|
||||
'country_id': self.env.company.country_id.id,
|
||||
'tax_ids': default_tax.ids,
|
||||
})
|
||||
self.assertEqual(self.product.taxes_id, default_tax)
|
||||
default_tax.fiscal_position_ids = False
|
||||
self.partner_a.country_id = self.env.company.country_id.id
|
||||
move = self._create_invoice_one_line(product_id=self.product, partner_id=self.partner_a, post=True)
|
||||
line = move.invoice_line_ids
|
||||
self.assertEqual(line.tax_ids, default_tax)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
import io
|
||||
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
import re
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.exceptions import RedirectWarning
|
||||
from odoo.tools import pdf
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import file_open
|
||||
from odoo.tools import file_open, mute_logger
|
||||
from odoo.tools.pdf import PdfFileReader, PdfFileWriter
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
|
|
@ -44,6 +44,8 @@ class TestIrActionsReport(AccountTestInvoicingCommon):
|
|||
test_record_report = self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account.action_account_original_vendor_bill', res_ids=in_invoice_1.id)
|
||||
self.assertTrue(test_record_report, "The PDF should have been generated")
|
||||
|
||||
# Document synchronization being enabled, avoid a warning when computing the number of page of the corrupted pdf.
|
||||
@mute_logger('odoo.addons.documents.models.documents_document')
|
||||
def test_download_with_encrypted_pdf(self):
|
||||
"""
|
||||
Same as test_download_one_corrupted_pdf
|
||||
|
|
@ -57,17 +59,20 @@ class TestIrActionsReport(AccountTestInvoicingCommon):
|
|||
for page_num in range(pdf_reader.getNumPages()):
|
||||
pdf_writer.addPage(pdf_reader.getPage(page_num))
|
||||
# Encrypt the PDF
|
||||
pdf_writer.encrypt('', use_128bit=True)
|
||||
pdf_writer.encrypt('')
|
||||
# Get the binary
|
||||
output_buffer = io.BytesIO()
|
||||
pdf_writer.write(output_buffer)
|
||||
encrypted_file = output_buffer.getvalue()
|
||||
|
||||
# we need to change the encryption value from 4 to 5 to simulate an encryption not used by PyPDF2
|
||||
encrypt_start = encrypted_file.find(b'/Encrypt')
|
||||
encrypt_end = encrypted_file.find(b'>>', encrypt_start)
|
||||
encrypt_version = encrypted_file[encrypt_start: encrypt_end]
|
||||
encrypted_file = encrypted_file.replace(encrypt_version, encrypt_version.replace(b'4', b'5'))
|
||||
# corrupt encryption: point the /Encrypt xref as a non-encrypt
|
||||
# (but valid otherwise pypdf skips it)
|
||||
encrypted_file, n = re.subn(
|
||||
b'/Encrypt (?P<index>\\d+) (?P<gen>\\d+) R',
|
||||
b'/Encrypt 1 \\g<gen> R',
|
||||
encrypted_file,
|
||||
)
|
||||
self.assertEqual(n, 1, "should have updated the /Encrypt entry")
|
||||
|
||||
in_invoice_1 = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
|
|
@ -76,7 +81,7 @@ class TestIrActionsReport(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
in_invoice_1.message_main_attachment_id = self.env['ir.attachment'].create({
|
||||
'datas': base64.b64encode(encrypted_file),
|
||||
'raw': encrypted_file,
|
||||
'name': attach_name,
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': 'account.move',
|
||||
|
|
@ -88,7 +93,7 @@ class TestIrActionsReport(AccountTestInvoicingCommon):
|
|||
in_invoice_2 = in_invoice_1.copy()
|
||||
|
||||
in_invoice_2.message_main_attachment_id = self.env['ir.attachment'].create({
|
||||
'datas': base64.b64encode(self.file),
|
||||
'raw': self.file,
|
||||
'name': attach_name,
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': 'account.move',
|
||||
|
|
|
|||
117
odoo-bringout-oca-ocb-account/account/tests/test_kpi_provider.py
Normal file
117
odoo-bringout-oca-ocb-account/account/tests/test_kpi_provider.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from odoo import Command
|
||||
from odoo.tests import tagged, TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestKpiProvider(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.partner_id = cls.env['res.partner'].create({'name': 'Someone'})
|
||||
|
||||
# Clean things for the test
|
||||
cls.env['account.move'].search([
|
||||
'|', ('state', '=', 'draft'),
|
||||
('statement_line_id.is_reconciled', '=', False),
|
||||
])._unlink_or_reverse()
|
||||
|
||||
def test_empty_kpi_summary(self):
|
||||
# Ensure that nothing gets reported when there is nothing to report
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [])
|
||||
|
||||
def test_kpi_summary(self):
|
||||
company_id = self.ref('base.main_company')
|
||||
account_id = self.env['account.account'].search([('company_ids', '=', company_id)], limit=1)
|
||||
base_move = {
|
||||
'company_id': company_id,
|
||||
'invoice_line_ids': [Command.create({'account_id': account_id.id, 'quantity': 15, 'price_unit': 10})],
|
||||
'partner_id': self.partner_id.id,
|
||||
}
|
||||
self.env['account.move'].create(
|
||||
[{**base_move, 'move_type': 'entry'}] * 2 +
|
||||
[{**base_move, 'move_type': 'out_invoice'}] * 3 +
|
||||
[{**base_move, 'move_type': 'out_refund'}] * 4 +
|
||||
[{**base_move, 'move_type': 'in_invoice'}] * 5 +
|
||||
[{**base_move, 'move_type': 'in_refund'}] * 6 +
|
||||
[{**base_move, 'move_type': 'out_receipt'}] * 7 +
|
||||
[{**base_move, 'move_type': 'in_receipt'}] * 8
|
||||
)
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [
|
||||
{'id': 'account_journal_type.general', 'name': 'Miscellaneous', 'type': 'integer', 'value': 2},
|
||||
{'id': 'account_journal_type.sale', 'name': 'Sales', 'type': 'integer', 'value': 3 + 4 + 7},
|
||||
{'id': 'account_journal_type.purchase', 'name': 'Purchase', 'type': 'integer', 'value': 5 + 6 + 8},
|
||||
])
|
||||
|
||||
def test_kpi_summary_shouldnt_report_posted_moves(self):
|
||||
company_id = self.ref('base.main_company')
|
||||
account_id = self.env['account.account'].search([('company_ids', '=', company_id)], limit=1).id
|
||||
move = self.env['account.move'].create({
|
||||
'company_id': company_id,
|
||||
'invoice_line_ids': [Command.create({'account_id': account_id, 'quantity': 15, 'price_unit': 10})],
|
||||
'partner_id': self.partner_id.id,
|
||||
'move_type': 'out_invoice',
|
||||
})
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [
|
||||
{'id': 'account_journal_type.sale', 'name': 'Sales', 'type': 'integer', 'value': 1},
|
||||
])
|
||||
|
||||
move.action_post()
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [])
|
||||
|
||||
def test_kpi_summary_reports_posted_but_to_check_moves(self):
|
||||
company_id = self.ref('base.main_company')
|
||||
account_id = self.env['account.account'].search([('company_ids', '=', company_id)], limit=1).id
|
||||
move = self.env['account.move'].create({
|
||||
'company_id': company_id,
|
||||
'invoice_line_ids': [Command.create({'account_id': account_id, 'quantity': 15, 'price_unit': 10})],
|
||||
'partner_id': self.partner_id.id,
|
||||
'move_type': 'out_invoice',
|
||||
})
|
||||
move.action_post()
|
||||
move.checked = False
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [
|
||||
{'id': 'account_journal_type.sale', 'name': 'Sales', 'type': 'integer', 'value': 1},
|
||||
])
|
||||
|
||||
move.button_set_checked()
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [])
|
||||
|
||||
def test_kpi_summary_reports_unreconciled_bank_statements(self):
|
||||
company_id = self.ref('base.main_company')
|
||||
account_id = self.env['account.account'].search([('company_ids', '=', company_id), ('account_type', '=', 'income')], limit=1).id
|
||||
move = self.env['account.move'].create({
|
||||
'company_id': company_id,
|
||||
'line_ids': [Command.create({'account_id': account_id, 'quantity': 15, 'price_unit': 10})],
|
||||
'partner_id': self.env.user.partner_id.id,
|
||||
'move_type': 'out_invoice',
|
||||
})
|
||||
move.action_post()
|
||||
|
||||
journal_id = self.env['account.journal'].create({
|
||||
'name': 'Bank',
|
||||
'type': 'bank',
|
||||
})
|
||||
bank_statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'line_ids': [Command.create({
|
||||
'date': '2025-09-15',
|
||||
'payment_ref': 'line_1',
|
||||
'journal_id': journal_id.id,
|
||||
'amount': move.amount_total,
|
||||
})],
|
||||
})
|
||||
|
||||
self.assertEqual(bank_statement.line_ids.move_id.state, 'posted')
|
||||
self.assertFalse(bank_statement.line_ids.is_reconciled)
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [
|
||||
{'id': 'account_journal_type.bank', 'name': 'Bank', 'type': 'integer', 'value': 1},
|
||||
])
|
||||
|
||||
move_line = move.line_ids.filtered(lambda line: line.account_type == 'asset_receivable')
|
||||
_st_liquidity_lines, st_suspense_lines, _st_other_lines = bank_statement.line_ids._seek_for_lines()
|
||||
st_suspense_lines.account_id = move_line.account_id
|
||||
(move_line + st_suspense_lines).reconcile()
|
||||
self.assertTrue(bank_statement.line_ids.is_reconciled)
|
||||
self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [])
|
||||
|
|
@ -4,13 +4,18 @@
|
|||
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mail.tests.common import MailCase
|
||||
from odoo.tests import Form, users
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTracking(AccountTestInvoicingCommon, TestMailCommon):
|
||||
class TestTracking(AccountTestInvoicingCommon, MailCase):
|
||||
|
||||
@classmethod
|
||||
def default_env_context(cls):
|
||||
# OVERRIDE
|
||||
return {}
|
||||
|
||||
def test_aml_change_tracking(self):
|
||||
""" tests that the field_groups is correctly set """
|
||||
|
|
@ -29,11 +34,65 @@ class TestTracking(AccountTestInvoicingCommon, TestMailCommon):
|
|||
new_value = account_move.invoice_line_ids.account_id
|
||||
|
||||
self.flush_tracking()
|
||||
self.assertTracking(account_move.message_ids, [
|
||||
# Isolate the tracked value for the invoice line because changing the account has recomputed the taxes.
|
||||
tracking_value = account_move.message_ids.sudo().tracking_value_ids\
|
||||
.filtered(lambda t: t.field_id.name == 'account_id' and t.old_value_integer == old_value.id)
|
||||
self.assertTracking(tracking_value.mail_message_id, [
|
||||
('account_id', 'many2one', old_value, new_value),
|
||||
])
|
||||
|
||||
tracking_value = account_move.message_ids.sudo().tracking_value_ids
|
||||
tracking_value._compute_field_groups()
|
||||
self.assertEqual(len(tracking_value), 1)
|
||||
self.assertTrue(tracking_value.field_id)
|
||||
field = self.env[tracking_value.field_id.model]._fields[tracking_value.field_id.name]
|
||||
self.assertFalse(field.groups, "There is no group on account.move.line.account_id")
|
||||
|
||||
self.assertEqual(tracking_value.field_groups, False, "There is no group on account.move.line.account_id")
|
||||
@users('admin')
|
||||
def test_invite_follower_account_moves(self):
|
||||
""" Test that the mail_followers_edit wizard works on both single and multiple account.move records """
|
||||
user_admin = self.env.ref('base.user_admin')
|
||||
user_admin.write({
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'email': 'test.admin@test.example.com',
|
||||
"name": "Mitchell Admin",
|
||||
'notification_type': 'inbox',
|
||||
'phone': '0455135790',
|
||||
})
|
||||
partner_admin = self.env.ref('base.partner_admin')
|
||||
multiple_account_moves = [
|
||||
{
|
||||
'description': 'Single account.move',
|
||||
'account_moves': [{'name': 'Test Single', 'partner_id': self.partner_a.id}],
|
||||
'expected_partners': self.partner_a | user_admin.partner_id,
|
||||
},
|
||||
{
|
||||
'description': 'Multiple account.moves',
|
||||
'account_moves': [
|
||||
{'name': 'Move 1', 'partner_id': self.partner_a.id},
|
||||
{'name': 'Move 2', 'partner_id': self.partner_b.id},
|
||||
],
|
||||
'expected_partners': self.partner_a | user_admin.partner_id,
|
||||
},
|
||||
]
|
||||
for move in multiple_account_moves:
|
||||
with self.subTest(move['description']):
|
||||
account_moves = self.env['account.move'].with_context(self._test_context).create(move['account_moves'])
|
||||
mail_invite = self.env['mail.followers.edit'].with_context({
|
||||
'default_res_model': 'account.move',
|
||||
'default_res_ids': account_moves.ids,
|
||||
}).with_user(user_admin).create({
|
||||
'partner_ids': [(4, self.partner_a.id), (4, user_admin.partner_id.id)],
|
||||
'notify': True,
|
||||
})
|
||||
with self.mock_mail_app(), self.mock_mail_gateway():
|
||||
mail_invite.edit_followers()
|
||||
|
||||
for account_move in account_moves:
|
||||
self.assertEqual(account_move.message_partner_ids, move['expected_partners'])
|
||||
|
||||
self.assertEqual(len(self._new_msgs), 1)
|
||||
self.assertEqual(len(self._mails), 1)
|
||||
self.assertNotSentEmail([partner_admin])
|
||||
self.assertNotified(
|
||||
self._new_msgs[0],
|
||||
[{'partner': partner_admin, 'type': 'inbox', 'is_read': False}]
|
||||
)
|
||||
|
|
|
|||
280
odoo-bringout-oca-ocb-account/account/tests/test_multivat.py
Normal file
280
odoo-bringout-oca-ocb-account/account/tests/test_multivat.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account.models.chart_template import AccountChartTemplate
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
|
||||
|
||||
def _get_chart_template_mapping(self, get_all=False):
|
||||
return {
|
||||
'local': {
|
||||
'name': 'local',
|
||||
'country_id': self.env.ref("base.be").id,
|
||||
'country_code': 'be',
|
||||
'modules': ['account'],
|
||||
'parent': None,
|
||||
'installed': True,
|
||||
},
|
||||
'foreign': {
|
||||
'name': 'foreign',
|
||||
'country_id': self.env.ref("base.fr").id,
|
||||
'country_code': 'fr',
|
||||
'modules': ['account'],
|
||||
'parent': None,
|
||||
'installed': True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def data_method_provider(chart_template_name, country_code):
|
||||
country = f"base.{country_code}"
|
||||
# this is used to simulated differences between xml_ids
|
||||
external_id_prefix = '' if chart_template_name == 'local' else f"{chart_template_name}_"
|
||||
|
||||
def test_data_getter(self, template_code):
|
||||
return {
|
||||
'template_data': {
|
||||
'code_digits': 6,
|
||||
'currency_id': 'base.EUR',
|
||||
},
|
||||
'res.company': {
|
||||
self.env.company.id: {
|
||||
'bank_account_code_prefix': '1000',
|
||||
'cash_account_code_prefix': '2000',
|
||||
'transfer_account_code_prefix': '3000',
|
||||
'income_account_id': f'{external_id_prefix}test_account_income_template',
|
||||
'expense_account_id': f'{external_id_prefix}test_account_expense_template',
|
||||
},
|
||||
},
|
||||
'account.account': {
|
||||
f'{external_id_prefix}test_account_tax_recoverable_template': {
|
||||
'name': f'{external_id_prefix}tax recoverable',
|
||||
'code': '411000',
|
||||
'account_type': 'asset_receivable',
|
||||
'reconcile': True,
|
||||
'non_trade': True,
|
||||
},
|
||||
f'{external_id_prefix}test_account_tax_receivable_template': {
|
||||
'name': f'{external_id_prefix}tax receivable',
|
||||
'code': '411200',
|
||||
'account_type': 'asset_receivable',
|
||||
'reconcile': True,
|
||||
'non_trade': True,
|
||||
},
|
||||
f'{external_id_prefix}test_account_advance_payment_tax_template': {
|
||||
'name': f'{external_id_prefix}advance tax payment',
|
||||
'code': '411900',
|
||||
'account_type': 'asset_current',
|
||||
},
|
||||
f'{external_id_prefix}test_account_tax_payable_template': {
|
||||
'name': f'{external_id_prefix}tax payable',
|
||||
'code': '451200',
|
||||
'account_type': 'liability_payable',
|
||||
'reconcile': True,
|
||||
'non_trade': True,
|
||||
},
|
||||
f'{external_id_prefix}test_account_cash_basis_transition_account_id': {
|
||||
'name': f'{external_id_prefix}cash basis transition account',
|
||||
'code': '451500',
|
||||
'account_type': 'liability_current',
|
||||
},
|
||||
f'{external_id_prefix}test_account_income_template': {
|
||||
'name': f'{external_id_prefix}income',
|
||||
'code': '600000',
|
||||
'account_type': 'income',
|
||||
},
|
||||
f'{external_id_prefix}test_account_expense_template': {
|
||||
'name': f'{external_id_prefix}expense',
|
||||
'code': '700000',
|
||||
'account_type': 'expense',
|
||||
},
|
||||
},
|
||||
'account.journal': self._get_account_journal(template_code),
|
||||
'account.tax.group': {
|
||||
'tax_group_taxes': {
|
||||
'name': f"{external_id_prefix}Taxes",
|
||||
'sequence': 0,
|
||||
'country_id': country,
|
||||
'tax_payable_account_id': f'{external_id_prefix}test_account_tax_payable_template',
|
||||
'tax_receivable_account_id': f'{external_id_prefix}test_account_tax_receivable_template',
|
||||
'advance_tax_payment_account_id': f'{external_id_prefix}test_account_advance_payment_tax_template',
|
||||
},
|
||||
},
|
||||
'account.tax': {
|
||||
**{
|
||||
xmlid: _tax_vals(name, amount, external_id_prefix)
|
||||
for name, xmlid, amount in (
|
||||
('Tax 1', 'test_tax_1_template', 15),
|
||||
('Tax 2', 'test_tax_2_template', 0),
|
||||
)
|
||||
},
|
||||
'test_composite_tax_template': {
|
||||
'name': 'Tax Grouped',
|
||||
'amount_type': 'group',
|
||||
'type_tax_use': 'purchase',
|
||||
'tax_group_id': 'tax_group_taxes',
|
||||
'children_tax_ids': 'test_tax_1_template,test_tax_2_template',
|
||||
}
|
||||
},
|
||||
}
|
||||
return test_data_getter
|
||||
|
||||
|
||||
def _tax_vals(name, amount, external_id_prefix, cash_basis=False, account_on_repartition=True):
|
||||
return {
|
||||
'name': name,
|
||||
'amount': amount,
|
||||
'type_tax_use': 'purchase',
|
||||
'tax_group_id': 'tax_group_taxes',
|
||||
'cash_basis_transition_account_id': f'{external_id_prefix}test_account_cash_basis_transition_account_id' if cash_basis else False,
|
||||
'tax_exigibility': 'on_payment' if cash_basis else 'on_invoice',
|
||||
'repartition_line_ids': [
|
||||
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'base'}),
|
||||
Command.create({'document_type': 'invoice', 'factor_percent': 100, 'repartition_type': 'tax',
|
||||
'account_id': f'{external_id_prefix}test_account_tax_recoverable_template' if account_on_repartition else False}),
|
||||
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'base'}),
|
||||
Command.create({'document_type': 'refund', 'factor_percent': 100, 'repartition_type': 'tax',
|
||||
'account_id': f'{external_id_prefix}test_account_tax_recoverable_template' if account_on_repartition else False}),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
@patch.object(AccountChartTemplate, '_get_chart_template_mapping', _get_chart_template_mapping)
|
||||
class TestMultiVAT(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def _use_chart_template(cls, company, chart_template_ref=None):
|
||||
test_get_data = data_method_provider("local", "be")
|
||||
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
|
||||
cls.env['account.chart.template'].try_loading('local', company=company, install_demo=False)
|
||||
|
||||
@classmethod
|
||||
@AccountTestInvoicingCommon.setup_country('be')
|
||||
@patch.object(AccountChartTemplate, '_get_chart_template_mapping', _get_chart_template_mapping)
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Setups a company with a custom chart template, containing a tax and a fiscal position.
|
||||
We need to add xml_ids to the templates because they are loaded from their xml_ids
|
||||
"""
|
||||
# Avoid creating data from AccountTestInvoicingCommon setUpClass
|
||||
# just use the override of the functions it provides
|
||||
super(AccountTestInvoicingCommon, cls).setUpClass()
|
||||
|
||||
foreign_country = cls.env.ref("base.fr")
|
||||
cls.foreign_vat_fpos = cls.env["account.fiscal.position"].create({
|
||||
"name": "FR foreign VAT",
|
||||
"auto_apply": True,
|
||||
"country_id": foreign_country.id,
|
||||
"foreign_vat": "FR23334175221",
|
||||
})
|
||||
|
||||
test_get_data = data_method_provider("foreign", "fr")
|
||||
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
|
||||
cls.foreign_vat_fpos.action_create_foreign_taxes()
|
||||
|
||||
def test_tax_and_tax_group_should_be_reachable_using_standard_api(self):
|
||||
# Ensure local and foreign tax is reachable using the custom ref api
|
||||
for xml_id in (
|
||||
# tax group
|
||||
'tax_group_taxes',
|
||||
'foreign_tax_group_taxes',
|
||||
# tax
|
||||
'test_tax_1_template',
|
||||
'test_tax_2_template',
|
||||
'test_composite_tax_template',
|
||||
'foreign_test_tax_1_template',
|
||||
'foreign_test_tax_2_template',
|
||||
'foreign_test_composite_tax_template'
|
||||
):
|
||||
with self.subTest(xml_id=xml_id):
|
||||
record = self.env["account.chart.template"].ref(xml_id, raise_if_not_found=False)
|
||||
self.assertTrue(record, "We should be able to retrieve the record")
|
||||
|
||||
def test_tax_group_data(self):
|
||||
# Ensure the correct country is set on tax group
|
||||
for xml_id, country_code in (('tax_group_taxes', 'BE'), ('foreign_tax_group_taxes', 'FR')):
|
||||
tax_group = self.env["account.chart.template"].ref(xml_id)
|
||||
with self.subTest(xml_id=xml_id, country_code=country_code):
|
||||
self.assertEqual(tax_group.country_id.code, country_code)
|
||||
|
||||
local_tax_group = self.env["account.chart.template"].ref('tax_group_taxes')
|
||||
foreign_tax_group = self.env["account.chart.template"].ref('foreign_tax_group_taxes')
|
||||
for field in ('tax_payable_account_id', 'tax_receivable_account_id', 'advance_tax_payment_account_id'):
|
||||
with self.subTest(field=field):
|
||||
self.assertTrue(foreign_tax_group[field], "This account should have been set")
|
||||
self.assertNotEqual(foreign_tax_group[field], local_tax_group[field],
|
||||
"A copy of the local tax group account should have been created and set")
|
||||
|
||||
def test_tax_data_should_be_consistent(self):
|
||||
# Ensure the correct country is set
|
||||
for xml_id, country_code in (
|
||||
# tax
|
||||
('test_tax_1_template', 'BE'),
|
||||
('test_tax_2_template', 'BE'),
|
||||
('foreign_test_tax_1_template', 'FR'),
|
||||
('foreign_test_tax_2_template', 'FR'),
|
||||
):
|
||||
model = self.env["account.chart.template"].ref(xml_id)
|
||||
with self.subTest(xml_id=xml_id, country_code=country_code):
|
||||
self.assertEqual(model.country_id.code, country_code)
|
||||
|
||||
tax = self.env["account.chart.template"].ref('foreign_test_tax_1_template')
|
||||
self.assertEqual(tax.country_id.code, 'FR')
|
||||
_base_line, tax_line = tax.invoice_repartition_line_ids
|
||||
self.assertEqual(tax_line.account_id.code, '411001',
|
||||
"The foreign tax account should be a new account with a code close to the local tax account code")
|
||||
|
||||
tax = self.env["account.chart.template"].ref('foreign_test_tax_2_template')
|
||||
self.assertEqual(tax.country_id.code, 'FR')
|
||||
_base_line, tax_line = tax.invoice_repartition_line_ids
|
||||
self.assertEqual(tax_line.account_id.code, '411001',
|
||||
"The previously created tax account should be reused for similar tax")
|
||||
|
||||
def test_children_taxes(self):
|
||||
# Ensure that group-type taxes are correctly linked to their children
|
||||
composite_taxes = ['test_composite_tax_template', 'foreign_test_composite_tax_template']
|
||||
children_taxes = {
|
||||
'test_composite_tax_template': ['test_tax_1_template', 'test_tax_2_template'],
|
||||
'foreign_test_composite_tax_template': ['foreign_test_tax_1_template', 'foreign_test_tax_2_template'],
|
||||
}
|
||||
for xml_id in composite_taxes:
|
||||
with self.subTest(xml_id=xml_id):
|
||||
record = self.env["account.chart.template"].ref(xml_id, raise_if_not_found=False)
|
||||
for i, child in enumerate(record.children_tax_ids):
|
||||
child_tax = self.env["account.chart.template"].ref(children_taxes[xml_id][i], raise_if_not_found=False)
|
||||
self.assertEqual(child.id, child_tax.id)
|
||||
|
||||
def test_multivat_cash_basis(self):
|
||||
def wrap_data_getter_for_caba(data_getter):
|
||||
def caba_data_getter(self, template_code):
|
||||
rslt = data_getter(self, template_code)
|
||||
|
||||
rslt['account.tax']['es.caba_0_tax'] = _tax_vals("Dudu 0", 0, 'es', cash_basis=True, account_on_repartition=False)
|
||||
rslt['account.tax']['es.caba_42_tax'] = _tax_vals("Dudu 42", 42, 'es', cash_basis=True)
|
||||
|
||||
return rslt
|
||||
|
||||
return caba_data_getter
|
||||
|
||||
foreign_country = self.env.ref("base.es")
|
||||
foreign_vat_fpos = self.env["account.fiscal.position"].create({
|
||||
"name": "ES foreign VAT",
|
||||
"auto_apply": True,
|
||||
"country_id": foreign_country.id,
|
||||
"foreign_vat": "ESA12345674",
|
||||
})
|
||||
|
||||
test_get_data = wrap_data_getter_for_caba(data_method_provider("foreign", "es"))
|
||||
with patch.object(AccountChartTemplate, '_get_chart_template_data', side_effect=test_get_data, autospec=True):
|
||||
foreign_vat_fpos.action_create_foreign_taxes()
|
||||
|
||||
created_taxes = self.env.ref('local_es.caba_0_tax') + self.env.ref('local_es.caba_42_tax')
|
||||
for tax in created_taxes:
|
||||
self.assertEqual(tax.tax_exigibility, 'on_payment')
|
||||
self.assertEqual(tax.cash_basis_transition_account_id.code, '411005')
|
||||
|
||||
self.assertTrue(self.env.company.tax_exigibility, "Creating foreign cash basis taxes should enable the cash basis setting on the company.")
|
||||
|
|
@ -2,57 +2,24 @@
|
|||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo import fields, Command
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tools.safe_eval import datetime
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountPaymentTerms(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('EUR', rounding=0.001)
|
||||
cls.pay_term_today = cls.env['account.payment.term'].create({
|
||||
'name': 'Today',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_next_month_on_the_15 = cls.env['account.payment.term'].create({
|
||||
'name': 'Next month on the 15th',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 0,
|
||||
'end_month': True,
|
||||
'days_after': 15,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_last_day_of_month = cls.env['account.payment.term'].create({
|
||||
'name': 'Last Day of month',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 0,
|
||||
'end_month': True
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_first_day_next_month = cls.env['account.payment.term'].create({
|
||||
'name': 'First day next month',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 0,
|
||||
'end_month': True,
|
||||
'days_after': 1,
|
||||
'value_amount': 100,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
@ -61,110 +28,133 @@ class TestAccountPaymentTerms(AccountTestInvoicingCommon):
|
|||
'name': 'Net 30 days',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 30,
|
||||
'value_amount': 100,
|
||||
'value': 'percent',
|
||||
'nb_days': 30,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_30_days_end_of_month = cls.env['account.payment.term'].create({
|
||||
'name': '30 days end of month',
|
||||
cls.pay_term_60_days = cls.env['account.payment.term'].create({
|
||||
'name': '60 days two lines',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 30,
|
||||
'end_month': True
|
||||
'value_amount': 30,
|
||||
'value': 'percent',
|
||||
'nb_days': 15,
|
||||
}),
|
||||
(0, 0, {
|
||||
'value_amount': 70,
|
||||
'value': 'percent',
|
||||
'nb_days': 45,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_1_month_end_of_month = cls.env['account.payment.term'].create({
|
||||
'name': '1 month, end of month',
|
||||
cls.pay_term_30_days = cls.env['account.payment.term'].create({
|
||||
'name': '60 days two lines',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'months': 1,
|
||||
'days': 0,
|
||||
'end_month': True
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_30_days_end_of_month_the_10 = cls.env['account.payment.term'].create({
|
||||
'name': '30 days end of month the 10th',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 30,
|
||||
'end_month': True,
|
||||
'days_after': 10,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_90_days_end_of_month_the_10 = cls.env['account.payment.term'].create({
|
||||
'name': '90 days end of month the 10',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'days': 90,
|
||||
'end_month': True,
|
||||
'days_after': 10,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_3_months_end_of_month_the_10 = cls.env['account.payment.term'].create({
|
||||
'name': '3 months end of month the 10',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'months': 3,
|
||||
'end_month': True,
|
||||
'days_after': 10,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_end_month_on_the_30th = cls.env['account.payment.term'].create({
|
||||
'name': 'End of month, the 30th',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'end_month': True,
|
||||
'days_after': 30,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_1_month_15_days_end_month_45_days = cls.env['account.payment.term'].create({
|
||||
'name': '1 month, 15 days, end month, 45 days',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'months': 1,
|
||||
'days': 15,
|
||||
'end_month': True,
|
||||
'days_after': 45,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_next_10_of_the_month = cls.env['account.payment.term'].create({
|
||||
'name': 'Next 10th of the month',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'balance',
|
||||
'months': 0,
|
||||
'days': -10,
|
||||
'end_month': True,
|
||||
'days_after': 10,
|
||||
'value_amount': 100,
|
||||
'value': 'percent',
|
||||
'nb_days': 15,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.invoice = cls.init_invoice('out_refund', products=cls.product_a+cls.product_b)
|
||||
|
||||
cls.pay_term_a = cls.env['account.payment.term'].create({
|
||||
'name': "turlututu",
|
||||
'early_discount': True,
|
||||
'discount_percentage': 10,
|
||||
'discount_days': 1,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'nb_days': 2,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_b = cls.env['account.payment.term'].create({
|
||||
'name': "tralala",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 50,
|
||||
'nb_days': 2,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 50,
|
||||
'nb_days': 4,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.pay_term_days_end_of_month_10 = cls.env['account.payment.term'].create({
|
||||
'name': "basic case",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'nb_days': 30,
|
||||
'delay_type': 'days_end_of_month_on_the',
|
||||
'days_next_month': 10,
|
||||
}),
|
||||
],
|
||||
})
|
||||
cls.pay_term_days_end_of_month_31 = cls.env['account.payment.term'].create({
|
||||
'name': "special case 31",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'nb_days': 30,
|
||||
'delay_type': 'days_end_of_month_on_the',
|
||||
'days_next_month': 31,
|
||||
}),
|
||||
],
|
||||
})
|
||||
cls.pay_term_days_end_of_month_30 = cls.env['account.payment.term'].create({
|
||||
'name': "special case 30",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'delay_type': 'days_end_of_month_on_the',
|
||||
'days_next_month': 30,
|
||||
'nb_days': 0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
cls.pay_term_days_end_of_month_29 = cls.env['account.payment.term'].create({
|
||||
'name': "special case 29",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'delay_type': 'days_end_of_month_on_the',
|
||||
'days_next_month': 29,
|
||||
'nb_days': 0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
cls.pay_term_days_end_of_month_days_next_month_0 = cls.env['account.payment.term'].create({
|
||||
'name': "special case days next month 0",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'delay_type': 'days_end_of_month_on_the',
|
||||
'days_next_month': 0,
|
||||
'nb_days': 30,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def assertPaymentTerm(self, pay_term, invoice_date, dates):
|
||||
with Form(self.invoice) as move_form:
|
||||
move_form.invoice_payment_term_id = pay_term
|
||||
|
|
@ -172,7 +162,7 @@ class TestAccountPaymentTerms(AccountTestInvoicingCommon):
|
|||
self.assertEqual(
|
||||
self.invoice.line_ids.filtered(
|
||||
lambda l: l.account_id == self.company_data['default_account_receivable']
|
||||
).mapped('date_maturity'),
|
||||
).sorted(key=lambda r: r.date_maturity).mapped('date_maturity'),
|
||||
[fields.Date.from_string(date) for date in dates],
|
||||
)
|
||||
|
||||
|
|
@ -180,223 +170,496 @@ class TestAccountPaymentTerms(AccountTestInvoicingCommon):
|
|||
self.assertPaymentTerm(self.pay_term_today, '2019-01-01', ['2019-01-01'])
|
||||
self.assertPaymentTerm(self.pay_term_today, '2019-01-15', ['2019-01-15'])
|
||||
self.assertPaymentTerm(self.pay_term_today, '2019-01-31', ['2019-01-31'])
|
||||
self.assertPaymentTerm(self.pay_term_next_month_on_the_15, '2019-01-01', ['2019-02-15'])
|
||||
self.assertPaymentTerm(self.pay_term_next_month_on_the_15, '2019-01-15', ['2019-02-15'])
|
||||
self.assertPaymentTerm(self.pay_term_next_month_on_the_15, '2019-01-31', ['2019-02-15'])
|
||||
self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-01', ['2019-01-31'])
|
||||
self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-15', ['2019-01-31'])
|
||||
self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-31', ['2019-01-31'])
|
||||
self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-01', ['2019-02-01'])
|
||||
self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-15', ['2019-02-01'])
|
||||
self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-31', ['2019-02-01'])
|
||||
|
||||
self.assertPaymentTerm(self.pay_term_net_30_days, '2022-01-01', ['2022-01-31'])
|
||||
self.assertPaymentTerm(self.pay_term_net_30_days, '2022-01-15', ['2022-02-14'])
|
||||
self.assertPaymentTerm(self.pay_term_net_30_days, '2022-01-31', ['2022-03-02'])
|
||||
self.assertPaymentTerm(self.pay_term_30_days_end_of_month, '2022-01-01', ['2022-01-31'])
|
||||
self.assertPaymentTerm(self.pay_term_30_days_end_of_month, '2022-01-15', ['2022-02-28'])
|
||||
self.assertPaymentTerm(self.pay_term_30_days_end_of_month, '2022-01-31', ['2022-03-31'])
|
||||
self.assertPaymentTerm(self.pay_term_1_month_end_of_month, '2022-01-01', ['2022-02-28'])
|
||||
self.assertPaymentTerm(self.pay_term_1_month_end_of_month, '2022-01-15', ['2022-02-28'])
|
||||
self.assertPaymentTerm(self.pay_term_1_month_end_of_month, '2022-01-31', ['2022-02-28'])
|
||||
self.assertPaymentTerm(self.pay_term_30_days_end_of_month_the_10, '2022-01-01', ['2022-02-10'])
|
||||
self.assertPaymentTerm(self.pay_term_30_days_end_of_month_the_10, '2022-01-15', ['2022-03-10'])
|
||||
self.assertPaymentTerm(self.pay_term_30_days_end_of_month_the_10, '2022-01-31', ['2022-04-10'])
|
||||
self.assertPaymentTerm(self.pay_term_90_days_end_of_month_the_10, '2022-01-01', ['2022-05-10'])
|
||||
self.assertPaymentTerm(self.pay_term_90_days_end_of_month_the_10, '2022-01-15', ['2022-05-10'])
|
||||
self.assertPaymentTerm(self.pay_term_90_days_end_of_month_the_10, '2022-01-31', ['2022-06-10'])
|
||||
self.assertPaymentTerm(self.pay_term_3_months_end_of_month_the_10, '2022-01-01', ['2022-05-10'])
|
||||
self.assertPaymentTerm(self.pay_term_3_months_end_of_month_the_10, '2022-01-15', ['2022-05-10'])
|
||||
self.assertPaymentTerm(self.pay_term_3_months_end_of_month_the_10, '2022-01-31', ['2022-05-10'])
|
||||
self.assertPaymentTerm(self.pay_term_1_month_15_days_end_month_45_days, '2022-01-01', ['2022-04-14'])
|
||||
self.assertPaymentTerm(self.pay_term_1_month_15_days_end_month_45_days, '2022-01-15', ['2022-05-15'])
|
||||
self.assertPaymentTerm(self.pay_term_1_month_15_days_end_month_45_days, '2022-01-31', ['2022-05-15'])
|
||||
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-01', ['2022-01-10'])
|
||||
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-09', ['2022-01-10'])
|
||||
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-10', ['2022-01-10'])
|
||||
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-15', ['2022-02-10'])
|
||||
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-31', ['2022-02-10'])
|
||||
|
||||
def test_payment_term_compute_method(self):
|
||||
def assert_payment_term_values(expected_values_list):
|
||||
res = pay_term._compute_terms(
|
||||
self.assertPaymentTerm(self.pay_term_60_days, '2022-01-01', ['2022-01-16', '2022-02-15'])
|
||||
self.assertPaymentTerm(self.pay_term_60_days, '2022-01-15', ['2022-01-30', '2022-03-01'])
|
||||
self.assertPaymentTerm(self.pay_term_60_days, '2022-01-31', ['2022-02-15', '2022-03-17'])
|
||||
|
||||
def test_wrong_payment_term(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['account.payment.term'].create({
|
||||
'name': 'Wrong Payment Term',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'value': 'percent',
|
||||
'value_amount': 50,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def test_payment_term_compute_method_with_cash_discount(self):
|
||||
self.pay_term_a.early_pay_discount_computation = 'included'
|
||||
computed_term_a = self.pay_term_a._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
|
||||
150, 150, 1, 1000, 1000,
|
||||
150.0, 150.0, 1.0, 1000.0, 1000.0,
|
||||
)
|
||||
self.assertEqual(len(res), len(expected_values_list))
|
||||
for values, (company_amount, discount_balance) in zip(res, expected_values_list):
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'company_amount': values['company_amount'],
|
||||
'discount_balance': values['discount_balance'],
|
||||
},
|
||||
{
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'total_amount': computed_term_a.get("total_amount"),
|
||||
'discount_balance': computed_term_a.get("discount_balance"),
|
||||
'line_ids': computed_term_a.get("line_ids"),
|
||||
},
|
||||
#What should be obtained
|
||||
{
|
||||
'total_amount': 1150.0,
|
||||
'discount_balance': 1035.0,
|
||||
'line_ids': [{
|
||||
'date': datetime.date(2016, 1, 3),
|
||||
'company_amount': 1150.0,
|
||||
'foreign_amount': 1150.0,
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
'company_amount': company_amount,
|
||||
'discount_balance': discount_balance,
|
||||
},
|
||||
def test_payment_term_compute_method_with_cash_discount_and_cash_rounding(self):
|
||||
foreign_currency = self.other_currency
|
||||
rate = self.env['res.currency']._get_conversion_rate(foreign_currency, self.env.company.currency_id, self.env.company, '2017-01-01')
|
||||
self.assertEqual(rate, 0.5)
|
||||
self.pay_term_a.early_pay_discount_computation = 'included'
|
||||
computed_term_a = self.pay_term_a._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), foreign_currency, self.env.company,
|
||||
75, 150, 1, 359.18, 718.35, cash_rounding=self.cash_rounding_a,
|
||||
)
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'total_amount': computed_term_a.get("total_amount"),
|
||||
'discount_balance': computed_term_a.get("discount_balance"),
|
||||
'discount_amount_currency': computed_term_a.get("discount_amount_currency"),
|
||||
'line_ids': computed_term_a.get("line_ids"),
|
||||
},
|
||||
# What should be obtained
|
||||
{
|
||||
'total_amount': 434.18,
|
||||
'discount_balance': 390.78,
|
||||
'discount_amount_currency': 781.55, # w/o cash rounding: 868.35 * 0.9 = 781.515
|
||||
'line_ids': [{
|
||||
'date': datetime.date(2016, 1, 3),
|
||||
'company_amount': 434.18,
|
||||
'foreign_amount': 868.35,
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_payment_term_compute_method_without_cash_discount(self):
|
||||
computed_term_b = self.pay_term_b._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
|
||||
150.0, 150.0, 1.0, 1000.0, 1000.0,
|
||||
)
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'total_amount': computed_term_b.get("total_amount"),
|
||||
'discount_balance': computed_term_b.get("discount_balance"),
|
||||
'line_ids': computed_term_b.get("line_ids"),
|
||||
},
|
||||
# What should be obtained
|
||||
{
|
||||
'total_amount': 1150.0,
|
||||
'discount_balance': 0,
|
||||
'line_ids': [{
|
||||
'date': datetime.date(2016, 1, 3),
|
||||
'company_amount': 575.0,
|
||||
'foreign_amount': 575.0,
|
||||
}, {
|
||||
'date': datetime.date(2016, 1, 5),
|
||||
'company_amount': 575.0,
|
||||
'foreign_amount': 575.0,
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_payment_term_compute_method_without_cash_discount_with_cash_rounding(self):
|
||||
foreign_currency = self.other_currency
|
||||
rate = self.env['res.currency']._get_conversion_rate(foreign_currency, self.env.company.currency_id, self.env.company, '2017-01-01')
|
||||
self.assertEqual(rate, 0.5)
|
||||
self.pay_term_a.early_pay_discount_computation = 'included'
|
||||
computed_term_b = self.pay_term_b._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), foreign_currency, self.env.company,
|
||||
75, 150, 1, 359.18, 718.35, cash_rounding=self.cash_rounding_a,
|
||||
)
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'total_amount': computed_term_b.get("total_amount"),
|
||||
'discount_balance': computed_term_b.get("discount_balance"),
|
||||
'discount_amount_currency': computed_term_b.get("discount_amount_currency"),
|
||||
'line_ids': computed_term_b.get("line_ids"),
|
||||
},
|
||||
# What should be obtained
|
||||
{
|
||||
'total_amount': 434.18,
|
||||
'discount_balance': 0,
|
||||
'discount_amount_currency': None,
|
||||
'line_ids': [{
|
||||
'date': datetime.date(2016, 1, 3),
|
||||
'company_amount': 217.1,
|
||||
'foreign_amount': 434.2,
|
||||
}, {
|
||||
'date': datetime.date(2016, 1, 5),
|
||||
'company_amount': 217.08,
|
||||
'foreign_amount': 434.15000000000003,
|
||||
}],
|
||||
},
|
||||
)
|
||||
# Cash rounding should not affect the totals
|
||||
self.assertAlmostEqual(434.18, sum(line['company_amount'] for line in computed_term_b['line_ids']))
|
||||
self.assertAlmostEqual(868.35, sum(line['foreign_amount'] for line in computed_term_b['line_ids']))
|
||||
|
||||
def test_payment_term_compute_method_early_excluded(self):
|
||||
self.pay_term_a.early_pay_discount_computation = 'excluded'
|
||||
computed_term_a = self.pay_term_a._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
|
||||
150.0, 150.0, 1.0, 1000.0, 1000.0,
|
||||
)
|
||||
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'total_amount': computed_term_a.get("total_amount"),
|
||||
'discount_balance': computed_term_a.get("discount_balance"),
|
||||
'line_ids': computed_term_a.get("line_ids"),
|
||||
},
|
||||
# What should be obtained
|
||||
{
|
||||
'total_amount': 1150.0,
|
||||
'discount_balance': 1050.0,
|
||||
'line_ids': [{
|
||||
'date': datetime.date(2016, 1, 3),
|
||||
'company_amount': 1150.0,
|
||||
'foreign_amount': 1150.0,
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_payment_term_residual_amount_on_last_line_with_fixed_amount_multi_currency(self):
|
||||
pay_term = self.env['account.payment.term'].create({
|
||||
'name': "test_payment_term_residual_amount_on_last_line",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value_amount': 50,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 50,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 0.02,
|
||||
'value': 'fixed',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
computed_term = pay_term._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.other_currency, self.env.company,
|
||||
0.0, 0.0, 1.0, 0.04, 0.09,
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
self.other_currency.round(l['foreign_amount']),
|
||||
self.company_data['currency'].round(l['company_amount']),
|
||||
)
|
||||
for l in computed_term['line_ids']
|
||||
],
|
||||
[(0.045, 0.02), (0.045, 0.02), (0.0, 0.0)],
|
||||
)
|
||||
|
||||
def test_payment_term_residual_amount_on_last_line(self):
|
||||
pay_term = self.env['account.payment.term'].create({
|
||||
'name': "turlututu",
|
||||
'name': "test_payment_term_residual_amount_on_last_line",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value_amount': 50,
|
||||
'value': 'percent',
|
||||
'value_amount': 10,
|
||||
'days': 2,
|
||||
'discount_percentage': 10,
|
||||
'discount_days': 1,
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 50,
|
||||
'value': 'percent',
|
||||
'value_amount': 20,
|
||||
'days': 4,
|
||||
'discount_percentage': 20,
|
||||
'discount_days': 3,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 20,
|
||||
'days': 6,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'balance',
|
||||
'days': 8,
|
||||
'discount_percentage': 20,
|
||||
'discount_days': 7,
|
||||
'nb_days': 0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
self.env.company.early_pay_discount_computation = 'included'
|
||||
assert_payment_term_values([
|
||||
(115.0, 103.5),
|
||||
(230.0, 184.0),
|
||||
(230.0, 0.0),
|
||||
(575.0, 460.0),
|
||||
])
|
||||
computed_term = pay_term._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
|
||||
0.0, 0.0, 1.0, 0.03, 0.03,
|
||||
)
|
||||
self.assertEqual(
|
||||
[self.env.company.currency_id.round(l['foreign_amount']) for l in computed_term['line_ids']],
|
||||
[0.02, 0.01],
|
||||
)
|
||||
|
||||
self.env.company.early_pay_discount_computation = 'excluded'
|
||||
assert_payment_term_values([
|
||||
(115.0, 105.0),
|
||||
(230.0, 190.0),
|
||||
(230.0, 0.0),
|
||||
(575.0, 475.0),
|
||||
])
|
||||
def test_payment_term_last_balance_line_with_fixed(self):
|
||||
pay_term = self.env['account.payment.term'].create({
|
||||
'name': 'test_payment_term_last_balance_line_with_fixed',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value_amount': 70,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 200,
|
||||
'value': 'fixed',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 30,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
def test_payment_term_compute_method_cash_rounding(self):
|
||||
"""Test that the payment terms are computed correctly in case we apply cash rounding.
|
||||
We check the amounts in document and company currency.
|
||||
We check that the cash rounding does not change the totals in document or company curreny.
|
||||
computed_term = pay_term._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
|
||||
0.0, 0.0, 1.0, 1000.0, 1000.0,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[self.env.company.currency_id.round(l['foreign_amount']) for l in computed_term['line_ids']],
|
||||
[700.0, 200.0, 100.0],
|
||||
)
|
||||
|
||||
def test_payment_term_last_balance_line_with_fixed_negative(self):
|
||||
pay_term = self.env['account.payment.term'].create({
|
||||
'name': 'test_payment_term_last_balance_line_with_fixed_negative',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value_amount': 70,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 500,
|
||||
'value': 'fixed',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 30,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
computed_term = pay_term._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
|
||||
0.0, 0.0, 1.0, 1000.0, 1000.0,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[self.env.company.currency_id.round(l['foreign_amount']) for l in computed_term['line_ids']],
|
||||
[700.0, 500.0, -200.0],
|
||||
)
|
||||
|
||||
def test_payment_term_last_balance_line_with_fixed_negative_fixed(self):
|
||||
pay_term = self.env['account.payment.term'].create({
|
||||
'name': 'test_payment_term_last_balance_line_with_fixed_negative_fixed',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value_amount': 70,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 500,
|
||||
'value': 'fixed',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 30,
|
||||
'value': 'percent',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value_amount': 200,
|
||||
'value': 'fixed',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
computed_term = pay_term._compute_terms(
|
||||
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
|
||||
0.0, 0.0, 1.0, 1000.0, 1000.0,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[self.env.company.currency_id.round(l['foreign_amount']) for l in computed_term['line_ids']],
|
||||
[700.0, 500.0, 300.0, -500.0],
|
||||
)
|
||||
|
||||
def test_payment_term_percent_round_calculation(self):
|
||||
"""
|
||||
def assert_payment_term_values(expected_values_list):
|
||||
foreign_currency = self.currency_data['currency']
|
||||
rate = self.env['res.currency']._get_conversion_rate(foreign_currency, self.env.company.currency_id, self.env.company, '2017-01-01')
|
||||
self.assertEqual(rate, 0.5)
|
||||
res = pay_term._compute_terms(
|
||||
fields.Date.from_string('2017-01-01'), foreign_currency, self.env.company,
|
||||
75, 150, 1, 359.18, 718.35, cash_rounding=self.cash_rounding_a
|
||||
)
|
||||
self.assertEqual(len(res), len(expected_values_list))
|
||||
|
||||
keys = ['company_amount', 'discount_balance', 'foreign_amount', 'discount_amount_currency']
|
||||
for index, (values, expected_values) in enumerate(zip(res, expected_values_list)):
|
||||
for key in keys:
|
||||
with self.subTest(index=index, key=key):
|
||||
self.assertAlmostEqual(values[key], expected_values[key])
|
||||
|
||||
total_company_amount = sum(value['company_amount'] for value in res)
|
||||
total_foreign_amount = sum(value['foreign_amount'] for value in res)
|
||||
self.assertAlmostEqual(total_company_amount, 434.18)
|
||||
self.assertAlmostEqual(total_foreign_amount, 868.35)
|
||||
|
||||
pay_term = self.env['account.payment.term'].create({
|
||||
'name': "turlututu",
|
||||
the sum function might not sum the floating numbers properly
|
||||
if there are a lot of lines with floating numbers
|
||||
so this test verifies the round function changes
|
||||
"""
|
||||
self.env['account.payment.term'].create({
|
||||
'name': "test_payment_term_percent_round_calculation",
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 10,
|
||||
'days': 2,
|
||||
'discount_percentage': 10,
|
||||
'discount_days': 1,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 20,
|
||||
'days': 4,
|
||||
'discount_percentage': 20,
|
||||
'discount_days': 3,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 20,
|
||||
'days': 6,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'balance',
|
||||
'days': 8,
|
||||
'discount_percentage': 20,
|
||||
'discount_days': 7,
|
||||
}),
|
||||
Command.create({'value_amount': 50, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 1.66, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 16.8, 'value': 'percent', 'nb_days': 0, }),
|
||||
],
|
||||
})
|
||||
|
||||
with self.subTest(test='included'):
|
||||
self.env.company.early_pay_discount_computation = 'included'
|
||||
assert_payment_term_values([
|
||||
{
|
||||
'company_amount': 43.43,
|
||||
'discount_balance': 39.11,
|
||||
'foreign_amount': 86.85,
|
||||
'discount_amount_currency': 78.20,
|
||||
},
|
||||
{
|
||||
'company_amount': 86.86,
|
||||
'discount_balance': 69.51,
|
||||
'foreign_amount': 173.70,
|
||||
'discount_amount_currency': 139.00,
|
||||
},
|
||||
{
|
||||
'company_amount': 86.86,
|
||||
'discount_balance': 0,
|
||||
'foreign_amount': 173.70,
|
||||
'discount_amount_currency': 0.00,
|
||||
},
|
||||
{
|
||||
'company_amount': 217.03,
|
||||
'discount_balance': 173.63,
|
||||
'foreign_amount': 434.10,
|
||||
'discount_amount_currency': 347.30,
|
||||
},
|
||||
])
|
||||
def test_payment_term_days_end_of_month_on_the(self):
|
||||
"""
|
||||
This test will check that payment terms with a delay_type 'days_end_of_month_on_the' works as expected.
|
||||
It will check if the date of the date maturity is correctly calculated depending on the invoice date and payment
|
||||
term selected.
|
||||
"""
|
||||
with Form(self.invoice) as basic_case:
|
||||
basic_case.invoice_payment_term_id = self.pay_term_days_end_of_month_10
|
||||
basic_case.invoice_date = '2023-12-12'
|
||||
|
||||
with self.subTest(test='excluded'):
|
||||
self.env.company.early_pay_discount_computation = 'excluded'
|
||||
assert_payment_term_values([
|
||||
{
|
||||
'company_amount': 43.43,
|
||||
'discount_balance': 39.86,
|
||||
'foreign_amount': 86.85,
|
||||
'discount_amount_currency': 79.70,
|
||||
},
|
||||
{
|
||||
'company_amount': 86.86,
|
||||
'discount_balance': 72.51,
|
||||
'foreign_amount': 173.70,
|
||||
'discount_amount_currency': 145.00,
|
||||
},
|
||||
{
|
||||
'company_amount': 86.86,
|
||||
'discount_balance': 0,
|
||||
'foreign_amount': 173.70,
|
||||
'discount_amount_currency': 0.00,
|
||||
},
|
||||
{
|
||||
'company_amount': 217.03,
|
||||
'discount_balance': 181.13,
|
||||
'foreign_amount': 434.10,
|
||||
'discount_amount_currency': 362.30,
|
||||
},
|
||||
])
|
||||
expected_date_basic_case = self.invoice.line_ids.filtered(lambda l: l.account_id == self.company_data['default_account_receivable']).mapped('date_maturity'),
|
||||
self.assertEqual(expected_date_basic_case[0], [fields.Date.from_string('2024-02-10')])
|
||||
|
||||
with Form(self.invoice) as special_case:
|
||||
special_case.invoice_payment_term_id = self.pay_term_days_end_of_month_31
|
||||
special_case.invoice_date = '2023-12-12'
|
||||
|
||||
expected_date_special_case = self.invoice.line_ids.filtered(lambda l: l.account_id == self.company_data['default_account_receivable']).mapped('date_maturity'),
|
||||
self.assertEqual(expected_date_special_case[0], [fields.Date.from_string('2024-02-29')])
|
||||
|
||||
def test_payment_term_labels(self):
|
||||
# create a payment term with 40% now, 30% in 30 days and 30% in 60 days
|
||||
multiple_installment_term = self.env['account.payment.term'].create({
|
||||
'name': "test_payment_term_labels",
|
||||
'line_ids': [
|
||||
Command.create({'value_amount': 40, 'value': 'percent', 'nb_days': 0, }),
|
||||
Command.create({'value_amount': 30, 'value': 'percent', 'nb_days': 30, }),
|
||||
Command.create({'value_amount': 30, 'value': 'percent', 'nb_days': 60, }),
|
||||
],
|
||||
})
|
||||
# create immediate payment term
|
||||
immediate_term = self.env['account.payment.term'].create({
|
||||
'name': 'Immediate',
|
||||
'line_ids': [
|
||||
Command.create({'value_amount': 100, 'value': 'percent', 'nb_days': 0, }),
|
||||
],
|
||||
})
|
||||
# create an invoice with immediate payment term
|
||||
invoice = self.init_invoice('out_invoice', products=self.product_a)
|
||||
invoice.invoice_payment_term_id = immediate_term
|
||||
# check the payment term labels
|
||||
invoice_terms = invoice.line_ids.filtered(lambda l: l.display_type == 'payment_term')
|
||||
self.assertEqual(invoice_terms[0].name, False)
|
||||
# change the payment term to the multiple installment term
|
||||
invoice.invoice_payment_term_id = multiple_installment_term
|
||||
invoice_terms = invoice.line_ids.filtered(lambda l: l.display_type == 'payment_term').sorted('date_maturity')
|
||||
self.assertEqual(invoice_terms[0].name, 'installment #1')
|
||||
self.assertEqual(invoice_terms[0].debit, invoice.amount_total * 0.4)
|
||||
self.assertEqual(invoice_terms[1].name, 'installment #2')
|
||||
self.assertEqual(invoice_terms[1].debit, invoice.amount_total * 0.3)
|
||||
self.assertEqual(invoice_terms[2].name, 'installment #3')
|
||||
self.assertEqual(invoice_terms[2].debit, invoice.amount_total * 0.3)
|
||||
|
||||
def test_payment_term_days_end_of_month_nb_days_0(self):
|
||||
"""
|
||||
This test will check that payment terms with a delay_type 'days_end_of_month_on_the'
|
||||
in combination with nb_days works as expected
|
||||
Invoice date = 2024-05-23
|
||||
# case 1
|
||||
'nb_days' = 0
|
||||
`days_next_month` = 29
|
||||
-> 2024-05-23 + 0 days = 2024-05-23
|
||||
=> `date_maturity` -> 2024-06-29
|
||||
# case 2
|
||||
'nb_days' = 0
|
||||
`days_next_month` = 31
|
||||
-> 2024-05-23 + 0 days = 2024-05-23
|
||||
=> `date_maturity` -> 2024-06-30
|
||||
"""
|
||||
self.pay_term_days_end_of_month_29.line_ids.nb_days = 0
|
||||
self.pay_term_days_end_of_month_31.line_ids.nb_days = 0
|
||||
with Form(self.invoice) as case_1:
|
||||
case_1.invoice_payment_term_id = self.pay_term_days_end_of_month_29
|
||||
case_1.invoice_date = '2024-05-23'
|
||||
|
||||
expected_date_case_1 = self.invoice.line_ids.filtered(
|
||||
lambda l: l.account_id == self.company_data['default_account_receivable']).mapped('date_maturity')
|
||||
self.assertEqual(expected_date_case_1, [fields.Date.from_string('2024-06-29')])
|
||||
|
||||
with Form(self.invoice) as case_2:
|
||||
case_2.invoice_payment_term_id = self.pay_term_days_end_of_month_31
|
||||
case_2.invoice_date = '2024-05-23'
|
||||
|
||||
expected_date_case_2 = self.invoice.line_ids.filtered(
|
||||
lambda l: l.account_id == self.company_data['default_account_receivable']).mapped('date_maturity')
|
||||
self.assertEqual(expected_date_case_2, [fields.Date.from_string('2024-06-30')])
|
||||
|
||||
def test_payment_term_days_end_of_month_nb_days_15(self):
|
||||
"""
|
||||
This test will check that payment terms with a delay_type 'days_end_of_month_on_the'
|
||||
in combination with nb_days works as expected
|
||||
Invoice date = 2024-05-23
|
||||
# case 1
|
||||
'nb_days' = 15
|
||||
`days_next_month` = 30
|
||||
-> 2024-05-23 + 15 days = 2024-06-07
|
||||
=> `date_maturity` -> 2024-07-30
|
||||
# case 2
|
||||
'nb_days' = 15
|
||||
`days_next_month` = 31
|
||||
-> 2024-05-23 + 15 days = 2024-06-07
|
||||
=> `date_maturity` -> 2024-07-31
|
||||
"""
|
||||
self.pay_term_days_end_of_month_30.line_ids.nb_days = 15
|
||||
self.pay_term_days_end_of_month_31.line_ids.nb_days = 15
|
||||
|
||||
with Form(self.invoice) as case_1:
|
||||
case_1.invoice_payment_term_id = self.pay_term_days_end_of_month_30
|
||||
case_1.invoice_date = '2024-05-24'
|
||||
|
||||
expected_date_case_1 = self.invoice.line_ids.filtered(
|
||||
lambda l: l.account_id == self.company_data['default_account_receivable']).mapped('date_maturity')
|
||||
self.assertEqual(expected_date_case_1, [fields.Date.from_string('2024-07-30')])
|
||||
|
||||
with Form(self.invoice) as case_2:
|
||||
case_2.invoice_payment_term_id = self.pay_term_days_end_of_month_31
|
||||
case_2.invoice_date = '2024-05-23'
|
||||
|
||||
expected_date_case_2 = self.invoice.line_ids.filtered(
|
||||
lambda l: l.account_id == self.company_data['default_account_receivable']).mapped('date_maturity')
|
||||
self.assertEqual(expected_date_case_2, [fields.Date.from_string('2024-07-31')])
|
||||
|
||||
def test_payment_term_days_end_of_month_days_next_month_0(self):
|
||||
with Form(self.invoice) as case_1:
|
||||
case_1.invoice_payment_term_id = self.pay_term_days_end_of_month_days_next_month_0
|
||||
case_1.invoice_date = '2024-04-22'
|
||||
|
||||
expected_date_case_1 = self.invoice.line_ids.filtered(
|
||||
lambda l: l.account_id == self.company_data['default_account_receivable']).mapped('date_maturity')
|
||||
self.assertEqual(expected_date_case_1, [fields.Date.from_string('2024-05-31')])
|
||||
|
||||
def test_payment_term_multi_company(self):
|
||||
"""
|
||||
|
|
@ -404,7 +667,8 @@ class TestAccountPaymentTerms(AccountTestInvoicingCommon):
|
|||
OdooBot has `res.company(1)` set as the default company. The test checks that the payment term correctly reflects
|
||||
the company associated with the move, independent of the user's default company.
|
||||
"""
|
||||
user_company, other_company = self.company_data_2.get('company'), self.company_data.get('company')
|
||||
user_company = self.env['res.company'].create({'name': 'user_company'})
|
||||
other_company = self.company_data.get('company')
|
||||
self.env.user.write({
|
||||
'company_ids': [user_company.id, other_company.id],
|
||||
'company_id': user_company.id,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
import json
|
||||
|
||||
from odoo import http
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools import file_open, mute_logger
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestPortalAttachment(AccountTestInvoicingHttpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.out_invoice = cls.env['account.move'].with_context(tracking_disable=True).create({
|
||||
'move_type': 'out_invoice',
|
||||
|
|
@ -38,33 +38,36 @@ class TestPortalAttachment(AccountTestInvoicingHttpCommon):
|
|||
self.authenticate(None, None)
|
||||
|
||||
# Test public user can't create attachment without token of document
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/add',
|
||||
data={
|
||||
'name': "new attachment",
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
},
|
||||
files=[('file', ('test.txt', b'test', 'plain/text'))],
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertIn("you do not have the rights", res.text)
|
||||
with file_open("addons/web/__init__.py") as file:
|
||||
res = self.url_open(
|
||||
url=f"{self.invoice_base_url}/mail/attachment/upload",
|
||||
data={
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
"thread_id": self.out_invoice.id,
|
||||
"thread_model": self.out_invoice._name,
|
||||
},
|
||||
files={"ufile": file},
|
||||
)
|
||||
self.assertEqual(res.status_code, 404)
|
||||
self.assertIn("The requested URL was not found on the server.", res.text)
|
||||
|
||||
# Test public user can create attachment with token
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/add',
|
||||
data={
|
||||
'name': "new attachment",
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'access_token': self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
files=[('file', ('test.txt', b'test', 'plain/text'))],
|
||||
)
|
||||
with file_open("addons/web/__init__.py") as file:
|
||||
res = self.url_open(
|
||||
url=f"{self.invoice_base_url}/mail/attachment/upload",
|
||||
data={
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
"thread_id": self.out_invoice.id,
|
||||
"thread_model": self.out_invoice._name,
|
||||
"token": self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
files={"ufile": file},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
create_res = json.loads(res.content.decode('utf-8'))
|
||||
data = json.loads(res.content.decode('utf-8'))['data']
|
||||
create_res = next(
|
||||
filter(lambda a: a["id"] == data["attachment_id"], data["store_data"]["ir.attachment"])
|
||||
)
|
||||
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
|
||||
|
||||
# Test created attachment is private
|
||||
|
|
@ -72,36 +75,48 @@ class TestPortalAttachment(AccountTestInvoicingHttpCommon):
|
|||
self.assertEqual(res_binary.status_code, 404)
|
||||
|
||||
# Test created access_token is working
|
||||
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
|
||||
res_binary = self.url_open(
|
||||
"/web/content/%d?access_token=%s"
|
||||
% (create_res["id"], create_res["raw_access_token"])
|
||||
)
|
||||
self.assertEqual(res_binary.status_code, 200)
|
||||
|
||||
# Test mimetype is neutered as non-admin
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/add',
|
||||
url=f"{self.invoice_base_url}/mail/attachment/upload",
|
||||
data={
|
||||
'name': "new attachment",
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'access_token': self.out_invoice._portal_ensure_token(),
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
"is_pending": True,
|
||||
"thread_id": self.out_invoice.id,
|
||||
"thread_model": self.out_invoice._name,
|
||||
"token": self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
files=[('file', ('test.svg', b'<svg></svg>', 'image/svg+xml'))],
|
||||
files={"ufile": ("test.svg", b'<svg></svg>', "image/svg+xml")},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
create_res = json.loads(res.content.decode('utf-8'))
|
||||
data = json.loads(res.content.decode('utf-8'))["data"]
|
||||
create_res = next(
|
||||
filter(lambda a: a["id"] == data["attachment_id"], data["store_data"]["ir.attachment"])
|
||||
)
|
||||
self.assertEqual(create_res['mimetype'], 'text/plain')
|
||||
|
||||
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
|
||||
res_binary = self.url_open(
|
||||
"/web/content/%d?access_token=%s"
|
||||
% (create_res["id"], create_res["raw_access_token"])
|
||||
)
|
||||
self.assertEqual(res_binary.headers['Content-Type'], 'text/plain; charset=utf-8')
|
||||
self.assertEqual(res_binary.content, b'<svg></svg>')
|
||||
|
||||
res_image = self.url_open('/web/image/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
|
||||
res_image = self.url_open(
|
||||
"/web/image/%d?access_token=%s"
|
||||
% (create_res["id"], create_res["raw_access_token"])
|
||||
)
|
||||
self.assertEqual(res_image.headers['Content-Type'], 'application/octet-stream')
|
||||
self.assertEqual(res_image.content, b'<svg></svg>')
|
||||
|
||||
# Test attachment can't be removed without valid token
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/remove',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/attachment/delete',
|
||||
json={
|
||||
'params': {
|
||||
'attachment_id': create_res['id'],
|
||||
|
|
@ -111,177 +126,198 @@ class TestPortalAttachment(AccountTestInvoicingHttpCommon):
|
|||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
|
||||
self.assertIn("you do not have the rights", res.text)
|
||||
self.assertIn("The requested URL was not found on the server.", res.text)
|
||||
|
||||
# Test attachment can be removed with token if "pending" state
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/remove',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/attachment/delete',
|
||||
json={
|
||||
'params': {
|
||||
'attachment_id': create_res['id'],
|
||||
'access_token': create_res['access_token'],
|
||||
"access_token": create_res["ownership_token"],
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
remove_res = json.loads(res.content.decode('utf-8'))['result']
|
||||
self.assertFalse(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
|
||||
self.assertTrue(remove_res is True)
|
||||
|
||||
# Test attachment can't be removed if not "pending" state
|
||||
# Test attachment can be removed with token if not "pending" state
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': 'an attachment',
|
||||
'access_token': self.env['ir.attachment']._generate_access_token(),
|
||||
})
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/remove',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/attachment/delete',
|
||||
json={
|
||||
'params': {
|
||||
'attachment_id': attachment.id,
|
||||
'access_token': attachment.access_token,
|
||||
"access_token": attachment._get_ownership_token(),
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', attachment.id)]))
|
||||
self.assertIn("not in a pending state", res.text)
|
||||
self.assertFalse(self.env['ir.attachment'].sudo().search([('id', '=', attachment.id)]))
|
||||
|
||||
# Test attachment can't be removed if attached to a message
|
||||
attachment.write({
|
||||
'res_model': 'mail.compose.message',
|
||||
'res_id': 0,
|
||||
# Test attachment can be removed if attached to a message
|
||||
attachment = self.env["ir.attachment"].create({
|
||||
"name": "an attachment",
|
||||
"res_model": "mail.compose.message",
|
||||
"res_id": 0,
|
||||
})
|
||||
attachment.flush_recordset()
|
||||
message = self.env['mail.message'].create({
|
||||
'attachment_ids': [(6, 0, attachment.ids)],
|
||||
})
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/remove',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/attachment/delete',
|
||||
json={
|
||||
'params': {
|
||||
'attachment_id': attachment.id,
|
||||
'access_token': attachment.access_token,
|
||||
"access_token": attachment._get_ownership_token(),
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(attachment.exists())
|
||||
self.assertIn("it is linked to a message", res.text)
|
||||
self.assertFalse(attachment.exists())
|
||||
message.sudo().unlink()
|
||||
|
||||
# Test attachment can't be associated if no attachment token.
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/mail/chatter_post',
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': 'an attachment',
|
||||
'res_model': 'mail.compose.message',
|
||||
'res_id': 0,
|
||||
})
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/message/post',
|
||||
json={
|
||||
'params': {
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'message': "test message 1",
|
||||
'attachment_ids': [attachment.id],
|
||||
'attachment_tokens': ['false'],
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'thread_model': self.out_invoice._name,
|
||||
'thread_id': self.out_invoice.id,
|
||||
'post_data': {
|
||||
'body': "test message 1",
|
||||
'attachment_ids': [attachment.id],
|
||||
"attachment_tokens": ["false"],
|
||||
},
|
||||
"token": self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn("The attachment %s does not exist or you do not have the rights to access it." % attachment.id, res.text)
|
||||
self.assertIn(
|
||||
"One or more attachments do not exist, or you do not have the rights to access them.",
|
||||
res.text,
|
||||
)
|
||||
|
||||
# Test attachment can't be associated if no main document token
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/mail/chatter_post',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/message/post',
|
||||
json={
|
||||
'params': {
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'message': "test message 1",
|
||||
'attachment_ids': [attachment.id],
|
||||
'attachment_tokens': [attachment.access_token],
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'thread_model': self.out_invoice._name,
|
||||
'thread_id': self.out_invoice.id,
|
||||
"post_data": {
|
||||
"body": "test message 1",
|
||||
'attachment_ids': [attachment.id],
|
||||
"attachment_tokens": [attachment._get_ownership_token()],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn("You are not allowed to access 'Journal Entry' (account.move) records.", res.text)
|
||||
self.assertIn("The requested URL was not found on the server.", res.text)
|
||||
|
||||
# Test attachment can't be associated if not "pending" state
|
||||
self.assertFalse(self.out_invoice.message_ids)
|
||||
# not messages which are sent by `_post_add_create` in the previous steps
|
||||
self.assertFalse(
|
||||
self.out_invoice.message_ids.filtered(lambda m: m.author_id == self.partner_a))
|
||||
attachment.write({'res_model': 'model'})
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/mail/chatter_post',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/message/post',
|
||||
json={
|
||||
'params': {
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'message': "test message 1",
|
||||
'attachment_ids': [attachment.id],
|
||||
'attachment_tokens': [attachment.access_token],
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'thread_model': self.out_invoice._name,
|
||||
'thread_id': self.out_invoice.id,
|
||||
"post_data": {
|
||||
"body": "test message 1",
|
||||
"attachment_ids": [attachment.id],
|
||||
"attachment_tokens": [attachment._get_ownership_token()],
|
||||
},
|
||||
'token': self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.out_invoice.invalidate_recordset(['message_ids'])
|
||||
self.assertEqual(len(self.out_invoice.message_ids), 1)
|
||||
self.assertEqual(self.out_invoice.message_ids.body, "<p>test message 1</p>")
|
||||
self.assertFalse(self.out_invoice.message_ids.attachment_ids)
|
||||
# not messages which are sent by `_post_add_create` in the previous steps
|
||||
message = self.out_invoice.message_ids.filtered(lambda m: m.author_id == self.partner_a)
|
||||
self.assertEqual(len(message), 1)
|
||||
self.assertEqual(message.body, "<p>test message 1</p>")
|
||||
self.assertFalse(message.attachment_ids)
|
||||
|
||||
# Test attachment can't be associated if not correct user
|
||||
attachment.write({'res_model': 'mail.compose.message'})
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/mail/chatter_post',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/message/post',
|
||||
json={
|
||||
'params': {
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'message': "test message 2",
|
||||
'attachment_ids': [attachment.id],
|
||||
'attachment_tokens': [attachment.access_token],
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'thread_model': self.out_invoice._name,
|
||||
'thread_id': self.out_invoice.id,
|
||||
"post_data": {
|
||||
"body": "test message 2",
|
||||
"attachment_ids": [attachment.id],
|
||||
"attachment_tokens": [attachment._get_ownership_token()],
|
||||
},
|
||||
'token': self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.out_invoice.invalidate_recordset(['message_ids'])
|
||||
self.assertEqual(len(self.out_invoice.message_ids), 2)
|
||||
self.assertEqual(self.out_invoice.message_ids[0].author_id, self.partner_a)
|
||||
self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 2</p>")
|
||||
self.assertEqual(self.out_invoice.message_ids[0].email_from, self.partner_a.email_formatted)
|
||||
self.assertFalse(self.out_invoice.message_ids.attachment_ids)
|
||||
# not messages which are sent by `_post_add_create` in the previous steps
|
||||
messages = self.out_invoice.message_ids.filtered(lambda m: m.author_id == self.partner_a)
|
||||
self.assertEqual(len(messages), 2)
|
||||
self.assertEqual(messages[0].author_id, self.partner_a)
|
||||
self.assertEqual(messages[0].body, "<p>test message 2</p>")
|
||||
self.assertEqual(messages[0].email_from, self.partner_a.email_formatted)
|
||||
self.assertFalse(messages.attachment_ids)
|
||||
|
||||
# Test attachment can be associated if all good (complete flow)
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/portal/attachment/add',
|
||||
url=f"{self.invoice_base_url}/mail/attachment/upload",
|
||||
data={
|
||||
'name': "final attachment",
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'access_token': self.out_invoice._portal_ensure_token(),
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
"is_pending": True,
|
||||
"thread_id": self.out_invoice.id,
|
||||
"thread_model": self.out_invoice._name,
|
||||
"token": self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
files=[('file', ('test.txt', b'test', 'plain/text'))],
|
||||
files={"ufile": ("final attachment", b'test', "plain/text")},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
create_res = json.loads(res.content.decode('utf-8'))
|
||||
data = json.loads(res.content.decode('utf-8'))['data']
|
||||
create_res = next(
|
||||
filter(lambda a: a["id"] == data["attachment_id"], data["store_data"]["ir.attachment"])
|
||||
)
|
||||
self.assertEqual(create_res['name'], "final attachment")
|
||||
|
||||
res = self.opener.post(
|
||||
url=f'{self.invoice_base_url}/mail/chatter_post',
|
||||
res = self.url_open(
|
||||
url=f'{self.invoice_base_url}/mail/message/post',
|
||||
json={
|
||||
'params': {
|
||||
'res_model': self.out_invoice._name,
|
||||
'res_id': self.out_invoice.id,
|
||||
'message': "test message 3",
|
||||
'attachment_ids': [create_res['id']],
|
||||
'attachment_tokens': [create_res['access_token']],
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
'thread_model': self.out_invoice._name,
|
||||
'thread_id': self.out_invoice.id,
|
||||
"post_data": {
|
||||
"body": "test message 3",
|
||||
"attachment_ids": [create_res['id']],
|
||||
"attachment_tokens": [create_res["ownership_token"]],
|
||||
},
|
||||
'token': self.out_invoice._portal_ensure_token(),
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.out_invoice.invalidate_recordset(['message_ids'])
|
||||
self.assertEqual(len(self.out_invoice.message_ids), 3)
|
||||
self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 3</p>")
|
||||
self.assertEqual(len(self.out_invoice.message_ids[0].attachment_ids), 1)
|
||||
# not messages which are sent by `_post_add_create` in previous steps
|
||||
messages = self.out_invoice.message_ids.filtered(lambda m: m.author_id == self.partner_a)
|
||||
self.assertEqual(len(messages), 3)
|
||||
self.assertEqual(messages[0].body, "<p>test message 3</p>")
|
||||
self.assertEqual(len(messages[0].attachment_ids), 1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
from odoo.fields import Command
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestPortalInvoice(AccountTestInvoicingHttpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_portal = cls._create_new_portal_user()
|
||||
cls.portal_partner = cls.user_portal.partner_id
|
||||
|
||||
def test_portal_my_invoice_detail_not_his_invoice(self):
|
||||
not_his_invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({'price_unit': 100})]
|
||||
})
|
||||
not_his_invoice.action_post()
|
||||
url = f'/my/invoices/{not_his_invoice.id}?report_type=pdf&download=True'
|
||||
self.authenticate(self.user_portal.login, self.user_portal.login)
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_portal_my_invoice_detail_download_pdf(self):
|
||||
invoice_with_pdf = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.portal_partner.id,
|
||||
'invoice_line_ids': [Command.create({'price_unit': 100})]
|
||||
})
|
||||
invoice_with_pdf.action_post()
|
||||
invoice_with_pdf._generate_and_send()
|
||||
self.assertTrue(invoice_with_pdf.invoice_pdf_report_id)
|
||||
|
||||
url = f'/my/invoices/{invoice_with_pdf.id}?report_type=pdf&download=True'
|
||||
self.authenticate(self.user_portal.login, self.user_portal.login)
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.content, invoice_with_pdf.invoice_pdf_report_id.raw)
|
||||
|
||||
def test_portal_my_invoice_detail_download_proforma(self):
|
||||
invoice_no_pdf = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.portal_partner.id,
|
||||
'invoice_line_ids': [Command.create({'price_unit': 100})]
|
||||
})
|
||||
invoice_no_pdf.action_post()
|
||||
self.assertFalse(invoice_no_pdf.invoice_pdf_report_id)
|
||||
|
||||
url = f'/my/invoices/{invoice_no_pdf.id}?report_type=pdf&download=True'
|
||||
self.authenticate(self.user_portal.login, self.user_portal.login)
|
||||
res = self.url_open(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn("Proforma", res.content.decode('utf-8'))
|
||||
|
|
@ -1,37 +1,131 @@
|
|||
from .common import AccountTestInvoicingCommon
|
||||
from odoo.tests.common import Form, tagged, new_test_user
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo import Command
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.tests.common import new_test_user
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class AccountProductCase(AccountTestInvoicingCommon):
|
||||
@tagged('post_install', 'post_install_l10n', '-at_install')
|
||||
class TestProduct(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.internal_user = new_test_user(
|
||||
cls.env, login="internal_user", groups="base.group_user"
|
||||
cls.env,
|
||||
login="internal_user",
|
||||
groups="base.group_user",
|
||||
)
|
||||
cls.account_manager_user = new_test_user(
|
||||
cls.env,
|
||||
login="account_manager_user",
|
||||
groups="account.group_account_manager",
|
||||
)
|
||||
|
||||
def test_internal_user_can_read_product_with_tax_and_tags(self):
|
||||
"""Internal users need read access to products, no matter their taxes."""
|
||||
# Add a tag to product_a's default tax
|
||||
self.company_data["company"].country_id = self.env.ref("base.us")
|
||||
tax_line_tag = self.env["account.account.tag"].create(
|
||||
{
|
||||
"name": "Tax tag",
|
||||
"applicability": "taxes",
|
||||
"country_id": self.company_data["company"].country_id.id,
|
||||
}
|
||||
)
|
||||
repartition_lines = (
|
||||
self.product_a.taxes_id.invoice_repartition_line_ids
|
||||
| self.product_a.taxes_id.refund_repartition_line_ids
|
||||
).filtered_domain([("repartition_type", "=", "tax")])
|
||||
repartition_lines.write({"tag_ids": [Command.link(tax_line_tag.id)]})
|
||||
tax_line_tag = self.env["account.account.tag"].create({
|
||||
"name": "Tax tag",
|
||||
"applicability": "taxes",
|
||||
})
|
||||
self.product_a.taxes_id.repartition_line_ids.tag_ids = tax_line_tag
|
||||
# Check that internal user can read product_a
|
||||
with Form(
|
||||
self.product_a.with_user(self.internal_user).with_context(lang="en_US")
|
||||
) as form_a:
|
||||
self.env.invalidate_all()
|
||||
with Form(self.product_a.with_user(self.internal_user)) as form_a:
|
||||
# The tax string itself is not very important here; we just check
|
||||
# it has a value and we can read it, so there were no access errors
|
||||
self.assertTrue(form_a.tax_string)
|
||||
|
||||
def test_multi_company_product_tax(self):
|
||||
""" Ensure default taxes are set for all companies on products with no company set. """
|
||||
product_without_company = self.env['product.template'].with_context(allowed_company_ids=self.env.company.ids).create({
|
||||
'name': 'Product Without a Company',
|
||||
})
|
||||
product_with_company = self.env['product.template'].with_context(allowed_company_ids=self.env.company.ids).create({
|
||||
'name': 'Product With a Company',
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
companies = self.env['res.company'].sudo().search([])
|
||||
# Product should have all the default taxes of the other companies.
|
||||
self.assertRecordValues(product_without_company.sudo(), [{
|
||||
'taxes_id': companies.account_sale_tax_id.ids,
|
||||
'supplier_taxes_id': companies.account_purchase_tax_id.ids,
|
||||
}]) # Take care that inactive default taxes won't be shown on the product
|
||||
# Product should have only the default tax of the company it belongs to.
|
||||
self.assertRecordValues(product_with_company.sudo(), [{
|
||||
'taxes_id': self.company_data['company'].account_sale_tax_id.ids,
|
||||
'supplier_taxes_id': self.company_data['company'].account_purchase_tax_id.ids,
|
||||
}])
|
||||
|
||||
def test_product_tax_with_company_and_branch(self):
|
||||
"""Ensure that setting a tax on a product overrides the default tax of branch companies.
|
||||
as branches share taxes with their parent company."""
|
||||
parent_company = self.env.company
|
||||
# Create a branch company and set a default sales tax.
|
||||
self.env['res.company'].create({
|
||||
'name': 'Branch Company',
|
||||
'parent_id': parent_company.id,
|
||||
'account_sale_tax_id': parent_company.account_sale_tax_id.id,
|
||||
})
|
||||
|
||||
tax_new = self.env['account.tax'].create({
|
||||
'name': "tax_new",
|
||||
'amount_type': 'percent',
|
||||
'amount': 21.0,
|
||||
'type_tax_use': 'sale',
|
||||
})
|
||||
|
||||
# Create a product in the parent company and set its sales tax to the new tax
|
||||
product = self.env['product.template'].with_context(allowed_company_ids=[parent_company.id]).create({
|
||||
'name': 'Product with new Tax',
|
||||
'taxes_id': tax_new.ids,
|
||||
})
|
||||
|
||||
self.assertEqual(product.taxes_id, tax_new, "The branch company default tax shouldn't be set if we set a different tax on the product from the parent company.")
|
||||
|
||||
def test_product_category_parent_account_fallback(self):
|
||||
"""When no account is set on a product category, accounts should be inherited from parent categories.
|
||||
Also covers the case where income and expense are defined at different hierarchy levels.
|
||||
"""
|
||||
grandparent_income = self.copy_account(self.company_data['default_account_revenue'])
|
||||
child_expense = self.copy_account(self.company_data['default_account_expense'])
|
||||
|
||||
grandparent_categ = self.env['product.category'].create({
|
||||
'name': 'Grandparent Category',
|
||||
'property_account_income_categ_id': grandparent_income.id,
|
||||
})
|
||||
parent_categ = self.env['product.category'].create({
|
||||
'name': 'Parent Category',
|
||||
'parent_id': grandparent_categ.id,
|
||||
'property_account_income_categ_id': False,
|
||||
'property_account_expense_categ_id': False,
|
||||
})
|
||||
child_categ = self.env['product.category'].create({
|
||||
'name': 'Child Category',
|
||||
'parent_id': parent_categ.id,
|
||||
'property_account_income_categ_id': False,
|
||||
'property_account_expense_categ_id': child_expense.id,
|
||||
})
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Test Product',
|
||||
'categ_id': child_categ.id,
|
||||
})
|
||||
|
||||
invoice, bill = self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({'product_id': product.id})],
|
||||
},
|
||||
{
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [Command.create({'product_id': product.id})],
|
||||
},
|
||||
])
|
||||
self.assertEqual(invoice.invoice_line_ids.account_id, grandparent_income,
|
||||
"Customer invoice line should use grandparent category's income account")
|
||||
self.assertEqual(bill.invoice_line_ids.account_id, child_expense,
|
||||
"Vendor bill line should use child category's expense account")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,72 @@
|
|||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestMergePartner(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.Partner = cls.env['res.partner']
|
||||
cls.Bank = cls.env['res.partner.bank']
|
||||
cls.Payment = cls.env['account.payment']
|
||||
|
||||
# Create partners
|
||||
cls.partner1 = cls.Partner.create({'name': 'Partner 1', 'email': 'partner1@example.com'})
|
||||
cls.partner2 = cls.Partner.create({'name': 'Partner 2', 'email': 'partner2@example.com'})
|
||||
cls.partner3 = cls.Partner.create({'name': 'Partner 3', 'email': 'partner3@example.com'})
|
||||
|
||||
# Create bank accounts
|
||||
cls.bank1 = cls.Bank.create({'acc_number': '12345', 'partner_id': cls.partner1.id})
|
||||
cls.bank2 = cls.Bank.create({'acc_number': '67890', 'partner_id': cls.partner2.id})
|
||||
cls.bank3 = cls.Bank.create({'acc_number': '12345', 'partner_id': cls.partner3.id})
|
||||
|
||||
# Create payments linked to bank accounts
|
||||
cls.payment1 = cls.Payment.create({
|
||||
'partner_id': cls.partner1.id,
|
||||
'partner_bank_id': cls.bank1.id,
|
||||
'amount': 100,
|
||||
'payment_type': 'outbound',
|
||||
'payment_method_id': cls.env.ref('account.account_payment_method_manual_out').id,
|
||||
'journal_id': cls.company_data['default_journal_bank'].id,
|
||||
})
|
||||
cls.payment2 = cls.Payment.create({
|
||||
'partner_id': cls.partner2.id,
|
||||
'partner_bank_id': cls.bank2.id,
|
||||
'amount': 200,
|
||||
'payment_type': 'outbound',
|
||||
'payment_method_id': cls.env.ref('account.account_payment_method_manual_out').id,
|
||||
'journal_id': cls.company_data['default_journal_bank'].id,
|
||||
})
|
||||
cls.payment3 = cls.Payment.create({
|
||||
'partner_id': cls.partner3.id,
|
||||
'partner_bank_id': cls.bank3.id,
|
||||
'amount': 200,
|
||||
'payment_type': 'outbound',
|
||||
'payment_method_id': cls.env.ref('account.account_payment_method_manual_out').id,
|
||||
'journal_id': cls.company_data['default_journal_bank'].id,
|
||||
})
|
||||
|
||||
def test_merge_partners_with_bank_accounts_linked_to_payments(self):
|
||||
wizard = self.env['base.partner.merge.automatic.wizard'].create({})
|
||||
wizard._merge([self.partner1.id, self.partner2.id], self.partner1)
|
||||
|
||||
self.assertFalse(self.partner2.exists(), "Source partner should be deleted after merge")
|
||||
self.assertTrue(self.partner1.exists(), "Destination partner should exist after merge")
|
||||
self.assertEqual(self.payment1.partner_id, self.partner1, "Payment should be linked to the destination partner")
|
||||
self.assertEqual(self.payment2.partner_id, self.partner1, "Payment should be linked to the destination partner")
|
||||
self.assertEqual(self.payment1.partner_bank_id.partner_id, self.partner1, "Payment's bank account should belong to the destination partner")
|
||||
self.assertEqual(self.payment2.partner_bank_id.partner_id, self.partner1, "Payment's bank account should belong to the destination partner")
|
||||
|
||||
def test_merge_partners_with_duplicate_bank_accounts_linked_to_payments(self):
|
||||
wizard = self.env['base.partner.merge.automatic.wizard'].create({})
|
||||
wizard._merge([self.partner1.id, self.partner3.id], self.partner1)
|
||||
|
||||
self.assertFalse(self.partner3.exists(), "Source partner should be deleted after merge")
|
||||
self.assertTrue(self.partner1.exists(), "Destination partner should exist after merge")
|
||||
self.assertEqual(self.payment1.partner_id, self.partner1, "Payment should be linked to the destination partner")
|
||||
self.assertEqual(self.payment3.partner_id, self.partner1, "Payment should be linked to the destination partner")
|
||||
self.assertEqual(self.payment1.partner_bank_id.partner_id, self.partner1, "Payment's bank account should belong to the destination partner")
|
||||
self.assertEqual(self.payment3.partner_bank_id.partner_id, self.partner1, "Payment's bank account should belong to the destination partner")
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form, TransactionCase
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import Form, tagged, TransactionCase
|
||||
from odoo import fields, api, SUPERUSER_ID, Command
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
|
@ -11,13 +10,13 @@ from freezegun import freeze_time
|
|||
from functools import reduce
|
||||
import json
|
||||
import psycopg2
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
|
||||
class TestSequenceMixinCommon(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company_data['company'].write({'fiscalyear_last_day': "31", 'fiscalyear_last_month': "3"})
|
||||
cls.test_move = cls.create_move()
|
||||
|
||||
|
|
@ -42,6 +41,13 @@ class TestSequenceMixinCommon(AccountTestInvoicingCommon):
|
|||
move.action_post()
|
||||
return move
|
||||
|
||||
def assertMoveName(cls, move, expected_name):
|
||||
if move.name_placeholder:
|
||||
cls.assertFalse(move.name, f"This move is potentially the first of the sequence, it shouldn't have a name while it is not posted. Got '{move.name}'.")
|
||||
cls.assertEqual(move.name_placeholder, expected_name, f"This move is potentially the first of the sequence, it doesn't have a name but a placeholder name which is currently '{move.name_placeholder}'. You expected '{expected_name}'.")
|
||||
else:
|
||||
cls.assertEqual(move.name, expected_name, f"Expected '{expected_name}' but got '{move.name}'.")
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSequenceMixin(TestSequenceMixinCommon):
|
||||
|
|
@ -58,23 +64,23 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
"""Change the sequence when we change the date iff it has never been posted."""
|
||||
# Check setup
|
||||
self.assertEqual(self.test_move.state, 'draft')
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
self.assertEqual(self.test_move.name_placeholder, 'MISC/15-16/01/0001')
|
||||
self.assertEqual(fields.Date.to_string(self.test_move.date), '2016-01-01')
|
||||
|
||||
# Never posetd, the number must change if we change the date
|
||||
self.test_move.date = '2020-02-02'
|
||||
self.assertEqual(self.test_move.name, 'MISC/2020/02/0001')
|
||||
self.assertMoveName(self.test_move, 'MISC/19-20/02/0001')
|
||||
|
||||
# We don't recompute user's input when posting
|
||||
self.test_move.name = 'MyMISC/2020/0000001'
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
|
||||
self.assertMoveName(self.test_move, 'MyMISC/2020/0000001')
|
||||
|
||||
# Has been posted, and it doesn't change anymore
|
||||
self.test_move.button_draft()
|
||||
self.test_move.date = '2020-01-02'
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
|
||||
self.assertMoveName(self.test_move, 'MyMISC/2020/0000001')
|
||||
|
||||
def test_sequence_change_date_with_quick_edit_mode(self):
|
||||
"""
|
||||
|
|
@ -96,16 +102,17 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
}),
|
||||
]
|
||||
})
|
||||
self.assertMoveName(bill, 'BILL/15-16/01/0001')
|
||||
bill = bill.copy({'date': '2016-02-01'})
|
||||
|
||||
self.assertEqual(bill.name, 'BILL/2016/02/0001')
|
||||
self.assertMoveName(bill, 'BILL/15-16/02/0001')
|
||||
with Form(bill) as bill_form:
|
||||
bill_form.date = '2016-02-02'
|
||||
self.assertEqual(bill_form.name, 'BILL/2016/02/0001')
|
||||
self.assertMoveName(bill_form, 'BILL/15-16/02/0001')
|
||||
bill_form.date = '2016-03-01'
|
||||
self.assertEqual(bill_form.name, 'BILL/2016/03/0001')
|
||||
self.assertMoveName(bill_form, 'BILL/15-16/03/0001')
|
||||
bill_form.date = '2017-01-01'
|
||||
self.assertEqual(bill_form.name, 'BILL/2017/01/0001')
|
||||
self.assertMoveName(bill_form, 'BILL/16-17/01/0001')
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'partner_id': 1,
|
||||
|
|
@ -119,14 +126,14 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
]
|
||||
})
|
||||
|
||||
self.assertEqual(invoice.name, 'INV/2016/00001')
|
||||
self.assertMoveName(invoice, 'INV/15-16/0001')
|
||||
with Form(invoice) as invoice_form:
|
||||
invoice_form.date = '2016-01-02'
|
||||
self.assertEqual(invoice_form.name, 'INV/2016/00001')
|
||||
self.assertMoveName(invoice_form, 'INV/15-16/0001')
|
||||
invoice_form.date = '2016-02-02'
|
||||
self.assertEqual(invoice_form.name, 'INV/2016/00001')
|
||||
self.assertMoveName(invoice_form, 'INV/15-16/0001')
|
||||
invoice_form.date = '2017-01-01'
|
||||
self.assertEqual(invoice_form.name, 'INV/2017/00001')
|
||||
self.assertMoveName(invoice_form, 'INV/16-17/0001')
|
||||
|
||||
def test_sequence_empty_editable_with_quick_edit_mode(self):
|
||||
""" Ensure the names of all but the first moves in a period are empty and editable in quick edit mode """
|
||||
|
|
@ -136,6 +143,7 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
'partner_id': 1,
|
||||
'move_type': 'in_invoice',
|
||||
'date': '2016-01-01',
|
||||
'invoice_date': '2016-01-01',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'name': 'line',
|
||||
|
|
@ -144,15 +152,14 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
]
|
||||
})
|
||||
# First move in a period gets a name
|
||||
self.assertEqual(bill_1.name, 'BILL/2016/01/0001')
|
||||
self.assertMoveName(bill_1, 'BILL/15-16/01/0001')
|
||||
|
||||
bill_2 = bill_1.copy({'date': '2016-01-02'})
|
||||
with Form(bill_2) as bill_2_form:
|
||||
# Subsequent moves in the same period get an empty editable name in draft mode
|
||||
self.assertFalse(bill_2_form.name)
|
||||
bill_2_form.name = 'BILL/2016/01/0002'
|
||||
self.assertEqual(bill_2_form.name, 'BILL/2016/01/0002')
|
||||
|
||||
bill_2.name = 'BILL/15-16/01/0002'
|
||||
self.assertMoveName(bill_2_form, 'BILL/15-16/01/0001')
|
||||
|
||||
bill_3 = bill_1.copy({'date': '2016-01-03'})
|
||||
bill_4 = bill_1.copy({'date': '2016-01-04'})
|
||||
|
|
@ -160,16 +167,16 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
|
||||
# Same works with updating multiple moves
|
||||
with Form(bill_3) as bill_3_form:
|
||||
self.assertEqual(bill_3_form.name, 'BILL/2016/02/0001')
|
||||
self.assertMoveName(bill_3_form, 'BILL/15-16/02/0001')
|
||||
|
||||
with Form(bill_4) as bill_4_form:
|
||||
self.assertFalse(bill_4_form.name)
|
||||
bill_4_form.name = 'BILL/2016/02/0002'
|
||||
self.assertEqual(bill_4_form.name, 'BILL/2016/02/0002')
|
||||
bill_4.name = 'BILL/15-16/02/0002'
|
||||
self.assertMoveName(bill_4_form, 'BILL/15-16/02/0001')
|
||||
|
||||
def test_sequence_draft_change_date(self):
|
||||
# When a draft entry is added to an empty period, it should get a name.
|
||||
# When a draft entry with a name is moved to a period already having entries, its name should be reset to '/'.
|
||||
# When a draft entry with a name is moved to a period already having entries, its name should be reset to False.
|
||||
|
||||
new_move = self.test_move.copy({'date': '2016-02-01'})
|
||||
new_multiple_move_1 = self.test_move.copy({'date': '2016-03-01'})
|
||||
|
|
@ -177,36 +184,37 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
new_moves = new_multiple_move_1 + new_multiple_move_2
|
||||
|
||||
# Empty period, so a name should be set
|
||||
self.assertEqual(new_move.name, 'MISC/2016/02/0001')
|
||||
self.assertEqual(new_multiple_move_1.name, 'MISC/2016/03/0001')
|
||||
self.assertEqual(new_multiple_move_2.name, 'MISC/2016/04/0001')
|
||||
self.assertMoveName(new_move, 'MISC/15-16/02/0001')
|
||||
self.assertMoveName(new_multiple_move_1, 'MISC/15-16/03/0001')
|
||||
self.assertMoveName(new_multiple_move_2, 'MISC/16-17/04/0001')
|
||||
|
||||
# Move to an existing period with another move in it
|
||||
# Move to an existing period with a posted move in it
|
||||
self.test_move.action_post()
|
||||
new_move.date = fields.Date.to_date('2016-01-10')
|
||||
new_moves.date = fields.Date.to_date('2016-01-15')
|
||||
|
||||
# Not an empty period, so names should be reset to '/' (draft)
|
||||
self.assertEqual(new_move.name, '/')
|
||||
self.assertEqual(new_multiple_move_1.name, '/')
|
||||
self.assertEqual(new_multiple_move_2.name, '/')
|
||||
# Not an empty period, so names should be reset to False (draft)
|
||||
self.assertMoveName(new_move, False)
|
||||
self.assertMoveName(new_multiple_move_1, False)
|
||||
self.assertMoveName(new_multiple_move_2, False)
|
||||
|
||||
# Move back to a period with no moves in it
|
||||
new_move.date = fields.Date.to_date('2016-02-01')
|
||||
new_moves.date = fields.Date.to_date('2016-03-01')
|
||||
|
||||
# All moves in the previously empty periods should be given a name instead of `/`
|
||||
self.assertEqual(new_move.name, 'MISC/2016/02/0001')
|
||||
self.assertEqual(new_multiple_move_1.name, 'MISC/2016/03/0001')
|
||||
# Since this is the second one in the same period, it should remain `/`
|
||||
self.assertEqual(new_multiple_move_2.name, '/')
|
||||
self.assertMoveName(new_move, 'MISC/15-16/02/0001')
|
||||
self.assertMoveName(new_multiple_move_1, 'MISC/15-16/03/0001')
|
||||
# Since this is the second one in the same period, both have the same pending name
|
||||
self.assertMoveName(new_multiple_move_2, 'MISC/15-16/03/0001')
|
||||
|
||||
# Move both moves back to different periods, both with already moves in it.
|
||||
# Move both moves back to different periods, both with already moves in it. One has a posted move in the sequence, the other not.
|
||||
new_multiple_move_1.date = fields.Date.to_date('2016-01-10')
|
||||
new_multiple_move_2.date = fields.Date.to_date('2016-02-10')
|
||||
|
||||
# Moves are not in empty periods, so names should be set to '/' (draft)
|
||||
self.assertEqual(new_multiple_move_1.name, '/')
|
||||
self.assertEqual(new_multiple_move_2.name, '/')
|
||||
# Moves are not in empty periods, but only the first hsa a posted move. So the first draft should be False and the second should get a name.
|
||||
self.assertMoveName(new_multiple_move_1, False)
|
||||
self.assertMoveName(new_multiple_move_2, 'MISC/15-16/02/0001')
|
||||
|
||||
# Change the journal of the last two moves (empty)
|
||||
journal = self.env['account.journal'].create({
|
||||
|
|
@ -217,15 +225,24 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
new_moves.journal_id = journal
|
||||
|
||||
# Both moves should be assigned a name, since no moves are in the journal and they are in different periods.
|
||||
self.assertEqual(new_multiple_move_1.name, 'AJ/2016/01/0001')
|
||||
self.assertEqual(new_multiple_move_2.name, 'AJ/2016/02/0001')
|
||||
self.assertMoveName(new_multiple_move_1, 'AJ/15-16/01/0001')
|
||||
self.assertMoveName(new_multiple_move_2, 'AJ/15-16/02/0001')
|
||||
|
||||
# When the date is removed in the form view, the name should not recompute
|
||||
with Form(new_multiple_move_1) as move_form:
|
||||
move_form.date = False
|
||||
self.assertEqual(new_multiple_move_1.name, 'AJ/2016/01/0001')
|
||||
move_form.date = fields.Date.to_date('2016-01-11')
|
||||
self.assertMoveName(new_multiple_move_1, 'AJ/15-16/01/0001')
|
||||
move_form.date = fields.Date.to_date('2016-01-10')
|
||||
|
||||
def test_sequence_draft_change_date_with_new_sequence(self):
|
||||
invoice_1 = self.test_move.copy({'date': '2016-02-01', 'journal_id': self.company_data['default_journal_sale'].id})
|
||||
invoice_2 = invoice_1.copy({'date': '2016-02-02'})
|
||||
|
||||
self.assertMoveName(invoice_2, 'INV/15-16/0001')
|
||||
invoice_1.name = 'INV/15-16/02/001'
|
||||
invoice_2.date = '2016-03-01'
|
||||
self.assertMoveName(invoice_2, 'INV/15-16/03/001')
|
||||
|
||||
def test_sequence_draft_first_of_period(self):
|
||||
"""
|
||||
| Step | Move | Action | Date | Name |
|
||||
|
|
@ -236,74 +253,69 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
| 4 | `A` | Cancel | 2023-02-01 | `2023/02/0001` | -> Assert
|
||||
"""
|
||||
move_a = self.test_move.copy({'date': '2023-02-01'})
|
||||
self.assertEqual(move_a.name, 'MISC/2023/02/0001')
|
||||
self.assertMoveName(move_a, 'MISC/22-23/02/0001')
|
||||
|
||||
move_b = self.test_move.copy({'date': '2023-02-02'})
|
||||
self.assertEqual(move_b.name, '/')
|
||||
self.assertMoveName(move_b, 'MISC/22-23/02/0001')
|
||||
|
||||
move_b.action_post()
|
||||
self.assertEqual(move_b.name, 'MISC/2023/02/0002')
|
||||
self.assertMoveName(move_b, 'MISC/22-23/02/0001')
|
||||
|
||||
# The first sequence slot is now taken by move_b, move_a's name and placeholder should be False.
|
||||
move_a.button_cancel()
|
||||
self.assertEqual(move_a.name, 'MISC/2023/02/0001')
|
||||
self.assertMoveName(move_a, False)
|
||||
|
||||
def test_journal_sequence(self):
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
self.assertMoveName(self.test_move, 'MISC/15-16/01/0001')
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
self.assertMoveName(self.test_move, 'MISC/15-16/01/0001')
|
||||
|
||||
copy1 = self.create_move(date=self.test_move.date)
|
||||
self.assertEqual(copy1.name, '/')
|
||||
self.assertMoveName(copy1, False)
|
||||
copy1.action_post()
|
||||
self.assertEqual(copy1.name, 'MISC/2016/01/0002')
|
||||
self.assertMoveName(copy1, 'MISC/15-16/01/0002')
|
||||
|
||||
copy2 = self.create_move(date=self.test_move.date)
|
||||
new_journal = self.test_move.journal_id.copy()
|
||||
new_journal.code = "MISC2"
|
||||
copy2.journal_id = new_journal
|
||||
self.assertEqual(copy2.name, 'MISC2/2016/01/0001')
|
||||
with Form(copy2) as move_form: # It is editable in the form
|
||||
with mute_logger('odoo.tests.common.onchange'):
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
self.assertIn(
|
||||
'The sequence will restart at 1 at the start of every year',
|
||||
move_form._perform_onchange(['name'])['warning']['message'],
|
||||
)
|
||||
move_form.journal_id = self.test_move.journal_id
|
||||
self.assertEqual(move_form.name, '/')
|
||||
move_form.journal_id = new_journal
|
||||
self.assertEqual(move_form.name, 'MISC2/2016/01/0001')
|
||||
with mute_logger('odoo.tests.common.onchange'):
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
self.assertIn(
|
||||
'The sequence will restart at 1 at the start of every year',
|
||||
move_form._perform_onchange(['name'])['warning']['message'],
|
||||
)
|
||||
self.assertMoveName(copy2, 'MISC2/15-16/01/0001')
|
||||
copy2.action_post()
|
||||
self.assertEqual(copy2.name, 'MyMISC/2016/0001')
|
||||
copy2.button_draft()
|
||||
with Form(copy2) as move_form: # It is editable in the form
|
||||
with self.assertLogs('odoo.tests.form') as cm:
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
self.assertTrue(cm.output[0].startswith('WARNING:odoo.tests.form.onchange:'))
|
||||
self.assertIn('The sequence will restart at 1 at the start of every year', cm.output[0])
|
||||
|
||||
copy2.name = False # Can't modify journal_id if name is set
|
||||
copy2.journal_id = self.test_move.journal_id
|
||||
self.assertMoveName(copy2, False)
|
||||
copy2.journal_id = new_journal
|
||||
self.assertMoveName(copy2, 'MISC2/15-16/01/0001')
|
||||
|
||||
copy2.name = 'MyMISC/2016/0001'
|
||||
copy2.action_post()
|
||||
self.assertMoveName(copy2, 'MyMISC/2016/0001')
|
||||
|
||||
copy3 = self.create_move(date=copy2.date, journal=new_journal)
|
||||
self.assertEqual(copy3.name, '/')
|
||||
with self.assertRaises(AssertionError):
|
||||
with Form(copy2) as move_form: # It is not editable in the form
|
||||
move_form.name = 'MyMISC/2016/0002'
|
||||
copy3.action_post()
|
||||
self.assertEqual(copy3.name, 'MyMISC/2016/0002')
|
||||
self.assertMoveName(copy3, False)
|
||||
copy3.name = 'MISC2/2016/00002'
|
||||
copy3.action_post()
|
||||
|
||||
copy4 = self.create_move(date=copy2.date, journal=new_journal)
|
||||
copy4.action_post()
|
||||
self.assertEqual(copy4.name, 'MISC2/2016/00003')
|
||||
self.assertMoveName(copy4, 'MISC2/2016/00003')
|
||||
|
||||
copy5 = self.create_move(date=copy2.date, journal=new_journal)
|
||||
copy5.date = '2021-02-02'
|
||||
copy5.action_post()
|
||||
self.assertEqual(copy5.name, 'MISC2/2021/00001')
|
||||
self.assertMoveName(copy5, 'MISC2/2021/00001')
|
||||
copy5.name = 'N\'importe quoi?'
|
||||
|
||||
copy6 = self.create_move(date=copy5.date, journal=new_journal)
|
||||
copy6.action_post()
|
||||
self.assertEqual(copy6.name, 'N\'importe quoi?1')
|
||||
self.assertMoveName(copy6, 'N\'importe quoi?1')
|
||||
|
||||
def test_journal_sequence_format(self):
|
||||
"""Test different format of sequences and what it becomes on another period"""
|
||||
|
|
@ -344,9 +356,9 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
self.test_move.name = f"{prefix}1"
|
||||
for c in range(2, 25):
|
||||
copy = self.create_move(date=self.test_move.date)
|
||||
copy.name = "/"
|
||||
copy.name = False
|
||||
copy.action_post()
|
||||
self.assertEqual(copy.name, f"{prefix}{c}")
|
||||
self.assertMoveName(copy, f"{prefix}{c}")
|
||||
|
||||
def test_journal_sequence_multiple_type(self):
|
||||
"""Domain is computed accordingly to different types."""
|
||||
|
|
@ -364,12 +376,12 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
all_moves = (entry + entry2 + invoice + invoice2 + refund + refund2)
|
||||
all_moves.name = False
|
||||
all_moves.action_post()
|
||||
self.assertEqual(entry.name, 'MISC/2016/01/0002')
|
||||
self.assertEqual(entry2.name, 'MISC/2016/01/0003')
|
||||
self.assertEqual(invoice.name, 'INV/2016/00001')
|
||||
self.assertEqual(invoice2.name, 'INV/2016/00002')
|
||||
self.assertEqual(refund.name, 'RINV/2016/00001')
|
||||
self.assertEqual(refund2.name, 'RINV/2016/00002')
|
||||
self.assertEqual(entry.name, 'MISC/15-16/01/0001')
|
||||
self.assertEqual(entry2.name, 'MISC/15-16/01/0002')
|
||||
self.assertEqual(invoice.name, 'INV/15-16/0001')
|
||||
self.assertEqual(invoice2.name, 'INV/15-16/0002')
|
||||
self.assertEqual(refund.name, 'RINV/15-16/0001')
|
||||
self.assertEqual(refund2.name, 'RINV/15-16/0002')
|
||||
|
||||
def test_journal_sequence_groupby_compute(self):
|
||||
"""The grouping optimization is correctly done."""
|
||||
|
|
@ -422,20 +434,20 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
self.create_move(date='2020-01-01', name='00000876-G 0002/2020')
|
||||
next_move = self.create_move(date='2020-01-01')
|
||||
next_move.action_post()
|
||||
self.assertEqual(next_move.name, '00000876-G 0002/2021') # Wait, I didn't want this!
|
||||
self.assertMoveName(next_move, '00000876-G 0002/2021') # Wait, I didn't want this!
|
||||
|
||||
next_move.button_draft()
|
||||
next_move.name = False
|
||||
next_move.journal_id.sequence_override_regex = r'^(?P<seq>\d*)(?P<suffix1>.*?)(?P<year>(\d{4})?)(?P<suffix2>)$'
|
||||
next_move.action_post()
|
||||
self.assertEqual(next_move.name, '00000877-G 0002/2020') # Pfew, better!
|
||||
self.assertMoveName(next_move, '00000877-G 0002/2020') # Pfew, better!
|
||||
next_move = self.create_move(date='2020-01-01')
|
||||
next_move.action_post()
|
||||
self.assertEqual(next_move.name, '00000878-G 0002/2020')
|
||||
self.assertMoveName(next_move, '00000878-G 0002/2020')
|
||||
|
||||
next_move = self.create_move(date='2017-05-02')
|
||||
next_move.action_post()
|
||||
self.assertEqual(next_move.name, '00000001-G 0002/2017')
|
||||
self.assertMoveName(next_move, '00000001-G 0002/2017')
|
||||
|
||||
def test_journal_override_sequence_regex_year(self):
|
||||
"""Override the sequence regex with a year syntax not matching the draft invoice name"""
|
||||
|
|
@ -467,8 +479,8 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
|
||||
# check if year is correctly extracted
|
||||
with self.assertRaises(ValidationError):
|
||||
self.create_move(date='2022-01-01', name='MISC/2021/22/00001') # year does not match
|
||||
self.create_move(date='2022-01-01', name='MISC/2022/22/00001') # fix the year in the name
|
||||
self.create_move(date='2022-01-01', name='MISC/2021/22/00001', post=True) # year does not match
|
||||
self.create_move(date='2022-01-01', name='MISC/2022/22/00001', post=True) # fix the year in the name
|
||||
|
||||
def test_journal_sequence_ordering(self):
|
||||
"""Entries are correctly sorted when posting multiple at once."""
|
||||
|
|
@ -485,20 +497,20 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
copies[4].date = '2019-03-05'
|
||||
copies[5].date = '2019-03-05'
|
||||
# that entry is actualy the first one of the period, so it already has a name
|
||||
# set it to '/' so that it is recomputed at post to be ordered correctly.
|
||||
copies[0].name = '/'
|
||||
# set it to False so that it is recomputed at post to be ordered correctly.
|
||||
copies[0].name = False
|
||||
copies.action_post()
|
||||
|
||||
# Ordered by date
|
||||
self.assertEqual(copies[0].name, 'XMISC/2019/00002')
|
||||
self.assertEqual(copies[1].name, 'XMISC/2019/00005')
|
||||
self.assertEqual(copies[2].name, 'XMISC/2019/00006')
|
||||
self.assertEqual(copies[3].name, 'XMISC/2019/00001')
|
||||
self.assertEqual(copies[4].name, 'XMISC/2019/00003')
|
||||
self.assertEqual(copies[5].name, 'XMISC/2019/00004')
|
||||
self.assertMoveName(copies[0], 'XMISC/2019/00002')
|
||||
self.assertMoveName(copies[1], 'XMISC/2019/00005')
|
||||
self.assertMoveName(copies[2], 'XMISC/2019/00006')
|
||||
self.assertMoveName(copies[3], 'XMISC/2019/00001')
|
||||
self.assertMoveName(copies[4], 'XMISC/2019/00003')
|
||||
self.assertMoveName(copies[5], 'XMISC/2019/00004')
|
||||
|
||||
# Can't have twice the same name
|
||||
with self.assertRaises(psycopg2.DatabaseError), mute_logger('odoo.sql_db'), self.env.cr.savepoint():
|
||||
with self.assertRaises(psycopg2.DatabaseError), mute_logger('odoo.sql_db'):
|
||||
copies[0].name = 'XMISC/2019/00001'
|
||||
|
||||
# Lets remove the order by date
|
||||
|
|
@ -537,7 +549,7 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
wizard.save().resequence()
|
||||
|
||||
self.assertEqual(copies[3].state, 'posted')
|
||||
self.assertEqual(copies[5].name, 'XMISC/2019/10005')
|
||||
self.assertMoveName(copies[5], 'XMISC/2019/10005')
|
||||
self.assertEqual(copies[5].state, 'draft')
|
||||
|
||||
def test_journal_resequence_in_between_2_years_pattern(self):
|
||||
|
|
@ -580,6 +592,40 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
{'name': 'XMISC/23-24/00002', 'state': 'posted'},
|
||||
))
|
||||
|
||||
def test_sequence_staggered_year(self):
|
||||
"""The sequence is correctly computed when the year is staggered."""
|
||||
self.env.company.quick_edit_mode = "out_and_in_invoices"
|
||||
self.env.company.fiscalyear_last_day = 15
|
||||
self.env.company.fiscalyear_last_month = '4'
|
||||
|
||||
# First bill in second half of first month of the fiscal year, which is
|
||||
# the start of the fiscal year
|
||||
bill = self.env['account.move'].create({
|
||||
'partner_id': 1,
|
||||
'move_type': 'in_invoice',
|
||||
'date': '2024-04-17',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'name': 'line',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
self.assertMoveName(bill, 'BILL/24-25/04/0001')
|
||||
# First bill in first half of first month of the fiscal year, which is
|
||||
# the end of the fiscal year
|
||||
bill_copy = bill.copy({'date': '2024-04-10', 'invoice_date': '2024-04-10'})
|
||||
bill_copy.action_post()
|
||||
self.assertMoveName(bill_copy, 'BILL/23-24/04/0001')
|
||||
# Second bill in first half of first month
|
||||
bill_copy_2 = bill.copy({'date': '2024-04-11', 'invoice_date': '2024-04-11'})
|
||||
bill_copy_2.action_post()
|
||||
self.assertMoveName(bill_copy_2, 'BILL/23-24/04/0002')
|
||||
# Second bill in second half of first month
|
||||
bill_copy_3 = bill.copy({'date': '2024-04-18', 'invoice_date': '2024-04-18'})
|
||||
bill_copy_3.action_post()
|
||||
self.assertMoveName(bill_copy_3, 'BILL/24-25/04/0001')
|
||||
|
||||
def test_sequence_get_more_specific(self):
|
||||
"""There is the ability to change the format (i.e. from yearly to montlhy)."""
|
||||
# Start with a continuous sequence
|
||||
|
|
@ -645,14 +691,15 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
'code': 'AJ',
|
||||
})
|
||||
move = self.env['account.move'].create({})
|
||||
self.assertEqual(move.name, 'MISC/2021/10/0001')
|
||||
self.assertMoveName(move, 'MISC/21-22/10/0001')
|
||||
with Form(move) as move_form:
|
||||
move_form.journal_id = journal
|
||||
self.assertEqual(move.name, 'AJ/2021/10/0001')
|
||||
self.assertMoveName(move, 'AJ/21-22/10/0001')
|
||||
|
||||
def test_sequence_move_name_related_field_well_computed(self):
|
||||
AccountMove = type(self.env['account.move'])
|
||||
_compute_name = AccountMove._compute_name
|
||||
|
||||
def _flushing_compute_name(self):
|
||||
self.env['account.move.line'].flush_model(fnames=['move_name'])
|
||||
_compute_name(self)
|
||||
|
|
@ -670,6 +717,7 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
|
||||
for move in payments.move_id:
|
||||
self.assertRecordValues(move.line_ids, [{'move_name': move.name}] * len(move.line_ids))
|
||||
self.assertRecordValues(move.line_ids, [{'name': "Manual Payment"}] * len(move.line_ids))
|
||||
|
||||
def test_resequence_payment_and_non_payment_without_payment_sequence(self):
|
||||
"""Resequence wizard could be open for different move type if the payment sequence is set to False on the journal."""
|
||||
|
|
@ -699,13 +747,101 @@ class TestSequenceMixin(TestSequenceMixinCommon):
|
|||
# we need to create another move to higer the sequence
|
||||
self.create_move(date='2025-10-17', post=True)
|
||||
move2.journal_id = move2.journal_id
|
||||
self.assertEqual(move2.name, 'MISC/2025/10/0002')
|
||||
self.assertEqual(move2.name, 'MISC/25-26/10/0002')
|
||||
|
||||
def test_limit_savepoint(self):
|
||||
with patch.object(self.env.cr, 'savepoint', Mock(wraps=self.env.cr.savepoint)) as mock:
|
||||
self.create_move(date='2020-01-01', post=True)
|
||||
mock.assert_called_once()
|
||||
with patch.object(self.env.cr, 'savepoint', Mock(wraps=self.env.cr.savepoint)) as mock:
|
||||
self.create_move(date='2020-01-01', post=True)
|
||||
mock.assert_not_called()
|
||||
with patch.object(self.env.cr, 'savepoint', Mock(wraps=self.env.cr.savepoint)) as mock:
|
||||
self.create_move(date='2021-01-01', post=True)
|
||||
mock.assert_called_once()
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSequenceGaps(TestSequenceMixinCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
move_1 = cls.create_move()
|
||||
move_2 = cls.create_move()
|
||||
move_3 = cls.create_move()
|
||||
all_moves = move_1 + move_2 + move_3
|
||||
all_moves.action_post()
|
||||
cls.all_moves = all_moves
|
||||
|
||||
def test_basic(self):
|
||||
self.assertEqual(self.all_moves.mapped('made_sequence_gap'), [False, False, False])
|
||||
|
||||
def test_first(self):
|
||||
new_move = self.create_move(name="NEW/000001")
|
||||
new_move.action_post()
|
||||
self.assertEqual(new_move.made_sequence_gap, False)
|
||||
|
||||
def test_unlink(self):
|
||||
self.all_moves[0].button_draft()
|
||||
self.all_moves[0].unlink()
|
||||
self.assertEqual(self.all_moves.exists().mapped('made_sequence_gap'), [True, False])
|
||||
self.all_moves[1].button_draft()
|
||||
self.all_moves[1].unlink()
|
||||
self.assertEqual(self.all_moves.exists().mapped('made_sequence_gap'), [True])
|
||||
|
||||
def test_unlink_2(self):
|
||||
self.all_moves[1].button_draft()
|
||||
self.all_moves[1].unlink()
|
||||
self.assertEqual(self.all_moves.exists().mapped('made_sequence_gap'), [False, True])
|
||||
self.all_moves[0].button_draft()
|
||||
self.all_moves[0].unlink()
|
||||
self.assertEqual(self.all_moves.exists().mapped('made_sequence_gap'), [True])
|
||||
|
||||
def test_change_sequence(self):
|
||||
previous = self.all_moves[1].name
|
||||
self.all_moves[1].name = '/'
|
||||
self.assertEqual(self.all_moves.mapped('made_sequence_gap'), [False, False, True])
|
||||
self.all_moves[1].name = previous
|
||||
self.assertEqual(self.all_moves.mapped('made_sequence_gap'), [False, False, False])
|
||||
|
||||
def test_change_multi(self):
|
||||
self.all_moves[0].name = '/'
|
||||
self.all_moves[1].name = '/'
|
||||
self.assertEqual(self.all_moves.mapped('made_sequence_gap'), [False, False, True])
|
||||
|
||||
def test_change_multi_2(self):
|
||||
self.all_moves[1].name = '/'
|
||||
self.all_moves[0].name = '/'
|
||||
self.assertEqual(self.all_moves.mapped('made_sequence_gap'), [False, False, True])
|
||||
|
||||
def test_null_change(self):
|
||||
self.all_moves[1].name = self.all_moves[1].name
|
||||
self.assertEqual(self.all_moves.mapped('made_sequence_gap'), [False, False, False])
|
||||
|
||||
def test_create_fill_gap(self):
|
||||
previous = self.all_moves[1].name
|
||||
self.all_moves[1].button_draft()
|
||||
self.all_moves[1].unlink()
|
||||
self.assertEqual(self.all_moves.exists().mapped('made_sequence_gap'), [False, True])
|
||||
new_move = self.create_move(name=previous)
|
||||
self.assertEqual(self.all_moves.exists().mapped('made_sequence_gap'), [False, False])
|
||||
self.assertEqual(new_move.made_sequence_gap, True)
|
||||
new_move.action_post()
|
||||
self.assertEqual(new_move.made_sequence_gap, False)
|
||||
|
||||
def test_create_gap(self):
|
||||
last = self.all_moves[2].name
|
||||
format_string, format_values = self.all_moves[0]._get_sequence_format_param(last)
|
||||
format_values['seq'] = format_values['seq'] + 10
|
||||
new_move = self.create_move(name=format_string.format(**format_values))
|
||||
new_move.action_post()
|
||||
self.assertEqual(new_move.made_sequence_gap, True)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSequenceMixinDeletion(TestSequenceMixinCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
journal = cls.env['account.journal'].create({
|
||||
'name': 'Test sequences - deletion',
|
||||
'code': 'SEQDEL',
|
||||
|
|
@ -763,9 +899,9 @@ class TestSequenceMixinConcurrency(TransactionCase):
|
|||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'line_ids': [(0, 0, {'name': 'name', 'account_id': account.id})]
|
||||
}] * 3)
|
||||
moves.name = '/'
|
||||
moves.name = False
|
||||
moves[0].action_post()
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', '/'])
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', False, False])
|
||||
env.cr.commit()
|
||||
self.data = {
|
||||
'move_ids': moves.ids,
|
||||
|
|
@ -783,7 +919,7 @@ class TestSequenceMixinConcurrency(TransactionCase):
|
|||
with self.env.registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
moves = env['account.move'].browse(self.data['move_ids'])
|
||||
moves.button_draft()
|
||||
moves.filtered(lambda x: x.state in ('posted', 'cancel')).button_draft()
|
||||
moves.posted_before = False
|
||||
moves.unlink()
|
||||
journal = env['account.journal'].browse(self.data['journal_id'])
|
||||
|
|
@ -814,6 +950,11 @@ class TestSequenceMixinConcurrency(TransactionCase):
|
|||
# check the values
|
||||
moves = env0['account.move'].browse(self.data['move_ids'])
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', 'CT/2016/01/0002', 'CT/2016/01/0003'])
|
||||
self.assertEqual(moves.mapped('sequence_prefix'), ['CT/2016/01/', 'CT/2016/01/', 'CT/2016/01/'])
|
||||
self.assertEqual(moves.mapped('sequence_number'), [1, 2, 3])
|
||||
self.assertEqual(moves.mapped('made_sequence_gap'), [False, False, False])
|
||||
for line in moves.line_ids:
|
||||
self.assertEqual(line.move_name, line.move_id.name)
|
||||
|
||||
def test_sequence_concurency_no_useless_lock(self):
|
||||
"""Do not lock needlessly when the sequence is not computed"""
|
||||
|
|
@ -834,4 +975,4 @@ class TestSequenceMixinConcurrency(TransactionCase):
|
|||
|
||||
# check the values
|
||||
moves = env0['account.move'].browse(self.data['move_ids'])
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', 'CT/2016/01/0002'])
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', False, 'CT/2016/01/0002'])
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSettings(AccountTestInvoicingCommon):
|
||||
|
||||
def test_switch_taxB2B_taxB2C(self):
|
||||
"""
|
||||
Since having users both in the tax B2B and tax B2C groups raise,
|
||||
modifications of the settings must be done in the right order;
|
||||
otherwise it is impossible to change the settings.
|
||||
"""
|
||||
# at each setting change, all users should be removed from one group and added to the other
|
||||
# so picking an arbitrary witness should be equivalent to checking that everything worked.
|
||||
config = self.env['res.config.settings'].create({})
|
||||
self.switch_tax_settings(config)
|
||||
|
||||
def switch_tax_settings(self, config):
|
||||
config.show_line_subtotals_tax_selection = "tax_excluded"
|
||||
config.flush_recordset()
|
||||
config.execute()
|
||||
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
|
||||
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False)
|
||||
|
||||
config.show_line_subtotals_tax_selection = "tax_included"
|
||||
config.flush_recordset()
|
||||
config.execute()
|
||||
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), False)
|
||||
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), True)
|
||||
|
||||
config.show_line_subtotals_tax_selection = "tax_excluded"
|
||||
config.flush_recordset()
|
||||
config.execute()
|
||||
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
|
||||
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False)
|
||||
|
||||
def test_switch_taxB2B_taxB2C_multicompany(self):
|
||||
"""
|
||||
Contrarily to the (apparently reasonable) assumption that adding users
|
||||
to group and removing them was symmetrical, it may not be the case
|
||||
if one is done in SQL and the other via the ORM.
|
||||
Because the latter automatically takes into account record rules that
|
||||
might make some users invisible.
|
||||
|
||||
This one is identical to the previous, except that we do the actions
|
||||
with a non-superuser user, and in a new company with one user in common
|
||||
with another company which has a different taxB2X setting.
|
||||
"""
|
||||
user = self.env.ref('base.user_admin')
|
||||
company = self.env['res.company'].create({'name': 'oobO'})
|
||||
user.write({'company_ids': [(4, company.id)], 'company_id': company.id})
|
||||
Settings = self.env['res.config.settings'].with_user(user.id)
|
||||
config = Settings.create({})
|
||||
|
||||
self.switch_tax_settings(config)
|
||||
|
||||
def test_switch_company_currency(self):
|
||||
"""
|
||||
A user should not be able to switch the currency of another company
|
||||
when that company already has posted account move lines.
|
||||
"""
|
||||
# Create company A (user's company)
|
||||
company_a = self.env['res.company'].create({
|
||||
'name': "Company A",
|
||||
})
|
||||
|
||||
# Get company B from test setup
|
||||
company_b = self.company_data['company']
|
||||
|
||||
# Create a purchase journal for company B
|
||||
journal = self.env['account.journal'].create({
|
||||
'name': "Vendor Bills Journal",
|
||||
'code': "VEND",
|
||||
'type': "purchase",
|
||||
'company_id': company_b.id,
|
||||
'currency_id': company_b.currency_id.id,
|
||||
})
|
||||
|
||||
# Create an invoice for company B
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': "in_invoice",
|
||||
'company_id': company_b.id,
|
||||
'journal_id': journal.id,
|
||||
})
|
||||
invoice.currency_id = self.env.ref('base.EUR').id
|
||||
|
||||
# Add a line to the invoice using an expense account
|
||||
self.env['account.move.line'].create({
|
||||
'move_id': invoice.id,
|
||||
'account_id': self.company_data["default_account_expense"].id,
|
||||
'name': "Test Invoice Line",
|
||||
'company_id': company_b.id,
|
||||
})
|
||||
|
||||
# Create a user that only belongs to company A
|
||||
user = self.env['res.users'].create({
|
||||
'name': "User A",
|
||||
'login': "user_a@example.com",
|
||||
'email': "user_a@example.com",
|
||||
'company_id': company_a.id,
|
||||
'company_ids': [Command.set([company_a.id])],
|
||||
'groups_id': [Command.set([
|
||||
self.env.ref('base.group_system').id,
|
||||
self.env.ref('base.group_erp_manager').id,
|
||||
self.env.ref('account.group_account_user').id,
|
||||
])],
|
||||
})
|
||||
|
||||
# Try to change company B's currency as user A (should raise UserError)
|
||||
user_env = self.env(user=user)
|
||||
with self.assertRaises(UserError):
|
||||
user_env['res.company'].browse(company_b.id).write({
|
||||
'currency_id': self.env.ref('base.EUR').id,
|
||||
})
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSetupWizard(AccountTestInvoicingCommon):
|
||||
|
||||
def test_setup_bank_account(self):
|
||||
"""
|
||||
Test that no error is raised when creating the bank setup wizard
|
||||
"""
|
||||
wizard = self.env['account.setup.bank.manual.config'].create([
|
||||
{
|
||||
'num_journals_without_account_bank': 1,
|
||||
'linked_journal_id': False,
|
||||
'acc_number': 'BE15001559627230',
|
||||
'bank_id': self.env['res.bank'].create({'name': 'Test bank'}).id,
|
||||
'bank_bic': False
|
||||
}
|
||||
])
|
||||
self.assertTrue(wizard)
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from odoo.addons.account.tools import (
|
||||
is_valid_structured_reference_be,
|
||||
is_valid_structured_reference_fi,
|
||||
is_valid_structured_reference_no_se,
|
||||
is_valid_structured_reference_si,
|
||||
is_valid_structured_reference_nl,
|
||||
is_valid_structured_reference_iso,
|
||||
is_valid_structured_reference,
|
||||
is_valid_structured_reference_for_country,
|
||||
)
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class StructuredReferenceTest(TransactionCase):
|
||||
|
||||
def test_structured_reference_iso(self):
|
||||
# Accepts references in structured format
|
||||
self.assertTrue(is_valid_structured_reference_iso(' RF18 5390 0754 7034 '))
|
||||
# Accept references in unstructured format
|
||||
self.assertTrue(is_valid_structured_reference_iso(' RF18539007547034'))
|
||||
# Validates with zero's added in front
|
||||
self.assertTrue(is_valid_structured_reference_iso('RF18000000000539007547034'))
|
||||
|
||||
# Does not validate invalid structured format
|
||||
self.assertFalse(is_valid_structured_reference_iso('18539007547034RF'))
|
||||
# Does not validate invalid checksum
|
||||
self.assertFalse(is_valid_structured_reference_iso('RF17539007547034'))
|
||||
# Validates the entire string
|
||||
self.assertFalse(is_valid_structured_reference_be('RF18539007547034-OTHER-RANDOM-STUFF'))
|
||||
|
||||
def test_structured_reference_be(self):
|
||||
# Accepts references in both structured formats
|
||||
self.assertTrue(is_valid_structured_reference_be(' +++020/3430/57642+++'))
|
||||
self.assertTrue(is_valid_structured_reference_be('***020/3430/57642*** '))
|
||||
# Accept references in unstructured format
|
||||
self.assertTrue(is_valid_structured_reference_be(' 020343057642'))
|
||||
self.assertTrue(is_valid_structured_reference_be('020/3430/57642'))
|
||||
# Validates edge case where result of % 97 = 0
|
||||
self.assertTrue(is_valid_structured_reference_be('020343053497'))
|
||||
|
||||
# Does not validate invalid structured format
|
||||
self.assertFalse(is_valid_structured_reference_be('***02/03430/57642***'))
|
||||
# Does not validate invalid checksum
|
||||
self.assertFalse(is_valid_structured_reference_be('020343057641'))
|
||||
# Validates the entire string
|
||||
self.assertFalse(is_valid_structured_reference_be('020343053497-OTHER-RANDOM-STUFF'))
|
||||
|
||||
def test_structured_reference_fi(self):
|
||||
# Accepts references in structured format
|
||||
self.assertTrue(is_valid_structured_reference_fi('2023 0000 98'))
|
||||
# Accept references in unstructured format
|
||||
self.assertTrue(is_valid_structured_reference_fi('2023000098'))
|
||||
# Validates with zero's added in front
|
||||
self.assertTrue(is_valid_structured_reference_fi('00000000002023000098'))
|
||||
|
||||
# Does not validate invalid structured format
|
||||
self.assertFalse(is_valid_structured_reference_fi('2023/0000/98'))
|
||||
# Does not validate invalid length
|
||||
self.assertFalse(is_valid_structured_reference_fi('000000000002023000098'))
|
||||
# Does not validate invalid checksum
|
||||
self.assertFalse(is_valid_structured_reference_fi('2023000095'))
|
||||
# Validates the entire string
|
||||
self.assertFalse(is_valid_structured_reference_fi('2023000098-OTHER-RANDOM-STUFF'))
|
||||
|
||||
def test_structured_reference_no_se(self):
|
||||
# Accepts references in structured format
|
||||
self.assertTrue(is_valid_structured_reference_no_se('1234 5678 97'))
|
||||
# Accept references in unstructured format
|
||||
self.assertTrue(is_valid_structured_reference_no_se('1234567897'))
|
||||
# Validates with zero's added in front
|
||||
self.assertTrue(is_valid_structured_reference_no_se('000001234567897'))
|
||||
|
||||
# Does not validate invalid structured format
|
||||
self.assertFalse(is_valid_structured_reference_no_se('1234/5678/97'))
|
||||
# Does not validate invalid checksum
|
||||
self.assertFalse(is_valid_structured_reference_no_se('1234567898'))
|
||||
# Validates the entire string
|
||||
self.assertFalse(is_valid_structured_reference_no_se('1234567897-OTHER-RANDOM-STUFF'))
|
||||
|
||||
def test_structured_reference_si(self):
|
||||
# Valid structured references (must have 2 hyphens and valid check digit)
|
||||
self.assertTrue(is_valid_structured_reference_si("SI01 25-20-85"))
|
||||
self.assertTrue(is_valid_structured_reference_si(" SI01 25 - 2 0-85 "))
|
||||
self.assertTrue(is_valid_structured_reference_si("SI01 19-1235-84505"))
|
||||
|
||||
# Invalid check digit
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01 25-20-84"))
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01 19-1235-84504"))
|
||||
|
||||
# Format errors - wrong prefix
|
||||
self.assertFalse(is_valid_structured_reference_si("SI02 25-20-85"))
|
||||
self.assertFalse(is_valid_structured_reference_si("0519123584503"))
|
||||
|
||||
# Format errors - missing or wrong hyphens
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01 252085"))
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01 25-2085"))
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01 25--20-85"))
|
||||
|
||||
# Format errors - non-numeric or empty parts
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01 ab-cd-ef"))
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01 25-20-"))
|
||||
self.assertFalse(is_valid_structured_reference_si("SI01"))
|
||||
|
||||
def test_structured_reference_nl(self):
|
||||
# Accepts 7 digits
|
||||
self.assertTrue(is_valid_structured_reference_nl('1234567'))
|
||||
|
||||
# Accepts 9 digits
|
||||
self.assertTrue(is_valid_structured_reference_nl('271234567'))
|
||||
|
||||
# Accepts 14 digits
|
||||
self.assertTrue(is_valid_structured_reference_nl('42234567890123'))
|
||||
|
||||
# Accepts 16 digits
|
||||
self.assertTrue(is_valid_structured_reference_nl('5000056789012345'))
|
||||
|
||||
# Accepts particular chase (check = 11 or 10)
|
||||
self.assertTrue(is_valid_structured_reference_nl('0123456788')) # Check of 123456788 == 11 then check = 0
|
||||
self.assertTrue(is_valid_structured_reference_nl('123456789107')) # Check of 23456789107 == 10 then check = 1
|
||||
|
||||
# Accepts spaces
|
||||
self.assertTrue(is_valid_structured_reference_nl('5 000 0567 8901 2345'))
|
||||
self.assertTrue(is_valid_structured_reference_nl(' 5000056789012345 '))
|
||||
|
||||
# Check the length
|
||||
self.assertFalse(is_valid_structured_reference_nl('123456'))
|
||||
self.assertFalse(is_valid_structured_reference_nl('12345678'))
|
||||
self.assertFalse(is_valid_structured_reference_nl('123456789012345'))
|
||||
self.assertFalse(is_valid_structured_reference_nl('12345678901234567'))
|
||||
# Check the checksum
|
||||
self.assertFalse(is_valid_structured_reference_nl('4000056789012345'))
|
||||
# Check the entire string
|
||||
self.assertFalse(is_valid_structured_reference_nl('5000056789012345-OTHER-RANDOM-STUFF'))
|
||||
|
||||
def test_structured_reference(self):
|
||||
# Accepts references in structured format
|
||||
self.assertTrue(is_valid_structured_reference(' RF18 5390 0754 7034 ')) # ISO
|
||||
self.assertTrue(is_valid_structured_reference(' +++020/3430/57642+++')) # BE
|
||||
self.assertTrue(is_valid_structured_reference('***020/3430/57642*** ')) # BE
|
||||
self.assertTrue(is_valid_structured_reference('2023 0000 98')) # FI
|
||||
self.assertTrue(is_valid_structured_reference('1234 5678 97')) # NO-SE
|
||||
self.assertTrue(is_valid_structured_reference("SI01 25-20-85")) # SI
|
||||
self.assertTrue(is_valid_structured_reference('5000056789012345')) # NL
|
||||
# Accept references in unstructured format
|
||||
self.assertTrue(is_valid_structured_reference(' RF18539007547034')) # ISO
|
||||
self.assertTrue(is_valid_structured_reference(' 020343057642')) # BE
|
||||
self.assertTrue(is_valid_structured_reference('2023000098')) # FI
|
||||
self.assertTrue(is_valid_structured_reference('1234567897')) # NO-SE
|
||||
self.assertTrue(is_valid_structured_reference(" SI01 25 - 2 0-85 ")) # SI
|
||||
self.assertTrue(is_valid_structured_reference('5 000 0567 8901 2345')) # NL
|
||||
# Validates with zero's added in front
|
||||
self.assertTrue(is_valid_structured_reference('RF18000000000539007547034')) # ISO
|
||||
self.assertTrue(is_valid_structured_reference('00000000002023000098')) # FI
|
||||
self.assertTrue(is_valid_structured_reference('000001234567897')) # NO-SE
|
||||
|
||||
# Does not validate invalid structured format
|
||||
self.assertFalse(is_valid_structured_reference('18539007547034RF')) # ISO
|
||||
self.assertFalse(is_valid_structured_reference('***02/03430/57642***')) # BE
|
||||
self.assertFalse(is_valid_structured_reference('2023/0000/98')) # FI
|
||||
self.assertFalse(is_valid_structured_reference('1234/5678/97')) # NO-SE
|
||||
self.assertFalse(is_valid_structured_reference("0519123584503")) # SI
|
||||
self.assertFalse(is_valid_structured_reference('(5)000 0567 8901 2345')) # NL
|
||||
# Does not validate invalid checksum
|
||||
self.assertFalse(is_valid_structured_reference('RF17539007547034')) # ISO
|
||||
self.assertFalse(is_valid_structured_reference('020343057641')) # BE
|
||||
self.assertFalse(is_valid_structured_reference('2023000095')) # FI
|
||||
self.assertFalse(is_valid_structured_reference('1234567898')) # NO-SE
|
||||
self.assertFalse(is_valid_structured_reference("SI01 19-1235-84504")) # SI
|
||||
self.assertFalse(is_valid_structured_reference('6000056789012345')) # NL
|
||||
|
||||
def test_structured_reference_for_country(self):
|
||||
# Valid structured references for supported countries
|
||||
self.assertTrue(is_valid_structured_reference_for_country('***020/3430/57642***', 'BE'))
|
||||
self.assertTrue(is_valid_structured_reference_for_country('2023 0000 98', 'FI'))
|
||||
self.assertTrue(is_valid_structured_reference_for_country('1234 5678 97', 'NO'))
|
||||
self.assertTrue(is_valid_structured_reference_for_country('1234 5678 97', 'SE'))
|
||||
self.assertTrue(is_valid_structured_reference_for_country('5000056789012345', 'NL'))
|
||||
self.assertTrue(is_valid_structured_reference_for_country('SI01 25-20-85', 'SI'))
|
||||
|
||||
# Fallback to ISO 11649 for unsupported countries
|
||||
self.assertTrue(is_valid_structured_reference_for_country('RF18 5390 0754 7034 ', 'FR'))
|
||||
|
||||
# Returns False for invalid references
|
||||
self.assertFalse(is_valid_structured_reference_for_country(''))
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,8 +8,8 @@ from odoo.tests import tagged
|
|||
class TaxReportTest(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_country_1 = cls.env['res.country'].create({
|
||||
'name': "The Old World",
|
||||
'code': 'YY',
|
||||
|
|
@ -71,11 +71,11 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
def _get_tax_tags(self, country, tag_name=None, active_test=True):
|
||||
domain = [('country_id', '=', country.id), ('applicability', '=', 'taxes')]
|
||||
if tag_name:
|
||||
domain.append(('name', 'like', '_' + tag_name))
|
||||
domain.append(('name', '=', tag_name))
|
||||
return self.env['account.account.tag'].with_context(active_test=active_test).search(domain)
|
||||
|
||||
def test_create_shared_tags(self):
|
||||
self.assertEqual(len(self._get_tax_tags(self.test_country_1, tag_name='01')), 2, "tax_tags expressions created for reports within the same countries using the same formula should create a single pair of tags.")
|
||||
self.assertEqual(len(self._get_tax_tags(self.test_country_1, tag_name='01')), 1, "tax_tags expressions created for reports within the same countries using the same formula should create a single tag.")
|
||||
|
||||
def test_add_expression(self):
|
||||
""" Adding a tax_tags expression creates new tags.
|
||||
|
|
@ -84,7 +84,7 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
self._create_basic_tax_report_line(self.tax_report_1, "new tax_tags line", 'tournicoti')
|
||||
tags_after = self._get_tax_tags(self.test_country_1)
|
||||
|
||||
self.assertEqual(len(tags_after), len(tags_before) + 2, "Two tags should have been created, +tournicoti and -tournicoti.")
|
||||
self.assertEqual(len(tags_after), len(tags_before) + 1)
|
||||
|
||||
def test_write_single_line_tagname_not_shared(self):
|
||||
""" Writing on the formula of a tax_tags expression should overwrite the name of the existing tags if they are not used in other formulas.
|
||||
|
|
@ -109,7 +109,7 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
new_tags = self.tax_report_line_1_1.expression_ids._get_matching_tags()
|
||||
|
||||
self.assertEqual(self._get_tax_tags(self.test_country_1, tag_name=original_tag_name), original_tags, "The original tags should be unchanged")
|
||||
self.assertEqual(len(self._get_tax_tags(self.test_country_1)), len(start_tags) + 2, "A + and - tag should have been created")
|
||||
self.assertEqual(len(self._get_tax_tags(self.test_country_1)), len(start_tags) + 1, "A tag should have been created")
|
||||
self.assertNotEqual(original_tags, new_tags, "New tags should have been assigned to the expression")
|
||||
|
||||
def test_write_multi_no_change(self):
|
||||
|
|
@ -130,7 +130,7 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
lines.expression_ids.write({'formula': 'crabe'})
|
||||
tags_after = self._get_tax_tags(self.test_country_1)
|
||||
|
||||
self.assertEqual(len(tags_before) + 2, len(tags_after), "Only two distinct tags should have been created.")
|
||||
self.assertEqual(len(tags_before) + 1, len(tags_after), "Only tag should have been created.")
|
||||
|
||||
line_1_1_tags = self.tax_report_line_1_1.expression_ids._get_matching_tags()
|
||||
line_2_2_tags = self.tax_report_line_2_2.expression_ids._get_matching_tags()
|
||||
|
|
@ -155,7 +155,7 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
country_1_tags_after_change = self._get_tax_tags(self.test_country_1)
|
||||
|
||||
self.assertEqual(country_1_tags_after_change, country_1_tags_after_copy, "Modifying the country should not have changed the tags in the original country.")
|
||||
self.assertEqual(len(country_2_tags_after_change), len(country_2_tags_before_change) + 2 * len(copied_report_1.line_ids), "Modifying the country should have created a new + and - tag in the new country for each tax_tags expression of the report.")
|
||||
self.assertEqual(len(country_2_tags_after_change), len(country_2_tags_before_change) + len(copied_report_1.line_ids), "Modifying the country should have created a tag in the new country for each tax_tags expression of the report.")
|
||||
|
||||
for original, copy in zip(self.tax_report_1.line_ids, copied_report_1.line_ids):
|
||||
original_tags = original.expression_ids._get_matching_tags()
|
||||
|
|
@ -185,7 +185,11 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
"""
|
||||
tag_name = "55b"
|
||||
tax_report_line = self._create_basic_tax_report_line(self.tax_report_1, "Line 55 bis", tag_name)
|
||||
test_tag = tax_report_line.expression_ids._get_matching_tags("+")
|
||||
test_tag = tax_report_line.expression_ids._get_matching_tags()
|
||||
self.env['account.tax.group'].create({
|
||||
'name': 'Tax group',
|
||||
'country_id': self.tax_report_1.country_id.id,
|
||||
})
|
||||
test_tax = self.env['account.tax'].create({
|
||||
'name': "Test tax",
|
||||
'amount_type': 'percent',
|
||||
|
|
@ -222,7 +226,6 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
tax_report_line.unlink()
|
||||
tags_after = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
|
||||
# only the +tag_name should be kept (and archived), -tag_name should be unlinked
|
||||
self.assertEqual(tags_after.mapped('tax_negate'), [False], "Unlinking a tax_tags expression should keep the tag if it was used on move lines, and unlink it otherwise.")
|
||||
self.assertEqual(tags_after.mapped('active'), [False], "Unlinking a tax_tags expression should archive the tag if it was used on move lines, and unlink it otherwise.")
|
||||
self.assertEqual(len(test_tax.invoice_repartition_line_ids.tag_ids), 0, "After a tag is archived it shouldn't be on tax repartition lines.")
|
||||
|
||||
|
|
@ -247,12 +250,18 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
"""
|
||||
tag_name = self.tax_report_line_1_55.expression_ids.formula
|
||||
tags_before = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
|
||||
tags_before[0].unlink() # we unlink one and archive the other, doesn't matter which one
|
||||
tags_before[1].active = False
|
||||
|
||||
tags_before.unlink()
|
||||
self._create_basic_tax_report_line(self.tax_report_1, "Line 55 bis", tag_name)
|
||||
tags_after = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
|
||||
self.assertEqual(len(tags_after), 2, "When creating a tax report line with an archived tag and it's complement doesn't exist, it should be re-created.")
|
||||
self.assertEqual(tags_after.mapped('name'), ['+' + tag_name, '-' + tag_name], "After creating a tax report line with an archived tag and when its complement doesn't exist, both a negative and a positive tag should be created.")
|
||||
self.assertEqual(len(tags_after), 1)
|
||||
self.assertEqual(tags_after.mapped('name'), [tag_name])
|
||||
|
||||
tags_before.active = False
|
||||
self._create_basic_tax_report_line(self.tax_report_1, "Line 55 bis", tag_name)
|
||||
tags_after = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
|
||||
self.assertEqual(len(tags_after), 1)
|
||||
self.assertEqual(tags_after.mapped('name'), [tag_name])
|
||||
|
||||
def test_change_engine_without_formula(self):
|
||||
aggregation_line = self.env['account.report.line'].create({
|
||||
|
|
@ -262,7 +271,7 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
Command.create({
|
||||
'label': 'balance',
|
||||
'engine': 'aggregation',
|
||||
'formula': 'Dudu',
|
||||
'formula': 'Dudu.balance',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
@ -272,9 +281,9 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
|
||||
aggregation_line.expression_ids.engine = 'tax_tags'
|
||||
|
||||
tags_after = self._get_tax_tags(self.test_country_1, tag_name='Dudu')
|
||||
self.assertEqual(len(tags_after), 2, "Changing the engine should have created tags")
|
||||
self.assertEqual(tags_after.mapped('name'), ['-Dudu', '+Dudu'])
|
||||
tags_after = self._get_tax_tags(self.test_country_1, tag_name='Dudu.balance')
|
||||
self.assertEqual(len(tags_after), 1, "Changing the engine should have created a tag")
|
||||
self.assertEqual(tags_after.mapped('name'), ['Dudu.balance'])
|
||||
|
||||
def test_change_engine_shared_tags(self):
|
||||
aggregation_line = self.env['account.report.line'].create({
|
||||
|
|
@ -284,13 +293,13 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
Command.create({
|
||||
'label': 'balance',
|
||||
'engine': 'aggregation',
|
||||
'formula': 'Dudu',
|
||||
'formula': 'Dudu.balance',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
tags_before = self._get_tax_tags(self.test_country_1, tag_name='01')
|
||||
self.assertEqual(len(tags_before), 2, "The tags should already exist because of another expression")
|
||||
self.assertEqual(len(tags_before), 1, "The tag should already exist because of another expression")
|
||||
|
||||
aggregation_line.expression_ids.write({'engine': 'tax_tags', 'formula': '01'})
|
||||
|
||||
|
|
@ -309,6 +318,6 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
|||
})
|
||||
|
||||
tags_after = self._get_tax_tags(self.test_country_1, tag_name='Buny')
|
||||
self.assertEqual(len(tags_after), 2, "Changing the formula should have renamed the tags")
|
||||
self.assertEqual(tags_after.mapped('name'), ['-Buny', '+Buny'])
|
||||
self.assertEqual(len(tags_after), 1, "Changing the formula should have renamed the tag")
|
||||
self.assertEqual(tags_after.mapped('name'), ['Buny'])
|
||||
self.assertEqual(tags_after, tags_to_rename, "Changing the formula should have renamed the tags")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account.tests.common import TestTaxCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxesBaseLinesTaxDetails(TestTaxCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.env.company.tax_calculation_rounding_method = 'round_globally'
|
||||
|
||||
def test_dispatch_delta_on_base_lines(self):
|
||||
""" Make sure that the base line delta is dispatched evenly on base lines.
|
||||
Needed for BIS3 rule PEPPOL-EN16931-R120.
|
||||
"""
|
||||
tax_21 = self.percent_tax(21.0)
|
||||
document = self.populate_document(self.init_document(
|
||||
lines=[
|
||||
{'price_unit': 10.04, 'discount': 10, 'tax_ids': tax_21},
|
||||
] + [
|
||||
{'price_unit': 1.04, 'discount': 10, 'tax_ids': tax_21},
|
||||
] * 10,
|
||||
))
|
||||
|
||||
line_1_expected_values = {
|
||||
'total_excluded': 9.04,
|
||||
'total_excluded_currency': 9.04,
|
||||
'total_included': 10.94,
|
||||
'total_included_currency': 10.94,
|
||||
'delta_total_excluded': 0.0,
|
||||
'delta_total_excluded_currency': 0.0,
|
||||
'manual_total_excluded': None,
|
||||
'manual_total_excluded_currency': None,
|
||||
'manual_tax_amounts': None,
|
||||
'taxes_data': [
|
||||
{
|
||||
'tax_id': tax_21.id,
|
||||
'tax_amount': 1.8699999999999999,
|
||||
'tax_amount_currency': 1.8699999999999999,
|
||||
'base_amount': 9.01,
|
||||
'base_amount_currency': 9.01,
|
||||
}
|
||||
],
|
||||
}
|
||||
line_2_expected_values = {
|
||||
'total_excluded': 0.94,
|
||||
'total_excluded_currency': 0.94,
|
||||
'total_included': 1.14,
|
||||
'total_included_currency': 1.14,
|
||||
'delta_total_excluded': 0.0,
|
||||
'delta_total_excluded_currency': 0.0,
|
||||
'manual_total_excluded': None,
|
||||
'manual_total_excluded_currency': None,
|
||||
'manual_tax_amounts': None,
|
||||
'taxes_data': [
|
||||
{
|
||||
'tax_id': tax_21.id,
|
||||
'tax_amount': 0.2,
|
||||
'tax_amount_currency': 0.2,
|
||||
'base_amount': 0.94,
|
||||
'base_amount_currency': 0.94,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
expected_values = {
|
||||
'base_lines_tax_details': [
|
||||
{
|
||||
**line_1_expected_values,
|
||||
'delta_total_excluded': -0.03,
|
||||
'delta_total_excluded_currency': -0.03,
|
||||
},
|
||||
{
|
||||
**line_2_expected_values,
|
||||
'delta_total_excluded': -0.01,
|
||||
'delta_total_excluded_currency': -0.01,
|
||||
'taxes_data': [
|
||||
{
|
||||
'tax_id': tax_21.id,
|
||||
'tax_amount': 0.19,
|
||||
'tax_amount_currency': 0.19,
|
||||
'base_amount': 0.9299999999999999,
|
||||
'base_amount_currency': 0.9299999999999999,
|
||||
}
|
||||
],
|
||||
},
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
line_2_expected_values,
|
||||
]
|
||||
}
|
||||
|
||||
self.assert_base_lines_tax_details(document, expected_values)
|
||||
self._run_js_tests()
|
||||
|
||||
def test_dispatch_delta_on_net_zero_tax(self):
|
||||
"""Check that the base line delta still is dispatched if net tax is zero."""
|
||||
def get_expected_values(base_values, tax):
|
||||
expected_tax_total = sum(values[2] for values in base_expected_values)
|
||||
self.assertTrue(
|
||||
tax.company_id.currency_id.is_zero(expected_tax_total),
|
||||
"Expected taxes should add up to zero.",
|
||||
)
|
||||
return {
|
||||
'base_lines_tax_details': [{
|
||||
'delta_total_excluded': 0.0,
|
||||
'delta_total_excluded_currency': 0.0,
|
||||
'manual_tax_amounts': None,
|
||||
'manual_total_excluded': None,
|
||||
'manual_total_excluded_currency': None,
|
||||
'taxes_data': [{
|
||||
'base_amount': total_excluded,
|
||||
'base_amount_currency': total_excluded,
|
||||
'tax_amount': tax_amount,
|
||||
'tax_amount_currency': tax_amount,
|
||||
'tax_id': tax.id,
|
||||
}],
|
||||
'total_excluded': total_excluded,
|
||||
'total_excluded_currency': total_excluded,
|
||||
'total_included': total_included,
|
||||
'total_included_currency': total_included,
|
||||
} for total_excluded, total_included, tax_amount in base_values],
|
||||
}
|
||||
|
||||
with self.subTest("19.99% tax, 100% global discount"):
|
||||
tax_19_99 = self.percent_tax(19.99)
|
||||
lines_vals = [{
|
||||
'price_unit': price,
|
||||
'quantity': 1,
|
||||
'tax_ids': tax_19_99,
|
||||
} for price in (19.99, 19.99, -39.98)]
|
||||
document = self.populate_document(self.init_document(lines_vals))
|
||||
|
||||
base_expected_values = [
|
||||
# (total_excluded, total_included, tax_amount),
|
||||
(19.99, 23.99, 4.0),
|
||||
(19.99, 23.99, 4.0),
|
||||
(-39.98, -47.97, -8.0),
|
||||
]
|
||||
expected_values = get_expected_values(base_expected_values, tax_19_99)
|
||||
self.assert_base_lines_tax_details(document, expected_values)
|
||||
self._run_js_tests()
|
||||
|
||||
with self.subTest("7% tax, 30% line discount, 100% global discount"):
|
||||
tax_7 = self.percent_tax(7.0)
|
||||
lines_vals = [{
|
||||
'price_unit': price,
|
||||
'quantity': 4,
|
||||
'discount': 30,
|
||||
'tax_ids': tax_7,
|
||||
} for price in (1068, 46, 46, 298, 5)]
|
||||
lines_vals.append({'price_unit': -4096.4, 'tax_ids': tax_7}) # 100% discount
|
||||
document = self.populate_document(self.init_document(lines_vals))
|
||||
|
||||
base_expected_values = [
|
||||
# (total_excluded, total_included, tax_amount),
|
||||
(2990.4, 3199.73, 209.33),
|
||||
(128.8, 137.82000000000002, 9.02),
|
||||
(128.8, 137.82000000000002, 9.02),
|
||||
(834.4, 892.81, 58.41),
|
||||
(14.0, 14.98, 0.98),
|
||||
(-4096.4, -4383.15, -286.76),
|
||||
]
|
||||
expected_values = get_expected_values(base_expected_values, tax_7)
|
||||
self.assert_base_lines_tax_details(document, expected_values)
|
||||
self._run_js_tests()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,360 @@
|
|||
from odoo.addons.account.tests.common import TestTaxCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxesDispatchingBaseLines(TestTaxCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.currency = cls.env.company.currency_id
|
||||
cls.foreign_currency = cls.setup_other_currency('EUR')
|
||||
|
||||
def test_dispatch_return_of_merchandise_lines(self):
|
||||
self.env.company.tax_calculation_rounding_method = 'round_globally'
|
||||
AccountTax = self.env['account.tax']
|
||||
tax1 = self.fixed_tax(1, include_base_amount=True)
|
||||
tax2 = self.percent_tax(21)
|
||||
taxes = tax1 + tax2
|
||||
|
||||
document_params = self.init_document(
|
||||
lines=[
|
||||
{'product_id': self.product_a, 'price_unit': 16.79, 'quantity': 10, 'tax_ids': taxes},
|
||||
{'product_id': self.product_a, 'price_unit': 16.79, 'quantity': 10, 'tax_ids': taxes},
|
||||
{'product_id': self.product_a, 'price_unit': 16.79, 'quantity': -12, 'tax_ids': taxes},
|
||||
],
|
||||
currency=self.foreign_currency,
|
||||
rate=0.5,
|
||||
)
|
||||
expected_values = {
|
||||
'same_tax_base': True,
|
||||
'currency_id': self.foreign_currency.id,
|
||||
'company_currency_id': self.currency.id,
|
||||
'base_amount_currency': 134.32,
|
||||
'base_amount': 268.64,
|
||||
'tax_amount_currency': 37.89,
|
||||
'tax_amount': 75.77,
|
||||
'total_amount_currency': 172.21,
|
||||
'total_amount': 344.41,
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'base_amount_currency': 134.32,
|
||||
'base_amount': 268.64,
|
||||
'tax_amount_currency': 37.89,
|
||||
'tax_amount': 75.77,
|
||||
'tax_groups': [
|
||||
{
|
||||
'id': taxes.tax_group_id.id,
|
||||
'base_amount_currency': 134.32,
|
||||
'base_amount': 268.64,
|
||||
'tax_amount_currency': 37.89,
|
||||
'tax_amount': 75.77,
|
||||
'display_base_amount_currency': 134.32,
|
||||
'display_base_amount': 268.64,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
document = self.populate_document(document_params)
|
||||
base_lines = document['lines']
|
||||
tax_totals = AccountTax._get_tax_totals_summary(base_lines, document['currency'], self.env.company)
|
||||
self._assert_tax_totals_summary(tax_totals, expected_values)
|
||||
|
||||
# Dispatch the return of product on the others base lines.
|
||||
self.assertEqual(len(base_lines), 3)
|
||||
base_lines = AccountTax._dispatch_return_of_merchandise_lines(document['lines'], self.env.company)
|
||||
AccountTax._squash_return_of_merchandise_lines(base_lines, self.env.company)
|
||||
self.assertEqual(len(base_lines), 2)
|
||||
self.assertEqual(base_lines[0]['quantity'], 0)
|
||||
self.assertEqual(base_lines[1]['quantity'], 8)
|
||||
tax_totals = AccountTax._get_tax_totals_summary(base_lines, document['currency'], self.env.company)
|
||||
self._assert_tax_totals_summary(tax_totals, expected_values)
|
||||
|
||||
def test_dispatch_return_of_merchandise_lines_no_match(self):
|
||||
self.env.company.tax_calculation_rounding_method = 'round_globally'
|
||||
AccountTax = self.env['account.tax']
|
||||
tax = self.percent_tax(21)
|
||||
|
||||
document_params = self.init_document(
|
||||
lines=[
|
||||
{'product_id': self.product_a, 'price_unit': 16.79, 'quantity': 10, 'tax_ids': tax},
|
||||
{'product_id': self.product_a, 'price_unit': 16.79, 'quantity': -2, 'tax_ids': []},
|
||||
],
|
||||
)
|
||||
expected_values = {
|
||||
'same_tax_base': False,
|
||||
'currency_id': self.currency.id,
|
||||
'base_amount_currency': 134.32,
|
||||
'tax_amount_currency': 35.26,
|
||||
'total_amount_currency': 169.58,
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'base_amount_currency': 134.32,
|
||||
'tax_amount_currency': 35.26,
|
||||
'tax_groups': [
|
||||
{
|
||||
'id': tax.tax_group_id.id,
|
||||
'base_amount_currency': 167.9,
|
||||
'tax_amount_currency': 35.26,
|
||||
'display_base_amount_currency': 167.9,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
document = self.populate_document(document_params)
|
||||
base_lines = document['lines']
|
||||
tax_totals = AccountTax._get_tax_totals_summary(base_lines, document['currency'], self.env.company)
|
||||
self._assert_tax_totals_summary(tax_totals, expected_values)
|
||||
|
||||
# Dispatch the return of product on the others base lines.
|
||||
# The dispatching should fail so no changes.
|
||||
self.assertEqual(len(base_lines), 2)
|
||||
base_lines = AccountTax._dispatch_return_of_merchandise_lines(document['lines'], self.env.company)
|
||||
AccountTax._squash_return_of_merchandise_lines(base_lines, self.env.company)
|
||||
self.assertEqual(len(base_lines), 2)
|
||||
tax_totals = AccountTax._get_tax_totals_summary(base_lines, document['currency'], self.env.company)
|
||||
self._assert_tax_totals_summary(tax_totals, expected_values)
|
||||
|
||||
def test_dispatch_global_discount_lines(self):
|
||||
self.env.company.tax_calculation_rounding_method = 'round_globally'
|
||||
AccountTax = self.env['account.tax']
|
||||
tax1 = self.fixed_tax(1, include_base_amount=True)
|
||||
tax2 = self.percent_tax(21)
|
||||
taxes = tax1 + tax2
|
||||
|
||||
document_params = self.init_document(
|
||||
lines=[
|
||||
{'product_id': self.product_a, 'price_unit': 33.58, 'quantity': 10, 'tax_ids': taxes},
|
||||
{'product_id': self.product_a, 'price_unit': 16.79, 'quantity': 10, 'tax_ids': taxes},
|
||||
],
|
||||
currency=self.foreign_currency,
|
||||
rate=0.5,
|
||||
)
|
||||
expected_values = {
|
||||
'same_tax_base': True,
|
||||
'currency_id': self.foreign_currency.id,
|
||||
'company_currency_id': self.currency.id,
|
||||
'base_amount_currency': 503.7,
|
||||
'base_amount': 1007.4,
|
||||
'tax_amount_currency': 129.98,
|
||||
'tax_amount': 259.95,
|
||||
'total_amount_currency': 633.68,
|
||||
'total_amount': 1267.35,
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'base_amount_currency': 503.7,
|
||||
'base_amount': 1007.4,
|
||||
'tax_amount_currency': 129.98,
|
||||
'tax_amount': 259.95,
|
||||
'tax_groups': [
|
||||
{
|
||||
'id': taxes.tax_group_id.id,
|
||||
'base_amount_currency': 503.7,
|
||||
'base_amount': 1007.4,
|
||||
'tax_amount_currency': 129.98,
|
||||
'tax_amount': 259.95,
|
||||
'display_base_amount_currency': 503.7,
|
||||
'display_base_amount': 1007.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
document = self.populate_document(document_params)
|
||||
base_lines = document['lines']
|
||||
tax_totals = AccountTax._get_tax_totals_summary(base_lines, document['currency'], self.env.company)
|
||||
self._assert_tax_totals_summary(tax_totals, expected_values)
|
||||
|
||||
# Global discount 20%.
|
||||
discount_base_lines = AccountTax._prepare_global_discount_lines(base_lines, self.env.company, 'percent', 20.0)
|
||||
base_lines += discount_base_lines
|
||||
AccountTax._add_tax_details_in_base_lines(base_lines, self.env.company)
|
||||
AccountTax._round_base_lines_tax_details(base_lines, self.env.company)
|
||||
tax_totals = AccountTax._get_tax_totals_summary(base_lines, document['currency'], self.env.company)
|
||||
expected_values = {
|
||||
'same_tax_base': True,
|
||||
'currency_id': self.foreign_currency.id,
|
||||
'company_currency_id': self.currency.id,
|
||||
'base_amount_currency': 402.96,
|
||||
'base_amount': 805.92,
|
||||
'tax_amount_currency': 108.82,
|
||||
'tax_amount': 217.64,
|
||||
'total_amount_currency': 511.78,
|
||||
'total_amount': 1023.56,
|
||||
'subtotals': [
|
||||
{
|
||||
'name': "Untaxed Amount",
|
||||
'base_amount_currency': 402.96,
|
||||
'base_amount': 805.92,
|
||||
'tax_amount_currency': 108.82,
|
||||
'tax_amount': 217.64,
|
||||
'tax_groups': [
|
||||
{
|
||||
'id': taxes.tax_group_id.id,
|
||||
'base_amount_currency': 402.96,
|
||||
'base_amount': 805.92,
|
||||
'tax_amount_currency': 108.82,
|
||||
'tax_amount': 217.64,
|
||||
'display_base_amount_currency': 402.96,
|
||||
'display_base_amount': 805.92,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
self._assert_tax_totals_summary(tax_totals, expected_values)
|
||||
|
||||
# Dispatch the global discount on the others base lines.
|
||||
self.assertEqual(len(base_lines), 3)
|
||||
base_lines[-1]['special_type'] = 'global_discount'
|
||||
base_lines = AccountTax._dispatch_global_discount_lines(base_lines, self.env.company)
|
||||
AccountTax._squash_global_discount_lines(base_lines, self.env.company)
|
||||
self.assertEqual(len(base_lines), 2)
|
||||
tax_totals = AccountTax._get_tax_totals_summary(base_lines, document['currency'], self.env.company)
|
||||
self._assert_tax_totals_summary(tax_totals, expected_values)
|
||||
|
||||
def test_dispatch_global_discount_lines_no_match(self):
|
||||
self.env.company.tax_calculation_rounding_method = 'round_globally'
|
||||
AccountTax = self.env['account.tax']
|
||||
tax = self.percent_tax(21)
|
||||
|
||||
document_params = self.init_document(
|
||||
lines=[
|
||||
{'product_id': self.product_a, 'price_unit': 33.58, 'quantity': 10, 'tax_ids': tax},
|
||||
{'product_id': self.product_a, 'price_unit': 16.79, 'quantity': 10, 'tax_ids': tax},
|
||||
{'product_id': self.product_a, 'price_unit': -50.0, 'quantity': 1, 'tax_ids': [], 'special_type': 'global_discount'},
|
||||
],
|
||||
)
|
||||
document = self.populate_document(document_params)
|
||||
base_lines = document['lines']
|
||||
|
||||
# Should fail to dispatch the global discount on the others base lines.
|
||||
self.assertEqual(len(base_lines), 3)
|
||||
base_lines = AccountTax._dispatch_global_discount_lines(base_lines, self.env.company)
|
||||
AccountTax._squash_global_discount_lines(base_lines, self.env.company)
|
||||
self.assertEqual(len(base_lines), 3)
|
||||
|
||||
def test_dispatch_taxes_into_new_base_lines(self):
|
||||
|
||||
def assert_tax_totals_summary_after_dispatching(document, exclude_function, expected_values):
|
||||
new_base_lines = AccountTax._dispatch_taxes_into_new_base_lines(
|
||||
base_lines=document['lines'],
|
||||
company=self.env.company,
|
||||
exclude_function=exclude_function,
|
||||
)
|
||||
|
||||
extra_base_lines = AccountTax._turn_removed_taxes_into_new_base_lines(new_base_lines, self.env.company)
|
||||
self.assert_tax_totals_summary(
|
||||
document={
|
||||
**document,
|
||||
'lines': new_base_lines + extra_base_lines,
|
||||
},
|
||||
expected_values=expected_values,
|
||||
soft_checking=True,
|
||||
)
|
||||
|
||||
AccountTax = self.env['account.tax']
|
||||
self.env.company.tax_calculation_rounding_method = 'round_globally'
|
||||
tax1 = self.fixed_tax(1, include_base_amount=True)
|
||||
tax2 = self.fixed_tax(5)
|
||||
tax3 = self.percent_tax(21)
|
||||
taxes = tax1 + tax2 + tax3
|
||||
|
||||
document_params = self.init_document(
|
||||
lines=[
|
||||
{'price_unit': 16.79, 'tax_ids': taxes},
|
||||
{'price_unit': 16.79, 'tax_ids': taxes},
|
||||
],
|
||||
currency=self.foreign_currency,
|
||||
rate=0.5,
|
||||
)
|
||||
document = self.populate_document(document_params)
|
||||
|
||||
expected_values = {
|
||||
'base_amount_currency': 33.58,
|
||||
'tax_amount_currency': 19.47,
|
||||
'total_amount_currency': 53.05,
|
||||
}
|
||||
self.assert_tax_totals_summary(document, expected_values, soft_checking=True)
|
||||
|
||||
assert_tax_totals_summary_after_dispatching(
|
||||
document=document,
|
||||
exclude_function=lambda base_line, tax_data: tax_data['tax'] == tax1,
|
||||
expected_values={
|
||||
**expected_values,
|
||||
'base_amount_currency': 35.58,
|
||||
'tax_amount_currency': 17.47,
|
||||
},
|
||||
)
|
||||
assert_tax_totals_summary_after_dispatching(
|
||||
document=document,
|
||||
exclude_function=lambda base_line, tax_data: tax_data['tax'] == tax2,
|
||||
expected_values={
|
||||
**expected_values,
|
||||
'base_amount_currency': 43.58,
|
||||
'tax_amount_currency': 9.47,
|
||||
},
|
||||
)
|
||||
assert_tax_totals_summary_after_dispatching(
|
||||
document=document,
|
||||
exclude_function=lambda base_line, tax_data: tax_data['tax'] == tax3,
|
||||
expected_values={
|
||||
**expected_values,
|
||||
'base_amount_currency': 41.05,
|
||||
'tax_amount_currency': 12.0,
|
||||
},
|
||||
)
|
||||
|
||||
taxes.price_include_override = 'tax_included'
|
||||
|
||||
document_params = self.init_document(
|
||||
lines=[
|
||||
{'price_unit': 21.53, 'tax_ids': taxes},
|
||||
{'price_unit': 21.53, 'tax_ids': taxes},
|
||||
],
|
||||
currency=self.foreign_currency,
|
||||
rate=0.5,
|
||||
)
|
||||
document = self.populate_document(document_params)
|
||||
|
||||
expected_values = {
|
||||
'base_amount_currency': 25.32,
|
||||
'tax_amount_currency': 17.74,
|
||||
'total_amount_currency': 43.06,
|
||||
}
|
||||
self.assert_tax_totals_summary(document, expected_values, soft_checking=True)
|
||||
|
||||
assert_tax_totals_summary_after_dispatching(
|
||||
document=document,
|
||||
exclude_function=lambda base_line, tax_data: tax_data['tax'] == tax1,
|
||||
expected_values={
|
||||
**expected_values,
|
||||
'base_amount_currency': 27.32,
|
||||
'tax_amount_currency': 15.74,
|
||||
},
|
||||
)
|
||||
assert_tax_totals_summary_after_dispatching(
|
||||
document=document,
|
||||
exclude_function=lambda base_line, tax_data: tax_data['tax'] == tax2,
|
||||
expected_values={
|
||||
**expected_values,
|
||||
'base_amount_currency': 35.32,
|
||||
'tax_amount_currency': 7.74,
|
||||
},
|
||||
)
|
||||
assert_tax_totals_summary_after_dispatching(
|
||||
document=document,
|
||||
exclude_function=lambda base_line, tax_data: tax_data['tax'] == tax3,
|
||||
expected_values={
|
||||
**expected_values,
|
||||
'base_amount_currency': 31.06,
|
||||
'tax_amount_currency': 12.0,
|
||||
},
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,87 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class AccountingTestTemplConsistency(TransactionCase):
|
||||
'''Test the templates consistency between some objects like account.account when account.account.template.
|
||||
'''
|
||||
|
||||
def get_model_fields(self, model, extra_domain=None):
|
||||
# Retrieve fields to compare
|
||||
domain = [
|
||||
('model', '=', model),
|
||||
('state', '=', 'base'),
|
||||
('related', '=', False),
|
||||
('compute', '=', False),
|
||||
('store', '=', True),
|
||||
]
|
||||
if extra_domain:
|
||||
domain += extra_domain
|
||||
return self.env['ir.model.fields'].search(domain)
|
||||
|
||||
def check_fields_consistency(self, model_from, model_to, exceptions=None):
|
||||
'''Check the consistency of fields from one model to another by comparing if all fields
|
||||
in the model_from are present in the model_to.
|
||||
:param model_from: The model to compare.
|
||||
:param model_to: The compared model.
|
||||
:param exceptions: Not copied model's fields.
|
||||
'''
|
||||
extra_domain = [('name', 'not in', exceptions)] if exceptions else []
|
||||
from_fields = self.get_model_fields(model_from, extra_domain=extra_domain).filtered_domain([('modules', '=', 'account')])
|
||||
|
||||
to_fields_set = set([f.name for f in self.get_model_fields(model_to)])
|
||||
for field in from_fields:
|
||||
assert field.name in to_fields_set,\
|
||||
'Missing field "%s" from "%s" in model "%s".' % (field.name, model_from, model_to)
|
||||
|
||||
def test_account_account_fields(self):
|
||||
'''Test fields consistency for ('account.account', 'account.account.template')
|
||||
'''
|
||||
self.check_fields_consistency(
|
||||
'account.account.template', 'account.account', exceptions=['chart_template_id', 'nocreate'])
|
||||
self.check_fields_consistency(
|
||||
'account.account', 'account.account.template', exceptions=['company_id', 'deprecated', 'opening_debit', 'opening_credit', 'allowed_journal_ids', 'group_id', 'root_id', 'is_off_balance', 'non_trade', 'include_initial_balance', 'internal_group'])
|
||||
|
||||
def test_account_tax_fields(self):
|
||||
'''Test fields consistency for ('account.tax', 'account.tax.template')
|
||||
'''
|
||||
self.check_fields_consistency('account.tax.template', 'account.tax', exceptions=['chart_template_id'])
|
||||
self.check_fields_consistency('account.tax', 'account.tax.template', exceptions=['company_id', 'country_id', 'real_amount'])
|
||||
self.check_fields_consistency('account.tax.repartition.line.template', 'account.tax.repartition.line', exceptions=['plus_report_expression_ids', 'minus_report_expression_ids'])
|
||||
self.check_fields_consistency('account.tax.repartition.line', 'account.tax.repartition.line.template', exceptions=['tag_ids', 'country_id', 'company_id', 'sequence'])
|
||||
|
||||
def test_fiscal_position_fields(self):
|
||||
'''Test fields consistency for ('account.fiscal.position', 'account.fiscal.position.template')
|
||||
'''
|
||||
#main
|
||||
self.check_fields_consistency('account.fiscal.position.template', 'account.fiscal.position', exceptions=['chart_template_id'])
|
||||
self.check_fields_consistency('account.fiscal.position', 'account.fiscal.position.template', exceptions=['active', 'company_id', 'states_count', 'foreign_vat'])
|
||||
#taxes
|
||||
self.check_fields_consistency('account.fiscal.position.tax.template', 'account.fiscal.position.tax')
|
||||
self.check_fields_consistency('account.fiscal.position.tax', 'account.fiscal.position.tax.template')
|
||||
#accounts
|
||||
self.check_fields_consistency('account.fiscal.position.account.template', 'account.fiscal.position.account')
|
||||
self.check_fields_consistency('account.fiscal.position.account', 'account.fiscal.position.account.template')
|
||||
|
||||
def test_reconcile_model_fields(self):
|
||||
'''Test fields consistency for ('account.reconcile.model', 'account.reconcile.model.template')
|
||||
'''
|
||||
self.check_fields_consistency('account.reconcile.model.template', 'account.reconcile.model', exceptions=['chart_template_id'])
|
||||
# exclude fields from inherited 'mail.thread'
|
||||
mail_thread_fields = [field.name for field in self.get_model_fields('mail.thread')]
|
||||
self.check_fields_consistency(
|
||||
'account.reconcile.model',
|
||||
'account.reconcile.model.template',
|
||||
exceptions=mail_thread_fields + ['active', 'company_id', 'past_months_limit', 'partner_mapping_line_ids'],
|
||||
)
|
||||
# lines
|
||||
self.check_fields_consistency('account.reconcile.model.line.template', 'account.reconcile.model.line', exceptions=['chart_template_id'])
|
||||
self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution', 'amount'])
|
||||
|
||||
def test_account_group_fields(self):
|
||||
'''Test fields consistency for ('account.group', 'account.group.template')
|
||||
'''
|
||||
self.check_fields_consistency('account.group', 'account.group.template', exceptions=['company_id', 'parent_path'])
|
||||
self.check_fields_consistency('account.group.template', 'account.group', exceptions=['chart_template_id'])
|
||||
|
|
@ -1,19 +1,23 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
from odoo import Command, fields
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestUi(AccountTestInvoicingCommon, odoo.tests.HttpCase):
|
||||
class TestUi(AccountTestInvoicingHttpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
all_moves = cls.env['account.move'].search([('move_type', '!=', 'entry')])
|
||||
all_moves = all_moves.filtered(lambda m: not m.inalterable_hash and m.state in ('posted', 'cancel'))
|
||||
# This field is only present in account_accountant
|
||||
if 'deferred_move_ids' in all_moves._fields:
|
||||
all_moves = all_moves.filtered(lambda m: not m.deferred_move_ids)
|
||||
all_moves.button_draft()
|
||||
all_moves.with_context(force_delete=True).unlink()
|
||||
|
||||
|
|
@ -31,18 +35,34 @@ class TestUi(AccountTestInvoicingCommon, odoo.tests.HttpCase):
|
|||
self.env.ref('base.user_admin').write({
|
||||
'company_id': self.env.company.id,
|
||||
'company_ids': [(4, self.env.company.id)],
|
||||
'email': 'mitchell.admin@example.com',
|
||||
})
|
||||
self.env.company.write({
|
||||
'country_id': None, # Also resets account_fiscal_country_id
|
||||
'account_sale_tax_id': None,
|
||||
'account_purchase_tax_id': None,
|
||||
'external_report_layout_id': self.env.ref('web.external_layout_standard').id,
|
||||
})
|
||||
|
||||
account_with_taxes = self.env['account.account'].search([('tax_ids', '!=', False), ('company_id', '=', self.env.company.id)])
|
||||
account_with_taxes = self.env['account.account'].search([('tax_ids', '!=', False), ('company_ids', '=', self.env.company.id)])
|
||||
account_with_taxes.write({
|
||||
'tax_ids': [Command.clear()],
|
||||
})
|
||||
self.start_tour("/web", 'account_tour', login="admin")
|
||||
|
||||
# Remove all posted invoices to enable 'create first invoice' button
|
||||
invoices = self.env['account.move'].search([('company_id', '=', self.env.company.id), ('move_type', '=', 'out_invoice')])
|
||||
for invoice in invoices:
|
||||
if invoice.state in ('cancel', 'posted'):
|
||||
invoice.button_draft()
|
||||
invoices.unlink()
|
||||
|
||||
# remove all entries in the miscellaneous journal to test the onboarding
|
||||
self.env['account.move'].search([
|
||||
('journal_id.type', '=', 'general'),
|
||||
('state', '=', 'draft'),
|
||||
]).unlink()
|
||||
|
||||
self.start_tour("/odoo", 'account_tour', login="admin")
|
||||
|
||||
def test_01_account_tax_groups_tour(self):
|
||||
self.env.ref('base.user_admin').write({
|
||||
|
|
@ -57,7 +77,7 @@ class TestUi(AccountTestInvoicingCommon, odoo.tests.HttpCase):
|
|||
'name': 'Account Tax Group Product',
|
||||
'standard_price': 600.0,
|
||||
'list_price': 147.0,
|
||||
'detailed_type': 'consu',
|
||||
'type': 'consu',
|
||||
})
|
||||
new_tax = self.env['account.tax'].create({
|
||||
'name': '10% Tour Tax',
|
||||
|
|
@ -67,4 +87,32 @@ class TestUi(AccountTestInvoicingCommon, odoo.tests.HttpCase):
|
|||
})
|
||||
product.supplier_taxes_id = new_tax
|
||||
|
||||
self.start_tour("/web", 'account_tax_group', login="admin")
|
||||
self.start_tour("/odoo", 'account_tax_group', login="admin")
|
||||
|
||||
def test_use_product_catalog_on_invoice(self):
|
||||
self.product.write({
|
||||
'is_favorite': True,
|
||||
'default_code': '0',
|
||||
})
|
||||
self.start_tour("/odoo/customer-invoices/new", 'test_use_product_catalog_on_invoice', login="admin")
|
||||
|
||||
def test_deductible_amount_column(self):
|
||||
self.assertFalse(self.env.user.has_group('account.group_partial_purchase_deductibility'))
|
||||
partner = self.env['res.partner'].create({'name': "Test Partner", 'email': "test@test.odoo.com"})
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': partner.id,
|
||||
'invoice_date': fields.Date.today(),
|
||||
'line_ids': [Command.create({'name': "T-shirt", 'deductible_amount': 50.0})],
|
||||
})
|
||||
move.action_post()
|
||||
self.assertTrue(self.env.user.has_group('account.group_partial_purchase_deductibility'))
|
||||
self.start_tour("/odoo/vendor-bills/new", 'deductible_amount_column', login=self.env.user.login)
|
||||
|
||||
def test_add_section_from_product_catalog_on_invoice_tour(self):
|
||||
self.product.write({'is_favorite': True})
|
||||
self.start_tour(
|
||||
'/odoo/customer-invoices/new',
|
||||
'test_add_section_from_product_catalog_on_invoice',
|
||||
login='admin',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ import time
|
|||
class TestTransferWizard(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.company = cls.company_data['company']
|
||||
cls.receivable_account = cls.company_data['default_account_receivable']
|
||||
cls.payable_account = cls.company_data['default_account_payable']
|
||||
cls.accounts = cls.env['account.account'].search([('reconcile', '=', False), ('company_id', '=', cls.company.id)], limit=5)
|
||||
cls.accounts = cls.env['account.account'].search([('reconcile', '=', False), ('company_ids', '=', cls.company.id)], limit=5)
|
||||
cls.journal = cls.company_data['default_journal_misc']
|
||||
|
||||
# Set rate for base currency to 1
|
||||
|
|
@ -193,8 +193,8 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
cls.move_2.action_post()
|
||||
|
||||
analytic_plan_1, analytic_plan_2 = cls.env['account.analytic.plan'].create([
|
||||
{'name': 'Plan Test 1', 'company_id': False},
|
||||
{'name': 'Plan Test 2', 'company_id': False},
|
||||
{'name': 'Plan Test 1'},
|
||||
{'name': 'Plan Test 2'},
|
||||
])
|
||||
cls.analytic_account_1, cls.analytic_account_2 = cls.env['account.analytic.account'].create([
|
||||
{
|
||||
|
|
@ -234,7 +234,7 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
}),
|
||||
Command.create({
|
||||
'account_id': self.payable_account.id,
|
||||
'balance': -460,
|
||||
'balance': -400,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
|
@ -242,16 +242,14 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
|
||||
self.assertRecordValues(move_with_tax.line_ids, [
|
||||
{'balance': 400, 'account_id': account_with_tax.id},
|
||||
{'balance': -460, 'account_id': self.payable_account.id},
|
||||
{'balance': 60, 'account_id': self.company_data['default_account_tax_purchase'].id},
|
||||
{'balance': -400, 'account_id': self.payable_account.id},
|
||||
])
|
||||
|
||||
# Open the transfer wizard
|
||||
|
||||
# We use a form to pass the context properly to the depends_context move_line_ids field
|
||||
context = {'active_model': 'account.move.line', 'active_ids': move_with_tax.line_ids[0].ids}
|
||||
context = {'active_model': 'account.move.line', 'active_ids': move_with_tax.line_ids[0].ids, 'default_action': 'change_period'}
|
||||
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
|
||||
wizard_form.action = 'change_period'
|
||||
wizard_form.date = '2019-05-01'
|
||||
wizard_form.journal_id = self.company_data['default_journal_misc']
|
||||
wizard_form.expense_accrual_account = expense_accrual_account
|
||||
|
|
@ -278,9 +276,8 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
active_move_lines = (self.move_1 + self.move_2).mapped('line_ids').filtered(lambda x: x.account_id.account_type in ('asset_receivable', 'liability_payable'))
|
||||
|
||||
# We use a form to pass the context properly to the depends_context move_line_ids field
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids, 'default_action': 'change_account'}
|
||||
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
|
||||
wizard_form.action = 'change_account'
|
||||
wizard_form.destination_account_id = self.receivable_account
|
||||
wizard_form.journal_id = self.journal
|
||||
wizard = wizard_form.save()
|
||||
|
|
@ -304,9 +301,8 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
active_move_lines = (self.move_1 + self.move_2).mapped('line_ids').filtered(lambda x: x.name in ('test1_3', 'test1_4', 'test1_5', 'test2_3', 'test2_4', 'test2_5', 'test2_6', 'test2_8'))
|
||||
|
||||
# We use a form to pass the context properly to the depends_context move_line_ids field
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids, 'default_action': 'change_account'}
|
||||
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
|
||||
wizard_form.action = 'change_account'
|
||||
wizard_form.destination_account_id = self.accounts[4]
|
||||
wizard_form.journal_id = self.journal
|
||||
wizard = wizard_form.save()
|
||||
|
|
@ -335,9 +331,8 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
active_move_lines = self.move_1.mapped('line_ids').filtered(lambda x: x.name in ('test1_6', 'test1_9'))
|
||||
|
||||
# We use a form to pass the context properly to the depends_context move_line_ids field
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids, 'default_action': 'change_account'}
|
||||
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
|
||||
wizard_form.action = 'change_account'
|
||||
wizard_form.destination_account_id = self.test_currency_account
|
||||
wizard_form.journal_id = self.journal
|
||||
wizard = wizard_form.save()
|
||||
|
|
@ -358,9 +353,8 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
active_move_lines = self.move_2.mapped('line_ids').filtered(lambda x: x.name in ('test2_9', 'test2_6', 'test2_8'))
|
||||
|
||||
# We use a form to pass the context properly to the depends_context move_line_ids field
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids, 'default_action': 'change_account'}
|
||||
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
|
||||
wizard_form.action = 'change_account'
|
||||
wizard_form.destination_account_id = self.receivable_account
|
||||
wizard_form.journal_id = self.journal
|
||||
wizard = wizard_form.save()
|
||||
|
|
@ -391,19 +385,32 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
'reconcile': True,
|
||||
})
|
||||
|
||||
account_with_tax = self.env['account.account'].create({
|
||||
'name': 'Auto Taxed',
|
||||
'code': 'autotaxed',
|
||||
'account_type': 'income',
|
||||
'tax_ids': [Command.link(self.company_data['default_tax_sale'].id)],
|
||||
})
|
||||
|
||||
# Create a move before the lock date
|
||||
move = self.env['account.move'].create({
|
||||
'journal_id': self.company_data['default_journal_sale'].id,
|
||||
'date': '2019-01-01',
|
||||
'line_ids': [
|
||||
Command.create({'account_id': self.accounts[0].id, 'debit': 1000, }),
|
||||
Command.create({'account_id': self.accounts[0].id, 'credit': 1000, }),
|
||||
Command.create({'account_id': account_with_tax.id, 'debit': 1000}),
|
||||
Command.create({'account_id': account_with_tax.id, 'credit': 1000}),
|
||||
]
|
||||
})
|
||||
move.action_post()
|
||||
|
||||
# Set the lock date
|
||||
move.company_id.write({'period_lock_date': '2019-02-28', 'fiscalyear_lock_date': '2019-02-28'})
|
||||
# (Purchase Lock Date not tested)
|
||||
move.company_id.write({
|
||||
'hard_lock_date': '2019-02-28',
|
||||
'fiscalyear_lock_date': '2019-02-28',
|
||||
'sale_lock_date': '2019-02-28',
|
||||
'tax_lock_date': '2019-02-28',
|
||||
})
|
||||
|
||||
# Open the transfer wizard at a date after the lock date
|
||||
wizard = self.env['account.automatic.entry.wizard'] \
|
||||
|
|
@ -414,9 +421,9 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
'journal_id': self.company_data['default_journal_misc'].id,
|
||||
})
|
||||
|
||||
# Check that the 'The date is being set prior to the user lock date' message appears.
|
||||
# Check that the 'The date is being set prior to ...' message appears.
|
||||
self.assertRecordValues(wizard, [{
|
||||
'lock_date_message': 'The date is being set prior to the user lock date 02/28/2019. '
|
||||
'lock_date_message': 'The date is being set prior to: Global Lock Date (02/28/2019), Hard Lock Date (02/28/2019), and Sales Lock Date (02/28/2019). '
|
||||
'The Journal Entry will be accounted on 03/31/2019 upon posting.'
|
||||
}])
|
||||
|
||||
|
|
@ -483,22 +490,20 @@ class TestTransferWizard(AccountTestInvoicingCommon):
|
|||
'journal_id': self.company_data['default_journal_misc'].id,
|
||||
'date': '2019-01-01',
|
||||
'line_ids': [
|
||||
Command.create({'account_id': self.accounts[2].id, 'currency_id': self.company.currency_id.id, 'amount_currency': 1000, 'debit': 1000, }),
|
||||
Command.create({'account_id': self.receivable_account.id, 'currency_id': self.test_currency_1.id, 'amount_currency': 0, 'credit': 1000, }),
|
||||
Command.create({'account_id': self.accounts[2].id, 'currency_id': self.company.currency_id.id, 'amount_currency': 1000, 'debit': 1000}),
|
||||
Command.create({'account_id': self.receivable_account.id, 'currency_id': self.test_currency_1.id, 'amount_currency': 0, 'credit': 1000}),
|
||||
]
|
||||
})
|
||||
move.action_post()
|
||||
|
||||
active_move_lines = move.line_ids.filtered(lambda line: line.account_id.id == self.receivable_account.id)
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
|
||||
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids, 'default_action': 'change_account'}
|
||||
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
|
||||
wizard_form.action = 'change_account'
|
||||
wizard_form.destination_account_id = self.accounts[0]
|
||||
wizard_form.journal_id = self.company_data['default_journal_misc']
|
||||
automatic_entry_wizard = wizard_form.save()
|
||||
transfer_move_id = automatic_entry_wizard.do_action()['res_id']
|
||||
|
||||
wizard = wizard_form.save()
|
||||
|
||||
transfer_move_id = wizard.do_action()['res_id']
|
||||
transfer_move = self.env['account.move'].browse(transfer_move_id)
|
||||
|
||||
source_line = transfer_move.line_ids.filtered(lambda x: x.account_id == self.receivable_account)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from datetime import date, timedelta
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestUnexpectedAmount(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context={
|
||||
**cls.env.context,
|
||||
'disable_abnormal_invoice_detection': False,
|
||||
})
|
||||
|
||||
def _invoice_vals(self, date='2020-01-01', price_unit=100):
|
||||
return {
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': date,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'name': 'product',
|
||||
'price_unit': price_unit,
|
||||
'tax_ids': [Command.clear()],
|
||||
})
|
||||
],
|
||||
}
|
||||
|
||||
def test_higher_amount(self):
|
||||
base = self.env['account.move'].create([self._invoice_vals(price_unit=100) for i in range(10)])
|
||||
base.action_post()
|
||||
bills = self.env['account.move'].create([
|
||||
self._invoice_vals(price_unit=100),
|
||||
self._invoice_vals(price_unit=200),
|
||||
self._invoice_vals(price_unit=50),
|
||||
self._invoice_vals(price_unit=10),
|
||||
])
|
||||
self.assertFalse(bills[0].abnormal_amount_warning, "The price of 100 is not deviant and thus shouldn't trigger a warning")
|
||||
self.assertTrue(bills[1].abnormal_amount_warning, "The price of 200 is deviant and thus should trigger a warning")
|
||||
self.assertTrue(bills[2].abnormal_amount_warning, "The price of 50 is deviant and thus should trigger a warning")
|
||||
self.assertTrue(bills[3].abnormal_amount_warning, "The price of 10 is deviant and thus should trigger a warning")
|
||||
|
||||
# cleaning the bills context to have an unbiased env test for the wizard trigerring
|
||||
bills = bills.with_context({
|
||||
k: v for k, v in self.env.context.items() if k != 'disable_abnormal_invoice_detection'
|
||||
})
|
||||
|
||||
wizard_thrown = bills[0].action_post()
|
||||
self.assertFalse(wizard_thrown,
|
||||
"Invoice is not deviant, no wizard should be thrown")
|
||||
|
||||
wizard_thrown = bills[1].action_post()
|
||||
self.assertFalse(wizard_thrown,
|
||||
"The amount is deviant but the context key isn't set, the wizard shouldn't be thrown")
|
||||
|
||||
wizard_thrown = bills[2].with_context(disable_abnormal_invoice_detection=True).action_post()
|
||||
self.assertFalse(wizard_thrown,
|
||||
"The amount is deviant and the context key is set to True, the wizard shouldn't be thrown")
|
||||
|
||||
wizard_thrown = bills[3].with_context(disable_abnormal_invoice_detection=False).action_post()
|
||||
self.assertTrue(wizard_thrown,
|
||||
"The amount is deviant and the context key is set to False, the wizard shouldn't be thrown")
|
||||
|
||||
def test_date_too_soon_year(self):
|
||||
base = self.env['account.move'].create([
|
||||
self._invoice_vals(date=f'{year}-01-01')
|
||||
for year in range(2000, 2010)
|
||||
])
|
||||
base.action_post()
|
||||
|
||||
move = self.env['account.move'].create(self._invoice_vals(date='2010-01-01'))
|
||||
self.assertFalse(move.abnormal_date_warning)
|
||||
move = self.env['account.move'].create(self._invoice_vals(date='2009-06-01'))
|
||||
self.assertTrue(move.abnormal_date_warning)
|
||||
|
||||
def test_date_too_soon_month(self):
|
||||
# We get one invoice on the last day of the month from December 2019 to September 2020
|
||||
base = self.env['account.move'].create([
|
||||
self._invoice_vals(date=date(2020, month, 1) - timedelta(days=1))
|
||||
for month in range(1, 11)
|
||||
])
|
||||
base.action_post()
|
||||
|
||||
# No issue in having an invoice missing a period, it is the vendor's responsibility
|
||||
move_november = self.env['account.move'].create(self._invoice_vals(date=date(2020, 11, 30)))
|
||||
self.assertFalse(move_november.abnormal_date_warning)
|
||||
# The next invoice being on the last day of november is expected
|
||||
move_october = self.env['account.move'].create(self._invoice_vals(date=date(2020, 10, 31)))
|
||||
self.assertFalse(move_october.abnormal_date_warning)
|
||||
# But any invoice before the threshold is not expected
|
||||
move_october2 = self.env['account.move'].create(self._invoice_vals(date=date(2020, 10, 20)))
|
||||
self.assertTrue(move_october2.abnormal_date_warning)
|
||||
|
||||
# If we posted the one with the abnormal date, then the other one becomes abnormal
|
||||
move_october2._post(soft=False)
|
||||
move_october.invalidate_recordset(['abnormal_date_warning'])
|
||||
self.assertTrue(move_october.abnormal_date_warning)
|
||||
Loading…
Add table
Add a link
Reference in a new issue