Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
from . import common
from . import test_account_asset
from . import test_account_auto_reconcile_wizard
from . import test_account_fiscal_year
from . import test_account_reconcile_wizard
from . import test_analytic_reports
from . import test_bank_rec_widget_common
from . import test_bank_rec_widget
from . import test_bank_rec_widget_tour
from . import test_board_compute
from . import test_change_lock_date_wizard
from . import test_deferred_management
from . import test_financial_report
from . import test_import_bank_statement
from . import test_prediction
from . import test_reconciliation_matching_rules
from . import test_reconciliation_widget
from . import test_reevaluation_asset
from . import test_signature
from . import test_tour
from . import test_ui

View File

@@ -0,0 +1,429 @@
from odoo import fields
import copy
import io
import unittest
from collections import Counter
from datetime import datetime, date
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
try:
from openpyxl import load_workbook
except ImportError:
load_workbook = None
from odoo import Command, fields
from odoo.exceptions import UserError
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
from odoo.tools.misc import formatLang, file_open
class TestAccountAssetCommon(AccountTestInvoicingCommon):
@classmethod
def create_asset(cls, value, periodicity, periods, degressive_factor=None, import_depreciation=0, **kwargs):
if degressive_factor is not None:
kwargs["method_progress_factor"] = degressive_factor
return cls.env['account.asset'].create({
'name': 'nice asset',
'account_asset_id': cls.company_data['default_account_assets'].id,
'account_depreciation_id': cls.company_data['default_account_assets'].copy().id,
'account_depreciation_expense_id': cls.company_data['default_account_expense'].id,
'journal_id': cls.company_data['default_journal_misc'].id,
'acquisition_date': "2020-02-01",
'prorata_computation_type': 'none',
'original_value': value,
'salvage_value': 0,
'method_number': periods,
'method_period': '12' if periodicity == "yearly" else '1',
'method': "linear",
'already_depreciated_amount_import': import_depreciation,
**kwargs,
})
@classmethod
def _get_depreciation_move_values(cls, date, depreciation_value, remaining_value, depreciated_value, state):
return {
'date': fields.Date.from_string(date),
'depreciation_value': depreciation_value,
'asset_remaining_value': remaining_value,
'asset_depreciated_value': depreciated_value,
'state': state,
}
class TestAccountReportsCommon(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.other_currency = cls.setup_other_currency('CAD')
cls.company_data_2 = cls.setup_other_company()
cls.company_data_2['company'].currency_id = cls.other_currency
cls.company_data_2['currency'] = cls.other_currency
@classmethod
def _generate_options(cls, report, date_from, date_to, default_options=None):
''' Create new options at a certain date.
:param report: The report.
:param date_from: A datetime object, str representation of a date or False.
:param date_to: A datetime object or str representation of a date.
:return: The newly created options.
'''
if isinstance(date_from, datetime):
date_from_str = fields.Date.to_string(date_from)
else:
date_from_str = date_from
if isinstance(date_to, datetime):
date_to_str = fields.Date.to_string(date_to)
else:
date_to_str = date_to
if not default_options:
default_options = {}
return report.get_options({
'selected_variant_id': report.id,
'date': {
'date_from': date_from_str,
'date_to': date_to_str,
'mode': 'range',
'filter': 'custom',
},
'show_account': True,
'show_currency': True,
**default_options,
})
def _update_comparison_filter(self, options, report, comparison_type, number_period, date_from=None, date_to=None):
''' Modify the existing options to set a new filter_comparison.
:param options: The report options.
:param report: The report.
:param comparison_type: One of the following values: ('no_comparison', 'custom', 'previous_period', 'previous_year').
:param number_period: The number of period to compare.
:param date_from: A datetime object for the 'custom' comparison_type.
:param date_to: A datetime object the 'custom' comparison_type.
:return: The newly created options.
'''
previous_options = {**options, 'comparison': {
**options['comparison'],
'date_from': date_from and date_from.strftime(DEFAULT_SERVER_DATE_FORMAT),
'date_to': date_to and date_to.strftime(DEFAULT_SERVER_DATE_FORMAT),
'filter': comparison_type,
'number_period': number_period,
}}
return report.get_options(previous_options)
def _update_multi_selector_filter(self, options, option_key, selected_ids):
''' Modify a selector in the options to select .
:param options: The report options.
:param option_key: The key to the option.
:param selected_ids: The ids to be selected.
:return: The newly created options.
'''
new_options = copy.deepcopy(options)
for c in new_options[option_key]:
c['selected'] = c['id'] in selected_ids
return new_options
def assertColumnPercentComparisonValues(self, lines, expected_values):
filtered_lines = self._filter_folded_lines(lines)
# Check number of lines.
self.assertEqual(len(filtered_lines), len(expected_values))
for value, expected_value in zip(filtered_lines, expected_values):
# Check number of columns.
key = 'column_percent_comparison_data'
self.assertEqual(len(value[key]) + 1, len(expected_value))
# Check name, value and class.
self.assertEqual((value['name'], value[key]['name'], value[key]['mode']), expected_value)
def assertHorizontalGroupTotal(self, lines, expected_values):
filtered_lines = self._filter_folded_lines(lines)
# Check number of lines.
self.assertEqual(len(filtered_lines), len(expected_values))
for line_dict_list, expected_values in zip(filtered_lines, expected_values):
column_values = [column['no_format'] for column in line_dict_list['columns']]
# Compare the Total column, Total column is there only under certain condition
if line_dict_list.get('horizontal_group_total_data'):
self.assertEqual(len(line_dict_list['columns']) + 1, len(expected_values[1:]))
# Compare the numbers column except the total
self.assertEqual(column_values, list(expected_values[1:-1]))
# Compare the total column
self.assertEqual(line_dict_list['horizontal_group_total_data']['no_format'], expected_values[-1])
else:
# No total column
self.assertEqual(len(line_dict_list['columns']), len(expected_values[1:]))
self.assertEqual(column_values, list(expected_values[1:]))
def assertHeadersValues(self, headers, expected_headers):
''' Helper to compare the headers returned by the _get_table method
with some expected results.
An header is a row of columns. Then, headers is a list of list of dictionary.
:param headers: The headers to compare.
:param expected_headers: The expected headers.
:return:
'''
# Check number of header lines.
self.assertEqual(len(headers), len(expected_headers))
for header, expected_header in zip(headers, expected_headers):
# Check number of columns.
self.assertEqual(len(header), len(expected_header))
for i, column in enumerate(header):
# Check name.
self.assertEqual(column['name'], self._convert_str_to_date(column['name'], expected_header[i]))
def assertIdenticalLines(self, reports):
"""Helper to compare report lines with the same `code` across multiple reports.
The helper checks the lines for similarity on:
- number of expressions
- expression label
- expression engine
- expression formula
- expression subformula
- expression date_scope
:param reports: (recordset of account.report) The reports to check
"""
def expression_to_comparable_values(expr):
return (
expr.label,
expr.engine,
expr.formula,
expr.subformula,
expr.date_scope
)
if not reports:
raise UserError('There are no reports to compare.')
visited_line_codes = set()
for line in reports.line_ids:
if not line.code or line.code in visited_line_codes:
continue
identical_lines = reports.line_ids.filtered(lambda l: l != line and l.code == line.code)
if not identical_lines:
continue
with self.subTest(line_code=line.code):
for tested_line in identical_lines:
self.assertCountEqual(
line.expression_ids.mapped(expression_to_comparable_values),
tested_line.expression_ids.mapped(expression_to_comparable_values),
(
f'The line with code {line.code} from reports "{line.report_id.name}" and '
f'"{tested_line.report_id.name}" has different expression values in both reports.'
)
)
visited_line_codes.add(line.code)
def assertLinesValues(self, lines, columns, expected_values, options, currency_map=None, ignore_folded=True):
''' Helper to compare the lines returned by the _get_lines method
with some expected results and ensuring the 'id' key of each line holds a unique value.
:param lines: See _get_lines.
:param columns: The columns index.
:param expected_values: A list of iterables.
:param options: The options from the current report.
:param currency_map: A map mapping each column_index to some extra options to test the lines:
- currency: The currency to be applied on the column.
- currency_code_index: The index of the column containing the currency code.
:param ignore_folded: Will not filter folded lines when True.
'''
if currency_map is None:
currency_map = {}
filtered_lines = self._filter_folded_lines(lines) if ignore_folded else lines
# Compare the table length to see if any line is missing
self.assertEqual(len(filtered_lines), len(expected_values))
# Compare cell by cell the current value with the expected one.
to_compare_list = []
for i, line in enumerate(filtered_lines):
compared_values = [[], []]
for j, index in enumerate(columns):
if index == 0:
current_value = line['name']
else:
# Some lines may not have columns, like title lines. In such case, no values should be provided for these.
# Note that the function expect a tuple, so the line still need a comma after the name value.
if j > len(expected_values[i]) - 1:
break
current_value = line['columns'][index-1].get('name', '')
current_figure_type = line['columns'][index - 1].get('figure_type', '')
expected_value = expected_values[i][j]
currency_data = currency_map.get(index, {})
used_currency = None
if 'currency' in currency_data:
used_currency = currency_data['currency']
elif 'currency_code_index' in currency_data:
currency_code = line['columns'][currency_data['currency_code_index'] - 1].get('name', '')
if currency_code:
used_currency = self.env['res.currency'].search([('name', '=', currency_code)], limit=1)
assert used_currency, "Currency having name=%s not found." % currency_code
if not used_currency:
used_currency = self.env.company.currency_id
if type(expected_value) in (int, float) and type(current_value) == str:
if current_figure_type and current_figure_type != 'monetary':
expected_value = str(expected_value)
elif options.get('multi_currency'):
expected_value = formatLang(self.env, expected_value, currency_obj=used_currency)
else:
expected_value = formatLang(self.env, expected_value, digits=used_currency.decimal_places)
compared_values[0].append(current_value)
compared_values[1].append(expected_value)
to_compare_list.append(compared_values)
errors = []
for i, to_compare in enumerate(to_compare_list):
if to_compare[0] != to_compare[1]:
errors += [
"\n==== Differences at index %s ====" % str(i),
"Current Values: %s" % str(to_compare[0]),
"Expected Values: %s" % str(to_compare[1]),
]
id_counts = Counter(line['id'] for line in lines)
duplicate_ids = {k: v for k, v in id_counts.items() if v > 1}
if duplicate_ids:
index_to_id = [
f"index={index:<6} name={line.get('name', 'no line name?!')} \tline_id={line.get('id', 'no line id?!')}"
for index, line in enumerate(lines)
if line.get('id', 'no line id?!') in duplicate_ids
]
errors += [
"\n==== There are lines sharing the same id ====",
"\n".join(index_to_id)
]
if errors:
self.fail('\n'.join(errors))
def _filter_folded_lines(self, lines):
""" Children lines returned for folded lines (for example, totals below sections) should be ignored when comparing the results
in assertLinesValues (their parents are folded, so they are not shown anyway). This function returns a filtered version of lines
list, without the chilren of folded lines.
"""
filtered_lines = []
folded_lines = set()
for line in lines:
if line.get('parent_id') in folded_lines:
folded_lines.add(line['id'])
else:
if line.get('unfoldable') and not line.get('unfolded'):
folded_lines.add(line['id'])
filtered_lines.append(line)
return filtered_lines
def _convert_str_to_date(self, ref, val):
if isinstance(ref, date) and isinstance(val, str):
return datetime.strptime(val, '%Y-%m-%d').date()
return val
@classmethod
def _create_tax_report_line(cls, name, report, tag_name=None, parent_line=None, sequence=None, code=None, formula=None):
""" Creates a tax report line
"""
create_vals = {
'name': name,
'code': code,
'report_id': report.id,
'sequence': sequence,
'expression_ids': [],
}
if tag_name and formula:
raise UserError("Can't use this helper to create a line with both tags and formula")
if tag_name:
create_vals['expression_ids'].append(Command.create({
"label": "balance",
"engine": "tax_tags",
"formula": tag_name,
}))
if parent_line:
create_vals['parent_id'] = parent_line.id
if formula:
create_vals['expression_ids'].append(Command.create({
"label": "balance",
"engine": "aggregation",
"formula": formula,
}))
return cls.env['account.report.line'].create(create_vals)
@classmethod
def _get_tag_ids(cls, sign, expressions, company=False):
""" Helper function to define tag ids for taxes """
return [(6, 0, cls.env['account.account.tag'].search([
('applicability', '=', 'taxes'),
('country_id.code', '=', (company or cls.env.company).account_fiscal_country_id.code),
('name', 'in', [f"{sign}{f}" for f in expressions.mapped('formula')]),
]).ids)]
@classmethod
def _get_basic_line_dict_id_from_report_line(cls, report_line):
""" Computes a full generic id for the provided report line (hence including the one of its parent as prefix), using no markup.
"""
report = report_line.report_id
if report_line.parent_id:
parent_line_id = cls._get_basic_line_dict_id_from_report_line(report_line.parent_id)
return report._get_generic_line_id(report_line._name, report_line.id, parent_line_id=parent_line_id)
return report._get_generic_line_id(report_line._name, report_line.id)
@classmethod
def _get_basic_line_dict_id_from_report_line_ref(cls, report_line_xmlid):
""" Same as _get_basic_line_dict_id_from_report_line, but from the line's xmlid, for convenience in the tests.
"""
return cls._get_basic_line_dict_id_from_report_line(cls.env.ref(report_line_xmlid))
@classmethod
def _get_audit_params_from_report_line(cls, options, report_line, report_line_dict, **kwargs):
return {
'report_line_id': report_line.id,
'calling_line_dict_id': report_line_dict['id'],
'expression_label': 'balance',
'column_group_key': next(iter(options['column_groups'])),
**kwargs,
}
def _report_compare_with_test_file(self, report, xml_file=None, test_xml=None):
report_xml = self.get_xml_tree_from_string(report['file_content'])
if xml_file and not test_xml:
with file_open(f"{self.test_module}/tests/expected_xmls/{xml_file}", 'rb') as fp:
test_xml = fp.read()
test_xml_tree = self.get_xml_tree_from_string(test_xml)
self.assertXmlTreeEqual(report_xml, test_xml_tree)
@classmethod
def _fill_tax_report_line_external_value(cls, target, amount, date):
cls.env['account.report.external.value'].create({
'company_id': cls.company_data['company'].id,
'target_report_expression_id': cls.env.ref(target).id,
'name': 'Manual value',
'date': fields.Date.from_string(date),
'value': amount,
})
def _test_xlsx_file(self, file_content, expected_values):
""" Takes in the binary content of a xlsx file and a dict of expected values.
It will then parse the file in order to compare the values with the expected ones.
The expected values dict format is:
'row_number': ['cell_1_val', 'cell_2_val', ...]
:param file_content: The binary content of the xlsx file
:param expected_values: The dict of expected values
"""
if load_workbook is None:
raise unittest.SkipTest("openpyxl not available")
report_file = io.BytesIO(file_content)
xlsx = load_workbook(filename=report_file, data_only=True)
sheet = xlsx.worksheets[0]
sheet_values = list(sheet.values)
for row, values in expected_values.items():
row_values = [v if v is not None else '' for v in sheet_values[row]]
for row_value, expected_value in zip(row_values, values):
self.assertEqual(row_value, expected_value)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,244 @@
from odoo import fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.exceptions import UserError
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAccountAutoReconcileWizard(AccountTestInvoicingCommon):
""" Tests the account automatic reconciliation and its wizard. """
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.comp_curr = cls.company_data['currency']
cls.foreign_curr = cls.setup_other_currency('EUR')
cls.misc_journal = cls.company_data['default_journal_misc']
cls.partners = cls.partner_a + cls.partner_b
cls.receivable_account = cls.company_data['default_account_receivable']
cls.payable_account = cls.company_data['default_account_payable']
cls.revenue_account = cls.company_data['default_account_revenue']
cls.test_date = fields.Date.from_string('2016-01-01')
def _create_many_lines(self):
self.line_1_group_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
self.line_2_group_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
self.line_3_group_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-03', partner=self.partner_a)
self.line_4_group_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-04', partner=self.partner_a)
self.line_5_group_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-05', partner=self.partner_a)
self.group_1 = self.line_1_group_1 + self.line_2_group_1 + self.line_3_group_1 + self.line_4_group_1 + self.line_5_group_1
self.line_1_group_2 = self.create_line_for_reconciliation(500.0, 500.0, self.comp_curr, '2016-01-01', partner=self.partner_b)
self.line_2_group_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.comp_curr, '2016-01-01', partner=self.partner_b)
self.line_3_group_2 = self.create_line_for_reconciliation(500.0, 500.0, self.comp_curr, '2017-01-02', partner=self.partner_b)
self.line_4_group_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.comp_curr, '2017-01-02', partner=self.partner_b)
self.group_2 = self.line_1_group_2 + self.line_2_group_2 + self.line_3_group_2 + self.line_4_group_2
self.line_1_group_3 = self.create_line_for_reconciliation(1500.0, 3000.0, self.foreign_curr, '2016-01-01', partner=self.partner_b)
self.line_2_group_3 = self.create_line_for_reconciliation(-1000.0, -3000.0, self.foreign_curr, '2017-01-01', partner=self.partner_b)
self.line_3_group_3 = self.create_line_for_reconciliation(3000.0, 3000.0, self.comp_curr, '2016-01-01', partner=self.partner_b)
self.line_4_group_3 = self.create_line_for_reconciliation(-3000.0, -3000.0, self.comp_curr, '2016-01-01', partner=self.partner_b)
self.group_3 = self.line_1_group_3 + self.line_2_group_3 + self.line_3_group_3 + self.line_4_group_3
self.line_1_group_4 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', account_1=self.payable_account, partner=self.partner_a)
self.line_2_group_4 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', account_1=self.payable_account, partner=self.partner_a)
self.group_4 = self.line_1_group_4 + self.line_2_group_4
def test_auto_reconcile_one_to_one(self):
self._create_many_lines()
should_be_reconciled = self.line_1_group_1 + self.line_2_group_1 + self.line_3_group_1 + self.line_4_group_1 \
+ self.line_1_group_2 + self.line_2_group_2 \
+ self.line_1_group_3 + self.line_2_group_3 + self.line_3_group_3 + self.line_4_group_3
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'account_ids': self.receivable_account.ids,
'partner_ids': self.partners.ids,
'search_mode': 'one_to_one',
})
wizard.auto_reconcile()
self.assertTrue(should_be_reconciled.full_reconcile_id)
self.assertEqual(self.line_1_group_1.full_reconcile_id, self.line_2_group_1.full_reconcile_id,
"Entries should be reconciled together since they are in the same group and have closer dates.")
self.assertEqual(self.line_3_group_1.full_reconcile_id, self.line_4_group_1.full_reconcile_id,
"Entries should be reconciled together since they are in the same group and have closer dates.")
self.assertEqual(self.line_1_group_2.full_reconcile_id, self.line_1_group_2.full_reconcile_id,
"Entries should be reconciled together since they are in the same group and have closer dates.")
self.assertEqual(self.line_1_group_3.full_reconcile_id, self.line_2_group_3.full_reconcile_id,
"Entries should be reconciled together since they are in the same group and have closer dates.")
self.assertEqual(self.line_3_group_3.full_reconcile_id, self.line_4_group_3.full_reconcile_id,
"Entries should be reconciled together since they are in the same group and have closer dates.")
self.assertNotEqual(self.line_2_group_3.full_reconcile_id, self.line_3_group_3.full_reconcile_id,
"Entries should NOT be reconciled together as they are of different currencies.")
self.assertFalse(self.line_5_group_1.reconciled,
"This entry shouldn't be reconciled since group 1 has an odd number of lines, they can't all be reconciled, and it's the most recent one.")
self.assertFalse((self.line_3_group_2 + self.line_4_group_2).full_reconcile_id,
"Entries shouldn't be reconciled since it's outside of accepted date range of the wizard.")
self.assertFalse((self.line_1_group_4 + self.line_2_group_4).full_reconcile_id,
"Entries shouldn't be reconciled since their account is out of the wizard's scope.")
def test_auto_reconcile_zero_balance(self):
self._create_many_lines()
should_be_reconciled = self.line_1_group_2 + self.line_2_group_2 + self.group_3
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'account_ids': self.receivable_account.ids,
'partner_ids': self.partners.ids,
'search_mode': 'zero_balance',
})
wizard.auto_reconcile()
self.assertTrue(should_be_reconciled.full_reconcile_id)
self.assertFalse(self.group_1.full_reconcile_id,
"Entries shouldn't be reconciled since their total balance is not zero.")
self.assertEqual((self.line_1_group_2 + self.line_2_group_2).mapped('matching_number'), [self.line_1_group_2.matching_number] * 2,
"Entries should be reconciled together as their total balance is zero.")
self.assertEqual((self.line_1_group_3 + self.line_2_group_3).mapped('matching_number'), [self.line_1_group_3.matching_number] * 2,
"Entries should be reconciled together as their total balance is zero with the same currency.")
self.assertEqual((self.line_3_group_3 + self.line_4_group_3).mapped('matching_number'), [self.line_3_group_3.matching_number] * 2,
"Lines 3 and 4 are reconciled but not with two first lines since their currency is different.")
self.assertFalse(self.group_4.full_reconcile_id,
"Entries shouldn't be reonciled since their account is out of the wizard's scope.")
def test_nothing_to_auto_reconcile(self):
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'account_ids': self.receivable_account.ids,
'partner_ids': self.partners.ids,
'search_mode': 'zero_balance',
})
with self.assertRaises(UserError):
wizard.auto_reconcile()
def test_auto_reconcile_no_account_nor_partner_one_to_one(self):
self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
})
reconciled_amls = wizard._auto_reconcile_one_to_one()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_auto_reconcile_no_account_nor_partner_zero_balance(self):
self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
})
reconciled_amls = wizard._auto_reconcile_zero_balance()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_auto_reconcile_no_account_one_to_one(self):
self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'partner_ids': self.partners.ids,
})
reconciled_amls = wizard._auto_reconcile_one_to_one()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_auto_reconcile_no_account_zero_balance(self):
self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'partner_ids': self.partners.ids,
})
reconciled_amls = wizard._auto_reconcile_zero_balance()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_auto_reconcile_no_partner_one_to_one(self):
self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'account_ids': self.receivable_account.ids,
})
reconciled_amls = wizard._auto_reconcile_one_to_one()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_auto_reconcile_no_partner_zero_balance(self):
self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'account_ids': self.receivable_account.ids,
})
reconciled_amls = wizard._auto_reconcile_zero_balance()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_auto_reconcile_rounding_one_to_one(self):
""" Checks that two lines with different values, currency rounding aside, are reconciled in one-to-one mode. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
# Need to manually update the values to bypass ORM
self.env.cr.execute(
"""
UPDATE account_move_line SET amount_residual_currency = 1000.0000001 WHERE id = %(line_1_id)s;
UPDATE account_move_line SET amount_residual_currency = -999.999999 WHERE id = %(line_2_id)s;
""",
{'line_1_id': line_1.id, 'line_2_id': line_2.id}
)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'account_ids': self.receivable_account.ids,
})
reconciled_amls = wizard._auto_reconcile_one_to_one()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_auto_reconcile_rounding_zero_balance(self):
""" Checks that two lines with different values, currency rounding aside, are reconciled in zero balance mode. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a)
line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a)
# Need to manually update the values to bypass ORM
self.env.cr.execute(
"""
UPDATE account_move_line SET amount_residual_currency = 1000.0000001 WHERE id = %(line_1_id)s;
UPDATE account_move_line SET amount_residual_currency = -999.999999 WHERE id = %(line_2_id)s;
""",
{'line_1_id': line_1.id, 'line_2_id': line_2.id}
)
wizard = self.env['account.auto.reconcile.wizard'].new({
'from_date': '2016-01-01',
'to_date': '2017-01-01',
'account_ids': self.receivable_account.ids,
})
reconciled_amls = wizard._auto_reconcile_zero_balance()
self.assertTrue(reconciled_amls.full_reconcile_id)
def test_preset_wizard(self):
""" Tests that giving lines_ids to wizard presets correctly values. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-30', partner=self.partner_a)
line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-31', partner=self.partner_a)
wizard = self.env['account.auto.reconcile.wizard'].with_context(domain=[('id', 'in', (line_1 + line_2).ids)]).create({})
self.assertRecordValues(wizard, [{
'account_ids': self.receivable_account.ids,
'partner_ids': self.partner_a.ids,
'from_date': fields.Date.from_string('2016-01-30'),
'to_date': fields.Date.from_string('2016-01-31'),
'search_mode': 'zero_balance',
}])
line_3 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-31', partner=self.partner_a)
line_4 = self.create_line_for_reconciliation(-500.0, -500.0, self.comp_curr, '2016-02-28', partner=None)
wizard = self.env['account.auto.reconcile.wizard'].with_context(domain=[('id', 'in', (line_3 + line_4).ids)]).create({})
self.assertRecordValues(wizard, [{
'account_ids': self.receivable_account.ids,
'partner_ids': [],
'from_date': fields.Date.from_string('2016-01-31'),
'to_date': fields.Date.from_string('2016-02-28'),
'search_mode': 'one_to_one',
}])

View File

@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo import fields
@tagged('post_install', '-at_install')
class TestFiscalPosition(AccountTestInvoicingCommon):
def check_compute_fiscal_year(self, company, date, expected_date_from, expected_date_to):
'''Compute the fiscal year at a certain date for the company passed as parameter.
Then, check if the result matches the 'expected_date_from'/'expected_date_to' dates.
:param company: The company.
:param date: The date belonging to the fiscal year.
:param expected_date_from: The expected date_from after computation.
:param expected_date_to: The expected date_to after computation.
'''
current_date = fields.Date.from_string(date)
res = company.compute_fiscalyear_dates(current_date)
self.assertEqual(res['date_from'], fields.Date.from_string(expected_date_from))
self.assertEqual(res['date_to'], fields.Date.from_string(expected_date_to))
def test_default_fiscal_year(self):
'''Basic case with a fiscal year xxxx-01-01 - xxxx-12-31.'''
company = self.env.company
company.fiscalyear_last_day = 31
company.fiscalyear_last_month = '12'
self.check_compute_fiscal_year(
company,
'2017-12-31',
'2017-01-01',
'2017-12-31',
)
self.check_compute_fiscal_year(
company,
'2017-01-01',
'2017-01-01',
'2017-12-31',
)
def test_leap_fiscal_year_1(self):
'''Case with a leap year ending the 29 February.'''
company = self.env.company
company.fiscalyear_last_day = 29
company.fiscalyear_last_month = '2'
self.check_compute_fiscal_year(
company,
'2016-02-29',
'2015-03-01',
'2016-02-29',
)
self.check_compute_fiscal_year(
company,
'2015-03-01',
'2015-03-01',
'2016-02-29',
)
def test_leap_fiscal_year_2(self):
'''Case with a leap year ending the 28 February.'''
company = self.env.company
company.fiscalyear_last_day = 28
company.fiscalyear_last_month = '2'
self.check_compute_fiscal_year(
company,
'2016-02-29',
'2015-03-01',
'2016-02-29',
)
self.check_compute_fiscal_year(
company,
'2016-03-01',
'2016-03-01',
'2017-02-28',
)
def test_custom_fiscal_year(self):
'''Case with custom fiscal years.'''
company = self.env.company
company.fiscalyear_last_day = 31
company.fiscalyear_last_month = '12'
# Create custom fiscal year covering the 6 first months of 2017.
self.env['account.fiscal.year'].create({
'name': '6 month 2017',
'date_from': '2017-01-01',
'date_to': '2017-05-31',
'company_id': company.id,
})
# Check before the custom fiscal year).
self.check_compute_fiscal_year(
company,
'2017-02-01',
'2017-01-01',
'2017-05-31',
)
# Check after the custom fiscal year.
self.check_compute_fiscal_year(
company,
'2017-11-01',
'2017-06-01',
'2017-12-31',
)
# Create custom fiscal year covering the 3 last months of 2017.
self.env['account.fiscal.year'].create({
'name': 'last 3 month 2017',
'date_from': '2017-10-01',
'date_to': '2017-12-31',
'company_id': company.id,
})
# Check inside the custom fiscal years.
self.check_compute_fiscal_year(
company,
'2017-07-01',
'2017-06-01',
'2017-09-30',
)

View File

@@ -0,0 +1,765 @@
import re
from odoo import Command, fields
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
@tagged('post_install', '-at_install')
class TestAccountReconcileWizard(AccountTestInvoicingCommon):
""" Tests the account reconciliation and its wizard. """
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.receivable_account = cls.company_data['default_account_receivable']
cls.payable_account = cls.company_data['default_account_payable']
cls.revenue_account = cls.company_data['default_account_revenue']
cls.payable_account_2 = cls.env['account.account'].create({
'name': 'Payable Account 2',
'account_type': 'liability_current',
'code': 'PAY2.TEST',
'reconcile': True
})
cls.write_off_account = cls.env['account.account'].create({
'name': 'Write-Off Account',
'account_type': 'liability_current',
'code': 'WO.TEST',
'reconcile': False
})
cls.misc_journal = cls.company_data['default_journal_misc']
cls.test_date = fields.Date.from_string('2016-01-01')
cls.company_currency = cls.company_data['currency']
cls.foreign_currency = cls.setup_other_currency('EUR')
cls.foreign_currency_2 = cls.setup_other_currency('XAF', rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)])
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
def assertWizardReconcileValues(self, selected_lines, input_values, wo_expected_values, expected_transfer_values=None):
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=selected_lines.ids,
).new(input_values)
if expected_transfer_values:
transfer_move = wizard.create_transfer()
# transfer move values
self.assertRecordValues(transfer_move.line_ids.sorted('balance'), expected_transfer_values)
# transfer warning message
self.assertTrue(wizard.transfer_warning_message)
regex_match = re.findall(r'([+-]*\d*,*\d+\.*\d+)', wizard.transfer_warning_message)
# match transferred amount
self.assertEqual(
float(regex_match[0].replace(',', '')),
transfer_move.amount_total_in_currency_signed or transfer_move.amount_total_signed
)
transfer_from_account = transfer_move.line_ids.filtered(lambda aml: 'Transfer from' in aml.name).account_id
transfer_to_account = transfer_move.line_ids.account_id - transfer_from_account
transfer_from_amls = transfer_move.line_ids.filtered(lambda aml: aml.account_id == transfer_from_account)
transfer_amount = sum(aml.balance for aml in transfer_from_amls)
# match account codes
if transfer_amount > 0:
self.assertEqual(regex_match[1:], [transfer_from_account.code, transfer_to_account.code])
else:
self.assertEqual(regex_match[1:], [transfer_to_account.code, transfer_from_account.code])
write_off_move = wizard.create_write_off()
self.assertRecordValues(write_off_move.line_ids.sorted('balance'), wo_expected_values)
wizard.reconcile()
if wizard.allow_partials or (
wizard.edit_mode
and wizard.reco_currency_id.compare_amounts(wizard.edit_mode_amount_currency, wizard.amount_currency)
):
# partial reconcile
self.assertTrue(len(selected_lines.matched_debit_ids) > 0 or len(selected_lines.matched_credit_ids) > 0)
else:
# full reconcile
self.assertTrue(selected_lines.full_reconcile_id)
self.assertRecordValues(
selected_lines,
[{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(selected_lines),
)
# -------------------------------------------------------------------------
# TESTS
# -------------------------------------------------------------------------
def test_wizard_should_not_open(self):
""" Test that when we reconcile two lines that belong to the same account and have a 0 balance should
reconcile silently and not open the write-off wizard.
"""
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01')
(line_1 + line_2).action_reconcile()
self.assertRecordValues(
line_1 + line_2,
[{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * 2
)
def test_wizard_should_open(self):
""" Test that when a write-off is required (because of transfer or non-zero balance) the wizard opens. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01')
line_3 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01')
line_4 = self.create_line_for_reconciliation(-900.0, -900.0, self.company_currency, '2016-01-01', account_1=self.payable_account)
for batch, sub_test_name in (
(line_1 + line_2, 'Batch with non-zero balance in company currency'),
(line_1 + line_3, 'Batch with non-zero balance in foreign currency'),
(line_1 + line_4, 'Batch with different accounts'),
):
with self.subTest(sub_test_name=sub_test_name):
returned_action = batch.action_reconcile()
self.assertEqual(returned_action.get('res_model'), 'account.reconcile.wizard')
def test_reconcile_silently_same_account(self):
""" When balance is 0 we can silently reconcile items. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01')
lines = (line_1 + line_2)
lines.action_reconcile()
self.assertTrue(lines.full_reconcile_id)
self.assertRecordValues(
lines,
[{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(lines),
)
def test_reconcile_silently_transfer(self):
""" When balance is 0, and we need a transfer, we do the transfer+reconcile silently. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01', account_1=self.payable_account)
lines = (line_1 + line_2)
lines.action_reconcile()
self.assertTrue(lines.full_reconcile_id)
self.assertRecordValues(
lines,
[{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(lines),
)
def test_write_off_same_currency(self):
""" Reconciliation of two lines with no transfer/foreign currencies/taxes/reco models."""
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
write_off_expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -500.0},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', 'balance': 500.0},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values)
def test_write_off_one_foreign_currency(self):
""" Reconciliation of two lines with one of the two using foreign currency should reconcile in foreign currency."""
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -500.0, 'amount_currency': -1500.0, 'currency_id': self.foreign_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 500.0, 'amount_currency': 1500.0, 'currency_id': self.foreign_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values)
def test_write_off_mixed_foreign_currencies(self):
""" Write off with multiple currencies should reconcile in company currency."""
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01')
line_3 = self.create_line_for_reconciliation(-400.0, -2400.0, self.foreign_currency_2, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2 + line_3, wizard_input_values, expected_values)
def test_write_off_one_foreign_currency_change_rate(self):
""" Tests that write-off use the correct rate from/at wizard's date. """
foreign_currency = self.setup_other_currency('CAD', rounding=0.001, rates=[('2016-01-01', 0.5), ('2017-01-01', 1 / 3)])
new_date = fields.Date.from_string('2017-02-01')
line_1 = self.create_line_for_reconciliation(-2000.0, -2000.0, self.company_currency, '2017-01-01') # conversion in 2017 => -666.67🍫
line_2 = self.create_line_for_reconciliation(2000.0, 1000.0, foreign_currency, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': new_date,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -1000.0, 'amount_currency': -333.333, 'currency_id': foreign_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 1000.0, 'amount_currency': 333.333, 'currency_id': foreign_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values)
def test_write_off_mixed_foreign_currencies_change_rate(self):
""" Tests that write-off use the correct rate from/at wizard's date. """
new_date = fields.Date.from_string('2017-02-01')
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01')
line_3 = self.create_line_for_reconciliation(-400.0, -2400.0, self.foreign_currency_2, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': new_date,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2 + line_3, wizard_input_values, expected_values)
def test_write_off_both_same_foreign_currency_ensure_no_exchange_diff(self):
""" Test that if both AMLs have the same foreign currency and rate, the amount in company currency
is computed on the write-off in such a way that no exchange diff is created.
"""
foreign_currency = self.setup_other_currency('CAD', rounding=0.01, rates=[('2016-01-01', 1 / 0.225)])
new_date = fields.Date.from_string('2017-02-01')
line_1 = self.create_line_for_reconciliation(21.38, 95.0, foreign_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(1.13, 5.0, foreign_currency, '2016-01-01')
line_3 = self.create_line_for_reconciliation(1.13, 5.0, foreign_currency, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': new_date,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -23.64, 'amount_currency': -105.0, 'currency_id': foreign_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 23.64, 'amount_currency': 105.0, 'currency_id': foreign_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2 + line_3, wizard_input_values, expected_values)
def test_write_off_with_transfer_account_same_currency(self):
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(100.0, 100.0, self.company_currency, '2016-01-01', account_1=self.payable_account)
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_transfer_values = [
{'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id},
{'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}',
'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id},
]
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -1100.0, 'amount_currency': -1100.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 1100.0, 'amount_currency': 1100.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values)
def test_write_off_with_transfer_account_one_foreign_currency(self):
line_1 = self.create_line_for_reconciliation(1100.0, 1100.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(100.0, 300.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account)
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_transfer_values = [
{'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': -100.0, 'amount_currency': -300.0, 'currency_id': self.foreign_currency.id},
{'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}',
'balance': 100.0, 'amount_currency': 300.0, 'currency_id': self.foreign_currency.id},
]
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -1200.0, 'amount_currency': -3600.0, 'currency_id': self.foreign_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 1200.0, 'amount_currency': 3600.0, 'currency_id': self.foreign_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values)
def test_write_off_with_complex_transfer(self):
partner_1 = self.env['res.partner'].create({'name': 'Test Partner 1'})
partner_2 = self.env['res.partner'].create({'name': 'Test Partner 2'})
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01', partner=partner_2)
line_2 = self.create_line_for_reconciliation(-100.0, -300.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account, partner=partner_1)
line_3 = self.create_line_for_reconciliation(-200.0, -200.0, self.company_currency, '2016-01-01', account_1=self.payable_account, partner=partner_2)
line_4 = self.create_line_for_reconciliation(-200.0, -600.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account, partner=partner_2)
line_5 = self.create_line_for_reconciliation(-200.0, -600.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account, partner=partner_2)
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_transfer_values = [
{'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}',
'balance': -400.0, 'amount_currency': -1200.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_2.id},
{'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}',
'balance': -200.0, 'amount_currency': -200.0, 'currency_id': self.company_currency.id, 'partner_id': partner_2.id},
{'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}',
'balance': -100.0, 'amount_currency': -300.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_1.id},
{'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': 100.0, 'amount_currency': 300.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_1.id},
{'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': 200.0, 'amount_currency': 200.0, 'currency_id': self.company_currency.id, 'partner_id': partner_2.id},
{'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': 400.0, 'amount_currency': 1200.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_2.id},
]
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -300.0, 'amount_currency': -900.0, 'currency_id': self.foreign_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 300.0, 'amount_currency': 900.0, 'currency_id': self.foreign_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2 + line_3 + line_4 + line_5, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values)
def test_write_off_with_tax(self):
""" Tests write-off with a tax set on the wizard. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01')
tax_recover_account_id = self.env['account.account'].create({
'name': 'Tax Account Test',
'account_type': 'liability_current',
'code': 'TAX.TEST',
'reconcile': False
})
base_tag = self.env['account.account.tag'].create({
'applicability': 'taxes',
'name': 'base_tax_tag',
'country_id': self.company_data['company'].country_id.id,
})
tax_tag = self.env['account.account.tag'].create({
'applicability': 'taxes',
'name': 'tax_tax_tag',
'country_id': self.company_data['company'].country_id.id,
})
tax_id = self.env['account.tax'].create({
'name': 'tax_test',
'amount_type': 'percent',
'amount': 25.0,
'type_tax_use': 'sale',
'company_id': self.company_data['company'].id,
'invoice_repartition_line_ids': [
Command.create({'factor_percent': 100, 'repartition_type': 'base', 'tag_ids': [Command.set(base_tag.ids)]}),
Command.create({'factor_percent': 100, 'account_id': tax_recover_account_id.id, 'tag_ids': [Command.set(tax_tag.ids)]}),
],
'refund_repartition_line_ids': [
Command.create({'factor_percent': 100, 'repartition_type': 'base', 'tag_ids': [Command.set(base_tag.ids)]}),
Command.create({'factor_percent': 100, 'account_id': tax_recover_account_id.id, 'tag_ids': [Command.set(tax_tag.ids)]}),
],
})
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'tax_id': tax_id.id,
'allow_partials': False,
'date': self.test_date,
}
write_off_expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -500.0},
{'account_id': tax_recover_account_id.id, 'name': f'{tax_id.name}', 'balance': 100.0},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', 'balance': 400.0},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values)
def test_reconcile_partials_allowed(self):
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01')
lines = line_1 + line_2
wizard_input_values = {
'allow_partials': True,
}
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=lines.ids,
).new(wizard_input_values)
wizard.reconcile()
self.assertTrue(len(lines.matched_debit_ids) > 0 or len(lines.matched_credit_ids) > 0)
def test_raise_lock_date_violation(self):
""" If a write-off violates the lock date we display a banner and change the date afterwards. """
company_id = self.company_data['company']
company_id.fiscalyear_lock_date = fields.Date.from_string('2016-12-01')
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-06-01')
line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-06-01')
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=(line_1 + line_2).ids,
).new({'date': self.test_date})
self.assertTrue(bool(wizard.lock_date_violated_warning_message))
def test_raise_reconcile_too_many_accounts(self):
""" If you try to reconcile lines from more than 2 accounts, it should raise an error. """
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01', account_1=self.payable_account)
line_3 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01', account_1=self.payable_account_2)
with self.assertRaises(UserError):
(line_1 + line_2 + line_3).action_reconcile()
def test_reconcile_no_receivable_no_payable_account(self):
""" If you try to reconcile lines in an account that is neither from payable nor receivable
it should reconcile in company currency.
"""
account = self.company_data['default_account_expense']
account.reconcile = True
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01', account_1=account)
line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01', account_1=account)
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_values = [
{'account_id': account.id, 'name': 'Write-Off Test Label',
'balance': -500.0, 'amount_currency': -500.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 500.0, 'amount_currency': 500.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values)
def test_reconcile_exchange_diff_foreign_currency(self):
""" When reconciling exchange_diff with amount_residual_currency = 0 we need to reconcile in company_currency.
"""
exchange_gain_account = self.company_data['company'].income_currency_exchange_account_id
exchange_gain_account.reconcile = True
line_1 = self.create_line_for_reconciliation(150.0, 0.0, self.foreign_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-100.0, 0.0, self.foreign_currency, '2016-01-01', account_1=exchange_gain_account)
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
# Note the transfer will always be in the currency of the line transferred
expected_transfer_values = [
{'account_id': self.receivable_account.id, 'name': f'Transfer from {exchange_gain_account.display_name}',
'balance': -100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id},
{'account_id': exchange_gain_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': 100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id},
]
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -50.0, 'amount_currency': -50.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 50.0, 'amount_currency': 50.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values)
def test_write_off_on_same_account(self):
""" When creating a write-off in the same account than the one used by the lines to reconcile,
the lines and the write-off should be fully reconciled.
"""
line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(2000.0, 2000.0, self.company_currency, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.receivable_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
write_off_expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -3000.0},
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': 3000.0},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values)
def test_reconcile_exchange_diff_foreign_currency_full(self):
""" When reconciling exchange_diff with amount_residual_currency = 0 we need to reconcile in company_currency.
"""
exchange_gain_account = self.company_data['company'].income_currency_exchange_account_id
exchange_gain_account.reconcile = True
line_1 = self.create_line_for_reconciliation(100.0, 0.0, self.foreign_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-100.0, 0.0, self.foreign_currency, '2016-01-01', account_1=exchange_gain_account)
lines = line_1 + line_2
lines.action_reconcile()
self.assertTrue(lines.full_reconcile_id)
self.assertRecordValues(
lines,
[{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(lines),
)
def test_write_off_kpmg_case(self):
""" Test that write-off does a full reconcile with 2 foreign currencies using a custom exchange rate. """
new_date = fields.Date.from_string('2017-02-01')
line_1 = self.create_line_for_reconciliation(1000.0, 1500.0, self.foreign_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(-900.0, -5400.0, self.foreign_currency_2, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': new_date,
}
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, [
{
'account_id': self.receivable_account.id,
'balance': -100.0,
'amount_currency': -150.0,
'currency_id': self.foreign_currency.id,
},
{
'account_id': self.write_off_account.id,
'balance': 100.0,
'amount_currency': 150.0,
'currency_id': self.foreign_currency.id,
},
])
def test_write_off_multi_curr_multi_residuals_force_partials(self):
""" Test that we raise an error when trying to reconcile lines with multiple residuals.
Here debit1 will be reconciled with credit1 first as they have the same currency.
Then residual of debit1 will try to reconcile with debit2 which is impossible
=> 2 residuals both in foreign currency, we don't know in which currency we should make the write-off
=> We should only allow partial reconciliation. """
debit_1 = self.create_line_for_reconciliation(2000.0, 12000.0, self.foreign_currency_2, '2016-01-01')
credit_1 = self.create_line_for_reconciliation(-1000.0, -6000.0, self.foreign_currency_2, '2016-01-01')
debit_2 = self.create_line_for_reconciliation(2000.0, 3000.0, self.foreign_currency, '2016-01-01')
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=(debit_1 + debit_2 + credit_1).ids,
).new()
self.assertRecordValues(wizard, [{'force_partials': True, 'allow_partials': True}])
def test_write_off_multi_curr_multi_residuals_exch_diff_force_partials(self):
debit_1 = self.create_line_for_reconciliation(2000.0, 0.0, self.foreign_currency_2, '2016-01-01')
credit_1 = self.create_line_for_reconciliation(-1000.0, 0.0, self.foreign_currency_2, '2016-01-01')
debit_2 = self.create_line_for_reconciliation(2000.0, 0.0, self.foreign_currency, '2016-01-01')
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=(debit_1 + debit_2 + credit_1).ids,
).new()
self.assertRecordValues(wizard, [{'force_partials': True, 'allow_partials': True}])
def test_reconcile_with_partner_change(self):
partner_1 = self.env['res.partner'].create({'name': 'Test Partner 1'})
partner_2 = self.env['res.partner'].create({'name': 'Test Partner 2'})
line_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01', partner=partner_1)
line_2 = self.create_line_for_reconciliation(2000.0, 2000.0, self.company_currency, '2016-01-01')
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.receivable_account.id,
'to_partner_id': partner_2.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
'tax_id': self.tax_sale_a.id,
}
write_off_expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -1000.0, 'partner_id': partner_1.id},
{'account_id': self.company_data['default_account_tax_sale'].id, 'name': '15%', 'balance': 130.43, 'partner_id': partner_2.id},
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': 869.57, 'partner_id': partner_2.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values)
def test_reconcile_with_partner_change_and_transfer(self):
partner_1 = self.env['res.partner'].create({'name': 'Test Partner 1'})
partner_2 = self.env['res.partner'].create({'name': 'Test Partner 2'})
line_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01', account_1=self.payable_account)
line_2 = self.create_line_for_reconciliation(2000.0, 2000.0, self.company_currency, '2016-01-01', partner=partner_1)
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.receivable_account.id,
'to_partner_id': partner_2.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_transfer_values = [
{'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}',
'balance': -1000.0, 'amount_currency': -1000.0, 'currency_id': self.company_currency.id},
{'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': 1000.0, 'amount_currency': 1000.0, 'currency_id': self.company_currency.id},
]
write_off_expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -1000.0, 'partner_id': partner_1.id},
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': 1000.0, 'partner_id': partner_2.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values, expected_transfer_values)
def test_reconcile_edit_mode_partial_foreign_curr(self):
line_1 = self.create_line_for_reconciliation(100.0, 300.0, self.foreign_currency, '2016-01-01')
wizard_input_values = {
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'date': self.test_date,
'edit_mode_amount_currency': 30.0,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -10.0, 'amount_currency': -30.0, 'currency_id': self.foreign_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 10.0, 'amount_currency': 30.0, 'currency_id': self.foreign_currency.id},
]
self.assertWizardReconcileValues(line_1, wizard_input_values, expected_values)
def test_reconcile_edit_mode_partial_company_curr(self):
line_1 = self.create_line_for_reconciliation(300.0, 300.0, self.company_currency, '2016-01-01')
wizard_input_values = {
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'date': self.test_date,
'edit_mode_amount_currency': 100.0,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1, wizard_input_values, expected_values)
def test_reconcile_edit_mode_partial_wrong_amount_raises(self):
line_1 = self.create_line_for_reconciliation(300.0, 300.0, self.company_currency, '2016-01-01')
wizard_input_values = {
'account_id': self.write_off_account.id,
}
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=line_1.ids,
).create(wizard_input_values)
with self.assertRaisesRegex(UserError, 'The amount of the write-off'):
wizard.edit_mode_amount_currency = -100.0
def test_reconcile_edit_mode_full_reconcile(self):
line_1 = self.create_line_for_reconciliation(300.0, 300.0, self.company_currency, '2016-01-01')
wizard_input_values = {
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'edit_mode_amount_currency': 300.0,
}
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -300.0, 'amount_currency': -300.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 300.0, 'amount_currency': 300.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1, wizard_input_values, expected_values)
def test_reconcile_same_currency_same_side_not_recpay(self):
"""
Test the reconciliation with two lines on the same side (debit/credit), same currency and not on a receivable/payable account
"""
current_assets_account = self.company_data['default_account_assets'].copy({'name': 'Current Assets', 'account_type': 'asset_current', 'reconcile': True})
line_1 = self.create_line_for_reconciliation(200, 200, self.company_currency, '2016-01-01', current_assets_account)
line_2 = self.create_line_for_reconciliation(200, 200, self.company_currency, '2016-01-01', current_assets_account)
# Test the opening of the wizard without input values
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=(line_1 + line_2).ids,
).new()
self.assertRecordValues(wizard, [{'is_write_off_required': True, 'amount': 400, 'amount_currency': 400}])
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_values = [
{'account_id': current_assets_account.id, 'name': 'Write-Off Test Label',
'balance': -400.0, 'amount_currency': -400.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 400.0, 'amount_currency': 400.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values)
def test_reconcile_foreign_currency_same_side_not_recpay(self):
"""
Test the reconciliation with two lines on the same side (debit/credit), one foreign currency and not on a receivable/payable account
"""
current_assets_account = self.company_data['default_account_assets'].copy({'name': 'Current Assets', 'account_type': 'asset_current', 'reconcile': True})
line_1 = self.create_line_for_reconciliation(200, 300, self.foreign_currency, '2016-01-01', current_assets_account)
line_2 = self.create_line_for_reconciliation(200, 200, self.company_currency, '2016-01-01', current_assets_account)
# Test the opening of the wizard without input values
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=(line_1 + line_2).ids,
).new()
self.assertRecordValues(wizard, [{'is_write_off_required': True, 'amount': 400, 'amount_currency': 400}])
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
expected_values = [
{'account_id': current_assets_account.id, 'name': 'Write-Off Test Label',
'balance': -400.0, 'amount_currency': -400.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 400.0, 'amount_currency': 400.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values)
def test_reconcile_same_side_exch_diff(self):
"""
Test the reconciliation with two lines on the same side (debit/credit), one exchange diff in foreign currency,
one regular aml in company currency
"""
exchange_gain_account = self.company_data['company'].income_currency_exchange_account_id
exchange_gain_account.reconcile = True
line_1 = self.create_line_for_reconciliation(150.0, 150.0, self.company_currency, '2016-01-01')
line_2 = self.create_line_for_reconciliation(100.0, 0.0, self.foreign_currency, '2016-01-01', account_1=exchange_gain_account)
wizard_input_values = {
'journal_id': self.misc_journal.id,
'account_id': self.write_off_account.id,
'label': 'Write-Off Test Label',
'allow_partials': False,
'date': self.test_date,
}
# Note the transfer will always be in the currency of the line transferred
expected_transfer_values = [
{'account_id': exchange_gain_account.id, 'name': f'Transfer to {self.receivable_account.display_name}',
'balance': -100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id},
{'account_id': self.receivable_account.id, 'name': f'Transfer from {exchange_gain_account.display_name}',
'balance': 100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id},
]
expected_values = [
{'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label',
'balance': -250.0, 'amount_currency': -250.0, 'currency_id': self.company_currency.id},
{'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label',
'balance': 250.0, 'amount_currency': 250.0, 'currency_id': self.company_currency.id},
]
self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values)

View File

@@ -0,0 +1,708 @@
from odoo import Command
from odoo.tests import tagged
from .common import TestAccountReportsCommon
@tagged('post_install', '-at_install')
class TestAnalyticReport(TestAccountReportsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.groups_id += cls.env.ref(
'analytic.group_analytic_accounting')
cls.report = cls.env.ref('fusion_accounting.profit_and_loss')
cls.report.write({'filter_analytic': True})
cls.analytic_plan_parent = cls.env['account.analytic.plan'].create({
'name': 'Plan Parent',
})
cls.analytic_plan_child = cls.env['account.analytic.plan'].create({
'name': 'Plan Child',
'parent_id': cls.analytic_plan_parent.id,
})
cls.analytic_account_parent = cls.env['account.analytic.account'].create({
'name': 'Account 1',
'plan_id': cls.analytic_plan_parent.id
})
cls.analytic_account_parent_2 = cls.env['account.analytic.account'].create({
'name': 'Account 2',
'plan_id': cls.analytic_plan_parent.id
})
cls.analytic_account_child = cls.env['account.analytic.account'].create({
'name': 'Account 3',
'plan_id': cls.analytic_plan_child.id
})
cls.analytic_account_parent_3 = cls.env['account.analytic.account'].create({
'name': 'Account 4',
'plan_id': cls.analytic_plan_parent.id
})
def test_report_group_by_analytic_plan(self):
out_invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2019-05-01',
'invoice_date': '2019-05-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 200.0,
'analytic_distribution': {
self.analytic_account_parent.id: 100,
},
}),
Command.create({
'product_id': self.product_b.id,
'price_unit': 200.0,
'analytic_distribution': {
self.analytic_account_child.id: 100,
},
}),
]
}])
out_invoice.action_post()
options = self._generate_options(
self.report,
'2019-01-01',
'2019-12-31',
default_options={
'analytic_plans_groupby': [self.analytic_plan_parent.id, self.analytic_plan_child.id],
}
)
lines = self.report._get_lines(options)
self.assertLinesValues(
# pylint: disable=bad-whitespace
lines,
[ 0, 1, 2],
[
['Revenue', 400.00, 200.00],
['Less Costs of Revenue', 0.00, 0.00],
['Gross Profit', 400.00, 200.00],
['Less Operating Expenses', 0.00, 0.00],
['Operating Income (or Loss)', 400.00, 200.00],
['Plus Other Income', 0.00, 0.00],
['Less Other Expenses', 0.00, 0.00],
['Net Profit', 400.00, 200.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
def test_report_analytic_filter(self):
out_invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2023-02-01',
'invoice_date': '2023-02-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 1000.0,
'analytic_distribution': {
self.analytic_account_parent.id: 100,
},
})
]
}])
out_invoice.action_post()
options = self._generate_options(
self.report,
'2023-01-01',
'2023-12-31',
default_options={
'analytic_accounts': [self.analytic_account_parent.id],
}
)
self.assertLinesValues(
# pylint: disable=C0326
# pylint: disable=bad-whitespace
self.report._get_lines(options),
[ 0, 1],
[
['Revenue', 1000.00],
['Less Costs of Revenue', 0.00],
['Gross Profit', 1000.00],
['Less Operating Expenses', 0.00],
['Operating Income (or Loss)', 1000.00],
['Plus Other Income', 0.00],
['Less Other Expenses', 0.00],
['Net Profit', 1000.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
# Set the unused analytic account in filter, as no move is
# using this account, the column should be empty
options['analytic_accounts'] = [self.analytic_account_child.id]
self.assertLinesValues(
# pylint: disable=C0326
# pylint: disable=bad-whitespace
self.report._get_lines(options),
[ 0, 1],
[
['Revenue', 0.00],
['Less Costs of Revenue', 0.00],
['Gross Profit', 0.00],
['Less Operating Expenses', 0.00],
['Operating Income (or Loss)', 0.00],
['Plus Other Income', 0.00],
['Less Other Expenses', 0.00],
['Net Profit', 0.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
def test_report_audit_analytic_filter(self):
out_invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2023-02-01',
'invoice_date': '2023-02-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 1000.0,
'analytic_distribution': {
self.analytic_account_parent.id: 100,
},
}),
Command.create({
'product_id': self.product_a.id,
'price_unit': 500.0,
'analytic_distribution': {
self.analytic_account_child.id: 100,
},
}),
],
}])
out_invoice.action_post()
options = self._generate_options(
self.report,
'2023-01-01',
'2023-12-31',
default_options={
'analytic_accounts': [self.analytic_account_parent.id],
}
)
lines = self.report._get_lines(options)
report_line = self.report.line_ids[0]
report_line_dict = next(x for x in lines if x['name'] == report_line.name)
action_dict = self.report.action_audit_cell(
options,
self._get_audit_params_from_report_line(options, report_line, report_line_dict),
)
audited_lines = self.env['account.move.line'].search(action_dict['domain'])
self.assertEqual(audited_lines, out_invoice.invoice_line_ids[0], "Only the line with the parent account should be shown")
def test_report_analytic_groupby_and_filter(self):
"""
Test that the analytic filter is applied on the groupby columns
"""
out_invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2023-02-01',
'invoice_date': '2023-02-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 1000.0,
'analytic_distribution': {
self.analytic_account_parent.id: 40,
self.analytic_account_child.id: 60,
},
})
]
}])
out_invoice.action_post()
# Test with only groupby
options = self._generate_options(
self.report,
'2023-01-01',
'2023-12-31',
default_options={
'analytic_accounts_groupby': [self.analytic_account_parent.id, self.analytic_account_child.id],
}
)
self.assertLinesValues(
# pylint: disable=C0326
# pylint: disable=bad-whitespace
self.report._get_lines(options),
[ 0, 1, 2, 3],
[
['Revenue', 400.00, 600.00, 1000.00],
['Less Costs of Revenue', 0.00, 0.00, 0.00],
['Gross Profit', 400.00, 600.00, 1000.00],
['Less Operating Expenses', 0.00, 0.00, 0.00],
['Operating Income (or Loss)', 400.00, 600.00, 1000.00],
['Plus Other Income', 0.00, 0.00, 0.00],
['Less Other Expenses', 0.00, 0.00, 0.00],
['Net Profit', 400.00, 600.00, 1000.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
# Adding analytic filter for the two analytic accounts used on the invoice line
# The two groupby columns should still be filled
options['analytic_accounts'] = [self.analytic_account_parent.id, self.analytic_account_child.id]
self.assertLinesValues(
# pylint: disable=C0326
# pylint: disable=bad-whitespace
self.report._get_lines(options),
[ 0, 1, 2, 3],
[
['Revenue', 400.00, 600.00, 1000.00],
['Less Costs of Revenue', 0.00, 0.00, 0.00],
['Gross Profit', 400.00, 600.00, 1000.00],
['Less Operating Expenses', 0.00, 0.00, 0.00],
['Operating Income (or Loss)', 400.00, 600.00, 1000.00],
['Plus Other Income', 0.00, 0.00, 0.00],
['Less Other Expenses', 0.00, 0.00, 0.00],
['Net Profit', 400.00, 600.00, 1000.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
# Keep only first analytic account on filter, the groupby column
# for this account should still be filled, unlike the other
options['analytic_accounts'] = [self.analytic_account_parent.id]
self.assertLinesValues(
# pylint: disable=C0326
# pylint: disable=bad-whitespace
self.report._get_lines(options),
[ 0, 1, 2, 3],
[
['Revenue', 400.00, 0.00, 1000.00],
['Less Costs of Revenue', 0.00, 0.00, 0.00],
['Gross Profit', 400.00, 0.00, 1000.00],
['Less Operating Expenses', 0.00, 0.00, 0.00],
['Operating Income (or Loss)', 400.00, 0.00, 1000.00],
['Plus Other Income', 0.00, 0.00, 0.00],
['Less Other Expenses', 0.00, 0.00, 0.00],
['Net Profit', 400.00, 0.00, 1000.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
# Keep only first analytic account on filter, the groupby column
# for this account should still be filled, unlike the other
options['analytic_accounts'] = [self.analytic_account_child.id]
self.assertLinesValues(
# pylint: disable=C0326
# pylint: disable=bad-whitespace
self.report._get_lines(options),
[ 0, 1, 2, 3],
[
['Revenue', 0.00, 600.00, 1000.00],
['Less Costs of Revenue', 0.00, 0.00, 0.00],
['Gross Profit', 0.00, 600.00, 1000.00],
['Less Operating Expenses', 0.00, 0.00, 0.00],
['Operating Income (or Loss)', 0.00, 600.00, 1000.00],
['Plus Other Income', 0.00, 0.00, 0.00],
['Less Other Expenses', 0.00, 0.00, 0.00],
['Net Profit', 0.00, 600.00, 1000.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
# Set an unused analytic account in filter, all the columns
# should be empty, as no move is using this account
options['analytic_accounts'] = [self.analytic_account_parent_2.id]
self.assertLinesValues(
# pylint: disable=C0326
# pylint: disable=bad-whitespace
self.report._get_lines(options),
[ 0, 1, 2, 3],
[
['Revenue', 0.00, 0.00, 0.00],
['Less Costs of Revenue', 0.00, 0.00, 0.00],
['Gross Profit', 0.00, 0.00, 0.00],
['Less Operating Expenses', 0.00, 0.00, 0.00],
['Operating Income (or Loss)', 0.00, 0.00, 0.00],
['Plus Other Income', 0.00, 0.00, 0.00],
['Less Other Expenses', 0.00, 0.00, 0.00],
['Net Profit', 0.00, 0.00, 0.00],
],
options,
currency_map={
1: {'currency': self.env.company.currency_id},
2: {'currency': self.env.company.currency_id},
},
)
def test_audit_cell_analytic_groupby_and_filter(self):
"""
Test that the analytic filters are applied on the auditing of the cells
"""
def _get_action_dict(options, column_index):
lines = self.report._get_lines(options)
report_line = self.report.line_ids[0]
report_line_dict = next(x for x in lines if x['name'] == report_line.name)
audit_param = self._get_audit_params_from_report_line(options, report_line, report_line_dict, column_group_key=list(options['column_groups'])[column_index])
return self.report.action_audit_cell(options, audit_param)
other_plan = self.env['account.analytic.plan'].create({'name': "Other Plan"})
other_account = self.env['account.analytic.account'].create({'name': "Other Account", 'plan_id': other_plan.id, 'active': True})
out_invoices = self.env['account.move'].create([
{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2023-02-01',
'invoice_date': '2023-02-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 1000.0,
'analytic_distribution': {
self.analytic_account_parent.id: 40,
self.analytic_account_child.id: 60,
}
}),
]
},
{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2023-02-01',
'invoice_date': '2023-02-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 2000.0,
'analytic_distribution': {
f'{self.analytic_account_parent.id},{other_account.id}': 100,
},
}),
]
}
])
out_invoices.action_post()
out_invoices = out_invoices.with_context(analytic_plan_id=self.analytic_plan_parent.id)
analytic_lines_parent = out_invoices.invoice_line_ids.analytic_line_ids.filtered(lambda line: line.auto_account_id == self.analytic_account_parent)
analytic_lines_other = out_invoices.with_context(analytic_plan_id=other_plan.id).invoice_line_ids.analytic_line_ids.filtered(lambda line: line.auto_account_id == other_account)
# Test with only groupby
options = self._generate_options(
self.report,
'2023-01-01',
'2023-12-31',
default_options={
'analytic_accounts_groupby': [self.analytic_account_parent.id, other_account.id],
}
)
action_dict = _get_action_dict(options, 0) # First Column => Parent
self.assertEqual(
self.env['account.analytic.line'].search(action_dict['domain']),
analytic_lines_parent,
"Only the Analytic Line related to the Parent should be shown",
)
action_dict = _get_action_dict(options, 1) # Second Column => Other
self.assertEqual(
self.env['account.analytic.line'].search(action_dict['domain']),
analytic_lines_other,
"Only the Analytic Line related to the Parent should be shown",
)
action_dict = _get_action_dict(options, 2) # Third Column => AMLs
self.assertEqual(
out_invoices.line_ids.filtered_domain(action_dict['domain']),
out_invoices.invoice_line_ids,
"Both amls should be shown",
)
# Adding analytic filter for the two analytic accounts used on the invoice line
options['analytic_accounts'] = [self.analytic_account_parent.id, other_account.id]
action_dict = _get_action_dict(options, 0) # First Column => Parent
self.assertEqual(
self.env['account.analytic.line'].search(action_dict['domain']),
analytic_lines_parent,
"Still only the Analytic Line related to the Parent should be shown",
)
action_dict = _get_action_dict(options, 1) # Second Column => Other
self.assertEqual(
self.env['account.analytic.line'].search(action_dict['domain']),
analytic_lines_other,
"Still only the Analytic Line related to the Parent should be shown",
)
action_dict = _get_action_dict(options, 2) # Third Column => AMLs
self.assertEqual(
out_invoices.line_ids.search(action_dict['domain']),
out_invoices.invoice_line_ids,
"Both amls should be shown",
)
def test_general_ledger_analytic_filter(self):
analytic_plan = self.env["account.analytic.plan"].create({
"name": "Default Plan",
})
analytic_account = self.env["account.analytic.account"].create({
"name": "Test Account",
"plan_id": analytic_plan.id,
})
invoice = self.init_invoice(
"out_invoice",
amounts=[100, 200],
invoice_date="2023-01-01",
)
invoice.action_post()
invoice.invoice_line_ids[0].analytic_distribution = {analytic_account.id: 100}
general_ledger_report = self.env.ref("fusion_accounting.general_ledger_report")
options = self._generate_options(
general_ledger_report,
"2023-01-01",
"2023-01-01",
default_options={
'analytic_accounts': [analytic_account.id],
'unfold_all': True,
}
)
self.assertLinesValues(
general_ledger_report._get_lines(options),
# Name Debit Credit Balance
[ 0, 5, 6, 7],
[
['400000 Product Sales', 0.00, 100.00, -100.00],
['INV/2023/00001', 0.00, 100.00, -100.00],
['Total 400000 Product Sales', 0.00, 100.00, -100.00],
['Total', 0.00, 100.00, -100.00],
],
options,
)
def test_analytic_groupby_with_horizontal_groupby(self):
out_invoice_1 = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2024-07-01',
'invoice_date': '2024-07-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_b.id,
'price_unit': 500.0,
'analytic_distribution': {
self.analytic_account_parent_2.id: 80,
self.analytic_account_parent_3.id: -10,
},
}),
]
}])
out_invoice_1.action_post()
out_invoice_2 = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2024-07-01',
'invoice_date': '2024-07-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 100.0,
'analytic_distribution': {
self.analytic_account_parent.id: 100,
},
}),
]
}])
out_invoice_2.action_post()
horizontal_group = self.env['account.report.horizontal.group'].create({
'name': 'Horizontal Group Journal Entries',
'report_ids': [self.report.id],
'rule_ids': [
Command.create({
'field_name': 'move_id', # this field is specific to account.move.line and not in account.analytic.line
'domain': f"[('id', 'in', {(out_invoice_1 + out_invoice_2).ids})]",
}),
],
})
options = self._generate_options(
self.report,
'2024-01-01',
'2024-12-31',
default_options={
'analytic_accounts_groupby': [self.analytic_account_parent.id, self.analytic_account_parent_2.id, self.analytic_account_parent_3.id],
'selected_horizontal_group_id': horizontal_group.id,
}
)
self.assertLinesValues(
self.report._get_lines(options),
# Horizontal groupby [ Move 2 ] [ Move 1 ]
# Analytic groupby A1 A2 A3 Balance A1 A2 A3 Balance
[ 0, 1, 2, 3, 4, 5, 6, 7, 8],
[
['Revenue', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00],
['Less Costs of Revenue', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
['Gross Profit', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00],
['Less Operating Expenses', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
['Operating Income (or Loss)', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00],
['Plus Other Income', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
['Less Other Expenses', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
['Net Profit', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00],
],
options,
)
def test_analytic_groupby_with_analytic_simulations(self):
"""
Create an analytic simulation (analytic line without a move line)
and check that it is taken into account in the report
"""
self.env['account.analytic.line'].create({
'name': 'Simulation',
'date': '2019-05-01',
'amount': 100.0,
'unit_amount': 1.0,
'company_id': self.env.company.id,
self.analytic_plan_parent._column_name(): self.analytic_account_parent.id,
'general_account_id': self.company_data['default_account_revenue'].id,
})
options = self._generate_options(
self.report,
'2019-01-01',
'2019-12-31',
default_options={
'analytic_plans_groupby': [self.analytic_plan_parent.id, self.analytic_plan_child.id],
'include_analytic_without_aml': True,
}
)
self.assertLinesValues(
self.report._get_lines(options),
[ 0, 1, 2],
[
('Revenue', 100.00, 0.00),
('Less Costs of Revenue', 0.00, 0.00),
('Gross Profit', 100.00, 0.00),
('Less Operating Expenses', 0.00, 0.00),
('Operating Income (or Loss)', 100.00, 0.00),
('Plus Other Income', 0.00, 0.00),
('Less Other Expenses', 0.00, 0.00),
('Net Profit', 100.00, 0.00),
],
options,
)
def test_analytic_groupby_plans_without_analytic_accounts(self):
"""
Ensure that grouping on several analytic plans without any analytic accounts works as expected
"""
analytic_plans_without_accounts = self.env['account.analytic.plan'].create([
{'name': 'Plan 1'},
{'name': 'Plan 2'},
])
options = self._generate_options(
self.report, '2019-01-01', '2019-12-31',
default_options={'analytic_plans_groupby': analytic_plans_without_accounts.ids}
)
self.assertEqual(
len(options['column_groups']), 3,
"the number of column groups should be 3, despite the 2 analytic plans having the exact same analytic accounts list"
)
self.assertLinesValues(
self.report._get_lines(options),
# Plan 1 Plan 2 Total
[ 0, 1, 2, 3],
[
('Revenue', 0.00, 0.00, 0.00),
('Less Costs of Revenue', 0.00, 0.00, 0.00),
('Gross Profit', 0.00, 0.00, 0.00),
('Less Operating Expenses', 0.00, 0.00, 0.00),
('Operating Income (or Loss)', 0.00, 0.00, 0.00),
('Plus Other Income', 0.00, 0.00, 0.00),
('Less Other Expenses', 0.00, 0.00, 0.00),
('Net Profit', 0.00, 0.00, 0.00),
],
options,
)
def test_profit_and_loss_multicompany_access_rights(self):
branch = self.env['res.company'].create([{
'name': "My Test Branch",
'parent_id': self.env.company.id,
}])
other_currency = self.setup_other_currency('EUR', rounding=0.001)
test_journal = self.env['account.journal'].create({
'name': 'Test Journal',
'code': 'TEST',
'type': 'sale',
'company_id': self.env.company.id,
'currency_id': other_currency.id,
})
test_user = self.env['res.users'].create({
'login': 'test',
'name': 'The King',
'email': 'noop@example.com',
'groups_id': [Command.link(self.env.ref('account.group_account_manager').id)],
'company_ids': [Command.link(self.env.company.id), Command.link(branch.id)],
})
self.env.invalidate_all()
options = self._generate_options(
self.report.with_user(test_user).with_company(branch), '2019-01-01', '2019-12-31',
)
lines = self.report._get_lines(options)
self.assertTrue(lines)
self.assertEqual(test_journal.display_name, "Test Journal (EUR)")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
class TestBankRecWidgetCommon(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.other_currency = cls.setup_other_currency('EUR')
cls.other_currency_2 = cls.setup_other_currency('CAD', rounding=0.001, rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)])
cls.other_currency_3 = cls.setup_other_currency('XAF', rounding=0.001, rates=[('2016-01-01', 12.0), ('2017-01-01', 8.0)])
@classmethod
def _create_invoice_line(cls, move_type, **kwargs):
''' Create an invoice on the fly.'''
kwargs.setdefault('partner_id', cls.partner_a.id)
kwargs.setdefault('invoice_date', '2017-01-01')
kwargs.setdefault('invoice_line_ids', [])
for one2many_values in kwargs['invoice_line_ids']:
one2many_values.setdefault('name', 'xxxx')
one2many_values.setdefault('quantity', 1)
one2many_values.setdefault('tax_ids', [])
invoice = cls.env['account.move'].create({
'move_type': move_type,
**kwargs,
'invoice_line_ids': [Command.create(x) for x in kwargs['invoice_line_ids']],
})
invoice.action_post()
return invoice.line_ids\
.filtered(lambda l: l.account_id.account_type in ('asset_receivable', 'liability_payable'))
@classmethod
def _create_st_line(cls, amount, date='2019-01-01', payment_ref='turlututu', **kwargs):
st_line = cls.env['account.bank.statement.line'].create({
'amount': amount,
'date': date,
'payment_ref': payment_ref,
'journal_id': kwargs.get('journal_id', cls.company_data['default_journal_bank'].id),
**kwargs,
})
# The automatic reconcile cron checks the create_date when considering st_lines to run on.
# create_date is a protected field so this is the only way to set it correctly
cls.env.cr.execute("UPDATE account_bank_statement_line SET create_date = %s WHERE id=%s",
(st_line.date, st_line.id))
return st_line
@classmethod
def _create_reconcile_model(cls, **kwargs):
return cls.env['account.reconcile.model'].create({
'name': "test",
'rule_type': 'invoice_matching',
'allow_payment_tolerance': True,
'payment_tolerance_type': 'percentage',
'payment_tolerance_param': 0.0,
**kwargs,
'line_ids': [
Command.create({
'account_id': cls.company_data['default_account_revenue'].id,
'amount_type': 'percentage',
'label': f"test {i}",
**line_vals,
})
for i, line_vals in enumerate(kwargs.get('line_ids', []))
],
})

View File

@@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
from odoo.addons.fusion_accounting.tests.test_bank_rec_widget_common import TestBankRecWidgetCommon
from odoo.tests import tagged, HttpCase
from odoo import Command
@tagged('post_install', '-at_install')
class TestBankRecWidget(TestBankRecWidgetCommon, HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.st_line1 = cls._create_st_line(1000.0, payment_ref="line1", sequence=1)
cls.st_line2 = cls._create_st_line(1000.0, payment_ref="line2", sequence=2)
cls._create_st_line(1000.0, payment_ref="line3", sequence=3)
cls._create_st_line(1000.0, payment_ref="line_credit", sequence=4, journal_id=cls.company_data['default_journal_credit'].id)
# INV/2019/00001:
cls._create_invoice_line(
'out_invoice',
partner_id=cls.partner_a.id,
invoice_date='2019-01-01',
invoice_line_ids=[{'price_unit': 1000.0}],
)
# INV/2019/00002:
cls._create_invoice_line(
'out_invoice',
partner_id=cls.partner_a.id,
invoice_date='2019-01-01',
invoice_line_ids=[{'price_unit': 1000.0}],
)
cls.env['account.reconcile.model']\
.search([('company_id', '=', cls.company_data['company'].id)])\
.write({'past_months_limit': None})
cls.reco_model_invoice = cls.env['account.reconcile.model'].create({
'name': "test reconcile create invoice",
'rule_type': 'writeoff_button',
'counterpart_type': 'sale',
'line_ids': [
Command.create({'amount_string': '50'}),
Command.create({'amount_string': '50'}),
],
})
def test_tour_bank_rec_widget(self):
self.start_tour('/odoo', 'fusion_accounting_bank_rec_widget', login=self.env.user.login)
self.assertRecordValues(self.st_line1.line_ids, [
# pylint: disable=C0326
{'account_id': self.st_line1.journal_id.default_account_id.id, 'balance': 1000.0, 'reconciled': False},
{'account_id': self.company_data['default_account_receivable'].id, 'balance': -1000.0, 'reconciled': True},
])
tax_account = self.company_data['default_tax_sale'].invoice_repartition_line_ids.account_id
self.assertRecordValues(self.st_line2.line_ids, [
# pylint: disable=C0326
{'account_id': self.st_line2.journal_id.default_account_id.id, 'balance': 1000.0, 'tax_ids': []},
{'account_id': self.company_data['default_account_payable'].id, 'balance': -869.57, 'tax_ids': self.company_data['default_tax_sale'].ids},
{'account_id': tax_account.id, 'balance': -130.43, 'tax_ids': []},
])
def test_tour_bank_rec_widget_ui(self):
bank2 = self.env['account.journal'].create({
'name': 'Bank2',
'type': 'bank',
'code': 'BNK2',
})
self._create_st_line(222.22, payment_ref="line4", sequence=4, journal_id=bank2.id)
# INV/2019/00003:
self._create_invoice_line(
'out_invoice',
partner_id=self.partner_a.id,
invoice_date='2019-01-01',
invoice_line_ids=[{'price_unit': 2000.0}],
)
self.st_line2.payment_ref = self.st_line2.payment_ref + ' - ' + 'INV/2019/00001'
self.start_tour('/odoo?debug=assets', 'fusion_accounting_bank_rec_widget_ui', timeout=120, login=self.env.user.login)
def test_tour_bank_rec_widget_rainbowman_reset(self):
self.start_tour('/odoo?debug=assets', 'fusion_accounting_bank_rec_widget_rainbowman_reset', login=self.env.user.login)
def test_tour_bank_rec_widget_statements(self):
self.start_tour('/odoo?debug=assets', 'fusion_accounting_bank_rec_widget_statements', login=self.env.user.login)
def test_tour_invoice_creation_from_reco_model(self):
""" Test if move is created and added as a new_aml line in bank reconciliation widget """
st_line = self._create_st_line(amount=1000, partner_id=self.partner_a.id)
wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({})
# The tour creates a move through reco model button, posts it, returns to widget and validates the move
self.start_tour(
'/odoo',
'fusion_accounting_bank_rec_widget_reconciliation_button',
login=self.env.user.login,
)
# Mount the validated statement line to confirm that information matches.
wizard._js_action_mount_st_line(st_line.id)
self.assertRecordValues(wizard.line_ids, [
{'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': 1000},
{'flag': 'aml', 'account_id': self.company_data['default_account_receivable'].id, 'balance': -1000},
])
# Check that the aml comes from a move, and not from the auto-balance line
self.assertTrue(wizard.line_ids[1].source_aml_move_id)
def test_tour_invoice_creation_reco_model_currency(self):
""" Test move creation through reconcile button when a foreign currency is used for the statement line """
st_line = self._create_st_line(
1800.0,
date='2019-02-01',
foreign_currency_id=self.other_currency.id, # rate 2:1
amount_currency=3600.0,
partner_id=self.partner_a.id,
)
wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({})
self.start_tour(
'/odoo',
'fusion_accounting_bank_rec_widget_reconciliation_button',
login=self.env.user.login,
)
# Mount the validated statement line to confirm that information matches.
wizard._js_action_mount_st_line(st_line.id)
# Move is created in the foreign currency, but in bank widget the balance appears in main currency.
# If aml was created from the reco model button, display name matches payment_ref.
self.assertRecordValues(wizard.line_ids, [
{'flag': 'liquidity', 'balance': 1800, 'amount_currency': 1800},
{'flag': 'aml', 'balance': -1800, 'amount_currency': -3600},
])
# Confirm that the aml comes from a move, and not from the auto-balance line
self.assertTrue(wizard.line_ids[1].source_aml_move_id)
def test_tour_invoice_creation_combined_reco_model(self):
""" Test creation of a move from a reconciliation model with different amount types """
self.reco_model_invoice.name = "old test" # rename previous reco model to be able to reuse the existing tour
self.env['account.reconcile.model'].create({
'name': "test reconcile combined",
'rule_type': 'writeoff_button',
'counterpart_type': 'purchase',
'line_ids': [
Command.create({
'amount_type': 'percentage_st_line',
'amount_string': '50',
}),
Command.create({
'amount_type': 'percentage',
'amount_string': '50',
'tax_ids': self.tax_purchase_b.ids,
}),
Command.create({
'amount_type': 'fixed',
'amount_string': '100',
'account_id': self.env.company.expense_currency_exchange_account_id.id,
'tax_ids': [Command.clear()] # remove default tax added
}),
# Regex line will not be added to move, as the label of st line does not include digits
Command.create({
'amount_type': 'regex',
'amount_string': r'BRT: ([\d,.]+)',
}),
],
})
st_line = self._create_st_line(amount=-1000, partner_id=self.partner_a.id, payment_ref="combined test")
wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({})
# The tour creates a move through reco model button, posts it, returns to widget and validates the move
self.start_tour(
'/odoo',
'fusion_accounting_bank_rec_widget_reconciliation_button',
login=self.env.user.login,
)
# Mount the validated statement line to confirm that widget line matches created move and balance line is added.
wizard._js_action_mount_st_line(st_line.id)
self.assertRecordValues(wizard.line_ids, [
{'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': -1000},
{'flag': 'aml', 'account_id': self.company_data['default_account_payable'].id, 'balance': 850},
{'flag': 'aml', 'account_id': self.company_data['default_account_payable'].id, 'balance': 150},
])
# Check that the aml comes from an existing move
move = wizard.line_ids[1].source_aml_move_id
self.assertTrue(move)
# The total price of these lines should match the percentage or fixed amount of reco model lines
self.assertRecordValues(move.line_ids, [
# 50% of statement line (of 1000.0)
{'price_total': 500, 'debit': 434.78, 'credit': 0, 'name': 'combined test', 'account_id': self.company_data['default_account_expense'].id},
# 50% of balance (of residual value = 500.0)
{'price_total': 250, 'debit': 217.39, 'credit': 0, 'name': 'combined test', 'account_id': self.company_data['default_account_expense'].id},
# fixed amount of 100.0, no tax in reco model line
{'price_total': 100, 'debit': 100, 'credit': 0, 'name': 'combined test', 'account_id': self.env.company.expense_currency_exchange_account_id.id},
# Tax for line 1 (65.22 + 434.78 = 500)
{'price_total': 0, 'debit': 65.22, 'credit': 0, 'name': '15%', 'account_id': self.company_data['default_account_tax_purchase'].id},
# Tax for line 1 (32.61 + 217.39 = 250)
{'price_total': 0, 'debit': 32.61, 'credit': 0, 'name': '15% (Copy)', 'account_id': self.company_data['default_account_tax_purchase'].id},
{'price_total': 0, 'debit': 0, 'credit': 850, 'name': 'combined test', 'account_id': self.company_data['default_account_payable'].id},
])

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,201 @@
from datetime import timedelta
from odoo import fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.fusion_accounting.wizard.account_change_lock_date import SOFT_LOCK_DATE_FIELDS
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.tools import frozendict
@tagged('post_install', '-at_install')
class TestChangeLockDateWizard(AccountTestInvoicingCommon):
def test_exception_generation(self):
"""
Test the exception generation from the wizard.
Note that exceptions for 'everyone' and 'forever' are not tested here.
They do not create an exception (no 'account.lock_exception' object), but just change the lock date.
(See `test_everyone_forever_exception`.)
"""
self.env['account.lock_exception'].search([]).sudo().unlink()
for lock_date_field in SOFT_LOCK_DATE_FIELDS:
with self.subTest(lock_date_field=lock_date_field), self.cr.savepoint() as sp:
# We can set the lock date if there is none.
self.env['account.change.lock.date'].create({lock_date_field: '2010-01-01'}).change_lock_date()
self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2010-01-01'))
# We can increase the lock date if there is one.
self.env['account.change.lock.date'].create({lock_date_field: '2011-01-01'}).change_lock_date()
self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2011-01-01'))
# We cannot remove the lock date; but we can create an exception
wizard = self.env['account.change.lock.date'].create({
lock_date_field: False,
'exception_applies_to': 'everyone',
'exception_duration': '1h',
'exception_reason': ':TestChangeLockDateWizard.test_exception_generation; remove',
})
wizard.change_lock_date()
self.assertEqual(self.env['account.lock_exception'].search_count([]), 1)
exception = self.env['account.lock_exception'].search([])
self.assertEqual(len(exception), 1)
self.assertRecordValues(exception, [{
lock_date_field: False,
'company_id': self.env.company.id,
'user_id': False,
'create_uid': self.env.user.id,
'end_datetime': self.env.cr.now() + timedelta(hours=1),
'reason': ':TestChangeLockDateWizard.test_exception_generation; remove',
}])
exception.sudo().unlink()
# Ensure we have not created any exceptions yet
self.assertEqual(self.env['account.lock_exception'].search_count([]), 0)
# We cannot decrease the lock date; but we can create an exception
self.env['account.change.lock.date'].create({lock_date_field: '2009-01-01'}).change_lock_date()
self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2011-01-01'))
exception = self.env['account.lock_exception'].search([])
self.assertEqual(len(exception), 1)
# Check lock date and default values on exception
self.assertRecordValues(exception, [{
lock_date_field: fields.Date.from_string('2009-01-01'),
'company_id': self.env.company.id,
'user_id': self.env.user.id,
'create_uid': self.env.user.id,
'end_datetime': self.env.cr.now() + timedelta(minutes=5),
'reason': False,
}])
sp.close() # Rollback to ensure all subtests start in the same situation
def test_exception_generation_multiple(self):
"""
Test the exception generation from the wizard.
Here we test the case that we create multiple exceptions at once.
This should create an exception object for every changed lock date.
"""
self.env['account.lock_exception'].search([]).sudo().unlink()
wizard = self.env['account.change.lock.date'].create({
'fiscalyear_lock_date': '2010-01-01',
'tax_lock_date': '2010-01-01',
'sale_lock_date': '2010-01-01',
'purchase_lock_date': '2010-01-01',
})
wizard.change_lock_date()
self.assertRecordValues(self.env.company, [{
'fiscalyear_lock_date': fields.Date.from_string('2010-01-01'),
'tax_lock_date': fields.Date.from_string('2010-01-01'),
'sale_lock_date': fields.Date.from_string('2010-01-01'),
'purchase_lock_date': fields.Date.from_string('2010-01-01'),
}])
wizard = self.env['account.change.lock.date'].create({
'fiscalyear_lock_date': '2009-01-01',
'tax_lock_date': '2009-01-01',
'sale_lock_date': '2009-01-01',
'purchase_lock_date': '2009-01-01',
'exception_applies_to': 'everyone',
'exception_duration': '1h',
'exception_reason': ':TestChangeLockDateWizard.test_exception_generation; remove',
})
wizard.change_lock_date()
exceptions = self.env['account.lock_exception'].search([])
self.assertEqual(len(exceptions), 4)
expected_exceptions = {
frozendict({
'lock_date_field': 'fiscalyear_lock_date',
'lock_date': fields.Date.from_string('2009-01-01'),
}),
frozendict({
'lock_date_field': 'tax_lock_date',
'lock_date': fields.Date.from_string('2009-01-01'),
}),
frozendict({
'lock_date_field': 'sale_lock_date',
'lock_date': fields.Date.from_string('2009-01-01'),
}),
frozendict({
'lock_date_field': 'purchase_lock_date',
'lock_date': fields.Date.from_string('2009-01-01'),
}),
}
created_exceptions = {
frozendict({
'lock_date_field': exception.lock_date_field,
'lock_date': exception.lock_date,
})
for exception in exceptions
}
self.assertSetEqual(created_exceptions, expected_exceptions)
def test_hard_lock_date(self):
self.env['account.lock_exception'].search([]).sudo().unlink()
# We can set the hard lock date if there is none.
self.env['account.change.lock.date'].create({'hard_lock_date': '2010-01-01'}).change_lock_date()
self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2010-01-01'))
# We can increase the hard lock date if there is one.
self.env['account.change.lock.date'].create({'hard_lock_date': '2011-01-01'}).change_lock_date()
self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2011-01-01'))
# We cannot decrease the hard lock date; not even with an exception.
wizard = self.env['account.change.lock.date'].create({
'hard_lock_date': '2009-01-01',
'exception_applies_to': 'everyone',
'exception_duration': '1h',
'exception_reason': ':TestChangeLockDateWizard.test_hard_lock_date',
})
with self.assertRaises(UserError), self.env.cr.savepoint():
wizard.change_lock_date()
self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2011-01-01'))
# We cannot remove the hard lock date; not even with an exception.
wizard = self.env['account.change.lock.date'].create({
'hard_lock_date': False,
'exception_applies_to': 'everyone',
'exception_duration': '1h',
'exception_reason': ':TestChangeLockDateWizard.test_hard_lock_date',
})
with self.assertRaises(UserError), self.env.cr.savepoint():
wizard.change_lock_date()
self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2011-01-01'))
self.assertEqual(self.env['account.lock_exception'].search_count([]), 0)
def test_everyone_forever_exception(self):
self.env['account.lock_exception'].search([]).sudo().unlink()
for lock_date_field in SOFT_LOCK_DATE_FIELDS:
with self.subTest(lock_date_field=lock_date_field), self.cr.savepoint() as sp:
self.env['account.change.lock.date'].create({lock_date_field: '2010-01-01'}).change_lock_date()
self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2010-01-01'))
# We can decrease the lock date with a 'forever' / 'everyone' exception.
self.env['account.change.lock.date'].create({
lock_date_field: '2009-01-01',
'exception_applies_to': 'everyone',
'exception_duration': 'forever',
'exception_reason': ':TestChangeLockDateWizard.test_everyone_forever_exception; remove',
}).change_lock_date()
self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2009-01-01'))
# We can remove the lock date with a 'forever' / 'everyone' exception.
self.env['account.change.lock.date'].create({
lock_date_field: False,
'exception_applies_to': 'everyone',
'exception_duration': 'forever',
'exception_reason': ':TestChangeLockDateWizard.test_everyone_forever_exception; remove',
}).change_lock_date()
self.assertEqual(self.env.company[lock_date_field], False)
# Ensure we have not created any exceptions
self.assertEqual(self.env['account.lock_exception'].search_count([]), 0)
sp.close() # Rollback to ensure all subtests start in the same situation

View File

@@ -0,0 +1,626 @@
# -*- coding: utf-8 -*-
# pylint: disable=C0326
import datetime
from odoo import Command, fields
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from freezegun import freeze_time
@tagged('post_install', '-at_install')
class TestDeferredManagement(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.expense_accounts = [cls.env['account.account'].create({
'name': f'Expense {i}',
'code': f'EXP{i}',
'account_type': 'expense',
}) for i in range(3)]
cls.revenue_accounts = [cls.env['account.account'].create({
'name': f'Revenue {i}',
'code': f'REV{i}',
'account_type': 'income',
}) for i in range(3)]
cls.company.deferred_expense_journal_id = cls.company_data['default_journal_misc'].id
cls.company.deferred_revenue_journal_id = cls.company_data['default_journal_misc'].id
cls.company.deferred_expense_account_id = cls.company_data['default_account_deferred_expense'].id
cls.company.deferred_revenue_account_id = cls.company_data['default_account_deferred_revenue'].id
cls.expense_lines = [
[cls.expense_accounts[0], 1000, '2023-01-01', '2023-04-30'], # 4 full months (=250/month)
[cls.expense_accounts[0], 1050, '2023-01-16', '2023-04-30'], # 3 full months + 15 days (=300/month)
[cls.expense_accounts[1], 1225, '2023-01-01', '2023-04-15'], # 3 full months + 15 days (=350/month)
[cls.expense_accounts[2], 1680, '2023-01-21', '2023-04-14'], # 2 full months + 10 days + 14 days (=600/month)
[cls.expense_accounts[2], 225, '2023-04-01', '2023-04-15'], # 15 days (=450/month)
]
cls.revenue_lines = [
[cls.revenue_accounts[0], 1000, '2023-01-01', '2023-04-30'], # 4 full months (=250/month)
[cls.revenue_accounts[0], 1050, '2023-01-16', '2023-04-30'], # 3 full months + 15 days (=300/month)
[cls.revenue_accounts[1], 1225, '2023-01-01', '2023-04-15'], # 3 full months + 15 days (=350/month)
[cls.revenue_accounts[2], 1680, '2023-01-21', '2023-04-14'], # 2 full months + 10 days + 14 days (=600/month)
[cls.revenue_accounts[2], 225, '2023-04-01', '2023-04-15'], # 15 days (=450/month)
]
def create_invoice(self, move_type, invoice_lines, date=None, post=True):
journal = self.company_data['default_journal_purchase'] if move_type == 'in_invoice' else self.company_data['default_journal_sale']
move = self.env['account.move'].create({
'move_type': move_type,
'partner_id': self.partner_a.id,
'date': date or '2023-01-01',
'invoice_date': date or '2023-01-01',
'journal_id': journal.id,
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'quantity': 1,
'account_id': account.id,
'price_unit': price_unit,
'deferred_start_date': start_date,
'deferred_end_date': end_date,
}) for account, price_unit, start_date, end_date in invoice_lines
]
})
if post:
move.action_post()
return move
def test_deferred_management_get_diff_dates(self):
def assert_get_diff_dates(start, end, expected):
diff = self.env['account.move']._get_deferred_diff_dates(fields.Date.to_date(start), fields.Date.to_date(end))
self.assertAlmostEqual(diff, expected, 3)
assert_get_diff_dates('2023-01-01', '2023-01-01', 0)
assert_get_diff_dates('2023-01-01', '2023-01-02', 1/30)
assert_get_diff_dates('2023-01-01', '2023-01-20', 19/30)
assert_get_diff_dates('2023-01-01', '2023-01-31', 29/30)
assert_get_diff_dates('2023-01-01', '2023-01-30', 29/30)
assert_get_diff_dates('2023-01-01', '2023-02-01', 1)
assert_get_diff_dates('2023-01-01', '2023-02-28', 1 + 29/30)
assert_get_diff_dates('2023-02-01', '2023-02-28', 29/30)
assert_get_diff_dates('2023-02-10', '2023-02-28', 20/30)
assert_get_diff_dates('2023-01-01', '2023-02-15', 1 + 14/30)
assert_get_diff_dates('2023-01-01', '2023-03-31', 2 + 29/30)
assert_get_diff_dates('2023-01-01', '2023-04-01', 3)
assert_get_diff_dates('2023-01-01', '2023-04-30', 3 + 29/30)
assert_get_diff_dates('2023-01-10', '2023-04-30', 3 + 20/30)
assert_get_diff_dates('2023-01-10', '2023-04-09', 2 + 29/30)
assert_get_diff_dates('2023-01-10', '2023-04-10', 3)
assert_get_diff_dates('2023-01-10', '2023-04-11', 3 + 1/30)
assert_get_diff_dates('2023-02-20', '2023-04-10', 1 + 20/30)
assert_get_diff_dates('2023-01-31', '2023-04-30', 3)
assert_get_diff_dates('2023-02-28', '2023-04-10', 1 + 10/30)
assert_get_diff_dates('2023-03-01', '2023-04-10', 1 + 9/30)
assert_get_diff_dates('2023-04-10', '2023-03-01', 1 + 9/30)
assert_get_diff_dates('2023-01-01', '2023-12-31', 11 + 29/30)
assert_get_diff_dates('2023-01-01', '2024-01-01', 12)
assert_get_diff_dates('2023-01-01', '2024-07-01', 18)
assert_get_diff_dates('2023-01-01', '2024-07-10', 18 + 9/30)
def test_get_ends_of_month(self):
def assertEndsOfMonths(start_date, end_date, expected):
self.assertEqual(
self.env['account.move.line']._get_deferred_ends_of_month(
fields.Date.to_date(start_date),
fields.Date.to_date(end_date)
),
[fields.Date.to_date(date) for date in expected]
)
assertEndsOfMonths('2023-01-01', '2023-01-01', ['2023-01-31'])
assertEndsOfMonths('2023-01-01', '2023-01-02', ['2023-01-31'])
assertEndsOfMonths('2023-01-01', '2023-01-20', ['2023-01-31'])
assertEndsOfMonths('2023-01-01', '2023-01-30', ['2023-01-31'])
assertEndsOfMonths('2023-01-01', '2023-01-31', ['2023-01-31'])
assertEndsOfMonths('2023-01-01', '2023-02-01', ['2023-01-31', '2023-02-28'])
assertEndsOfMonths('2023-01-01', '2023-02-28', ['2023-01-31', '2023-02-28'])
assertEndsOfMonths('2023-02-01', '2023-02-28', ['2023-02-28'])
assertEndsOfMonths('2023-02-10', '2023-02-28', ['2023-02-28'])
assertEndsOfMonths('2023-01-01', '2023-02-15', ['2023-01-31', '2023-02-28'])
assertEndsOfMonths('2023-01-01', '2023-03-31', ['2023-01-31', '2023-02-28', '2023-03-31'])
assertEndsOfMonths('2023-01-01', '2023-04-01', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30'])
assertEndsOfMonths('2023-01-01', '2023-04-30', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30'])
assertEndsOfMonths('2023-01-10', '2023-04-30', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30'])
assertEndsOfMonths('2023-01-10', '2023-04-09', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30'])
def test_deferred_abnormal_dates(self):
"""
Test that we correctly detect abnormal dates.
In the deferred computations, we always assume that both the start and end date are inclusive
E.g: 1st January -> 31st December is *exactly* 1 year = 12 months
However, the user may instead put 1st January -> 1st January of next year which is then
12 months + 1/30 month = 12.03 months which may result in odd amounts when deferrals are created.
This is what we call abnormal dates.
Other cases were the number of months is not round should not be handled and are not considered abnormal.
"""
move = self.create_invoice('in_invoice', [
[self.expense_accounts[0], 0, '2023-01-01', '2023-12-30'],
[self.expense_accounts[0], 1, '2023-01-01', '2023-12-31'],
[self.expense_accounts[0], 2, '2023-01-01', '2024-01-01'],
[self.expense_accounts[0], 3, '2023-01-01', '2024-01-02'],
[self.expense_accounts[0], 4, '2023-01-01', '2024-01-31'],
[self.expense_accounts[0], 5, '2023-01-01', '2024-02-01'],
[self.expense_accounts[0], 6, '2023-01-02', '2024-02-01'],
[self.expense_accounts[0], 7, '2023-01-02', '2024-02-02'],
[self.expense_accounts[0], 8, '2023-01-31', '2024-01-30'],
[self.expense_accounts[0], 9, '2023-01-31', '2024-02-28'], # 29 days in Feb 2024
# Following one is abnormal because we have a full months in February (= 30 accounting days) + 1 day in January
[self.expense_accounts[0], 10, '2023-01-31', '2024-02-29'],
[self.expense_accounts[0], 11, '2023-02-01', '2024-02-29'],
], post=True)
lines = move.invoice_line_ids.sorted('price_unit')
self.assertFalse(lines[0].has_abnormal_deferred_dates)
self.assertFalse(lines[1].has_abnormal_deferred_dates)
self.assertTrue(lines[2].has_abnormal_deferred_dates)
self.assertFalse(lines[3].has_abnormal_deferred_dates)
self.assertFalse(lines[4].has_abnormal_deferred_dates)
self.assertTrue(lines[5].has_abnormal_deferred_dates)
self.assertFalse(lines[6].has_abnormal_deferred_dates)
self.assertTrue(lines[7].has_abnormal_deferred_dates)
self.assertFalse(lines[8].has_abnormal_deferred_dates)
self.assertFalse(lines[9].has_abnormal_deferred_dates)
self.assertTrue(lines[10].has_abnormal_deferred_dates)
self.assertFalse(lines[11].has_abnormal_deferred_dates)
def test_deferred_expense_generate_entries_method(self):
# The deferred entries are NOT generated when the invoice is validated if the method is set to 'manual'.
self.company.generate_deferred_expense_entries_method = 'manual'
move2 = self.create_invoice('in_invoice', [self.expense_lines[0]], post=True)
self.assertEqual(len(move2.deferred_move_ids), 0)
# Test that the deferred entries are generated when the invoice is validated.
self.company.generate_deferred_expense_entries_method = 'on_validation'
move = self.create_invoice('in_invoice', [self.expense_lines[0]], post=True)
self.assertEqual(len(move.deferred_move_ids), 5) # 1 for the invoice deferred + 4 for the deferred entries
# See test_deferred_expense_credit_note for the values
def test_deferred_expense_reset_to_draft(self):
"""
Test that the deferred entries are deleted/reverted when the invoice is reset to draft.
"""
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 1680, '2023-01-21', '2023-04-14')], date='2023-03-15')
self.assertEqual(len(move.deferred_move_ids), 5)
move.button_draft()
self.assertFalse(move.deferred_move_ids)
# With a lock date, we should reverse the moves that cannot be deleted
move.action_post() # Post the move to create the deferred entries with 'on_validation' method
self.assertEqual(len(move.deferred_move_ids), 5)
move.company_id.fiscalyear_lock_date = fields.Date.to_date('2023-02-15')
move.button_draft()
# January deferred entry is in lock period, so it is reversed, not deleted, thus we have one deferred entry and its revert
self.assertEqual(len(move.deferred_move_ids), 2)
self.assertEqual(move.deferred_move_ids[0].date, fields.Date.to_date('2023-02-28'))
self.assertEqual(move.deferred_move_ids[1].date, fields.Date.to_date('2023-01-31'))
# If we repost the move, it should be allowed
move.action_post()
self.assertEqual(len(move.deferred_move_ids), 2 + 5)
def assert_invoice_lines(self, move, expected_values, source_account, deferred_account):
deferred_moves = move.deferred_move_ids.sorted('date')
for deferred_move, expected_value in zip(deferred_moves, expected_values):
expected_date, expense_line_debit, expense_line_credit, deferred_line_debit, deferred_line_credit = expected_value
self.assertRecordValues(deferred_move, [{
'state': 'posted',
'move_type': 'entry',
'partner_id': self.partner_a.id,
'date': fields.Date.to_date(expected_date),
}])
expense_line = deferred_move.line_ids.filtered(lambda line: line.account_id == source_account)
self.assertRecordValues(expense_line, [
{'debit': expense_line_debit, 'credit': expense_line_credit, 'partner_id': self.partner_a.id},
])
deferred_line = deferred_move.line_ids.filtered(lambda line: line.account_id == deferred_account)
self.assertEqual(deferred_line.debit, deferred_line_debit)
self.assertEqual(deferred_line.credit, deferred_line_credit)
def test_default_tax_on_account_not_on_deferred_entries(self):
"""
Test that the default taxes on an account are not calculated on deferral entries, since this would impact the
tax report.
"""
revenue_account_with_taxes = self.env['account.account'].create({
'name': 'Revenue with Taxes',
'code': 'REVWTAXES',
'account_type': 'income',
'tax_ids': [Command.set(self.tax_sale_a.ids)]
})
move = self.create_invoice(
'out_invoice',
[[revenue_account_with_taxes, 1000, '2023-01-01', '2023-04-30']],
date='2022-12-10'
)
expected_line_values = [
# Date [Line expense] [Line deferred]
('2022-12-10', 1000, 0, 0, 1000),
('2023-01-31', 0, 250, 250, 0),
('2023-02-28', 0, 250, 250, 0),
('2023-03-31', 0, 250, 250, 0),
]
self.assert_invoice_lines(
move,
expected_line_values,
revenue_account_with_taxes,
self.company_data['default_account_deferred_revenue']
)
for deferred_move in move.deferred_move_ids:
# There are no extra lines besides the two lines we checked before
self.assertEqual(len(deferred_move.line_ids), 2)
def test_deferred_values(self):
"""
Test that the debit/credit values are correctly computed, even after a credit note is issued.
"""
expected_line_values1 = [
# Date [Line expense] [Line deferred]
('2022-12-10', 0, 1000, 1000, 0),
('2023-01-31', 250, 0, 0, 250),
('2023-02-28', 250, 0, 0, 250),
('2023-03-31', 250, 0, 0, 250),
]
expected_line_values2 = [
# Date [Line expense] [Line deferred]
('2022-12-10', 1000, 0, 0, 1000),
('2023-01-31', 0, 250, 250, 0),
('2023-02-28', 0, 250, 250, 0),
('2023-03-31', 0, 250, 250, 0),
]
# Vendor bill and credit note
move = self.create_invoice('in_invoice', [self.expense_lines[0]], post=True, date='2022-12-10')
self.assert_invoice_lines(move, expected_line_values1, self.expense_accounts[0], self.company_data['default_account_deferred_expense'])
reverse_move = move._reverse_moves()
self.assert_invoice_lines(reverse_move, expected_line_values2, self.expense_accounts[0], self.company_data['default_account_deferred_expense'])
# Customer invoice and credit note
move2 = self.create_invoice('out_invoice', [self.revenue_lines[0]], post=True, date='2022-12-10')
self.assert_invoice_lines(move2, expected_line_values2, self.revenue_accounts[0], self.company_data['default_account_deferred_revenue'])
reverse_move2 = move2._reverse_moves()
self.assert_invoice_lines(reverse_move2, expected_line_values1, self.revenue_accounts[0], self.company_data['default_account_deferred_revenue'])
def test_deferred_values_rounding(self):
"""
Test that the debit/credit values are correctly computed when values are rounded
"""
# Vendor Bill
expense_line = [self.expense_accounts[0], 500, '2020-08-07', '2020-12-07']
expected_line_values = [
# Date [Line expense] [Line deferred]
('2020-08-07', 0, 500, 500, 0),
('2020-08-31', 99.17, 0, 0, 99.17),
('2020-09-30', 123.97, 0, 0, 123.97),
('2020-10-31', 123.97, 0, 0, 123.97),
('2020-11-30', 123.97, 0, 0, 123.97),
('2020-12-07', 28.92, 0, 0, 28.92),
]
self.assertEqual(self.company.currency_id.round(sum(x[1] for x in expected_line_values)), 500)
move = self.create_invoice('in_invoice', [expense_line], date='2020-08-07')
self.assert_invoice_lines(move, expected_line_values, self.expense_accounts[0], self.company_data['default_account_deferred_expense'])
# Customer invoice
revenue_line = [self.revenue_accounts[0], 500, '2020-08-07', '2020-12-07']
expected_line_values = [
# Date [Line expense] [Line deferred]
('2020-08-07', 500, 0, 0, 500),
('2020-08-31', 0, 99.17, 99.17, 0),
('2020-09-30', 0, 123.97, 123.97, 0),
('2020-10-31', 0, 123.97, 123.97, 0),
('2020-11-30', 0, 123.97, 123.97, 0),
('2020-12-07', 0, 28.92, 28.92, 0),
]
self.assertEqual(self.company.currency_id.round(sum(x[2] for x in expected_line_values)), 500)
move = self.create_invoice('out_invoice', [revenue_line], post=True, date='2020-08-07')
self.assert_invoice_lines(move, expected_line_values, self.revenue_accounts[0], self.company_data['default_account_deferred_revenue'])
def test_deferred_expense_avoid_useless_deferred_entries(self):
"""
If we have an invoice with a start date in the beginning of the month, and an end date in the end of the month,
we should not create the deferred entries because the original invoice will be totally deferred
on the last day of the month, but the full amount will be accounted for on the same day too, thus
cancelling each other. Therefore we should not create the deferred entries.
"""
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 1680, '2023-01-01', '2023-01-31')], date='2023-01-01')
self.assertEqual(len(move.deferred_move_ids), 0)
def test_deferred_expense_single_period_entries(self):
"""
If we have an invoice covering only one period, we should only avoid creating deferral entries when the
accounting date is the same as the period for the deferral. Otherwise we should still generate a deferral entry.
"""
self.company.deferred_expense_amount_computation_method = 'month'
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 1680, '2023-02-01', '2023-02-28')])
self.assertRecordValues(move.deferred_move_ids, [
{'date': fields.Date.to_date('2023-01-01')},
{'date': fields.Date.to_date('2023-02-28')},
])
def test_taxes_deferred_after_date_added(self):
"""
Test that applicable taxes get deferred also when the dates of the base line are filled in after a first save.
"""
expected_line_values = [
# Date [Line expense] [Line deferred]
('2022-12-10', 0, 1000, 1000, 0),
('2022-12-10', 0, 100, 100, 0),
('2023-01-31', 250, 0, 0, 250),
('2023-01-31', 25, 0, 0, 25),
('2023-02-28', 250, 0, 0, 250),
('2023-02-28', 25, 0, 0, 25),
('2023-03-31', 250, 0, 0, 250),
('2023-03-31', 25, 0, 0, 25),
]
partially_deductible_tax = self.env['account.tax'].create({
'name': 'Partially deductible Tax',
'amount': 20,
'amount_type': 'percent',
'type_tax_use': 'purchase',
'invoice_repartition_line_ids': [
Command.create({'repartition_type': 'base'}),
Command.create({
'factor_percent': 50,
'repartition_type': 'tax',
'use_in_tax_closing': False
}),
Command.create({
'factor_percent': 50,
'repartition_type': 'tax',
'account_id': self.company_data['default_account_tax_purchase'].id,
'use_in_tax_closing': True
}),
],
'refund_repartition_line_ids': [
Command.create({'repartition_type': 'base'}),
Command.create({
'factor_percent': 50,
'repartition_type': 'tax',
'use_in_tax_closing': False
}),
Command.create({
'factor_percent': 50,
'repartition_type': 'tax',
'account_id': self.company_data['default_account_tax_purchase'].id,
'use_in_tax_closing': True
}),
],
})
move = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'date': '2022-12-10',
'invoice_date': '2022-12-10',
'journal_id': self.company_data['default_journal_purchase'].id,
'invoice_line_ids': [
Command.create({
'quantity': 1,
'account_id': self.expense_lines[0][0].id,
'price_unit': self.expense_lines[0][1],
'tax_ids': [Command.set(partially_deductible_tax.ids)],
})
]
})
move.invoice_line_ids.write({
'deferred_start_date': self.expense_lines[0][2],
'deferred_end_date': self.expense_lines[0][3],
})
move.action_post()
self.assert_invoice_lines(move, expected_line_values, self.expense_accounts[0], self.company_data['default_account_deferred_expense'])
def test_deferred_tax_key(self):
"""
Test that the deferred tax key is correctly computed.
and is the same between _compute_tax_key and _compute_all_tax
"""
lines = [
[self.expense_accounts[0], 1000, '2023-01-01', '2023-04-30'],
[self.expense_accounts[0], 1000, False, False],
]
move = self.create_invoice('in_invoice', lines, post=True)
original_amount_total = move.amount_total
self.assertEqual(len(move.line_ids.filtered(lambda l: l.display_type == 'tax')), 1)
move.button_draft()
move.action_post()
# The number of tax lines shouldn't change, nor the total amount
self.assertEqual(len(move.line_ids.filtered(lambda l: l.display_type == 'tax')), 1)
self.assertEqual(move.amount_total, original_amount_total)
def test_compute_empty_start_date(self):
"""
Test that the deferred start date is computed when empty and posting the move.
"""
lines = [[self.expense_accounts[0], 1000, False, '2023-04-30']]
move = self.create_invoice('in_invoice', lines, post=False)
# We don't have a deferred date in the beginning
self.assertFalse(move.line_ids[0].deferred_start_date)
move.action_post()
# Deferred start date is set after post
self.assertEqual(move.line_ids[0].deferred_start_date, datetime.date(2023, 1, 1))
move.button_draft()
move.line_ids[0].deferred_start_date = False
move.invoice_date = '2023-02-01'
# Start date is set when changing invoice date
self.assertEqual(move.line_ids[0].deferred_start_date, datetime.date(2023, 2, 1))
move.line_ids[0].deferred_start_date = False
move.line_ids[0].deferred_end_date = '2023-05-31'
# Start date is set when changing deferred end date
self.assertEqual(move.line_ids[0].deferred_start_date, datetime.date(2023, 2, 1))
def test_deferred_on_accounting_date(self):
"""
When we are in `on_validation` mode, the deferral of the total amount should happen on the
accounting date of the move.
"""
move = self.create_invoice(
'in_invoice',
[(self.expense_accounts[0], 1680, '2023-01-01', '2023-02-28')],
date='2023-01-10',
post=False
)
move.date = '2023-01-15'
move.action_post()
self.assertRecordValues(move.deferred_move_ids, [
{'date': fields.Date.to_date('2023-01-15')},
{'date': fields.Date.to_date('2023-01-31')},
{'date': fields.Date.to_date('2023-02-28')},
])
def test_deferred_entries_not_created_on_future_invoice(self):
"""Test that we don't create deferred entries on a future posted invoice"""
tomorrow = fields.Date.to_date(fields.Date.today()) + datetime.timedelta(days=1)
move = self.create_invoice(
'out_invoice',
[(self.expense_accounts[0], 1680, tomorrow, tomorrow + datetime.timedelta(days=100))],
date=tomorrow,
post=False
)
move.auto_post = "at_date"
move._post()
self.assertFalse(move.deferred_move_ids)
with freeze_time(tomorrow):
self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger()
self.assertEqual(move.state, 'posted')
self.assertTrue(move.deferred_move_ids)
def test_deferred_entries_created_on_auto_post_invoice(self):
"""Test that deferred entries are created on an invoice with auto_post set to 'at_date'"""
yesterday = fields.Date.to_date(fields.Date.today()) - datetime.timedelta(days=1)
move = self.create_invoice(
'out_invoice',
[(self.expense_accounts[0], 1680, yesterday, yesterday + datetime.timedelta(days=45))],
date=yesterday,
post=False
)
move.auto_post = "at_date"
move._post()
self.assertEqual(move.state, 'posted')
self.assertTrue(move.deferred_move_ids)
def test_deferred_compute_method_full_months(self):
"""
Test that the deferred amount is correctly computed when the new full_months method computation is used
"""
self.company.deferred_expense_amount_computation_method = 'full_months'
dates = (('2024-06-05', '2025-06-04'), ('2024-06-30', '2025-06-29'))
for (date_from, date_to) in dates:
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, date_from, date_to)], date='2024-06-05')
self.assertRecordValues(move.deferred_move_ids.sorted('date'), [
{'date': fields.Date.to_date('2024-06-05'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-06-30'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-07-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-08-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-09-30'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-10-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-11-30'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-12-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-01-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-02-28'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-03-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-04-30'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-05-31'), 'amount_total': 1000},
# 0 for June 2025, so no move created
])
# Start of month <=> Equal per month method
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-07-01', '2025-06-30')], date='2024-07-01')
self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [
{'date': fields.Date.to_date('2024-07-01'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-07-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-08-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-09-30'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-10-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-11-30'), 'amount_total': 1000},
{'date': fields.Date.to_date('2024-12-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-01-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-02-28'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-03-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-04-30'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-05-31'), 'amount_total': 1000},
{'date': fields.Date.to_date('2025-06-30'), 'amount_total': 1000},
])
# Nothing to defer, everything is in the same month
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-01', '2024-01-16')], date='2024-01-01')
self.assertFalse(move.deferred_move_ids)
# Round period of 2 months -> Divide by 2
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-01', '2024-02-29')], date='2024-01-01')
self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [
{'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-01-31'), 'amount_total': 6000},
{'date': fields.Date.to_date('2024-02-29'), 'amount_total': 6000},
])
# Round period of 2 months -> Divide by 2
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-15', '2024-03-14')], date='2024-01-01')
self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [
{'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-01-31'), 'amount_total': 6000},
{'date': fields.Date.to_date('2024-02-29'), 'amount_total': 6000},
])
# Period of exactly one month: full amount should be in Jan. So we revert 1st Jan, and account for 31st Jan <=> don't generate anything
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-15', '2024-02-14')], date='2024-01-01')
self.assertFalse(move.deferred_move_ids)
# Not-round period of 1.5 month with only one end of month in January (same explanation as above)
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-01', '2024-02-15')], date='2024-01-01')
self.assertFalse(move.deferred_move_ids)
# Not-round period of 1.5+ month with only one end of month in January (same explanation as above)
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-05', '2024-02-15')], date='2024-01-01')
self.assertFalse(move.deferred_move_ids)
# Period of exactly one month: full amount should be in Feb. So we revert 1st Jan, and account for all on 29th Feb.
# Deferrals are in different months for this case, so we should the deferrals should be generated.
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-02-15', '2024-03-14')], date='2024-01-01')
self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [
{'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-02-29'), 'amount_total': 12000},
])
# Not-round period of 1.5+ month: full amount should be in Feb. So we revert 1st Jan, and account for all on 29th Feb.
# Deferrals are in different months for this case, so we should the deferrals should be generated.
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-02-05', '2024-03-15')], date='2024-01-01')
self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [
{'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-02-29'), 'amount_total': 12000},
])
# Not-round period of 1.5 month with 2 ends of months, so divide balance by 2
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-16', '2024-02-29')], date='2024-01-01')
self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [
{'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-01-31'), 'amount_total': 6000},
{'date': fields.Date.to_date('2024-02-29'), 'amount_total': 6000},
])
# Not-round period of 2.5 month, with 3 ends of months, so divide balance by 3
move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-16', '2024-03-31')], date='2024-01-01')
self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [
{'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000},
{'date': fields.Date.to_date('2024-01-31'), 'amount_total': 4000},
{'date': fields.Date.to_date('2024-02-29'), 'amount_total': 4000},
{'date': fields.Date.to_date('2024-03-31'), 'amount_total': 4000},
])

View File

@@ -0,0 +1,919 @@
# -*- coding: utf-8 -*-
# pylint: disable=C0326
from .common import TestAccountReportsCommon
from odoo import fields, Command
from odoo.tests import tagged
from freezegun import freeze_time
@tagged('post_install', '-at_install')
class TestFinancialReport(TestAccountReportsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# ==== Partners ====
cls.partner_c = cls._create_partner(name='partner_c')
# ==== Accounts ====
# Cleanup existing "Current year earnings" accounts since we can only have one by company.
cls.env['account.account'].search([
('company_ids', 'in', (cls.company_data['company'] + cls.company_data_2['company']).ids),
('account_type', '=', 'equity_unaffected'),
]).unlink()
account_type_data = [
('asset_receivable', {'reconcile': True}),
('liability_payable', {'reconcile': True}),
('asset_cash', {}),
('asset_current', {}),
('asset_prepayments', {}),
('asset_fixed', {}),
('asset_non_current', {}),
('equity', {}),
('equity_unaffected', {}),
('income', {}),
]
accounts = cls.env['account.account'].create([{
**data[1],
'name': 'account%s' % i,
'code': 'code%s' % i,
'account_type': data[0],
} for i, data in enumerate(account_type_data)])
accounts_2 = cls.env['account.account'].create([{
**data[1],
'name': 'account%s' % (i + 100),
'code': 'code%s' % (i + 100),
'account_type': data[0],
'company_ids': [Command.link(cls.company_data_2['company'].id)]
} for i, data in enumerate(account_type_data)])
for account in accounts_2:
account.code = account.with_company(cls.company_data_2['company']).code
# ==== Custom filters ====
cls.horizontal_group = cls.env['account.report.horizontal.group'].create({
'name': 'Horizontal Group',
'rule_ids': [
Command.create({
'field_name': 'partner_id',
'domain': f"[('id', 'in', {(cls.partner_a + cls.partner_b).ids})]",
}),
Command.create({
'field_name': 'account_id',
'domain': f"[('id', 'in', {accounts[:2].ids})]",
}),
],
})
# ==== Journal entries ====
cls.move_2019 = cls.env['account.move'].create({
'move_type': 'entry',
'date': fields.Date.from_string('2019-01-01'),
'line_ids': [
(0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_b.id}),
(0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_c.id}),
(0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 200.0, 'credit': 0.0, 'account_id': accounts[1].id, 'partner_id': cls.partner_b.id}),
(0, 0, {'debit': 0.0, 'credit': 300.0, 'account_id': accounts[2].id, 'partner_id': cls.partner_c.id}),
(0, 0, {'debit': 400.0, 'credit': 0.0, 'account_id': accounts[3].id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 0.0, 'credit': 1100.0, 'account_id': accounts[4].id, 'partner_id': cls.partner_b.id}),
(0, 0, {'debit': 700.0, 'credit': 0.0, 'account_id': accounts[6].id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 0.0, 'credit': 800.0, 'account_id': accounts[7].id, 'partner_id': cls.partner_b.id}),
(0, 0, {'debit': 800.0, 'credit': 0.0, 'account_id': accounts[8].id, 'partner_id': cls.partner_c.id}),
],
})
cls.move_2019.action_post()
cls.move_2018 = cls.env['account.move'].create({
'move_type': 'entry',
'date': fields.Date.from_string('2018-01-01'),
'line_ids': [
(0, 0, {'debit': 1000.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 0.0, 'credit': 1000.0, 'account_id': accounts[2].id, 'partner_id': cls.partner_b.id}),
(0, 0, {'debit': 250.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 0.0, 'credit': 250.0, 'account_id': accounts[9].id, 'partner_id': cls.partner_a.id}),
],
})
cls.move_2018.action_post()
cls.move_2017 = cls.env['account.move'].with_company(cls.company_data_2['company']).create({
'move_type': 'entry',
'date': fields.Date.from_string('2017-01-01'),
'line_ids': [
(0, 0, {'debit': 2000.0, 'credit': 0.0, 'account_id': accounts_2[0].id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 0.0, 'credit': 4000.0, 'account_id': accounts_2[2].id, 'partner_id': cls.partner_b.id}),
(0, 0, {'debit': 0.0, 'credit': 5000.0, 'account_id': accounts_2[4].id, 'partner_id': cls.partner_c.id}),
(0, 0, {'debit': 7000.0, 'credit': 0.0, 'account_id': accounts_2[6].id, 'partner_id': cls.partner_a.id}),
],
})
cls.move_2017.action_post()
cls.report = cls.env.ref('fusion_accounting.balance_sheet')
cls.report_no_parent_id = cls.env["account.report"].create({
'name': "Test report",
'column_ids': [
Command.create({
'name': 'Balance',
'expression_label': 'balance',
'sequence': 1
})
],
'line_ids': [
Command.create({
'name': "Invisible Partner A line",
'code': "INVA",
'sequence': 1,
'hierarchy_level': 0,
'groupby': "account_id",
'foldable': True,
'expression_ids': [Command.clear(), Command.create({
'label': 'balance',
'engine': 'domain',
'formula': [("partner_id", "=", cls.partner_a.id)],
'subformula': 'sum',
})],
}),
Command.create({
'name': "Invisible Partner B line",
'code': "INVB",
'sequence': 2,
'hierarchy_level': 0,
'groupby': "account_id",
'foldable': True,
'expression_ids': [Command.clear(), Command.create({
'label': 'balance',
'engine': 'domain',
'formula': [("partner_id", "=", cls.partner_b.id)],
'subformula': 'sum',
})],
}),
Command.create({
'name': "Total of Invisible lines",
'code': "INVT",
'sequence': 3,
'hierarchy_level': 0,
'expression_ids': [Command.clear(), Command.create({
'label': 'balance',
'engine': 'aggregation',
'formula': 'INVA.balance + INVB.balance',
})],
}),
],
})
def _build_generic_id_from_financial_line(self, financial_rep_ln_xmlid):
report_line = self.env.ref(financial_rep_ln_xmlid)
return '-account.financial.html.report.line-%s' % report_line.id
def _get_line_id_from_generic_id(self, generic_id):
return int(generic_id.split('-')[-1])
def test_financial_report_strict_range_on_report_lines_with_no_parent_id(self):
""" Tests that lines with no parent can be correctly filtered by date range """
self.report_no_parent_id.filter_multi_company = 'disabled'
options = self._generate_options(self.report_no_parent_id, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31'))
lines = self.report_no_parent_id._get_lines(options)
self.assertLinesValues(
lines,
# Name Balance
[ 0, 1],
[
('Invisible Partner A line', 1150.0),
('Invisible Partner B line', -1675.0),
('Total of Invisible lines', -525.0),
],
options,
)
def test_financial_report_strict_empty_range_on_report_lines_with_no_parent_id(self):
""" Tests that lines with no parent can be correctly filtered by date range with no invoices"""
self.report_no_parent_id.filter_multi_company = 'disabled'
options = self._generate_options(self.report_no_parent_id, fields.Date.from_string('2019-03-01'), fields.Date.from_string('2019-03-31'))
lines = self.report_no_parent_id._get_lines(options)
self.assertLinesValues(
lines,
# Name Balance
[ 0, 1],
[
('Invisible Partner A line', 0.0),
('Invisible Partner B line', 0.0),
('Total of Invisible lines', 0.0),
],
options,
)
@freeze_time("2016-06-06")
def test_balance_sheet_today_current_year_earnings(self):
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2016-02-02',
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'price_unit': 110,
'tax_ids': [],
})]
})
invoice.action_post()
self.report.filter_multi_company = 'disabled'
options = self._generate_options(self.report, fields.Date.from_string('2016-06-01'), fields.Date.from_string('2016-06-06'))
options['date']['filter'] = 'today'
lines = self.report._get_lines(options)
self.assertLinesValues(
lines,
# Name Balance
[ 0, 1],
[
('ASSETS', 110.0),
('Current Assets', 110.0),
('Bank and Cash Accounts', 0.0),
('Receivables', 110.0),
('Current Assets', 0.0),
('Prepayments', 0.0),
('Total Current Assets', 110.0),
('Plus Fixed Assets', 0.0),
('Plus Non-current Assets', 0.0),
('Total ASSETS', 110.0),
('LIABILITIES', 0.0),
('Current Liabilities', 0.0),
('Current Liabilities', 0.0),
('Payables', 0.0),
('Total Current Liabilities', 0.0),
('Plus Non-current Liabilities', 0.0),
('Total LIABILITIES', 0.0),
('EQUITY', 110.0),
('Unallocated Earnings', 110.0),
('Current Year Unallocated Earnings', 110.0),
('Previous Years Unallocated Earnings', 0.0),
('Total Unallocated Earnings', 110.0),
('Retained Earnings', 0.0),
('Current Year Retained Earnings', 0.0),
('Previous Years Retained Earnings', 0.0),
('Total Retained Earnings', 0.0),
('Total EQUITY', 110.0),
('LIABILITIES + EQUITY', 110.0),
],
options,
)
@freeze_time("2016-05-05")
def test_balance_sheet_last_month_vs_custom_current_year_earnings(self):
"""
Checks the balance sheet calls the right period of the P&L when using last_month date filter, or an equivalent custom filter
(this used to fail due to options regeneration made by the P&L's get_options())"
"""
to_invoice = [('15', '11'), ('15', '12'), ('16', '01'), ('16', '02'), ('16', '03'), ('16', '04')]
for year, month in to_invoice:
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': f'20{year}-{month}-01',
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'price_unit': 1000,
'tax_ids': [],
})]
})
invoice.action_post()
expected_result =[
('ASSETS', 6000.0),
('Current Assets', 6000.0),
('Bank and Cash Accounts', 0.0),
('Receivables', 6000.0),
('Current Assets', 0.0),
('Prepayments', 0.0),
('Total Current Assets', 6000.0),
('Plus Fixed Assets', 0.0),
('Plus Non-current Assets', 0.0),
('Total ASSETS', 6000.0),
('LIABILITIES', 0.0),
('Current Liabilities', 0.0),
('Current Liabilities', 0.0),
('Payables', 0.0),
('Total Current Liabilities', 0.0),
('Plus Non-current Liabilities', 0.0),
('Total LIABILITIES', 0.0),
('EQUITY', 6000.0),
('Unallocated Earnings', 6000.0),
('Current Year Unallocated Earnings', 4000.0),
('Previous Years Unallocated Earnings', 2000.0),
('Total Unallocated Earnings', 6000.0),
('Retained Earnings', 0.0),
('Current Year Retained Earnings', 0.0),
('Previous Years Retained Earnings', 0.0),
('Total Retained Earnings', 0.0),
('Total EQUITY', 6000.0),
('LIABILITIES + EQUITY', 6000.0),
]
self.report.filter_multi_company = 'disabled'
options = self._generate_options(self.report, fields.Date.from_string('2016-05-05'), fields.Date.from_string('2016-05-05'))
# End of Last Month
options['date']['filter'] = 'last_month'
lines = self.report._get_lines(options)
self.assertLinesValues(
lines,
# Name Balance
[ 0, 1],
expected_result,
options,
)
# Custom
options['date']['filter'] = 'custom'
lines = self.report._get_lines(options)
self.assertLinesValues(
lines,
# Name Balance
[ 0, 1],
expected_result,
options,
)
def test_financial_report_single_company(self):
line_id = self._get_basic_line_dict_id_from_report_line_ref('fusion_accounting.account_financial_report_bank_view0')
self.report.filter_multi_company = 'disabled'
options = self._generate_options(self.report, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31'))
options['unfolded_lines'] = [line_id]
lines = self.report._get_lines(options)
self.assertLinesValues(
lines,
# Name Balance
[ 0, 1],
[
('ASSETS', 50.0),
('Current Assets', -650.0),
('Bank and Cash Accounts', -1300.0),
('code2 account2', -1300.0),
('Total Bank and Cash Accounts', -1300.0),
('Receivables', 1350.0),
('Current Assets', 400.0),
('Prepayments', -1100.0),
('Total Current Assets', -650.0),
('Plus Fixed Assets', 0.0),
('Plus Non-current Assets', 700.0),
('Total ASSETS', 50.0),
('LIABILITIES', -200.0),
('Current Liabilities', -200.0),
('Current Liabilities', 0.0),
('Payables', -200.0),
('Total Current Liabilities', -200.0),
('Plus Non-current Liabilities', 0.0),
('Total LIABILITIES', -200.0),
('EQUITY', 250.0),
('Unallocated Earnings', -550.0),
('Current Year Unallocated Earnings', -800.0),
('Previous Years Unallocated Earnings', 250.0),
('Total Unallocated Earnings', -550.0),
('Retained Earnings', 800.0),
('Current Year Retained Earnings', 800.0),
('Previous Years Retained Earnings', 0.0),
('Total Retained Earnings', 800.0),
('Total EQUITY', 250.0),
('LIABILITIES + EQUITY', 50.0),
],
options,
)
unfolded_lines = self.report._get_unfolded_lines(lines, line_id)
self.assertLinesValues(
unfolded_lines,
# Name Balance
[ 0, 1],
[
('Bank and Cash Accounts', -1300.0),
('code2 account2', -1300.0),
('Total Bank and Cash Accounts', -1300.0),
],
options,
)
def test_financial_report_multi_company_currency(self):
line_id = self._get_basic_line_dict_id_from_report_line_ref('fusion_accounting.account_financial_report_bank_view0')
options = self._generate_options(self.report, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31'))
options['unfolded_lines'] = [line_id]
lines = self.report._get_lines(options)
self.assertLinesValues(
lines,
# Name Balance
[ 0, 1],
[
('ASSETS', 50.0),
('Current Assets', -4150.0),
('Bank and Cash Accounts', -3300.0),
('code102 account102', -2000.0),
('code2 account2', -1300.0),
('Total Bank and Cash Accounts', -3300.0),
('Receivables', 2350.0),
('Current Assets', 400.0),
('Prepayments', -3600.0),
('Total Current Assets', -4150.0),
('Plus Fixed Assets', 0.0),
('Plus Non-current Assets', 4200.0),
('Total ASSETS', 50.0),
('LIABILITIES', -200.0),
('Current Liabilities', -200.0),
('Current Liabilities', 0.0),
('Payables', -200.0),
('Total Current Liabilities', -200.0),
('Plus Non-current Liabilities', 0.0),
('Total LIABILITIES', -200.0),
('EQUITY', 250.0),
('Unallocated Earnings', -550.0),
('Current Year Unallocated Earnings', -800.0),
('Previous Years Unallocated Earnings', 250.0),
('Total Unallocated Earnings', -550.0),
('Retained Earnings', 800.0),
('Current Year Retained Earnings', 800.0),
('Previous Years Retained Earnings', 0.0),
('Total Retained Earnings', 800.0),
('Total EQUITY', 250.0),
('LIABILITIES + EQUITY', 50.0),
],
options,
)
unfolded_lines = self.report._get_unfolded_lines(lines, line_id)
self.assertLinesValues(
unfolded_lines,
# Name Balance
[ 0, 1],
[
('Bank and Cash Accounts', -3300.0),
('code102 account102', -2000.0),
('code2 account2', -1300.0),
('Total Bank and Cash Accounts', -3300.0),
],
options,
)
def test_financial_report_comparison(self):
line_id = self._get_basic_line_dict_id_from_report_line_ref('fusion_accounting.account_financial_report_bank_view0')
options = self._generate_options(self.report, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31'))
options = self._update_comparison_filter(options, self.report, 'custom', 1, date_to=fields.Date.from_string('2018-12-31'))
options['unfolded_lines'] = [line_id]
lines = self.report._get_lines(options)
self.assertColumnPercentComparisonValues(
lines,
[
('ASSETS', '-80.0%', 'red'),
('Current Assets', '27.7%', 'red'),
('Bank and Cash Accounts', '10.0%', 'red'),
('code102 account102', '0.0%', 'muted'),
('code2 account2', '30.0%', 'red'),
('Total Bank and Cash Accounts', '10.0%', 'red'),
('Receivables', '4.4%', 'green'),
('Current Assets', 'n/a', 'muted'),
('Prepayments', '44.0%', 'red'),
('Total Current Assets', '27.7%', 'red'),
('Plus Fixed Assets', 'n/a', 'muted'),
('Plus Non-current Assets', '20.0%', 'green'),
('Total ASSETS', '-80.0%', 'red'),
('LIABILITIES', 'n/a', 'muted'),
('Current Liabilities', 'n/a', 'muted'),
('Current Liabilities', 'n/a', 'muted'),
('Payables', 'n/a', 'muted'),
('Total Current Liabilities', 'n/a', 'muted'),
('Plus Non-current Liabilities', 'n/a', 'muted'),
('Total LIABILITIES', 'n/a', 'muted'),
('EQUITY', '0.0%', 'muted'),
('Unallocated Earnings', '-320.0%', 'red'),
('Current Year Unallocated Earnings', '-420.0%', 'red'),
('Previous Years Unallocated Earnings', 'n/a', 'muted'),
('Total Unallocated Earnings', '-320.0%', 'red'),
('Retained Earnings', 'n/a', 'muted'),
('Current Year Retained Earnings', 'n/a', 'muted'),
('Previous Years Retained Earnings', 'n/a', 'muted'),
('Total Retained Earnings', 'n/a', 'muted'),
('Total EQUITY', '0.0%', 'muted'),
('LIABILITIES + EQUITY', '-80.0%', 'green'),
]
)
def test_financial_report_horizontal_group(self):
line_id = self._get_basic_line_dict_id_from_report_line_ref('fusion_accounting.account_financial_report_receivable0')
self.report.horizontal_group_ids |= self.horizontal_group
options = self._generate_options(
self.report,
fields.Date.from_string('2019-01-01'),
fields.Date.from_string('2019-12-31'),
default_options={
'unfolded_lines': [line_id],
'selected_horizontal_group_id': self.horizontal_group.id,
}
)
options = self._update_comparison_filter(options, self.report, 'custom', 1, date_to=fields.Date.from_string('2018-12-31'))
lines = self.report._get_lines(options)
self.assertHeadersValues(
options['column_headers'],
[
['As of 12/31/2019', 'As of 12/31/2018'],
['partner_a', 'partner_b'],
['code0 account0', 'code1 account1'],
]
)
self.assertLinesValues(
lines,
[ 0, 1, 2, 3, 4, 5, 6, 7, 8],
[
('ASSETS', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0),
('Current Assets', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0),
('Bank and Cash Accounts', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Receivables', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0),
('code0 account0', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0),
('Total Receivables', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0),
('Current Assets', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Prepayments', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Total Current Assets', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0),
('Plus Fixed Assets', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Plus Non-current Assets', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Total ASSETS', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0),
('LIABILITIES', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0),
('Current Liabilities', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0),
('Current Liabilities', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Payables', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0),
('Total Current Liabilities', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0),
('Plus Non-current Liabilities', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Total LIABILITIES', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0),
('EQUITY', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Current Year Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Previous Years Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Total Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Current Year Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Previous Years Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Total Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('Total EQUITY', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
('LIABILITIES + EQUITY', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0),
],
options,
)
def test_financial_report_horizontal_group_total(self):
"""
In case we don't have comparison, just one column and one level of groupby a new column is added which is the total
of the horizontal group
"""
horizontal_group = self.env['account.report.horizontal.group'].create({
'name': 'Horizontal Group total',
'rule_ids': [
Command.create({
'field_name': 'partner_id',
'domain': f"[('id', 'in', {(self.partner_a + self.partner_b).ids})]",
}),
],
})
self.report.horizontal_group_ids |= horizontal_group
options = self._generate_options(self.report, '2019-01-01', '2019-12-31', default_options={'selected_horizontal_group_id': horizontal_group.id})
self.assertHeadersValues(
options['column_headers'],
[
['As of 12/31/2019'],
['partner_a', 'partner_b'],
]
)
self.assertTrue(options['show_horizontal_group_total'])
# Since we don't calculate the value when totals below section is activated, we disable it
self.env.company.totals_below_sections = False
self.assertHorizontalGroupTotal(
self.report._get_lines(options),
[
('ASSETS', 6900.0, -4075.0, 2825.0),
('Current Assets', 2700.0, -4075.0, -1375.0),
('Bank and Cash Accounts', 0.0, -3000.0, -3000.0),
('Receivables', 2300.0, 25.0, 2325.0),
('Current Assets', 400.0, 0.0, 400.0),
('Prepayments', 0.0, -1100.0, -1100.0),
('Plus Fixed Assets', 0.0, 0.0, 0.0),
('Plus Non-current Assets', 4200.0, 0.0, 4200.0),
('LIABILITIES', 0.0, -200.0, -200.0),
('Current Liabilities', 0.0, -200.0, -200.0),
('Current Liabilities', 0.0, 0.0, 0.0),
('Payables', 0.0, -200.0, -200.0),
('Plus Non-current Liabilities', 0.0, 0.0, 0.0),
('EQUITY', 250.0, 800.0, 1050.0),
('Unallocated Earnings', 250.0, 0.0, 250.0),
('Current Year Unallocated Earnings', 0.0, 0.0, 0.0),
('Previous Years Unallocated Earnings', 250.0, 0.0, 250.0),
('Retained Earnings', 0.0, 800.0, 800.0),
('Current Year Retained Earnings', 0.0, 800.0, 800.0),
('Previous Years Retained Earnings', 0.0, 0.0, 0.0),
('LIABILITIES + EQUITY', 250.0, 600.0, 850.0),
],
)
options = self._generate_options(self.report, '2019-01-01', '2019-12-31', default_options={'selected_horizontal_group_id': horizontal_group.id})
options = self._update_comparison_filter(options, self.report, 'custom', 1, date_to=fields.Date.from_string('2018-12-31'))
self.assertHeadersValues(
options['column_headers'],
[
['As of 12/31/2019', 'As of 12/31/2018'],
['partner_a', 'partner_b'],
]
)
self.assertFalse(options['show_horizontal_group_total'])
self.assertHorizontalGroupTotal(
self.report._get_lines(options),
[
('ASSETS', 6900.0, -4075.0, 5750.0, -3000.0),
('Current Assets', 2700.0, -4075.0, 2250.0, -3000.0),
('Bank and Cash Accounts', 0.0, -3000.0, 0.0, -3000.0),
('Receivables', 2300.0, 25.0, 2250.0, 0.0),
('Current Assets', 400.0, 0.0, 0.0, 0.0),
('Prepayments', 0.0, -1100.0, 0.0, 0.0),
('Plus Fixed Assets', 0.0, 0.0, 0.0, 0.0),
('Plus Non-current Assets', 4200.0, 0.0, 3500.0, 0.0),
('LIABILITIES', 0.0, -200.0, 0.0, 0.0),
('Current Liabilities', 0.0, -200.0, 0.0, 0.0),
('Current Liabilities', 0.0, 0.0, 0.0, 0.0),
('Payables', 0.0, -200.0, 0.0, 0.0),
('Plus Non-current Liabilities', 0.0, 0.0, 0.0, 0.0),
('EQUITY', 250.0, 800.0, 250.0, 0.0),
('Unallocated Earnings', 250.0, 0.0, 250.0, 0.0),
('Current Year Unallocated Earnings', 0.0, 0.0, 250.0, 0.0),
('Previous Years Unallocated Earnings', 250.0, 0.0, 0.0, 0.0),
('Retained Earnings', 0.0, 800.0, 0.0, 0.0),
('Current Year Retained Earnings', 0.0, 800.0, 0.0, 0.0),
('Previous Years Retained Earnings', 0.0, 0.0, 0.0, 0.0),
('LIABILITIES + EQUITY', 250.0, 600.0, 250.0, 0.0),
],
)
def test_hide_if_zero_with_no_formulas(self):
"""
Check if a report line stays displayed when hide_if_zero is True and no formulas
is set on the line but has some child which have balance != 0
We check also if the line is hidden when all its children have balance == 0
"""
account1, account2 = self.env['account.account'].create([{
'name': "test_financial_report_1",
'code': "42241",
'account_type': "asset_fixed",
}, {
'name': "test_financial_report_2",
'code': "42242",
'account_type': "asset_fixed",
}])
moves = self.env['account.move'].create([
{
'move_type': 'entry',
'date': '2019-04-01',
'line_ids': [
(0, 0, {'debit': 3.0, 'credit': 0.0, 'account_id': account1.id}),
(0, 0, {'debit': 0.0, 'credit': 3.0, 'account_id': self.company_data['default_account_revenue'].id}),
],
},
{
'move_type': 'entry',
'date': '2019-05-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 1.0, 'account_id': account2.id}),
(0, 0, {'debit': 1.0, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
],
},
{
'move_type': 'entry',
'date': '2019-04-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 3.0, 'account_id': account2.id}),
(0, 0, {'debit': 3.0, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
],
},
])
moves.action_post()
moves.line_ids.flush_recordset()
report = self.env["account.report"].create({
'name': "test_financial_report_sum",
'column_ids': [
Command.create({
'name': "Balance",
'expression_label': 'balance',
'sequence': 1,
}),
],
'line_ids': [
Command.create({
'name': "Title",
'code': 'TT',
'hide_if_zero': True,
'sequence': 0,
'children_ids': [
Command.create({
'name': "report_line_1",
'code': 'TEST_L1',
'sequence': 1,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'domain',
'formula': f"[('account_id', '=', {account1.id})]",
'subformula': 'sum',
'date_scope': 'from_beginning',
}),
],
}),
Command.create({
'name': "report_line_2",
'code': 'TEST_L2',
'sequence': 2,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'domain',
'formula': f"[('account_id', '=', {account2.id})]",
'subformula': 'sum',
'date_scope': 'from_beginning',
}),
],
}),
]
}),
],
})
# TODO without this, the create() puts newIds in the sublines, and flushing doesn't help. Seems to be an ORM bug.
self.env.invalidate_all()
options = self._generate_options(report, fields.Date.from_string('2019-05-01'), fields.Date.from_string('2019-05-01'))
options = self._update_comparison_filter(options, report, 'previous_period', 2)
self.assertLinesValues(
report._get_lines(options),
[ 0, 1, 2, 3],
[
("Title", '', '', ''),
("report_line_1", 3.0, 3.0, 0.0),
("report_line_2", -4.0, -3.0, 0.0),
],
options,
)
move = self.env['account.move'].create({
'move_type': 'entry',
'date': '2019-05-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 3.0, 'account_id': account1.id}),
(0, 0, {'debit': 4.0, 'credit': 0.0, 'account_id': account2.id}),
(0, 0, {'debit': 0.0, 'credit': 1.0, 'account_id': self.company_data['default_account_revenue'].id}),
],
})
move.action_post()
move.line_ids.flush_recordset()
# With the comparison still on, the lines shouldn't be hidden
self.assertLinesValues(
report._get_lines(options),
[ 0, 1, 2, 3],
[
("Title", '', '', ''),
("report_line_1", 0.0, 3.0, 0.0),
("report_line_2", 0.0, -3.0, 0.0),
],
options,
)
# Removing the comparison should hide the lines, as they will be 0 in every considered period (the current one)
options = self._update_comparison_filter(options, report, 'previous_period', 0)
self.assertLinesValues(report._get_lines(options), [0, 1, 2, 3], [], options)
def test_option_hierarchy(self):
""" Check that the report lines are correct when the option "Hierarchy and subtotals is ticked"""
self.env['account.group'].create({
'name': 'Sales',
'code_prefix_start': '40',
'code_prefix_end': '49',
})
move = self.env['account.move'].create({
'date': '2020-02-02',
'line_ids': [
Command.create({
'account_id': self.company_data['default_account_revenue'].id,
'name': 'name',
})
],
})
move.action_post()
move.line_ids.flush_recordset()
profit_and_loss_report = self.env.ref('fusion_accounting.profit_and_loss')
line_id = self._get_basic_line_dict_id_from_report_line_ref('fusion_accounting.account_financial_report_revenue0')
options = self._generate_options(profit_and_loss_report, '2020-02-01', '2020-02-28')
options['unfolded_lines'] = [line_id]
options['hierarchy'] = True
self.env.company.totals_below_sections = False
lines = profit_and_loss_report._get_lines(options)
unfolded_lines = profit_and_loss_report._get_unfolded_lines(lines, line_id)
unfolded_lines = [{'name': line['name'], 'level': line['level']} for line in unfolded_lines]
self.assertEqual(
unfolded_lines,
[
{'level': 1, 'name': 'Revenue'},
{'level': 2, 'name': '40-49 Sales'},
{'level': 3, 'name': '400000 Product Sales'},
]
)
def test_option_hierarchy_with_no_group_lines(self):
""" Check that the report lines of 'No Group' have correct ids with the option 'Hierarchy and subtotals' """
self.env['account.group'].create({
'name': 'Sales',
'code_prefix_start': '45',
'code_prefix_end': '49',
})
move = self.env['account.move'].create({
'date': '2020-02-02',
'line_ids': [
Command.create({
'account_id': self.company_data['default_account_revenue'].id,
'name': 'name',
})
],
})
move.action_post()
move.line_ids.flush_recordset()
profit_and_loss_report = self.env.ref('fusion_accounting.profit_and_loss')
line_id = self._get_basic_line_dict_id_from_report_line_ref('fusion_accounting.account_financial_report_revenue0')
options = self._generate_options(profit_and_loss_report, '2020-02-01', '2020-02-28')
options['unfolded_lines'] = [line_id]
options['hierarchy'] = True
self.env.company.totals_below_sections = False
lines = profit_and_loss_report._get_lines(options)
lines_array = [{'name': line['name'], 'level': line['level']} for line in lines]
self.assertEqual(
lines_array,
[
{'name': 'Revenue', 'level': 1},
{'name': '(No Group)', 'level': 2},
{'name': '400000 Product Sales', 'level': 3},
{'name': 'Less Costs of Revenue', 'level': 1},
{'name': 'Gross Profit', 'level': 0},
{'name': 'Less Operating Expenses', 'level': 1},
{'name': 'Operating Income (or Loss)', 'level': 0},
{'name': 'Plus Other Income', 'level': 1},
{'name': 'Less Other Expenses', 'level': 1},
{'name': 'Net Profit', 'level': 0},
]
)
self.assertEqual(lines[1]['id'], lines[0]['id'] + '|' + '~account.group~')
def test_parse_line_id(self):
line_id_1 = self.env['account.report']._parse_line_id('markup1~account.account~5|markup2~res.partner~8|markup3~~')
line_id_2 = self.env['account.report']._parse_line_id('~account.report~14|{"groupby_prefix_group": "~"}~account.report~21')
self.assertEqual(line_id_1, [('markup1', 'account.account', 5), ('markup2', 'res.partner', 8), ('markup3', None, None)])
self.assertEqual(line_id_2, [('', 'account.report', 14), ({"groupby_prefix_group": "~"}, 'account.report', 21)])

View File

@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Fusion Accounting - Tests. Copyright (C) 2026 Nexa Systems Inc.
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo import fields
from odoo.exceptions import UserError, ValidationError
from odoo.tests import tagged
from odoo.tools import file_open
@tagged('post_install', '-at_install')
class TestAccountBankStatementImportCSV(AccountTestInvoicingCommon):
def _import_file(self, csv_file_path, csv_fields=False):
# Create a bank account and journal corresponding to the CSV file (same currency and account number)
bank_journal = self.env['account.journal'].create({
'name': 'Bank 123456',
'code': 'BNK67',
'type': 'bank',
'bank_acc_number': '123456',
'currency_id': self.env.ref("base.USD").id,
})
# Use an import wizard to process the file
with file_open(csv_file_path, 'rb') as csv_file:
action = bank_journal.create_document_from_attachment(self.env['ir.attachment'].create({
'mimetype': 'text/csv',
'name': 'test_csv.csv',
'raw': csv_file.read(),
}).ids)
import_wizard = self.env['base_import.import'].browse(
action['params']['context']['wizard_id']
).with_context(action['params']['context'])
import_wizard_options = {
'date_format': '%m %d %y',
'keep_matches': False,
'encoding': 'utf-8',
'fields': [],
'quoting': '"',
'bank_stmt_import': True,
'headers': True,
'separator': ';',
'float_thousand_separator': ',',
'float_decimal_separator': '.',
'advanced': False,
}
import_wizard_fields = csv_fields or ['date', False, 'payment_ref', 'amount', 'balance']
import_wizard.execute_import(import_wizard_fields, [], import_wizard_options, dryrun=False)
def test_csv_file_import(self):
self._import_file('fusion_accounting/test_csv_file/test_csv.csv')
# Check the imported bank statement
imported_statement = self.env['account.bank.statement'].search([('company_id', '=', self.env.company.id)])
self.assertRecordValues(imported_statement, [{
'reference': 'test_csv.csv',
'balance_start': 21699.55,
'balance_end_real': 23462.55,
}])
self.assertRecordValues(imported_statement.line_ids.sorted(lambda line: (line.date, line.payment_ref)), [
{'date': fields.Date.from_string('2015-02-02'), 'amount': 3728.87, 'payment_ref': 'ACH CREDIT"AMERICAN EXPRESS-SETTLEMENT'},
{'date': fields.Date.from_string('2015-02-02'), 'amount': -500.08, 'payment_ref': 'DEBIT CARD 6906 EFF 02-01"01/31 INDEED 203-564-2400 CT'},
{'date': fields.Date.from_string('2015-02-02'), 'amount': -240.00, 'payment_ref': 'DEBIT CARD 6906 EFF 02-01"01/31 MAILCHIMP MAILCHIMP.COMGA'},
{'date': fields.Date.from_string('2015-02-02'), 'amount': -2064.82, 'payment_ref': 'DEBIT CARD 6906"02/02 COMFORT INNS SAN FRANCISCOCA'},
{'date': fields.Date.from_string('2015-02-02'), 'amount': -41.64, 'payment_ref': 'DEBIT CARD 6906"BAYSIDE MARKET/1 SAN FRANCISCO CA'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': 2500.00, 'payment_ref': 'ACH CREDIT"CHECKFLUID INC -013015'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -25.00, 'payment_ref': 'ACH DEBIT"AUTHNET GATEWAY -BILLING'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -7500.00, 'payment_ref': 'ACH DEBIT"WW 222 BROADWAY -ACH'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -45.86, 'payment_ref': 'DEBIT CARD 6906"02/02 DISTRICT SF SAN FRANCISCOCA'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -1123.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -1123.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': -4344.66, 'payment_ref': 'DEBIT CARD 6906"02/03 IBM USED PC 888S 188-874-6742 NY'},
{'date': fields.Date.from_string('2015-02-03'), 'amount': 8366.00, 'payment_ref': 'DEPOSIT-WIRED FUNDS"TVET OPERATING PLLC'},
{'date': fields.Date.from_string('2015-02-04'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/03 VIR ATL 9327 180-08628621 CT'},
{'date': fields.Date.from_string('2015-02-04'), 'amount': -204.23, 'payment_ref': 'DEBIT CARD 6906"02/04 GROUPON INC 877-788-7858 IL'},
{'date': fields.Date.from_string('2015-02-05'), 'amount': 9518.40, 'payment_ref': 'ACH CREDIT"MERCHE-SOLUTIONS-MERCH DEP'},
])
def test_csv_file_import_with_missing_values(self):
self._import_file('fusion_accounting/test_csv_file/test_csv_missing_values.csv', ['transaction_type', 'ref', 'payment_ref', 'debit', 'credit'])
imported_statement = self.env['account.bank.statement'].search([('company_id', '=', self.env.company.id)])
self.assertEqual(len(imported_statement.line_ids), 2)
self.assertRecordValues(imported_statement.line_ids.sorted(lambda line: line.amount), [
{'transaction_type': 'TRANSFER', 'ref': 'bank_ref_1', 'payment_ref': 'bank_statement_line_1', 'sequence': 0, 'amount': 1000.0},
{'transaction_type': 'TRANSFER', 'ref': False, 'payment_ref': 'bank_statement_line_2', 'sequence': 1, 'amount': 3500.0},
])
def test_csv_file_import_non_ordered(self):
with self.assertRaises(UserError):
self._import_file('fusion_accounting/test_csv_file/test_csv_non_sorted.csv')
def test_csv_file_empty_date(self):
with self.assertRaises(UserError):
self._import_file('fusion_accounting/test_csv_file/test_csv_empty_date.csv')
def test_csv_file_import_without_amount(self):
csv_fields = ['date', False, 'payment_ref', 'balance']
with self.assertRaisesRegex(ValidationError, "Make sure that an Amount or Debit and Credit is in the file."):
self._import_file('fusion_accounting/test_csv_file/test_csv_without_amount.csv', csv_fields)

View File

@@ -0,0 +1,203 @@
# -*- encoding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo import fields, Command
from odoo.tests import Form, tagged
@tagged('post_install', '-at_install')
class TestBillsPrediction(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company.predict_bill_product = True
cls.test_partners = cls.env['res.partner'].create([{'name': 'test partner %s' % i} for i in range(7)])
accounts_data = [{
'code': 'test%s' % i,
'name': name,
'account_type': 'expense',
} for i, name in enumerate((
"Test Maintenance and Repair",
"Test Purchase of services, studies and preparatory work",
"Test Various Contributions",
"Test Rental Charges",
"Test Purchase of commodity",
))]
cls.test_accounts = cls.env['account.account'].create(accounts_data)
cls.frozen_today = fields.Date.today()
def _create_bill(self, vendor, line_name, expected_account, account_to_set=None, post=True):
''' Create a new vendor bill to test the prediction.
:param vendor: The vendor to set on the invoice.
:param line_name: The name of the invoice line that will be used to predict.
:param expected_account: The expected predicted account.
:param account_to_set: The optional account to set as a correction of the predicted account.
:return: The newly created vendor bill.
'''
invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
invoice_form.partner_id = vendor
invoice_form.invoice_date = self.frozen_today
with invoice_form.invoice_line_ids.new() as invoice_line_form:
# Set the default account to avoid "account_id is a required field" in case of bad configuration.
invoice_line_form.account_id = self.company_data['default_journal_purchase'].default_account_id
invoice_line_form.quantity = 1.0
invoice_line_form.price_unit = 42.0
invoice_line_form.name = line_name
invoice = invoice_form.save()
invoice_line = invoice.invoice_line_ids
self.assertEqual(
invoice_line.account_id,
expected_account,
"Account '%s' should have been predicted instead of '%s'" % (
expected_account.display_name,
invoice_line.account_id.display_name,
),
)
if account_to_set:
invoice_line.account_id = account_to_set
if post:
invoice.action_post()
return invoice
def test_account_prediction_flow(self):
default_account = self.company_data['default_journal_purchase'].default_account_id
self._create_bill(self.test_partners[0], "Maintenance and repair", self.test_accounts[0])
self._create_bill(self.test_partners[5], "Subsidies obtained", default_account, account_to_set=self.test_accounts[1])
self._create_bill(self.test_partners[6], "Prepare subsidies file", default_account, account_to_set=self.test_accounts[1])
self._create_bill(self.test_partners[6], "Prepare subsidies file", self.test_accounts[1])
self._create_bill(self.test_partners[1], "Contributions January", self.test_accounts[2])
self._create_bill(self.test_partners[2], "Coca-cola", default_account, account_to_set=self.test_accounts[4])
self._create_bill(self.test_partners[1], "Contribution February", self.test_accounts[2])
self._create_bill(self.test_partners[3], "Electricity Bruxelles", default_account, account_to_set=self.test_accounts[3])
self._create_bill(self.test_partners[3], "Electricity Grand-Rosière", self.test_accounts[3])
self._create_bill(self.test_partners[2], "Purchase of coca-cola", self.test_accounts[4])
self._create_bill(self.test_partners[4], "Crate of coca-cola", default_account, account_to_set=self.test_accounts[4])
self._create_bill(self.test_partners[4], "Crate of coca-cola", self.test_accounts[4])
self._create_bill(self.test_partners[1], "March", self.test_accounts[2])
def test_account_prediction_from_label_expected_behavior(self):
"""Prevent the prediction from being annoying."""
default_account = self.company_data['default_journal_purchase'].default_account_id
payable_account = self.company_data['default_account_payable'].copy()
payable_account.write({'name': f'Account payable - {self.test_accounts[0].name}'})
# There is no prior result, we take the default account, but we don't post
self._create_bill(self.test_partners[0], self.test_partners[0].name, default_account, post=False)
# There is no prior result, we take the default account
self._create_bill(self.test_partners[0], "Drinks", default_account, account_to_set=self.test_accounts[0])
# There is only one prior account for the partner, we take that one
self._create_bill(self.test_partners[0], "Desert", self.test_accounts[0], account_to_set=self.test_accounts[1])
# We find something close enough, take that one
self._create_bill(self.test_partners[0], "Drinks too", self.test_accounts[0])
# There is no clear preference for any account (both previous accounts have the same rank)
# don't make any prediction and let the default behavior fill the account
invoice = self._create_bill(self.test_partners[0], "Main course", default_account)
invoice.button_draft()
with Form(invoice) as move_form:
with move_form.invoice_line_ids.edit(0) as line_form:
# There isn't any account clearly better than the manually set one, we keep the current one
line_form.account_id = self.test_accounts[2]
line_form.name = "Apple"
self.assertEqual(line_form.account_id, self.test_accounts[2])
# There is an account that looks clearly better, use it
line_form.name = "Second desert"
self.assertEqual(line_form.account_id, self.test_accounts[1])
def test_account_prediction_with_product(self):
product = self.env['product.product'].create({
'name': 'product_a',
'lst_price': 1000.0,
'standard_price': 800.0,
'property_account_income_id': self.company_data['default_account_revenue'].id,
'property_account_expense_id': self.company_data['default_account_expense'].id,
})
invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
invoice_form.partner_id = self.test_partners[0]
invoice_form.invoice_date = self.frozen_today
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.product_id = product
invoice_line_form.name = "Maintenance and repair"
invoice = invoice_form.save()
self.assertRecordValues(invoice.invoice_line_ids, [{
'name': "Maintenance and repair",
'product_id': product.id,
'account_id': self.company_data['default_account_expense'].id,
}])
def test_product_prediction_price_subtotal_computation(self):
invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
invoice_form.partner_id = self.test_partners[0]
invoice_form.invoice_date = self.frozen_today
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.product_id = self.product_a
invoice = invoice_form.save()
invoice.action_post()
self.product_a.supplier_taxes_id = [Command.set(self.tax_purchase_b.ids)]
invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
invoice_form.partner_id = self.test_partners[0]
invoice_form.invoice_date = self.frozen_today
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.name = 'product_a'
invoice = invoice_form.save()
self.assertRecordValues(invoice.invoice_line_ids, [{
'quantity': 1.0,
'price_unit': 800.0,
'price_subtotal': 800.0,
'balance': 800.0,
'tax_ids': self.tax_purchase_b.ids,
}])
# In case a unit price is already set we do not update the unit price
invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
invoice_form.partner_id = self.test_partners[0]
invoice_form.invoice_date = self.frozen_today
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.price_unit = 42.0
invoice_line_form.name = 'product_a'
invoice = invoice_form.save()
self.assertRecordValues(invoice.invoice_line_ids, [{
'quantity': 1.0,
'price_unit': 42.0,
'price_subtotal': 42.0,
'balance': 42.0,
'tax_ids': self.tax_purchase_b.ids,
}])
# In case a tax is already set we do not update the taxes
invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
invoice_form.partner_id = self.test_partners[0]
invoice_form.invoice_date = self.frozen_today
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.tax_ids = self.tax_purchase_a
invoice_line_form.name = 'product_a'
invoice = invoice_form.save()
self.assertRecordValues(invoice.invoice_line_ids, [{
'quantity': 1.0,
'price_unit': 800.0,
'price_subtotal': 800.0,
'balance': 800.0,
'tax_ids': self.tax_purchase_a.ids,
}])

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Fusion Accounting - Tests. Copyright (C) 2026 Nexa Systems Inc.
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class TestReconciliationWidget(ValuationReconciliationTestCommon):
def test_no_stock_account_in_reconciliation_proposition(self):
"""
We check if no stock interim account is present in the reconcialiation proposition,
with both standard and custom stock accounts
"""
avco_1 = self.stock_account_product_categ.copy({'property_cost_method': 'average'})
# We need a product category with custom stock accounts
avco_2 = self.stock_account_product_categ.copy({
'property_cost_method': 'average',
'property_stock_account_input_categ_id': self.company_data['default_account_stock_in'].copy().id,
'property_stock_account_output_categ_id': self.company_data['default_account_stock_out'].copy().id,
'property_stock_journal': avco_1.property_stock_journal.copy().id,
'property_stock_valuation_account_id': self.company_data['default_account_stock_valuation'].copy().id
})
move_1, move_2 = self.env['account.move'].create([
{
'move_type': 'entry',
'name': 'Entry 1',
'journal_id': avco_1.property_stock_journal.id,
'line_ids': [
(0, 0, {
'account_id': avco_1.property_stock_account_input_categ_id.id,
'debit': 0.0,
'credit': 100.0
}),
(0, 0, {
'account_id': avco_1.property_stock_valuation_account_id.id,
'debit': 100.0,
'credit': 0.0
})
]
},
{
'move_type': 'entry',
'name': 'Entry 2',
'journal_id': avco_2.property_stock_journal.id,
'line_ids': [
(0, 0, {
'account_id': avco_2.property_stock_account_input_categ_id.id,
'debit': 0.0,
'credit': 100.0
}),
(0, 0, {
'account_id': avco_2.property_stock_valuation_account_id.id,
'debit': 100.0,
'credit': 0.0
})
]
},
])
(move_1 + move_2).action_post()
statement = self.env['account.bank.statement'].create({
'balance_start': 0.0,
'balance_end_real': -100.0,
'line_ids': [(0, 0, {
'payment_ref': 'test',
'amount': -100.0,
'journal_id': self.company_data['default_journal_bank'].id,
})]
})
wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=statement.line_ids.id).new({})
amls = self.env['account.move.line'].search(wizard._prepare_embedded_views_data()['amls']['domain'])
stock_accounts = (
avco_1.property_stock_account_input_categ_id + avco_2.property_stock_account_input_categ_id
+ avco_1.property_stock_account_output_categ_id + avco_2.property_stock_account_output_categ_id
)
stock_res = [line for line in amls if line.account_id in stock_accounts]
self.assertEqual(len(stock_res), 0)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
import base64
from odoo import Command
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
@tagged('post_install', '-at_install')
class TestInvoiceSignature(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
if cls.env.ref('base.module_sign').state != 'installed':
cls.skipTest(cls, "`sign` module not installed")
cls.env.company.sign_invoice = True
cls.signature_fake_1 = base64.b64encode(b"fake_signature_1")
cls.signature_fake_2 = base64.b64encode(b"fake_signature_2")
cls.user.sign_signature = cls.signature_fake_1
cls.another_user = cls.env['res.users'].create({
'name': 'another accountant',
'login': 'another_accountant',
'password': 'another_accountant',
'groups_id': [
Command.set(cls.env.user.groups_id.ids),
],
'sign_signature': cls.signature_fake_2,
})
cls.invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'journal_id': cls.company_data['default_journal_sale'].id,
'invoice_line_ids': [
Command.create({
'product_id': cls.product_a.id,
'quantity': 1,
'price_unit': 1,
})
]
})
def test_draft_invoice_shouldnt_have_signature(self):
self.assertEqual(self.invoice.state, 'draft')
self.assertFalse(self.invoice.show_signature_area, "the signature area shouldn't appear on a draft invoice")
def test_posted_invoice_should_have_signature(self):
self.invoice.action_post()
self.assertTrue(self.invoice.show_signature_area,
"the signature area should appear on posted invoice when the `sign_invoice` settings is True")
def test_invoice_from_company_without_signature_settings_shouldnt_have_signature(self):
self.env.company.sign_invoice = False
self.invoice.action_post()
self.assertFalse(self.invoice.show_signature_area,
"the signature area shouldn't appear when the `sign_invoice` settings is False")
def test_invoice_signing_user_should_be_the_user_that_posted_it(self):
self.assertFalse(self.invoice.signing_user,
"invoice that weren't created by automated action shouldn't have a signing user")
self.assertEqual(self.invoice.signature, False, "There shouldn't be any signature if there isn't a signing user")
self.invoice.action_post()
self.assertEqual(self.invoice.signing_user, self.user, "The signing user should be the user that posted the invoice")
self.assertEqual(self.invoice.signature, self.signature_fake_1, "The signature should be from `self.user`")
self.invoice.button_draft()
self.invoice.with_user(self.another_user).action_post()
self.assertEqual(self.invoice.signing_user, self.another_user,
"The signing user should be the user that posted the invoice")
self.assertEqual(self.invoice.signature, self.signature_fake_2, "The signature should be from `self.another_user`")
def test_invoice_signing_user_should_be_reprensative_user_if_there_is_one(self):
self.env.company.signing_user = self.another_user # set the representative user of the company
self.invoice.action_post()
self.assertEqual(self.invoice.signing_user, self.another_user, "The signing user should be the representative person set in the settings")
self.assertEqual(self.invoice.signature, self.signature_fake_2, "The signature should be from `self.another_user`, the representative user")
def test_setting_representative_user_shouldnt_change_signer_of_already_posted_invoice(self):
# Note: Changing this behavior might not be a good idea as having all account.move updated at once
# would be very costly
self.invoice.action_post()
self.env.company.signing_user = self.another_user # set the representative user of the company
self.assertEqual(self.invoice.signing_user, self.user,
"The signing user should be the one that posted the invoice even if a representative has been added later on")
self.assertEqual(self.invoice.signature, self.signature_fake_1, "The signature should be from `self.user`")

View File

@@ -0,0 +1,48 @@
# Fusion Accounting - Tests. Copyright (C) 2026 Nexa Systems Inc.
import odoo.tests
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
@odoo.tests.tagged('post_install_l10n', 'post_install', '-at_install')
class TestAccountantTours(AccountTestInvoicingHttpCommon):
def test_account_merge_wizard_tour(self):
companies = self.env['res.company'].create([
{'name': 'tour_company_1'},
{'name': 'tour_company_2'},
])
self.env['account.account'].create([
{
'company_ids': [Command.set(companies[0].ids)],
'code': "100001",
'name': "Current Assets",
'account_type': 'asset_current',
},
{
'company_ids': [Command.set(companies[0].ids)],
'code': "100002",
'name': "Non-Current Assets",
'account_type': 'asset_non_current',
},
{
'company_ids': [Command.set(companies[1].ids)],
'code': "200001",
'name': "Current Assets",
'account_type': 'asset_current',
},
{
'company_ids': [Command.set(companies[1].ids)],
'code': "200002",
'name': "Non-Current Assets",
'account_type': 'asset_non_current',
},
])
self.env.ref('base.user_admin').write({
'company_id': companies[0].id,
'company_ids': [Command.set(companies.ids)],
})
self.start_tour("/odoo", 'account_merge_wizard_tour', login="admin", cookies={"cids": f"{companies[0].id}-{companies[1].id}"})

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Fusion Accounting - Tests. Copyright (C) 2026 Nexa Systems Inc.
import logging
from odoo import Command, fields
from odoo.addons.account.tests.common import AccountTestMockOnlineSyncCommon
import odoo.tests
_logger = logging.getLogger(__name__)
@odoo.tests.tagged('-at_install', 'post_install')
class TestUi(AccountTestMockOnlineSyncCommon):
def test_accountant_tour(self):
# Reset country and fiscal country, so that fields added by localizations are
# hidden and non-required, and don't make the tour crash.
# Also remove default taxes from the company and its accounts, to avoid inconsistencies
# with empty fiscal country.
self.env.company.write({
'country_id': None, # Also resets account_fiscal_country_id
'account_sale_tax_id': None,
'account_purchase_tax_id': None,
})
# An unconfigured bank journal is required for the connect bank step
self.env['account.journal'].create({
'type': 'bank',
'name': 'Empty Bank',
'code': 'EBJ',
})
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()],
})
# This tour doesn't work with demo data on runbot
all_moves = self.env['account.move'].search([('company_id', '=', self.env.company.id), ('move_type', '!=', 'entry')])
all_moves.filtered(lambda m: not m.inalterable_hash and not m.deferred_move_ids and m.state != 'draft').button_draft()
all_moves.with_context(force_delete=True).unlink()
# We need at least two bank statement lines to reconcile for the tour.
bnk = self.env['account.account'].create({
'code': 'X1014',
'name': 'Bank Current Account - (test)',
'account_type': 'asset_cash',
})
journal = self.env['account.journal'].create({
'name': 'Bank - Test',
'code': 'TBNK',
'type': 'bank',
'default_account_id': bnk.id,
})
self.env['account.bank.statement.line'].create([{
'journal_id': journal.id,
'amount': 100,
'date': fields.Date.today(),
'payment_ref': 'stl_0001',
}, {
'journal_id': journal.id,
'amount': 200,
'date': fields.Date.today(),
'payment_ref': 'stl_0002',
}])
self.start_tour("/odoo", 'fusion_accounting_tour', login="admin")