Initial commit: Accounting packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:47 +02:00
commit 4ef34c2317
2661 changed files with 1709616 additions and 0 deletions

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from . import test_account_move_reconcile
from . import test_account_move_payments_widget
from . import test_account_move_out_invoice
from . import test_account_move_out_refund
from . import test_account_move_in_invoice
from . import test_account_move_in_refund
from . import test_account_move_entry
from . import test_account_move_date_algorithm
from . import test_invoice_tax_totals
from . import test_account_inalterable_hash
from . import test_account_journal
from . import test_account_account
from . import test_account_tax
from . import test_account_analytic
from . import test_account_payment
from . import test_account_bank_statement
from . import test_account_move_partner_count
from . import test_account_move_rounding
from . import test_account_invoice_report
from . import test_account_move_line_tax_details
from . import test_account_journal_dashboard
from . import test_chart_template
from . import test_fiscal_position
from . import test_sequence_mixin
from . import test_settings
from . import test_tax
from . import test_invoice_taxes
from . import test_templates_consistency
from . import test_account_all_l10n
from . import test_reconciliation_matching_rules
from . import test_account_onboarding
from . import test_portal_attachment
from . import test_product
from . import test_tax_report
from . import test_transfer_wizard
from . import test_account_incoming_supplier_invoice
from . import test_payment_term
from . import test_account_payment_register
from . import test_tour
from . import test_early_payment_discount
from . import test_ir_actions_report
from . import test_download_xsds
from . import test_mail_tracking_value

View file

@ -0,0 +1,778 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, Command
from odoo.tests.common import TransactionCase, HttpCase, tagged, Form
import json
import time
import base64
from lxml import etree
class AccountTestInvoicingCommon(TransactionCase):
@classmethod
def safe_copy(cls, record):
return record and record.copy()
@classmethod
def copy_account(cls, account, default=None):
suffix_nb = 1
while True:
new_code = '%s.%s' % (account.code, suffix_nb)
if account.search_count([('company_id', '=', account.company_id.id), ('code', '=', new_code)]):
suffix_nb += 1
else:
return account.copy(default={**(default or {}), 'code': new_code})
@classmethod
def setUpClass(cls, chart_template_ref=None):
super(AccountTestInvoicingCommon, cls).setUpClass()
assert 'post_install' in cls.test_tags, 'This test requires a CoA to be installed, it should be tagged "post_install"'
if chart_template_ref:
chart_template = cls.env.ref(chart_template_ref)
else:
chart_template = cls.env.ref('l10n_generic_coa.configurable_chart_template', raise_if_not_found=False)
if not chart_template:
cls.tearDownClass()
# skipTest raises exception
cls.skipTest(cls, "Accounting Tests skipped because the user's company has no chart of accounts.")
# Create user.
user = cls.env['res.users'].create({
'name': 'Because I am accountman!',
'login': 'accountman',
'password': 'accountman',
'groups_id': [
(6, 0, cls.env.user.groups_id.ids),
(4, cls.env.ref('account.group_account_manager').id),
(4, cls.env.ref('account.group_account_user').id),
],
})
user.partner_id.email = 'accountman@test.com'
# Shadow the current environment/cursor with one having the report user.
# This is mandatory to test access rights.
cls.env = cls.env(user=user)
cls.cr = cls.env.cr
cls.company_data_2 = cls.setup_company_data('company_2_data', chart_template=chart_template)
cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template)
user.write({
'company_ids': [Command.set((cls.company_data['company'] + cls.company_data_2['company']).ids)],
'company_id': cls.company_data['company'].id,
})
cls.currency_data = cls.setup_multi_currency_data()
# ==== Taxes ====
cls.tax_sale_a = cls.company_data['default_tax_sale']
cls.tax_sale_b = cls.safe_copy(cls.company_data['default_tax_sale'])
cls.tax_purchase_a = cls.company_data['default_tax_purchase']
cls.tax_purchase_b = cls.safe_copy(cls.company_data['default_tax_purchase'])
cls.tax_armageddon = cls.setup_armageddon_tax('complex_tax', cls.company_data)
# ==== Products ====
cls.product_a = cls.env['product.product'].create({
'name': 'product_a',
'uom_id': cls.env.ref('uom.product_uom_unit').id,
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
'lst_price': 1000.0,
'standard_price': 800.0,
'property_account_income_id': cls.company_data['default_account_revenue'].id,
'property_account_expense_id': cls.company_data['default_account_expense'].id,
'taxes_id': [Command.set(cls.tax_sale_a.ids)],
'supplier_taxes_id': [Command.set(cls.tax_purchase_a.ids)],
})
cls.product_b = cls.env['product.product'].create({
'name': 'product_b',
'uom_id': cls.env.ref('uom.product_uom_dozen').id,
'uom_po_id': cls.env.ref('uom.product_uom_dozen').id,
'lst_price': 200.0,
'standard_price': 160.0,
'property_account_income_id': cls.copy_account(cls.company_data['default_account_revenue']).id,
'property_account_expense_id': cls.copy_account(cls.company_data['default_account_expense']).id,
'taxes_id': [Command.set((cls.tax_sale_a + cls.tax_sale_b).ids)],
'supplier_taxes_id': [Command.set((cls.tax_purchase_a + cls.tax_purchase_b).ids)],
})
# ==== Fiscal positions ====
cls.fiscal_pos_a = cls.env['account.fiscal.position'].create({
'name': 'fiscal_pos_a',
'tax_ids': ([(0, None, {
'tax_src_id': cls.tax_sale_a.id,
'tax_dest_id': cls.tax_sale_b.id,
})] if cls.tax_sale_b else []) + ([(0, None, {
'tax_src_id': cls.tax_purchase_a.id,
'tax_dest_id': cls.tax_purchase_b.id,
})] if cls.tax_purchase_b else []),
'account_ids': [
(0, None, {
'account_src_id': cls.product_a.property_account_income_id.id,
'account_dest_id': cls.product_b.property_account_income_id.id,
}),
(0, None, {
'account_src_id': cls.product_a.property_account_expense_id.id,
'account_dest_id': cls.product_b.property_account_expense_id.id,
}),
],
})
# ==== Payment terms ====
cls.pay_terms_a = cls.env.ref('account.account_payment_term_immediate')
cls.pay_terms_b = cls.env['account.payment.term'].create({
'name': '30% Advance End of Following Month',
'note': 'Payment terms: 30% Advance End of Following Month',
'line_ids': [
(0, 0, {
'value': 'percent',
'value_amount': 30.0,
'days': 0,
}),
(0, 0, {
'value': 'balance',
'value_amount': 0.0,
'months': 1,
'end_month': True,
}),
],
})
# ==== Partners ====
cls.partner_a = cls.env['res.partner'].create({
'name': 'partner_a',
'property_payment_term_id': cls.pay_terms_a.id,
'property_supplier_payment_term_id': cls.pay_terms_a.id,
'property_account_receivable_id': cls.company_data['default_account_receivable'].id,
'property_account_payable_id': cls.company_data['default_account_payable'].id,
'company_id': False,
})
cls.partner_b = cls.env['res.partner'].create({
'name': 'partner_b',
'property_payment_term_id': cls.pay_terms_b.id,
'property_supplier_payment_term_id': cls.pay_terms_b.id,
'property_account_position_id': cls.fiscal_pos_a.id,
'property_account_receivable_id': cls.company_data['default_account_receivable'].copy().id,
'property_account_payable_id': cls.company_data['default_account_payable'].copy().id,
'company_id': False,
})
cls.partner_agrolait = cls.env['res.partner'].create({
'name': 'Deco Agrolait',
'is_company': True,
'country_id': cls.env.ref('base.us').id,
})
cls.partner_agrolait_id = cls.partner_agrolait.id
# ==== Cash rounding ====
cls.cash_rounding_a = cls.env['account.cash.rounding'].create({
'name': 'add_invoice_line',
'rounding': 0.05,
'strategy': 'add_invoice_line',
'profit_account_id': cls.company_data['default_account_revenue'].copy().id,
'loss_account_id': cls.company_data['default_account_expense'].copy().id,
'rounding_method': 'UP',
})
cls.cash_rounding_b = cls.env['account.cash.rounding'].create({
'name': 'biggest_tax',
'rounding': 0.05,
'strategy': 'biggest_tax',
'rounding_method': 'DOWN',
})
# ==== Payment methods ====
bank_journal = cls.company_data['default_journal_bank']
cls.inbound_payment_method_line = bank_journal.inbound_payment_method_line_ids[0]
cls.outbound_payment_method_line = bank_journal.outbound_payment_method_line_ids[0]
@classmethod
def setup_company_data(cls, company_name, chart_template=None, **kwargs):
''' Create a new company having the name passed as parameter.
A chart of accounts will be installed to this company: the same as the current company one.
The current user will get access to this company.
:param chart_template: The chart template to be used on this new company.
:param company_name: The name of the company.
:return: A dictionary will be returned containing all relevant accounting data for testing.
'''
def search_account(company, chart_template, field_name, domain):
template_code = chart_template[field_name].code
domain = [('company_id', '=', company.id)] + domain
account = None
if template_code:
account = cls.env['account.account'].search(domain + [('code', '=like', template_code + '%')], limit=1)
if not account:
account = cls.env['account.account'].search(domain, limit=1)
return account
chart_template = chart_template or cls.env.company.chart_template_id
company = cls.env['res.company'].create({
'name': company_name,
**kwargs,
})
cls.env.user.company_ids |= company
chart_template.try_loading(company=company, install_demo=False)
# The currency could be different after the installation of the chart template.
if kwargs.get('currency_id'):
company.write({'currency_id': kwargs['currency_id']})
return {
'company': company,
'currency': company.currency_id,
'default_account_revenue': cls.env['account.account'].search([
('company_id', '=', company.id),
('account_type', '=', 'income'),
('id', '!=', company.account_journal_early_pay_discount_gain_account_id.id)
], limit=1),
'default_account_expense': cls.env['account.account'].search([
('company_id', '=', company.id),
('account_type', '=', 'expense'),
('id', '!=', company.account_journal_early_pay_discount_loss_account_id.id)
], limit=1),
'default_account_receivable': search_account(company, chart_template, 'property_account_receivable_id', [
('account_type', '=', 'asset_receivable')
]),
'default_account_payable': cls.env['account.account'].search([
('company_id', '=', company.id),
('account_type', '=', 'liability_payable')
], limit=1),
'default_account_assets': cls.env['account.account'].search([
('company_id', '=', company.id),
('account_type', '=', 'asset_current')
], limit=1),
'default_account_tax_sale': company.account_sale_tax_id.mapped('invoice_repartition_line_ids.account_id'),
'default_account_tax_purchase': company.account_purchase_tax_id.mapped('invoice_repartition_line_ids.account_id'),
'default_journal_misc': cls.env['account.journal'].search([
('company_id', '=', company.id),
('type', '=', 'general')
], limit=1),
'default_journal_sale': cls.env['account.journal'].search([
('company_id', '=', company.id),
('type', '=', 'sale')
], limit=1),
'default_journal_purchase': cls.env['account.journal'].search([
('company_id', '=', company.id),
('type', '=', 'purchase')
], limit=1),
'default_journal_bank': cls.env['account.journal'].search([
('company_id', '=', company.id),
('type', '=', 'bank')
], limit=1),
'default_journal_cash': cls.env['account.journal'].search([
('company_id', '=', company.id),
('type', '=', 'cash')
], limit=1),
'default_tax_sale': company.account_sale_tax_id,
'default_tax_purchase': company.account_purchase_tax_id,
}
@classmethod
def setup_multi_currency_data(cls, default_values=None, rate2016=3.0, rate2017=2.0):
default_values = default_values or {}
foreign_currency = cls.env['res.currency'].create({
'name': 'Gold Coin',
'symbol': '',
'rounding': 0.001,
'position': 'after',
'currency_unit_label': 'Gold',
'currency_subunit_label': 'Silver',
**default_values,
})
rates = cls.env['res.currency.rate'].create([{
'name': '1900-01-01',
'rate': 1,
'currency_id': foreign_currency.id,
'company_id': cls.env.company.id,
}, {
'name': '2016-01-01',
'rate': rate2016,
'currency_id': foreign_currency.id,
'company_id': cls.env.company.id,
}, {
'name': '2017-01-01',
'rate': rate2017,
'currency_id': foreign_currency.id,
'company_id': cls.env.company.id,
}])
return {
'currency': foreign_currency,
'rates': rates,
}
@classmethod
def setup_armageddon_tax(cls, tax_name, company_data):
return cls.env['account.tax'].create({
'name': '%s (group)' % tax_name,
'amount_type': 'group',
'amount': 0.0,
'country_id': company_data['company'].account_fiscal_country_id.id,
'children_tax_ids': [
(0, 0, {
'name': '%s (child 1)' % tax_name,
'amount_type': 'percent',
'amount': 20.0,
'country_id': company_data['company'].account_fiscal_country_id.id,
'price_include': True,
'include_base_amount': True,
'tax_exigibility': 'on_invoice',
'invoice_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
}),
(0, 0, {
'factor_percent': 40,
'repartition_type': 'tax',
'account_id': company_data['default_account_tax_sale'].id,
}),
(0, 0, {
'factor_percent': 60,
'repartition_type': 'tax',
# /!\ No account set.
}),
],
'refund_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
}),
(0, 0, {
'factor_percent': 40,
'repartition_type': 'tax',
'account_id': company_data['default_account_tax_sale'].id,
}),
(0, 0, {
'factor_percent': 60,
'repartition_type': 'tax',
# /!\ No account set.
}),
],
}),
(0, 0, {
'name': '%s (child 2)' % tax_name,
'amount_type': 'percent',
'amount': 10.0,
'country_id': company_data['company'].account_fiscal_country_id.id,
'tax_exigibility': 'on_payment',
'cash_basis_transition_account_id': cls.safe_copy(company_data['default_account_tax_sale']).id,
'invoice_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
}),
(0, 0, {
'repartition_type': 'tax',
'account_id': company_data['default_account_tax_sale'].id,
}),
],
'refund_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
}),
(0, 0, {
'repartition_type': 'tax',
'account_id': company_data['default_account_tax_sale'].id,
}),
],
}),
],
})
@classmethod
def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=None, amounts=None, taxes=None, company=False, currency=None):
products = [] if products is None else products
amounts = [] if amounts is None else amounts
move_form = Form(cls.env['account.move'] \
.with_company(company or cls.env.company) \
.with_context(default_move_type=move_type, account_predictive_bills_disable_prediction=True))
move_form.invoice_date = invoice_date or fields.Date.from_string('2019-01-01')
# According to the state or type of the invoice, the date field is sometimes visible or not
# Besides, the date field can be put multiple times in the view
# "invisible": "['|', ('state', '!=', 'draft'), ('auto_post', '!=', 'at_date')]"
# "invisible": ['|', '|', ('state', '!=', 'draft'), ('auto_post', '=', 'no'), ('auto_post', '=', 'at_date')]
# "invisible": "['&', ('move_type', 'in', ['out_invoice', 'out_refund', 'out_receipt']), ('quick_edit_mode', '=', False)]"
# :TestAccountMoveOutInvoiceOnchanges, :TestAccountMoveOutRefundOnchanges, .test_00_debit_note_out_invoice, :TestAccountEdi
if not move_form._get_modifier('date', 'invisible'):
move_form.date = move_form.invoice_date
move_form.partner_id = partner or cls.partner_a
if currency:
move_form.currency_id = currency
for product in (products or []):
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = product
if taxes is not None:
line_form.tax_ids.clear()
for tax in taxes:
line_form.tax_ids.add(tax)
for amount in (amounts or []):
with move_form.invoice_line_ids.new() as line_form:
line_form.name = "test line"
# We use account_predictive_bills_disable_prediction context key so that
# this doesn't trigger prediction in case enterprise (hence account_predictive_bills) is installed
line_form.price_unit = amount
if taxes is not None:
line_form.tax_ids.clear()
for tax in taxes:
line_form.tax_ids.add(tax)
rslt = move_form.save()
if post:
rslt.action_post()
return rslt
def _create_invoice(self, move_type='out_invoice', invoice_amount=50, currency_id=None, partner_id=None, date_invoice=None, payment_term_id=False, auto_validate=False, taxes=None, state=None):
if move_type == 'entry':
raise AssertionError("Unexpected move_type : 'entry'.")
if not taxes:
taxes = self.env['account.tax']
date_invoice = date_invoice or time.strftime('%Y') + '-07-01'
invoice_vals = {
'move_type': move_type,
'partner_id': partner_id or self.partner_agrolait.id,
'invoice_date': date_invoice,
'date': date_invoice,
'invoice_line_ids': [Command.create({
'name': 'product that cost %s' % invoice_amount,
'quantity': 1,
'price_unit': invoice_amount,
'tax_ids': [Command.set(taxes.ids)],
})]
}
if payment_term_id:
invoice_vals['invoice_payment_term_id'] = payment_term_id
if currency_id:
invoice_vals['currency_id'] = currency_id
invoice = self.env['account.move'].with_context(default_move_type=move_type).create(invoice_vals)
if state == 'cancel':
invoice.write({'state': 'cancel'})
elif auto_validate or state == 'posted':
invoice.action_post()
return invoice
def create_invoice(self, move_type='out_invoice', invoice_amount=50, currency_id=None):
return self._create_invoice(move_type=move_type, invoice_amount=invoice_amount, currency_id=currency_id, auto_validate=True)
@classmethod
def _create_tax_tag(cls, name, country_id=None):
return cls.env['account.account.tag'].create({
'name': name,
'applicability': 'taxes',
'country_id': country_id or cls.company_data['company'].country_id.id,
})
@classmethod
def _create_tax(cls, name, amount, amount_type='percent', type_tax_use='sale', tag_names=None, children_taxes=None, tax_exigibility='on_invoice', **kwargs):
if not tag_names:
tag_names = {}
tag_commands = {
type_rep_line: [(Command.set(cls._create_tax_tag(tags).ids))]
for type_rep_line, tags in tag_names.items()
}
vals = {
'name': name,
'amount': amount,
'amount_type': amount_type,
'type_tax_use': type_tax_use,
'tax_exigibility': tax_exigibility,
'children_tax_ids': [Command.set(children_taxes.ids)] if children_taxes else None,
'invoice_repartition_line_ids': [
Command.create({
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': tag_commands.get('invoice_base'),
}),
Command.create({
'factor_percent': 100,
'repartition_type': 'tax',
'tag_ids': tag_commands.get('invoice_tax'),
}),
] if not children_taxes else None,
'refund_repartition_line_ids': [
Command.create({
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': tag_commands.get('refund_base'),
}),
Command.create({
'factor_percent': 100,
'repartition_type': 'tax',
'tag_ids': tag_commands.get('refund_tax'),
}),
] if not children_taxes else None,
**kwargs,
}
return cls.env['account.tax'].create(vals)
def assertInvoiceValues(self, move, expected_lines_values, expected_move_values):
def sort_lines(lines):
return lines.sorted(lambda line: (line.sequence, not bool(line.tax_line_id), line.name or '', line.balance))
self.assertRecordValues(sort_lines(move.line_ids.sorted()), expected_lines_values)
self.assertRecordValues(move, [expected_move_values])
def assert_invoice_outstanding_to_reconcile_widget(self, invoice, expected_amounts):
""" Check the outstanding widget before the reconciliation.
:param invoice: An invoice.
:param expected_amounts: A map <move_id> -> <amount>
"""
invoice.invalidate_recordset(['invoice_outstanding_credits_debits_widget'])
widget_vals = invoice.invoice_outstanding_credits_debits_widget
if widget_vals:
current_amounts = {vals['move_id']: vals['amount'] for vals in widget_vals['content']}
else:
current_amounts = {}
self.assertDictEqual(current_amounts, expected_amounts)
def assert_invoice_outstanding_reconciled_widget(self, invoice, expected_amounts):
""" Check the outstanding widget after the reconciliation.
:param invoice: An invoice.
:param expected_amounts: A map <move_id> -> <amount>
"""
invoice.invalidate_recordset(['invoice_payments_widget'])
widget_vals = invoice.invoice_payments_widget
if widget_vals:
current_amounts = {vals['move_id']: vals['amount'] for vals in widget_vals['content']}
else:
current_amounts = {}
self.assertDictEqual(current_amounts, expected_amounts)
####################################################
# Xml Comparison
####################################################
def _turn_node_as_dict_hierarchy(self, node):
''' Turn the node as a python dictionary to be compared later with another one.
Allow to ignore the management of namespaces.
:param node: A node inside an xml tree.
:return: A python dictionary.
'''
tag_split = node.tag.split('}')
tag_wo_ns = tag_split[-1]
attrib_wo_ns = {k: v for k, v in node.attrib.items() if '}' not in k}
return {
'tag': tag_wo_ns,
'namespace': None if len(tag_split) < 2 else tag_split[0],
'text': (node.text or '').strip(),
'attrib': attrib_wo_ns,
'children': [self._turn_node_as_dict_hierarchy(child_node) for child_node in node.getchildren()],
}
def assertXmlTreeEqual(self, xml_tree, expected_xml_tree):
''' Compare two lxml.etree.
:param xml_tree: The current tree.
:param expected_xml_tree: The expected tree.
'''
def assertNodeDictEqual(node_dict, expected_node_dict):
''' Compare nodes created by the `_turn_node_as_dict_hierarchy` method.
:param node_dict: The node to compare with.
:param expected_node_dict: The expected node.
'''
# Check tag.
self.assertEqual(node_dict['tag'], expected_node_dict['tag'])
# Check attributes.
node_dict_attrib = {k: '___ignore___' if expected_node_dict['attrib'].get(k) == '___ignore___' else v
for k, v in node_dict['attrib'].items()}
expected_node_dict_attrib = {k: v for k, v in expected_node_dict['attrib'].items() if v != '___remove___'}
self.assertDictEqual(
node_dict_attrib,
expected_node_dict_attrib,
"Element attributes are different for node %s" % node_dict['tag'],
)
# Check text.
if expected_node_dict['text'] != '___ignore___':
self.assertEqual(
node_dict['text'],
expected_node_dict['text'],
"Element text are different for node %s" % node_dict['tag'],
)
# Check children.
self.assertEqual(
[child['tag'] for child in node_dict['children']],
[child['tag'] for child in expected_node_dict['children']],
"Number of children elements for node %s is different." % node_dict['tag'],
)
for child_node_dict, expected_child_node_dict in zip(node_dict['children'], expected_node_dict['children']):
assertNodeDictEqual(child_node_dict, expected_child_node_dict)
assertNodeDictEqual(
self._turn_node_as_dict_hierarchy(xml_tree),
self._turn_node_as_dict_hierarchy(expected_xml_tree),
)
def with_applied_xpath(self, xml_tree, xpath):
''' Applies the xpath to the xml_tree passed as parameter.
:param xml_tree: An instance of etree.
:param xpath: The xpath to apply as a string.
:return: The resulting etree after applying the xpaths.
'''
diff_xml_tree = etree.fromstring('<data>%s</data>' % xpath)
return self.env['ir.ui.view'].apply_inheritance_specs(xml_tree, diff_xml_tree)
def get_xml_tree_from_attachment(self, attachment):
''' Extract an instance of etree from an ir.attachment.
:param attachment: An ir.attachment.
:return: An instance of etree.
'''
return etree.fromstring(base64.b64decode(attachment.with_context(bin_size=False).datas))
def get_xml_tree_from_string(self, xml_tree_str):
''' Convert the string passed as parameter to an instance of etree.
:param xml_tree_str: A string representing an xml.
:return: An instance of etree.
'''
return etree.fromstring(xml_tree_str)
class AccountTestInvoicingHttpCommon(AccountTestInvoicingCommon, HttpCase):
pass
class TestAccountReconciliationCommon(AccountTestInvoicingCommon):
"""Tests for reconciliation (account.tax)
Test used to check that when doing a sale or purchase invoice in a different currency,
the result will be balanced.
"""
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.company = cls.company_data['company']
cls.company.currency_id = cls.env.ref('base.EUR')
cls.currency_swiss_id = cls.env.ref("base.CHF").id
cls.currency_usd_id = cls.env.ref("base.USD").id
cls.currency_euro_id = cls.env.ref("base.EUR").id
cls.account_rcv = cls.company_data['default_account_receivable']
cls.account_rsa = cls.company_data['default_account_payable']
cls.product = cls.env['product.product'].create({
'name': 'Product Product 4',
'standard_price': 500.0,
'list_price': 750.0,
'type': 'consu',
'categ_id': cls.env.ref('product.product_category_all').id,
})
cls.bank_journal_euro = cls.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'})
cls.account_euro = cls.bank_journal_euro.default_account_id
cls.bank_journal_usd = cls.env['account.journal'].create({'name': 'Bank US', 'type': 'bank', 'code': 'BNK68', 'currency_id': cls.currency_usd_id})
cls.account_usd = cls.bank_journal_usd.default_account_id
cls.fx_journal = cls.company.currency_exchange_journal_id
cls.diff_income_account = cls.company.income_currency_exchange_account_id
cls.diff_expense_account = cls.company.expense_currency_exchange_account_id
cls.expense_account = cls.company_data['default_account_expense']
# cash basis intermediary account
cls.tax_waiting_account = cls.env['account.account'].create({
'name': 'TAX_WAIT',
'code': 'TWAIT',
'account_type': 'liability_current',
'reconcile': True,
'company_id': cls.company.id,
})
# cash basis final account
cls.tax_final_account = cls.env['account.account'].create({
'name': 'TAX_TO_DEDUCT',
'code': 'TDEDUCT',
'account_type': 'asset_current',
'company_id': cls.company.id,
})
cls.tax_base_amount_account = cls.env['account.account'].create({
'name': 'TAX_BASE',
'code': 'TBASE',
'account_type': 'asset_current',
'company_id': cls.company.id,
})
cls.company.account_cash_basis_base_account_id = cls.tax_base_amount_account.id
# Journals
cls.purchase_journal = cls.company_data['default_journal_purchase']
cls.cash_basis_journal = cls.env['account.journal'].create({
'name': 'Test CABA',
'code': 'tCABA',
'type': 'general',
})
cls.general_journal = cls.company_data['default_journal_misc']
# Tax Cash Basis
cls.tax_cash_basis = cls.env['account.tax'].create({
'name': 'cash basis 20%',
'type_tax_use': 'purchase',
'company_id': cls.company.id,
'country_id': cls.company.account_fiscal_country_id.id,
'amount': 20,
'tax_exigibility': 'on_payment',
'cash_basis_transition_account_id': cls.tax_waiting_account.id,
'invoice_repartition_line_ids': [
(0,0, {
'repartition_type': 'base',
}),
(0,0, {
'repartition_type': 'tax',
'account_id': cls.tax_final_account.id,
}),
],
'refund_repartition_line_ids': [
(0,0, {
'repartition_type': 'base',
}),
(0,0, {
'repartition_type': 'tax',
'account_id': cls.tax_final_account.id,
}),
],
})
cls.env['res.currency.rate'].create([
{
'currency_id': cls.env.ref('base.EUR').id,
'name': '2010-01-02',
'rate': 1.0,
}, {
'currency_id': cls.env.ref('base.USD').id,
'name': '2010-01-02',
'rate': 1.2834,
}, {
'currency_id': cls.env.ref('base.USD').id,
'name': time.strftime('%Y-06-05'),
'rate': 1.5289,
}
])
def create_invoice_partner(self, move_type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
return self._create_invoice(
move_type=move_type,
invoice_amount=invoice_amount,
currency_id=currency_id,
partner_id=partner_id,
payment_term_id=payment_term_id,
auto_validate=True
)

View file

@ -0,0 +1,458 @@
# -*- coding: utf-8 -*-
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.tests.common import Form
from odoo.exceptions import UserError, ValidationError
from odoo.tools import mute_logger
import psycopg2
from freezegun import freeze_time
@tagged('post_install', '-at_install')
class TestAccountAccount(AccountTestInvoicingCommon):
def test_changing_account_company(self):
''' Ensure you can't change the company of an account.account if there are some journal entries '''
self.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'line_ids': [
(0, 0, {
'name': 'line_debit',
'account_id': self.company_data['default_account_revenue'].id,
}),
(0, 0, {
'name': 'line_credit',
'account_id': self.company_data['default_account_revenue'].id,
}),
],
})
with self.assertRaises(UserError), self.cr.savepoint():
self.company_data['default_account_revenue'].company_id = self.company_data_2['company']
def test_toggle_reconcile(self):
''' Test the feature when the user sets an account as reconcile/not reconcile with existing journal entries. '''
account = self.company_data['default_account_revenue']
move = self.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'line_ids': [
(0, 0, {
'account_id': account.id,
'currency_id': self.currency_data['currency'].id,
'debit': 100.0,
'credit': 0.0,
'amount_currency': 200.0,
}),
(0, 0, {
'account_id': account.id,
'currency_id': self.currency_data['currency'].id,
'debit': 0.0,
'credit': 100.0,
'amount_currency': -200.0,
}),
],
})
move.action_post()
self.env['account.move.line'].flush_model()
self.assertRecordValues(move.line_ids, [
{'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
{'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
])
# Set the account as reconcile and fully reconcile something.
account.reconcile = True
self.env.invalidate_all()
self.assertRecordValues(move.line_ids, [
{'reconciled': False, 'amount_residual': 100.0, 'amount_residual_currency': 200.0},
{'reconciled': False, 'amount_residual': -100.0, 'amount_residual_currency': -200.0},
])
move.line_ids.reconcile()
self.assertRecordValues(move.line_ids, [
{'reconciled': True, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
{'reconciled': True, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
])
# Set back to a not reconcile account and check the journal items.
move.line_ids.remove_move_reconcile()
account.reconcile = False
self.env.invalidate_all()
self.assertRecordValues(move.line_ids, [
{'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
{'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
])
def test_toggle_reconcile_with_partials(self):
''' Test the feature when the user sets an account as reconcile/not reconcile with partial reconciliation. '''
account = self.company_data['default_account_revenue']
move = self.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'line_ids': [
(0, 0, {
'account_id': account.id,
'currency_id': self.currency_data['currency'].id,
'debit': 100.0,
'credit': 0.0,
'amount_currency': 200.0,
}),
(0, 0, {
'account_id': account.id,
'currency_id': self.currency_data['currency'].id,
'debit': 0.0,
'credit': 50.0,
'amount_currency': -100.0,
}),
(0, 0, {
'account_id': self.company_data['default_account_expense'].id,
'currency_id': self.currency_data['currency'].id,
'debit': 0.0,
'credit': 50.0,
'amount_currency': -100.0,
}),
],
})
move.action_post()
# Set the account as reconcile and partially reconcile something.
account.reconcile = True
self.env.invalidate_all()
move.line_ids.filtered(lambda line: line.account_id == account).reconcile()
# Try to set the account as a not-reconcile one.
with self.assertRaises(UserError), self.cr.savepoint():
account.reconcile = False
def test_toggle_reconcile_outstanding_account(self):
''' Test the feature when the user sets an account as not reconcilable when a journal
is configured with this account as the payment credit or debit account.
Since such an account should be reconcilable by nature, a ValidationError is raised.'''
with self.assertRaises(ValidationError), self.cr.savepoint():
self.company_data['default_journal_bank'].company_id.account_journal_payment_debit_account_id.reconcile = False
with self.assertRaises(ValidationError), self.cr.savepoint():
self.company_data['default_journal_bank'].company_id.account_journal_payment_credit_account_id.reconcile = False
def test_remove_account_from_account_group(self):
"""Test if an account is well removed from account group"""
group = self.env['account.group'].create({
'name': 'test_group',
'code_prefix_start': 401000,
'code_prefix_end': 402000,
'company_id': self.env.company.id
})
account_1 = self.company_data['default_account_revenue'].copy({'code': 401000})
account_2 = self.company_data['default_account_revenue'].copy({'code': 402000})
self.assertRecordValues(account_1 + account_2, [{'group_id': group.id}] * 2)
group.code_prefix_end = 401000
self.assertRecordValues(account_1 + account_2, [{'group_id': group.id}, {'group_id': False}])
def test_name_create(self):
"""name_create should only be possible when importing
Code and Name should be split
"""
with self.assertRaises(UserError):
self.env['account.account'].name_create('550003 Existing Account')
# account code is mandatory and providing a name without a code should raise an error
with self.assertRaises(psycopg2.DatabaseError), mute_logger('odoo.sql_db'):
self.env['account.account'].with_context(import_file=True).name_create('Existing Account')
account_id = self.env['account.account'].with_context(import_file=True).name_create('550003 Existing Account')[0]
account = self.env['account.account'].browse(account_id)
self.assertEqual(account.code, "550003")
self.assertEqual(account.name, "Existing Account")
def test_compute_account_type(self):
existing_account = self.company_data['default_account_revenue']
# account_type should be computed
new_account_code = self.env['account.account']._search_new_account_code(
company=existing_account.company_id,
digits=len(existing_account.code),
prefix=existing_account.code[:-1])
new_account = self.env['account.account'].create({
'code': new_account_code,
'name': 'A new account'
})
self.assertEqual(new_account.account_type, existing_account.account_type)
# account_type should not be altered
alternate_account = self.env['account.account'].search([('account_type', '!=', existing_account.account_type)], limit=1)
alternate_code = self.env['account.account']._search_new_account_code(
company=alternate_account.company_id,
digits=len(alternate_account.code),
prefix=alternate_account.code[:-1])
new_account.code = alternate_code
self.assertEqual(new_account.account_type, existing_account.account_type)
def test_compute_current_balance(self):
""" Test if an account's current_balance is computed correctly """
account_payable = self.company_data['default_account_payable']
account_receivable = self.company_data['default_account_receivable']
payable_debit_move = {
'line_ids': [
(0, 0, {'name': 'debit', 'account_id': account_payable.id, 'debit': 100.0, 'credit': 0.0}),
(0, 0, {'name': 'credit', 'account_id': account_receivable.id, 'debit': 0.0, 'credit': 100.0}),
],
}
payable_credit_move = {
'line_ids': [
(0, 0, {'name': 'credit', 'account_id': account_payable.id, 'debit': 0.0, 'credit': 100.0}),
(0, 0, {'name': 'debit', 'account_id': account_receivable.id, 'debit': 100.0, 'credit': 0.0}),
],
}
self.assertEqual(account_payable.current_balance, 0)
self.env['account.move'].create(payable_debit_move).action_post()
account_payable._compute_current_balance()
self.assertEqual(account_payable.current_balance, 100)
self.env['account.move'].create(payable_credit_move).action_post()
account_payable._compute_current_balance()
self.assertEqual(account_payable.current_balance, 0)
self.env['account.move'].create(payable_credit_move).action_post()
account_payable._compute_current_balance()
self.assertEqual(account_payable.current_balance, -100)
self.env['account.move'].create(payable_credit_move).button_cancel()
account_payable._compute_current_balance()
self.assertEqual(account_payable.current_balance, -100, 'Canceled invoices/bills should not be used when computing the balance')
# draft invoice
self.env['account.move'].create(payable_credit_move)
account_payable._compute_current_balance()
self.assertEqual(account_payable.current_balance, -100, 'Draft invoices/bills should not be used when computing the balance')
def test_name_create_account_code_only(self):
"""
Test account creation with only a code, with and without space
"""
account_id = self.env['account.account'].with_context(import_file=True).name_create('550003')[0]
account = self.env['account.account'].browse(account_id)
self.assertEqual(account.code, "550003")
self.assertEqual(account.name, "")
account_id = self.env['account.account'].with_context(import_file=True).name_create('550004 ')[0]
account = self.env['account.account'].browse(account_id)
self.assertEqual(account.code, "550004")
self.assertEqual(account.name, "")
def test_name_create_account_name_with_number(self):
"""
Test the case when a code is provided and the account name contains a number in the first word
"""
account_id = self.env['account.account'].with_context(import_file=True).name_create('550005 CO2')[0]
account = self.env['account.account'].browse(account_id)
self.assertEqual(account.code, "550005")
self.assertEqual(account.name, "CO2")
account_id = self.env['account.account'].with_context(import_file=True, default_account_type='expense').name_create('CO2')[0]
account = self.env['account.account'].browse(account_id)
self.assertEqual(account.code, "CO2")
self.assertEqual(account.name, "")
def test_create_account(self):
"""
Test creating an account with code and name without name_create
"""
account = self.env['account.account'].create({
'code': '314159',
'name': 'A new account',
'account_type': 'expense',
})
self.assertEqual(account.code, "314159")
self.assertEqual(account.name, "A new account")
# name split is only possible through name_create, so an error should be raised
with self.assertRaises(psycopg2.DatabaseError), mute_logger('odoo.sql_db'):
account = self.env['account.account'].create({
'name': '314159 A new account',
'account_type': 'expense',
})
# it doesn't matter whether the account name contains numbers or not
account = self.env['account.account'].create({
'code': '31415',
'name': 'CO2-contributions',
'account_type': 'expense'
})
self.assertEqual(account.code, "31415")
self.assertEqual(account.name, "CO2-contributions")
def test_account_name_onchange(self):
"""
Test various scenarios when creating an account via a form
"""
account_form = Form(self.env['account.account'])
account_form.name = "A New Account 1"
# code should not be set
self.assertEqual(account_form.code, False)
self.assertEqual(account_form.name, "A New Account 1")
account_form.name = "314159 A New Account"
# the name should be split into code and name
self.assertEqual(account_form.code, "314159")
self.assertEqual(account_form.name, "A New Account")
account_form.code = False
account_form.name = "314159 "
# the name should be moved to code
self.assertEqual(account_form.code, "314159")
self.assertEqual(account_form.name, "")
account_form.code = "314159"
account_form.name = "CO2-contributions"
# the name should not overwrite the code
self.assertEqual(account_form.code, "314159")
self.assertEqual(account_form.name, "CO2-contributions")
account_form.code = False
account_form.name = "CO2-contributions"
# the name should overwrite the code
self.assertEqual(account_form.code, "CO2-contributions")
self.assertEqual(account_form.name, "")
# should save the account correctly
account_form.code = False
account_form.name = "314159"
account = account_form.save()
self.assertEqual(account.code, "314159")
self.assertEqual(account.name, "")
# can change the name of an existing account without overwriting the code
account_form.name = "123213 Test"
self.assertEqual(account_form.code, "314159")
self.assertEqual(account_form.name, "123213 Test")
account_form.code = False
account_form.name = "Only letters"
# saving a form without a code should not be possible
with self.assertRaises(AssertionError):
account_form.save()
@freeze_time('2023-09-30')
def test_generate_account_suggestions(self):
"""
Test the generation of account suggestions for a partner.
- Creates: partner and a account move of that partner.
- Checks if the most frequent account for the partner matches created account (with recent move).
- Sets the account as deprecated and checks that it no longer appears in the suggestions.
* since tested function takes into account last 2 years, we use freeze_time
"""
partner = self.env['res.partner'].create({'name': 'partner_test_generate_account_suggestions'})
account = self.company_data['default_account_revenue']
self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner.id,
'invoice_date': '2023-09-30',
'line_ids': [Command.create({'price_unit': 100, 'account_id': account.id})]
})
results_1 = self.env['account.account']._get_most_frequent_accounts_for_partner(
company_id=self.env.company.id,
partner_id=partner.id,
move_type="out_invoice"
)
self.assertEqual(account.id, results_1[0], "Account with most account_moves should be listed first")
account.deprecated = True
account.flush_recordset(['deprecated'])
results_2 = self.env['account.account']._get_most_frequent_accounts_for_partner(
company_id=self.env.company.id,
partner_id=partner.id,
move_type="out_invoice"
)
self.assertFalse(account.id in results_2, "Deprecated account should NOT appear in account suggestions")
@freeze_time('2017-01-01')
def test_account_opening_balance(self):
company = self.env.company
account = self.company_data['default_account_revenue']
balancing_account = company.get_unaffected_earnings_account()
self.assertFalse(company.account_opening_move_id)
account.opening_debit = 300
self.cr.precommit.run()
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
# pylint: disable=bad-whitespace
{'account_id': account.id, 'balance': 300.0},
{'account_id': balancing_account.id, 'balance': -300.0},
])
account.opening_credit = 500
self.cr.precommit.run()
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
# pylint: disable=bad-whitespace
{'account_id': account.id, 'balance': 300.0},
{'account_id': balancing_account.id, 'balance': 200.0},
{'account_id': account.id, 'balance': -500.0},
])
account.opening_balance = 0
self.cr.precommit.run()
self.assertFalse(company.account_opening_move_id.line_ids)
account.currency_id = self.currency_data['currency']
account.opening_debit = 100
self.cr.precommit.run()
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
# pylint: disable=bad-whitespace
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
{'account_id': balancing_account.id, 'balance': -100.0, 'amount_currency': -100.0},
])
company.account_opening_move_id.write({'line_ids': [
Command.create({
'account_id': account.id,
'balance': 100.0,
'amount_currency': 200.0,
'currency_id': account.currency_id.id,
}),
Command.create({
'account_id': balancing_account.id,
'balance': -100.0,
}),
]})
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
# pylint: disable=bad-whitespace
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
{'account_id': balancing_account.id, 'balance': -100.0, 'amount_currency': -100.0},
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
{'account_id': balancing_account.id, 'balance': -100.0, 'amount_currency': -100.0},
])
account.opening_credit = 1000
self.cr.precommit.run()
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
# pylint: disable=bad-whitespace
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
{'account_id': balancing_account.id, 'balance': 800.0, 'amount_currency': 800.0},
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
{'account_id': account.id, 'balance': -1000.0, 'amount_currency': -2000.0},
])
account.opening_debit = 1000
self.cr.precommit.run()
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
# pylint: disable=bad-whitespace
{'account_id': account.id, 'balance': 1000.0, 'amount_currency': 2000.0},
{'account_id': account.id, 'balance': -1000.0, 'amount_currency': -2000.0},
])

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
import logging
from odoo.tests import standalone
_logger = logging.getLogger(__name__)
@standalone('all_l10n')
def test_all_l10n(env):
""" This test will install all the l10n_* modules.
As the module install is not yet fully transactional, the modules will
remain installed after the test.
"""
assert env.ref('base.module_account').demo, "Need the demo to test with data"
l10n_mods = env['ir.module.module'].search([
('name', 'like', 'l10n%'),
('state', '=', 'uninstalled'),
])
l10n_mods.button_immediate_install()
env.reset() # clear the set of environments
env = env() # get an environment that refers to the new registry
coas = env['account.chart.template'].search([
('id', 'not in', env['res.company'].search([]).chart_template_id.ids)
])
for coa in coas:
cname = 'company_%s' % str(coa.id)
company = env['res.company'].create({
'name': cname,
'country_id': coa.country_id.id,
})
env.user.company_ids += company
env.user.company_id = company
_logger.info('Testing COA: %s (company: %s)' % (coa.name, cname))
try:
with env.cr.savepoint():
coa.try_loading()
except Exception:
_logger.error("Error when creating COA %s", coa.name, exc_info=True)

View file

@ -0,0 +1,378 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
from odoo.exceptions import UserError, ValidationError
from odoo import Command
@tagged('post_install', '-at_install')
class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.env.user.groups_id += cls.env.ref('analytic.group_analytic_accounting')
# By default, tests are run with the current user set on the first company.
cls.env.user.company_id = cls.company_data['company']
cls.default_plan = cls.env['account.analytic.plan'].create({'name': 'Default', 'company_id': False})
cls.analytic_account_a = cls.env['account.analytic.account'].create({
'name': 'analytic_account_a',
'plan_id': cls.default_plan.id,
'company_id': False,
})
cls.analytic_account_b = cls.env['account.analytic.account'].create({
'name': 'analytic_account_b',
'plan_id': cls.default_plan.id,
'company_id': False,
})
cls.analytic_account_c = cls.env['account.analytic.account'].create({
'name': 'analytic_account_c',
'plan_id': cls.default_plan.id,
'company_id': False,
})
cls.analytic_account_d = cls.env['account.analytic.account'].create({
'name': 'analytic_account_d',
'plan_id': cls.default_plan.id,
'company_id': False,
})
def get_analytic_lines(self, invoice):
return self.env['account.analytic.line'].search([
('move_line_id', 'in', invoice.line_ids.ids),
]).sorted('amount')
def create_invoice(self, partner, product, move_type='out_invoice'):
return self.env['account.move'].create([{
'move_type': move_type,
'partner_id': partner.id,
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [Command.create({
'product_id': product.id,
})]
}])
def test_changing_analytic_company(self):
""" Ensure you can't change the company of an account.analytic.account if there are analytic lines linked to
the account
"""
self.env['account.analytic.line'].create({
'name': 'company specific account',
'account_id': self.analytic_account_a.id,
'amount': 100,
})
# Set a different company on the analytic account.
with self.assertRaises(UserError), self.cr.savepoint():
self.analytic_account_a.company_id = self.company_data_2['company']
# Making the analytic account not company dependent is allowed.
self.analytic_account_a.company_id = False
def test_analytic_lines(self):
''' Ensures analytic lines are created when posted and are recreated when editing the account.move'''
out_invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'price_unit': 200.0,
'analytic_distribution': {
self.analytic_account_a.id: 100,
self.analytic_account_b.id: 50,
},
})]
}])
out_invoice.action_post()
# Analytic lines are created when posting the invoice
self.assertRecordValues(self.get_analytic_lines(out_invoice), [{
'amount': 100,
'account_id': self.analytic_account_b.id,
}, {
'amount': 200,
'account_id': self.analytic_account_a.id,
'partner_id': self.partner_a.id,
'product_id': self.product_a.id,
}])
# Analytic lines are updated when a posted invoice's distribution changes
out_invoice.invoice_line_ids.analytic_distribution = {
self.analytic_account_a.id: 100,
self.analytic_account_b.id: 25,
}
self.assertRecordValues(self.get_analytic_lines(out_invoice), [{
'amount': 50,
'account_id': self.analytic_account_b.id,
}, {
'amount': 200,
'account_id': self.analytic_account_a.id,
}])
# Analytic lines are deleted when resetting to draft
out_invoice.button_draft()
self.assertFalse(self.get_analytic_lines(out_invoice))
def test_analytic_lines_rounding(self):
""" Ensures analytic lines rounding errors are spread across all lines, in such a way that summing them gives the right amount.
For example, when distributing 100% of the the price, the sum of analytic lines should be exactly equal to the price. """
# in this scenario,
# 94% of 182.25 = 171.315 rounded to 171.32
# 2% of 182.25 = 3.645 rounded to 3.65
# 3 * 3.65 + 171.32 = 182.27
# we remove 0.01 to two lines to counter the rounding errors.
out_invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'price_unit': 182.25,
'analytic_distribution': {
self.analytic_account_a.id: 94,
self.analytic_account_b.id: 2,
self.analytic_account_c.id: 2,
self.analytic_account_d.id: 2,
},
})]
}])
out_invoice.action_post()
self.assertRecordValues(self.get_analytic_lines(out_invoice), [
{
'amount': 3.64,
'account_id': self.analytic_account_b.id,
},
{
'amount': 3.65,
'account_id': self.analytic_account_d.id,
},
{
'amount': 3.65,
'account_id': self.analytic_account_c.id,
},
{
'amount': 171.31,
'account_id': self.analytic_account_a.id,
},
])
out_invoice.button_draft()
# in this scenario,
# 25% of 182.25 = 45.5625 rounded to 45.56
# 45.56 * 4 = 182.24
# we add 0.01 to one of the line to counter the rounding errors.
out_invoice.invoice_line_ids[0].analytic_distribution = {
self.analytic_account_a.id: 25,
self.analytic_account_b.id: 25,
self.analytic_account_c.id: 25,
self.analytic_account_d.id: 25,
}
out_invoice.action_post()
self.assertRecordValues(self.get_analytic_lines(out_invoice), [
{
'amount': 45.56,
'account_id': self.analytic_account_d.id,
},
{
'amount': 45.56,
'account_id': self.analytic_account_c.id,
},
{
'amount': 45.56,
'account_id': self.analytic_account_b.id,
},
{
'amount': 45.57,
'account_id': self.analytic_account_a.id,
},
])
def test_model_score(self):
"""Test that the models are applied correctly based on the score"""
self.env['account.analytic.distribution.model'].create([{
'product_id': self.product_a.id,
'analytic_distribution': {self.analytic_account_a.id: 100}
}, {
'partner_id': self.partner_a.id,
'product_id': self.product_a.id,
'analytic_distribution': {self.analytic_account_b.id: 100}
}])
# Partner and product match, score 2
invoice = self.create_invoice(self.partner_a, self.product_a)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# Match the partner but not the product, score 0
invoice = self.create_invoice(self.partner_a, self.product_b)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False)
# Product match, score 1
invoice = self.create_invoice(self.partner_b, self.product_a)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_a.id): 100})
# No rule match with the product, score 0
invoice = self.create_invoice(self.partner_b, self.product_b)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False)
def test_model_application(self):
"""Test that the distribution is recomputed if and only if it is needed when changing the partner."""
self.env['account.analytic.distribution.model'].create([{
'partner_id': self.partner_a.id,
'analytic_distribution': {self.analytic_account_a.id: 100},
'company_id': False,
}, {
'partner_id': self.partner_b.id,
'analytic_distribution': {self.analytic_account_b.id: 100},
'company_id': False,
}])
invoice = self.create_invoice(self.env['res.partner'], self.product_a)
# No model is found, don't put anything
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False)
# A model is found, set the new values
invoice.partner_id = self.partner_a
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_a.id): 100})
# A model is found, set the new values
invoice.partner_id = self.partner_b
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# No model is found, don't change previously set values
invoice.partner_id = invoice.company_id.partner_id
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# No model is found, don't change previously set values
invoice.partner_id = False
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# It manual value is not erased in form view when saving
with Form(invoice) as invoice_form:
invoice_form.partner_id = self.partner_a
with invoice_form.invoice_line_ids.edit(0) as line_form:
self.assertEqual(line_form.analytic_distribution, {str(self.analytic_account_a.id): 100})
line_form.analytic_distribution = {self.analytic_account_b.id: 100}
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
def test_mandatory_plan_validation(self):
invoice = self.create_invoice(self.partner_b, self.product_a)
self.default_plan.write({
'applicability_ids': [Command.create({
'business_domain': 'invoice',
'product_categ_id': self.product_a.categ_id.id,
'applicability': 'mandatory',
})]
})
# ValidationError is raised only when validate_analytic is in the context and the distribution is != 100
with self.assertRaisesRegex(ValidationError, '100% analytic distribution.'):
invoice.with_context({'validate_analytic': True}).action_post()
invoice.invoice_line_ids.analytic_distribution = {self.analytic_account_b.id: 100.01}
with self.assertRaisesRegex(ValidationError, '100% analytic distribution.'):
invoice.with_context({'validate_analytic': True}).action_post()
invoice.invoice_line_ids.analytic_distribution = {self.analytic_account_b.id: 99.9}
with self.assertRaisesRegex(ValidationError, '100% analytic distribution.'):
invoice.with_context({'validate_analytic': True}).action_post()
invoice.invoice_line_ids.analytic_distribution = {self.analytic_account_b.id: 100}
invoice.with_context({'validate_analytic': True}).action_post()
self.assertEqual(invoice.state, 'posted')
# reset and post without the validate_analytic context key
invoice.button_draft()
invoice.invoice_line_ids.analytic_distribution = {self.analytic_account_b.id: 0.9}
invoice.action_post()
self.assertEqual(invoice.state, 'posted')
def test_mandatory_plan_validation_mass_posting(self):
"""
In case of mass posting, we should still check for mandatory analytic plans. This may raise a RedirectWarning,
if more than one entry was selected for posting, or a ValidationError if only one entry was selected.
"""
invoice1 = self.create_invoice(self.partner_a, self.product_a)
invoice2 = self.create_invoice(self.partner_b, self.product_a)
self.default_plan.write({
'applicability_ids': [Command.create({
'business_domain': 'invoice',
'product_categ_id': self.product_a.categ_id.id,
'applicability': 'mandatory',
})]
})
vam = self.env['validate.account.move'].create({'force_post': True})
for invoices in [invoice1, invoice1 | invoice2]:
with self.subTest(invoices=invoices):
with self.assertRaises(Exception):
vam.with_context({
'active_model': 'account.move',
'active_ids': [invoice1.id, invoice2.id],
'validate_analytic': True,
}).validate_move()
self.assertTrue('posted' not in invoices.mapped('state'))
def test_set_anaylytic_distribution_posted_line(self):
"""
Test that we can set the analytic distribution on the product line of a move, when the line has tax with
repartition lines used in tax closing. Although the change can not be applied on the tax line, we should
not raise any error.
"""
tax = self.tax_purchase_a.copy({
'name': 'taXXX',
'invoice_repartition_line_ids': [
Command.create({
'repartition_type': 'base',
'use_in_tax_closing': False,
}),
Command.create({
'repartition_type': 'tax',
'factor_percent': 50,
'use_in_tax_closing': False,
}),
Command.create({
'repartition_type': 'tax',
'factor_percent': 50,
'account_id': self.company_data['default_account_tax_purchase'].id,
'use_in_tax_closing': True,
}),
],
'refund_repartition_line_ids': [
Command.create({
'repartition_type': 'base',
'use_in_tax_closing': False,
}),
Command.create({
'repartition_type': 'tax',
'factor_percent': 50,
'use_in_tax_closing': False,
}),
Command.create({
'repartition_type': 'tax',
'factor_percent': 50,
'account_id': self.company_data['default_account_tax_purchase'].id,
'use_in_tax_closing': True,
}),
],
})
bill = self.create_invoice(self.partner_a, self.product_a, move_type='in_invoice')
bill.invoice_line_ids.tax_ids = [Command.set(tax.ids)]
bill.action_post()
line = bill.line_ids.filtered(lambda l: l.display_type == 'product')
line.write({'analytic_distribution': {self.analytic_account_a.id: 100}})
self.assertEqual(line.analytic_distribution, {str(self.analytic_account_a.id): 100})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,269 @@
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.models import Model
from odoo.tests import tagged
from odoo.tests.common import Form
from odoo import fields
from odoo.exceptions import UserError
from odoo.tools import format_date
@tagged('post_install', '-at_install')
class TestAccountMoveInalterableHash(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def test_account_move_inalterable_hash(self):
"""Test that we cannot alter a field used for the computation of the inalterable hash"""
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
move = self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True)
with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
move.inalterable_hash = 'fake_hash'
with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
move.secure_sequence_number = 666
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.name = "fake name"
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.date = fields.Date.from_string('2023-01-02')
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.company_id = 666
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.write({
'company_id': 666,
'date': fields.Date.from_string('2023-01-03')
})
with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Account.*"):
move.line_ids[0].account_id = move.line_ids[1]['account_id']
with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Partner.*"):
move.line_ids[0].partner_id = 666
# The following fields are not part of the hash so they can be modified
move.ref = "bla"
move.line_ids[0].date_maturity = fields.Date.from_string('2023-01-02')
def test_account_move_hash_integrity_report(self):
"""Test the hash integrity report"""
moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
)
moves.action_post()
# No records to be hashed because the restrict mode is not activated yet
integrity_check = moves.company_id._check_hash_integrity()['results'][0] # First journal
self.assertEqual(integrity_check['msg_cover'], 'This journal is not in strict mode.')
# No records to be hashed even if the restrict mode is activated because the hashing is not retroactive
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], 'There isn\'t any journal entry flagged for data inalterability yet for this journal.')
# Everything should be correctly hashed and verified
new_moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-03", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_a, "2023-01-05", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-06", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_a, "2023-01-07", amounts=[1000, 2000])
)
new_moves.action_post()
moves |= new_moves
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[2].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report.
# We need to bypass the write method of account.move to do so.
Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')
# Revert the previous change
Model.write(moves[4], {'date': fields.Date.from_string("2023-01-05")})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*')
# Let's try with the one of the subfields
Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_b.id})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')
# Let's try with the inalterable_hash field itself
Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_a.id}) # Revert the previous change
Model.write(moves[-1], {'inalterable_hash': 'fake_hash'})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')
def test_account_move_hash_versioning_1(self):
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
This test focuses on the case where the user has only moves with the old hash algorithm."""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
)
moves.with_context(hash_version=1).action_post()
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
# We need to bypass the write method of account.move to do so.
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
def test_account_move_hash_versioning_2(self):
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
This test focuses on the case where the user has only moves with the new hash algorithm."""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves.action_post()
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
# We need to bypass the write method of account.move to do so.
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
def test_account_move_hash_versioning_v1_to_v2(self):
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
This test focuses on the case where the user has moves with both hash algorithms."""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves_v1 = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves_v1.with_context(hash_version=1).action_post()
fields_v1 = moves_v1.with_context(hash_version=1)._get_integrity_hash_fields()
moves_v2 = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves_v2.with_context(hash_version=2).action_post()
fields_v2 = moves_v2._get_integrity_hash_fields()
self.assertNotEqual(fields_v1, fields_v2) # Make sure two different hash algorithms were used
moves = moves_v1 | moves_v2
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
# We need to bypass the write method of account.move to do so.
Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')
# Let's revert the change and make sure that we cannot use the v1 after the v2.
# This means we don't simply check whether the move is correctly hashed with either algorithms,
# but that we can only use v2 after v1 and not go back to v1 afterwards.
Model.write(moves[4], {'date': fields.Date.from_string("2023-01-02")}) # Revert the previous change
moves_v1_bis = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-10", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-11", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-12", amounts=[1000, 2000])
)
moves_v1_bis.with_context(hash_version=1).action_post()
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves_v1_bis[0].id}.')
def test_account_move_hash_versioning_3(self):
"""
Version 2 does not take into account floating point representation issues.
Test that version 3 covers correctly this case
"""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves_v3 = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[30*0.17, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves_v3.action_post()
# invalidate cache
moves_v3[0].line_ids[0].invalidate_recordset()
integrity_check_v3 = moves_v3.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check_v3['msg_cover'], f'Entries are hashed from {moves_v3[0].name}.*')
def test_account_move_hash_versioning_v2_to_v3(self):
"""
We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
This test focuses on the case with version 2 and version 3.
"""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves_v2 = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves_v2.with_context(hash_version=2).action_post()
moves_v3 = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves_v3.with_context(hash_version=3).action_post()
moves = moves_v2 | moves_v3
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
self.assertEqual(integrity_check['first_move_date'],
format_date(self.env, fields.Date.to_string(moves[0].date)))
self.assertEqual(integrity_check['last_move_date'],
format_date(self.env, fields.Date.to_string(moves[-1].date)))
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
def test_account_move_hash_with_cash_rounding(self):
# Enable inalterable hash
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
# Required for `invoice_cash_rounding_id` to be visible in the view
self.env.user.groups_id += self.env.ref('account.group_cash_rounding')
# Test 'add_invoice_line' rounding
invoice = self.init_invoice('out_invoice', products=self.product_a+self.product_b)
move_form = Form(invoice)
# Add a cash rounding having 'add_invoice_line'.
move_form.invoice_cash_rounding_id = self.cash_rounding_a
with move_form.invoice_line_ids.edit(0) as line_form:
line_form.price_unit = 999.99
move_form.save()
# Should not raise
invoice.action_post()
self.assertEqual(invoice.amount_total, 1410.0)
self.assertEqual(invoice.amount_untaxed, 1200.0)
self.assertEqual(invoice.amount_tax, 210)
self.assertEqual(len(invoice.invoice_line_ids), 2)
self.assertEqual(len(invoice.line_ids), 6)

View file

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
import json
@tagged('post_install', '-at_install')
class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test-company.odoo.com')
cls.internal_user = cls.env['res.users'].create({
'name': 'Internal User',
'login': 'internal.user@test.odoo.com',
'email': 'internal.user@test.odoo.com',
})
cls.supplier_partner = cls.env['res.partner'].create({
'name': 'Your Supplier',
'email': 'supplier@other.company.com',
'supplier_rank': 10,
})
cls.journal = cls.company_data['default_journal_purchase']
journal_alias = cls.env['mail.alias'].create({
'alias_name': 'test-bill',
'alias_model_id': cls.env.ref('account.model_account_move').id,
'alias_defaults': json.dumps({
'move_type': 'in_invoice',
'company_id': cls.env.user.company_id.id,
'journal_id': cls.journal.id,
}),
})
cls.journal.write({'alias_id': journal_alias.id})
def test_supplier_invoice_mailed_from_supplier(self):
message_parsed = {
'message_id': 'message-id-dead-beef',
'subject': 'Incoming bill',
'from': '%s <%s>' % (self.supplier_partner.name, self.supplier_partner.email),
'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
'body': "You know, that thing that you bought.",
'attachments': [b'Hello, invoice'],
}
invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
message_ids = invoice.message_ids
self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
following_partners = invoice.message_follower_ids.mapped('partner_id')
self.assertEqual(following_partners, self.env.user.partner_id)
self.assertRegex(invoice.name, r'BILL/\d{4}/\d{2}/0001')
def test_supplier_invoice_forwarded_by_internal_user_without_supplier(self):
""" In this test, the bill was forwarded by an employee,
but no partner email address is found in the body."""
message_parsed = {
'message_id': 'message-id-dead-beef',
'subject': 'Incoming bill',
'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email),
'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
'body': "You know, that thing that you bought.",
'attachments': [b'Hello, invoice'],
}
invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
message_ids = invoice.message_ids
self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
following_partners = invoice.message_follower_ids.mapped('partner_id')
self.assertEqual(following_partners, self.env.user.partner_id | self.internal_user.partner_id)
def test_supplier_invoice_forwarded_by_internal_with_supplier_in_body(self):
""" In this test, the bill was forwarded by an employee,
and the partner email address is found in the body."""
message_parsed = {
'message_id': 'message-id-dead-beef',
'subject': 'Incoming bill',
'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email),
'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
'body': "Mail sent by %s <%s>:\nYou know, that thing that you bought." % (self.supplier_partner.name, self.supplier_partner.email),
'attachments': [b'Hello, invoice'],
}
invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
message_ids = invoice.message_ids
self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
following_partners = invoice.message_follower_ids.mapped('partner_id')
self.assertEqual(following_partners, self.env.user.partner_id | self.internal_user.partner_id)
def test_supplier_invoice_forwarded_by_internal_with_internal_in_body(self):
""" In this test, the bill was forwarded by an employee,
and the internal user email address is found in the body."""
message_parsed = {
'message_id': 'message-id-dead-beef',
'subject': 'Incoming bill',
'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email),
'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
'body': "Mail sent by %s <%s>:\nYou know, that thing that you bought." % (self.internal_user.name, self.internal_user.email),
'attachments': [b'Hello, invoice'],
}
invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
message_ids = invoice.message_ids
self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
following_partners = invoice.message_follower_ids.mapped('partner_id')
self.assertEqual(following_partners, self.env.user.partner_id | self.internal_user.partner_id)

View file

@ -0,0 +1,173 @@
# -*- 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 TestAccountInvoiceReport(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.invoices = cls.env['account.move'].create([
{
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': fields.Date.from_string('2016-01-01'),
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [
(0, None, {
'product_id': cls.product_a.id,
'quantity': 3,
'price_unit': 750,
}),
(0, None, {
'product_id': cls.product_a.id,
'quantity': 1,
'price_unit': 3000,
}),
]
},
{
'move_type': 'out_receipt',
'invoice_date': fields.Date.from_string('2016-01-01'),
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [
(0, None, {
'product_id': cls.product_a.id,
'quantity': 1,
'price_unit': 6000,
}),
]
},
{
'move_type': 'out_refund',
'partner_id': cls.partner_a.id,
'invoice_date': fields.Date.from_string('2017-01-01'),
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [
(0, None, {
'product_id': cls.product_a.id,
'quantity': 1,
'price_unit': 1200,
}),
]
},
{
'move_type': 'in_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': fields.Date.from_string('2016-01-01'),
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [
(0, None, {
'product_id': cls.product_a.id,
'quantity': 1,
'price_unit': 60,
}),
]
},
{
'move_type': 'in_receipt',
'partner_id': cls.partner_a.id,
'invoice_date': fields.Date.from_string('2016-01-01'),
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [
(0, None, {
'product_id': cls.product_a.id,
'quantity': 1,
'price_unit': 60,
}),
]
},
{
'move_type': 'in_refund',
'partner_id': cls.partner_a.id,
'invoice_date': fields.Date.from_string('2017-01-01'),
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [
(0, None, {
'product_id': cls.product_a.id,
'quantity': 1,
'price_unit': 12,
}),
]
},
])
def assertInvoiceReportValues(self, expected_values_list):
reports = self.env['account.invoice.report'].search([('company_id', '=', self.company_data['company'].id)], order='price_subtotal DESC, quantity ASC')
expected_values_dict = [{
'price_average': vals[0],
'price_subtotal': vals[1],
'quantity': vals[2],
} for vals in expected_values_list]
self.assertRecordValues(reports, expected_values_dict)
def test_invoice_report_multiple_types(self):
self.assertInvoiceReportValues([
#price_average price_subtotal quantity
[2000, 2000, 1],
[1000, 1000, 1],
[250, 750, 3],
[6, 6, 1],
[20, -20, -1],
[20, -20, -1],
[600, -600, -1],
])
def test_avg_price_calculation(self):
"""
Check that the average is correctly calculated based on the total price and quantity:
3 lines:
- 10 units * 10$
- 5 units * 5$
- 20 units * 2$
Total quantity: 35
Total price: 165$
Average: 165 / 35 = 4.71
"""
product = self.product_a.copy()
invoice = self.env["account.move"].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': fields.Date.from_string('2016-01-01'),
'currency_id': self.env.company.currency_id.id,
'invoice_line_ids': [
(0, None, {
'product_id': product.id,
'quantity': 10,
'price_unit': 10,
}),
(0, None, {
'product_id': product.id,
'quantity': 5,
'price_unit': 5,
}),
(0, None, {
'product_id': product.id,
'quantity': 20,
'price_unit': 2,
}),
]
})
invoice.action_post()
report = self.env['account.invoice.report'].read_group(
[('product_id', '=', product.id)],
['price_subtotal:sum', 'quantity:sum', 'price_average:avg'],
[],
)
self.assertEqual(report[0]['quantity'], 35)
self.assertEqual(report[0]['price_subtotal'], 165)
self.assertEqual(round(report[0]['price_average'], 2), 4.71)
# ensure that it works with only 'price_average:avg' in fields
report = self.env['account.invoice.report'].read_group(
[('product_id', '=', product.id)],
['price_average:avg'],
[],
)
self.assertEqual(round(report[0]['price_average'], 2), 4.71)

View file

@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
from unittest.mock import patch
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
from odoo.tests import tagged
from odoo.exceptions import UserError, ValidationError
@tagged('post_install', '-at_install')
class TestAccountJournal(AccountTestInvoicingCommon):
def test_constraint_currency_consistency_with_accounts(self):
''' The accounts linked to a bank/cash journal must share the same foreign currency
if specified.
'''
journal_bank = self.company_data['default_journal_bank']
journal_bank.currency_id = self.currency_data['currency']
# Try to set a different currency on the 'debit' account.
with self.assertRaises(ValidationError), self.cr.savepoint():
journal_bank.default_account_id.currency_id = self.company_data['currency']
def test_changing_journal_company(self):
''' Ensure you can't change the company of an account.journal if there are some journal entries '''
self.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'journal_id': self.company_data['default_journal_sale'].id,
})
with self.assertRaises(UserError), self.cr.savepoint():
self.company_data['default_journal_sale'].company_id = self.company_data_2['company']
def test_account_control_create_journal_entry(self):
move_vals = {
'line_ids': [
(0, 0, {
'name': 'debit',
'account_id': self.company_data['default_account_revenue'].id,
'debit': 100.0,
'credit': 0.0,
}),
(0, 0, {
'name': 'credit',
'account_id': self.company_data['default_account_expense'].id,
'debit': 0.0,
'credit': 100.0,
}),
],
}
# Should fail because 'default_account_expense' is not allowed.
self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_revenue']
with self.assertRaises(UserError), self.cr.savepoint():
self.env['account.move'].create(move_vals)
# Should be allowed because both accounts are accepted.
self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_expense']
self.env['account.move'].create(move_vals)
def test_account_control_existing_journal_entry(self):
self.env['account.move'].create({
'line_ids': [
(0, 0, {
'name': 'debit',
'account_id': self.company_data['default_account_revenue'].id,
'debit': 100.0,
'credit': 0.0,
}),
(0, 0, {
'name': 'credit',
'account_id': self.company_data['default_account_expense'].id,
'debit': 0.0,
'credit': 100.0,
}),
],
})
# There is already an other line using the 'default_account_expense' account.
with self.assertRaises(ValidationError), self.cr.savepoint():
self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_revenue']
# Assigning both should be allowed
self.company_data['default_journal_misc'].account_control_ids = \
self.company_data['default_account_revenue'] + self.company_data['default_account_expense']
def test_account_journal_add_new_payment_method_multi(self):
"""
Test the automatic creation of payment method lines with the mode set to multi
"""
Method_get_payment_method_information = AccountPaymentMethod._get_payment_method_information
def _get_payment_method_information(self):
res = Method_get_payment_method_information(self)
res['multi'] = {'mode': 'multi', 'domain': [('type', '=', 'bank')]}
return res
with patch.object(AccountPaymentMethod, '_get_payment_method_information', _get_payment_method_information):
self.env['account.payment.method'].sudo().create({
'name': 'Multi method',
'code': 'multi',
'payment_type': 'inbound'
})
journals = self.env['account.journal'].search([('inbound_payment_method_line_ids.code', '=', 'multi')])
# The two bank journals have been set
self.assertEqual(len(journals), 2)
def test_remove_payment_method_lines(self):
"""
Payment method lines are a bit special in the way their removal is handled.
If they are linked to a payment at the moment of the deletion, they won't be deleted but the journal_id will be
set to False.
If they are not linked to any payment, they will be deleted as expected.
"""
# Linked to a payment. It will not be deleted, but its journal_id will be set to False.
first_method = self.inbound_payment_method_line
self.env['account.payment'].create({
'amount': 100.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'payment_method_line_id': first_method.id,
})
first_method.unlink()
self.assertFalse(first_method.journal_id)
# Not linked to anything. It will be deleted.
second_method = self.outbound_payment_method_line
second_method.unlink()
self.assertFalse(second_method.exists())
def test_account_journal_alias_name(self):
journal = self.company_data['default_journal_purchase']
self.assertEqual(journal.alias_name, 'vendor-bills-company_1_data')
journal.name = ''
journal.alias_name = False
self.assertEqual(journal.alias_name, 'bill-company_1_data')
journal.code = ''
journal.alias_name = False
self.assertEqual(journal.alias_name, 'purchase-company_1_data')
company_2_id = str(self.company_data_2['company'].id)
journal_2 = self.company_data_2['default_journal_sale']
self.company_data_2['company'].name = ''
journal_2.alias_name = False
self.assertEqual(journal_2.alias_name, 'customer-invoices-' + company_2_id)
journal_2.name = ''
journal_2.alias_name = False
self.assertEqual(journal_2.alias_name, 'inv-' + company_2_id)
journal_2.code = ''
journal_2.alias_name = False
self.assertEqual(journal_2.alias_name, 'sale-' + company_2_id)
def test_account_journal_duplicates(self):
new_journals = self.env["account.journal"].with_context(import_file=True).create([
{"name": "OD_BLABLA"},
{"name": "OD_BLABLU"},
])
self.assertEqual(sorted(new_journals.mapped("code")), ["GEN1", "OD_BL"], "The journals should be set correctly")
def test_archive_used_journal(self):
journal = self.env['account.journal'].create({
'name': 'Test Journal',
'type': 'sale',
'code': 'A',
})
check_method = self.env['account.payment.method'].sudo().create({
'name': 'Test',
'code': 'check_printing_expense_test',
'payment_type': 'outbound',
})
self.env['account.payment.method.line'].create({
'name': 'Check',
'payment_method_id': check_method.id,
'journal_id': journal.id
})
journal.action_archive()
self.assertFalse(journal.active)
def test_archive_multiple_journals(self):
journals = self.env['account.journal'].create([{
'name': 'Test Journal 1',
'type': 'sale',
'code': 'A1'
}, {
'name': 'Test Journal 2',
'type': 'sale',
'code': 'A2'
}])
# Archive the Journals
journals.action_archive()
self.assertFalse(journals[0].active)
self.assertFalse(journals[1].active)
# Unarchive the Journals
journals.action_unarchive()
self.assertTrue(journals[0].active)
self.assertTrue(journals[1].active)

View file

@ -0,0 +1,200 @@
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.tools.misc import format_amount
@tagged('post_install', '-at_install')
class TestAccountJournalDashboard(AccountTestInvoicingCommon):
@freeze_time("2019-01-22")
def test_customer_invoice_dashboard(self):
journal = self.company_data['default_journal_sale']
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'journal_id': journal.id,
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-21',
'date': '2019-01-21',
'invoice_line_ids': [(0, 0, {
'product_id': self.product_a.id,
'quantity': 40.0,
'name': 'product test 1',
'discount': 10.00,
'price_unit': 2.27,
'tax_ids': [],
})]
})
refund = self.env['account.move'].create({
'move_type': 'out_refund',
'journal_id': journal.id,
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-21',
'date': '2019-01-21',
'invoice_line_ids': [(0, 0, {
'product_id': self.product_a.id,
'quantity': 1.0,
'name': 'product test 1',
'price_unit': 13.3,
'tax_ids': [],
})]
})
# Check Draft
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEqual(dashboard_data['number_draft'], 2)
self.assertIn('68.42', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 0)
self.assertIn('0.00', dashboard_data['sum_waiting'])
# Check Both
invoice.action_post()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEqual(dashboard_data['number_draft'], 1)
self.assertIn('-\N{ZERO WIDTH NO-BREAK SPACE}13.30', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 1)
self.assertIn('81.72', dashboard_data['sum_waiting'])
# Check waiting payment
refund.action_post()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEqual(dashboard_data['number_draft'], 0)
self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 2)
self.assertIn('68.42', dashboard_data['sum_waiting'])
# Check partial
payment = self.env['account.payment'].create({
'amount': 10.0,
'payment_type': 'outbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
})
payment.action_post()
(refund + payment.move_id).line_ids\
.filtered(lambda line: line.account_type == 'asset_receivable')\
.reconcile()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEqual(dashboard_data['number_draft'], 0)
self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 2)
self.assertIn('78.42', dashboard_data['sum_waiting'])
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEqual(dashboard_data['number_late'], 2)
self.assertIn('78.42', dashboard_data['sum_late'])
def test_sale_purchase_journal_for_multi_currency_purchase(self):
currency = self.currency_data['currency']
company_currency = self.company_data['currency']
invoice = self.env['account.move'].create({
'move_type': 'in_invoice',
'invoice_date': '2017-01-01',
'date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': currency.id,
'invoice_line_ids': [
(0, 0, {'name': 'test', 'price_unit': 200})
],
})
invoice.action_post()
payment = self.env['account.payment'].create({
'amount': 90.0,
'date': '2016-01-01',
'payment_type': 'outbound',
'partner_type': 'supplier',
'partner_id': self.partner_a.id,
'currency_id': currency.id,
})
payment.action_post()
(invoice + payment.move_id).line_ids.filtered_domain([
('account_id', '=', self.company_data['default_account_payable'].id)
]).reconcile()
dashboard_data = self.company_data['default_journal_purchase'].get_journal_dashboard_datas()
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_waiting'])
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_late'])
def test_sale_purchase_journal_for_multi_currency_sale(self):
currency = self.currency_data['currency']
company_currency = self.company_data['currency']
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'invoice_date': '2017-01-01',
'date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': currency.id,
'invoice_line_ids': [
(0, 0, {'name': 'test', 'price_unit': 200})
],
})
invoice.action_post()
payment = self.env['account.payment'].create({
'amount': 90.0,
'date': '2016-01-01',
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
'currency_id': currency.id,
})
payment.action_post()
(invoice + payment.move_id).line_ids.filtered_domain([
('account_id', '=', self.company_data['default_account_receivable'].id)
]).reconcile()
dashboard_data = self.company_data['default_journal_sale'].get_journal_dashboard_datas()
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_waiting'])
self.assertEqual(format_amount(self.env, 70, company_currency), dashboard_data['sum_late'])
def test_gap_in_sequence_warning(self):
journal = self.company_data['default_journal_sale']
self.assertFalse(journal._query_has_sequence_holes()) # No moves so no gap
moves = self.env['account.move'].create([{
'move_type': 'out_invoice',
'journal_id': journal.id,
'partner_id': self.partner_a.id,
'invoice_date': f'1900-01-{i+1:02d}',
'date': f'2019-01-{i+1:02d}',
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'quantity': 40.0,
'name': 'product test 1',
'price_unit': 2.27,
'tax_ids': [],
})]
} for i in range(10)]).sorted('date')
gap_date = moves[3].date
moves[:8].action_post() # Only post 8 moves and keep 2 draft moves
self.assertFalse(journal._query_has_sequence_holes()) # no gap, no gap warning, and draft moves shouldn't trigger the warning
moves[2:4].button_draft()
self.assertFalse(journal._query_has_sequence_holes()) # no gap (with draft moves using sequence numbers), no gap warning
moves[3].unlink()
self.assertTrue(journal.has_sequence_holes) # gap due to missing sequence, gap warning
moves[2].action_post()
self.company_data['company'].write({'fiscalyear_lock_date': gap_date + relativedelta(days=1)})
self.assertFalse(journal._query_has_sequence_holes()) # gap but prior to lock-date, no gap warning
moves[6].button_draft()
moves[6].button_cancel()
self.assertTrue(journal._query_has_sequence_holes()) # gap due to canceled move using a sequence, gap warning

View file

@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
from odoo import fields, Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
import freezegun
@tagged('post_install', '-at_install')
class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon):
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
def _create_invoice(self, move_type, date, **kwargs):
return self.env['account.move'].create({
'invoice_date': date,
'partner_id': self.partner_a.id,
**kwargs,
'move_type': move_type,
'date': date,
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 1200.0,
'tax_ids': [],
**line_kwargs,
})
for line_kwargs in kwargs.get('invoice_line_ids', [{}])
],
})
def _create_payment(self, date, **kwargs):
return self.env['account.payment'].create({
'partner_id': self.partner_a.id,
'payment_type': 'inbound',
'partner_type': 'customer',
**kwargs,
'date': date,
})
def _set_lock_date(self, lock_date, period_lock_date=None):
self.env.company.fiscalyear_lock_date = fields.Date.from_string(lock_date)
self.env.company.period_lock_date = fields.Date.from_string(period_lock_date)
def _reverse_invoice(self, invoice):
move_reversal = self.env['account.move.reversal']\
.with_context(active_model="account.move", active_ids=invoice.ids)\
.create({
'journal_id': invoice.journal_id.id,
'reason': "no reason",
'refund_method': 'cancel',
})
reversal = move_reversal.reverse_moves()
return self.env['account.move'].browse(reversal['res_id'])
# -------------------------------------------------------------------------
# TESTS
# -------------------------------------------------------------------------
@freezegun.freeze_time('2017-01-12')
def test_out_invoice_date_with_lock_date(self):
self._set_lock_date('2016-12-31')
move = self._create_invoice('out_invoice', '2016-01-01')
move.action_post()
self.assertRecordValues(move, [{
'invoice_date': fields.Date.from_string('2016-01-01'),
'date': fields.Date.from_string('2017-01-12'),
}])
@freezegun.freeze_time('2017-01-12')
def test_out_invoice_reverse_date_with_lock_date(self):
move = self._create_invoice('out_invoice', '2016-01-01')
move.action_post()
self._set_lock_date('2016-12-31')
reverse_move = self._reverse_invoice(move)
self.assertRecordValues(reverse_move, [{
'invoice_date': fields.Date.from_string('2017-01-12'),
'date': fields.Date.from_string('2017-01-12'),
}])
@freezegun.freeze_time('2017-01-12')
def test_out_refund_date_with_lock_date(self):
self._set_lock_date('2016-12-31')
move = self._create_invoice('out_refund', '2016-01-01')
move.action_post()
self.assertRecordValues(move, [{
'invoice_date': fields.Date.from_string('2016-01-01'),
'date': fields.Date.from_string('2017-01-12'),
}])
@freezegun.freeze_time('2017-01-12')
def test_out_refund_reverse_date_with_lock_date(self):
move = self._create_invoice('out_refund', '2016-01-01')
move.action_post()
self._set_lock_date('2016-12-31')
reverse_move = self._reverse_invoice(move)
self.assertRecordValues(reverse_move, [{'date': fields.Date.from_string('2017-01-12')}])
@freezegun.freeze_time('2017-01-12')
def test_in_invoice_date_with_lock_date(self):
self._set_lock_date('2016-12-31')
move = self._create_invoice('in_invoice', '2016-01-01')
move.action_post()
self.assertRecordValues(move, [{
'invoice_date': fields.Date.from_string('2016-01-01'),
'date': fields.Date.from_string('2017-01-12'),
}])
@freezegun.freeze_time('2017-01-12')
def test_in_invoice_reverse_date_with_lock_date(self):
move = self._create_invoice('in_invoice', '2016-01-01')
move.action_post()
self._set_lock_date('2016-12-31')
reverse_move = self._reverse_invoice(move)
self.assertRecordValues(reverse_move, [{
'invoice_date': fields.Date.from_string('2017-01-12'),
'date': fields.Date.from_string('2017-01-12'),
}])
@freezegun.freeze_time('2017-01-12')
def test_in_refund_date_with_lock_date(self):
self._set_lock_date('2016-12-31')
move = self._create_invoice('in_refund', '2016-01-01')
move.action_post()
self.assertRecordValues(move, [{
'invoice_date': fields.Date.from_string('2016-01-01'),
'date': fields.Date.from_string('2017-01-12'),
}])
@freezegun.freeze_time('2017-01-12')
def test_in_refund_reverse_date_with_lock_date(self):
move = self._create_invoice('in_refund', '2016-01-01')
move.action_post()
self._set_lock_date('2016-12-31')
reverse_move = self._reverse_invoice(move)
self.assertRecordValues(reverse_move, [{'date': fields.Date.from_string('2017-01-12')}])
@freezegun.freeze_time('2017-02-12')
def test_reconcile_with_lock_date(self):
invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.currency_data['currency'].id)
refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.currency_data['currency'].id)
(invoice + refund).action_post()
self._set_lock_date('2017-01-31')
res = (invoice + refund).line_ids\
.filtered(lambda x: x.account_id.account_type == 'asset_receivable')\
.reconcile()
exchange_move = res['partials'].exchange_move_id
self.assertRecordValues(exchange_move, [{
'date': fields.Date.from_string('2017-02-01'),
'amount_total_signed': 200.0,
}])
@freezegun.freeze_time('2017-02-12')
def test_unreconcile_with_lock_date(self):
invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.currency_data['currency'].id)
refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.currency_data['currency'].id)
(invoice + refund).action_post()
res = (invoice + refund).line_ids\
.filtered(lambda x: x.account_id.account_type == 'asset_receivable')\
.reconcile()
exchange_move = res['partials'].exchange_move_id
self._set_lock_date('2017-01-31')
(invoice + refund).line_ids.remove_move_reconcile()
reverse_exchange_move = exchange_move.line_ids.matched_credit_ids.credit_move_id.move_id
self.assertRecordValues(reverse_exchange_move, [{
'date': fields.Date.from_string('2017-02-12'),
'amount_total_signed': 200.0,
}])
def test_caba_with_lock_date(self):
self.env.company.tax_exigibility = True
tax_waiting_account = self.env['account.account'].create({
'name': 'TAX_WAIT',
'code': 'TWAIT',
'account_type': 'liability_current',
'reconcile': True,
})
tax = self.env['account.tax'].create({
'name': 'cash basis 10%',
'type_tax_use': 'sale',
'amount': 10,
'tax_exigibility': 'on_payment',
'cash_basis_transition_account_id': tax_waiting_account.id,
})
invoice = self._create_invoice(
'out_invoice', '2016-01-01',
currency_id=self.currency_data['currency'].id,
invoice_line_ids=[{'tax_ids': [Command.set(tax.ids)]}],
)
payment = self._create_payment('2016-02-01', amount=invoice.amount_total)
(invoice + payment.move_id).action_post()
self._set_lock_date('2017-01-03')
with freezegun.freeze_time('2017-01-12'):
(invoice + payment.move_id).line_ids\
.filtered(lambda x: x.account_id.account_type == 'asset_receivable')\
.reconcile()
caba_move = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)])
self.assertRecordValues(caba_move, [{
'date': fields.Date.from_string('2017-01-12'),
'amount_total_signed': 440.0,
}])
self._set_lock_date('2017-02-01')
with freezegun.freeze_time('2017-03-12'):
(invoice + payment.move_id).line_ids.remove_move_reconcile()
reverse_exchange_move = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)]) - caba_move
self.assertRecordValues(reverse_exchange_move, [{
'date': fields.Date.from_string('2017-02-28'),
'amount_total_signed': 440.0,
}])
@freezegun.freeze_time('2023-05-01')
def test_caba_with_different_lock_dates(self):
"""
Test the date of the CABA move when reconciling a payment with an invoice
with date before fiscalyear_period but after period_lock_date either when
having accountant rights or not.
"""
self.env.company.tax_exigibility = True
tax_waiting_account = self.env['account.account'].create({
'name': 'TAX_WAIT',
'code': 'TWAIT',
'account_type': 'liability_current',
'reconcile': True,
})
tax = self.env['account.tax'].create({
'name': 'cash basis 10%',
'type_tax_use': 'sale',
'amount': 10,
'tax_exigibility': 'on_payment',
'cash_basis_transition_account_id': tax_waiting_account.id,
})
self._set_lock_date('2023-01-01', '2023-02-01')
for group, expected_date in (
('account.group_account_manager', '2023-01-30'),
('account.group_account_invoice', '2023-05-01'),
):
with self.subTest(group=group, expected_date=expected_date):
self.env.user.groups_id = [Command.set(self.env.ref(group).ids)]
self.assertTrue(self.env.user.user_has_groups(group))
invoice = self._create_invoice(
'out_invoice', '2023-01-02',
invoice_line_ids=[{'tax_ids': [Command.set(tax.ids)]}],
)
payment = self._create_payment('2023-01-30', amount=invoice.amount_total)
(invoice + payment.move_id).action_post()
(invoice + payment.move_id).line_ids\
.filtered(lambda x: x.account_id.account_type == 'asset_receivable')\
.reconcile()
caba_move = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)])
self.assertRecordValues(caba_move, [{
'date': fields.Date.from_string(expected_date),
}])

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAccountMovePartnerCount(AccountTestInvoicingCommon):
def test_account_move_count(self):
self.env['account.move'].create([
{
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
},
{
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
},
]).action_post()
self.assertEqual(self.partner_a.supplier_rank, 1)
self.assertEqual(self.partner_a.customer_rank, 1)

View file

@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo import Command
@tagged('post_install', '-at_install')
class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.receivable_account = cls.company_data['default_account_receivable']
cls.payable_account = cls.company_data['default_account_payable']
cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
'name': 'Stars',
'symbol': '',
'currency_unit_label': 'Stars',
'currency_subunit_label': 'Little Stars',
}, rate2016=6.0, rate2017=4.0)
cls.curr_1 = cls.company_data['currency']
cls.curr_2 = cls.currency_data['currency']
cls.curr_3 = cls.currency_data_2['currency']
cls.payment_2016_curr_1 = cls.env['account.move'].create({
'date': '2016-01-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -500.0, 'currency_id': cls.curr_1.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 500.0, 'currency_id': cls.curr_1.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
],
})
cls.payment_2016_curr_1.action_post()
cls.payment_2016_curr_2 = cls.env['account.move'].create({
'date': '2016-01-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
],
})
cls.payment_2016_curr_2.action_post()
cls.payment_2017_curr_2 = cls.env['account.move'].create({
'date': '2017-01-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
],
})
cls.payment_2017_curr_2.action_post()
cls.payment_2016_curr_3 = cls.env['account.move'].create({
'date': '2016-01-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
],
})
cls.payment_2016_curr_3.action_post()
cls.payment_2017_curr_3 = cls.env['account.move'].create({
'date': '2017-01-01',
'line_ids': [
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
],
})
cls.payment_2017_curr_3.action_post()
# -------------------------------------------------------------------------
# TESTS
# -------------------------------------------------------------------------
def test_outstanding_payments_single_currency(self):
''' Test the outstanding payments widget on invoices having the same currency
as the company one.
'''
# Customer invoice of 2500.0 in curr_1.
out_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.curr_1.id,
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
})
out_invoice.action_post()
# Vendor bill of 2500.0 in curr_1.
in_invoice = self.env['account.move'].create({
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.curr_1.id,
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
})
in_invoice.action_post()
expected_amounts = {
self.payment_2016_curr_1.id: 500.0,
self.payment_2016_curr_2.id: 500.0,
self.payment_2017_curr_2.id: 500.0,
self.payment_2016_curr_3.id: 500.0,
self.payment_2017_curr_3.id: 500.0,
}
self.assert_invoice_outstanding_to_reconcile_widget(out_invoice, expected_amounts)
self.assert_invoice_outstanding_to_reconcile_widget(in_invoice, expected_amounts)
def test_outstanding_payments_foreign_currency(self):
''' Test the outstanding payments widget on invoices having a foreign currency. '''
# Customer invoice of 2500.0 in curr_1.
out_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.curr_2.id,
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
})
out_invoice.action_post()
# Vendor bill of 2500.0 in curr_1.
in_invoice = self.env['account.move'].create({
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.curr_2.id,
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
})
in_invoice.action_post()
expected_amounts = {
self.payment_2016_curr_1.id: 1500.0,
self.payment_2016_curr_2.id: 1550.0,
self.payment_2017_curr_2.id: 950.0,
self.payment_2016_curr_3.id: 1500.0,
self.payment_2017_curr_3.id: 1000.0,
}
self.assert_invoice_outstanding_to_reconcile_widget(out_invoice, expected_amounts)
self.assert_invoice_outstanding_to_reconcile_widget(in_invoice, expected_amounts)
def test_payments_with_exchange_difference_payment(self):
''' Test the payments widget on invoices having a foreign currency that triggers an exchange difference on the payment. '''
# Customer invoice of 300 in GOL at exchage rate 3:1. 300 GOL -> 100 USD
out_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2016-01-01',
'invoice_date': '2016-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'price_unit': 300,
'tax_ids': [],
})],
})
out_invoice.action_post()
# Payment at exchange rate 2:1. 300 GOL -> 150 USD
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=out_invoice.ids)\
.create({'payment_date': '2017-01-01'})\
._create_payments()
expected_amounts = {payment.move_id.id: 300.0}
# Get the exchange difference move.
for ln in out_invoice.line_ids:
if ln.matched_credit_ids.exchange_move_id:
expected_amounts[ln.matched_credit_ids.exchange_move_id.id] = 50.0
self.assert_invoice_outstanding_reconciled_widget(out_invoice, expected_amounts)
def test_payments_with_exchange_difference_invoice(self):
''' Test the payments widget on invoices having a foreign currency that triggers an exchange difference on the invoice. '''
# Customer invoice of 300 in GOL at exchage rate 2:1. 300 GOL -> 150 USD
out_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'price_unit': 300,
'tax_ids': [],
})],
})
out_invoice.action_post()
# Payment at exchange rate 3:1. 300 GOL -> 100 USD
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=out_invoice.ids)\
.create({'payment_date': '2016-01-01'})\
._create_payments()
expected_amounts = {payment.move_id.id: 300.0}
# Get the exchange difference move.
for ln in out_invoice.line_ids:
if ln.matched_credit_ids.exchange_move_id:
expected_amounts[ln.matched_credit_ids.exchange_move_id.id] = 50.0
self.assert_invoice_outstanding_reconciled_widget(out_invoice, expected_amounts)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAccountMoveRounding(AccountTestInvoicingCommon):
def test_move_line_rounding(self):
"""Whatever arguments we give to the creation of an account move,
in every case the amounts should be properly rounded to the currency's precision.
In other words, we don't fall victim of the limitation introduced by 9d87d15db6dd40
Here the rounding should be done according to company_currency_id, which is a related
on move_id.company_id.currency_id.
In principle, it should not be necessary to add it to the create values,
since it is supposed to be computed by the ORM...
"""
move = self.env['account.move'].create({
'line_ids': [
(0, 0, {'debit': 100.0 / 3, 'account_id': self.company_data['default_account_revenue'].id}),
(0, 0, {'credit': 100.0 / 3, 'account_id': self.company_data['default_account_revenue'].id}),
],
})
self.assertEqual(
[(33.33, 0.0), (0.0, 33.33)],
move.line_ids.mapped(lambda x: (x.debit, x.credit)),
"Quantities should have been rounded according to the currency."
)

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
from odoo.tests.common import tagged
@tagged('post_install', '-at_install')
class TestTourRenderInvoiceReport(AccountTestInvoicingHttpCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.env.user.write({
'groups_id': [
(6, 0, (cls.env.ref('account.group_account_manager') + cls.env.ref('base.group_system')).ids),
],
})
cls.out_invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': '2019-05-01',
'date': '2019-05-01',
'invoice_line_ids': [
(0, 0, {'name': 'line1', 'price_unit': 100.0}),
],
})
cls.out_invoice.action_post()
report_layout = cls.env.ref('web.report_layout_standard')
cls.company_data['company'].write({
'primary_color': '#123456',
'secondary_color': '#789101',
'external_report_layout_id': report_layout.view_id.id,
})
cls.env.ref('account.account_invoices_without_payment').report_type = 'qweb-html'
def test_render_account_document_layout(self):
self.start_tour('/web', 'account_render_report', login=self.env.user.login, timeout=200)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.exceptions import UserError
@tagged('post_install', '-at_install')
class TestAccountTax(AccountTestInvoicingCommon):
def test_changing_tax_company(self):
''' Ensure you can't change the company of an account.tax if there are some journal entries '''
# Avoid duplicate key value violates unique constraint "account_tax_name_company_uniq".
self.company_data['default_tax_sale'].name = 'test_changing_account_company'
self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_line_ids': [
(0, 0, {
'name': 'invoice_line',
'quantity': 1.0,
'price_unit': 100.0,
'tax_ids': [(6, 0, self.company_data['default_tax_sale'].ids)],
}),
],
})
with self.assertRaises(UserError), self.cr.savepoint():
self.company_data['default_tax_sale'].company_id = self.company_data_2['company']

View file

@ -0,0 +1,465 @@
import logging
from odoo import Command
from odoo.addons.account.models.chart_template import update_taxes_from_templates
from odoo.exceptions import ValidationError
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('post_install', '-at_install')
class TestChartTemplate(TransactionCase):
@classmethod
def setUpClass(cls):
""" Set up a company with the generic chart template, containing two taxes and a fiscal position.
We need to add xml_ids to the templates because they are loaded from their xml_ids
"""
super().setUpClass()
us_country_id = cls.env.ref('base.us').id
cls.company = cls.env['res.company'].create({
'name': 'TestCompany1',
'country_id': us_country_id,
'account_fiscal_country_id': us_country_id,
})
cls.chart_template_xmlid = 'l10n_test.test_chart_template_xmlid'
cls.chart_template = cls.env['account.chart.template']._load_records([{
'xml_id': cls.chart_template_xmlid,
'values': {
'name': 'Test Chart Template US',
'currency_id': cls.env.ref('base.USD').id,
'bank_account_code_prefix': 1000,
'cash_account_code_prefix': 2000,
'transfer_account_code_prefix': 3000,
'country_id': us_country_id,
}
}])
account_templates = cls.env['account.account.template']._load_records([{
'xml_id': 'account.test_account_income_template',
'values':
{
'name': 'property_income_account',
'code': '222221',
'account_type': 'income',
'chart_template_id': cls.chart_template.id,
}
}, {
'xml_id': 'account.test_account_expense_template',
'values':
{
'name': 'property_expense_account',
'code': '222222',
'account_type': 'expense',
'chart_template_id': cls.chart_template.id,
}
}])
cls.chart_template.property_account_income_categ_id = account_templates[0].id
cls.chart_template.property_account_expense_categ_id = account_templates[1].id
cls.fiscal_position_template = cls._create_fiscal_position_template('account.test_fiscal_position_template',
'US fiscal position test', us_country_id)
cls.tax_template_1 = cls._create_tax_template('account.test_tax_template_1', 'Tax name 1', 1, tag_name='tag_name_1')
cls.tax_template_2 = cls._create_tax_template('account.test_tax_template_2', 'Tax name 2', 2, tag_name='tag_name_2')
cls.fiscal_position_tax_template_1 = cls._create_fiscal_position_tax_template(
cls.fiscal_position_template, 'account.test_fp_tax_template_1', cls.tax_template_1, cls.tax_template_2
)
cls.chart_template.try_loading(company=cls.company, install_demo=False)
cls.fiscal_position = cls.env['account.fiscal.position'].search([
('company_id', '=', cls.company.id),
('name', '=', cls.fiscal_position_template.name),
])
@classmethod
def create_tax_template(cls, name, template_name, amount):
# TODO to remove in master
logging.warning("Deprecated method, please use _create_tax_template() instead")
return cls._create_tax_template(template_name, name, amount, tag_name=None)
@classmethod
def _create_group_tax_template(cls, tax_template_xmlid, name, chart_template_id=None, active=True):
children_1 = cls._create_tax_template(f'{tax_template_xmlid}_children1', f'{name}_children_1', 10, active=active)
children_2 = cls._create_tax_template(f'{tax_template_xmlid}_children2', f'{name}_children_2', 15, active=active)
return cls.env['account.tax.template']._load_records([{
'xml_id': tax_template_xmlid,
'values': {
'name': name,
'amount_type': 'group',
'type_tax_use': 'none',
'active': active,
'chart_template_id': chart_template_id if chart_template_id else cls.chart_template.id,
'children_tax_ids': [Command.set((children_1 + children_2).ids)],
},
}])
@classmethod
def _create_tax_template(cls, tax_template_xmlid, name, amount, tag_name=None, chart_template_id=None, account_data=None, active=True):
if tag_name:
tag = cls.env['account.account.tag'].create({
'name': tag_name,
'applicability': 'taxes',
'country_id': cls.company.account_fiscal_country_id.id,
})
if account_data:
account_vals = {
'name': account_data['name'],
'code': account_data['code'],
'account_type': 'liability_current',
}
# We have to instantiate both the template and the record since we suppose accounts are already created.
account_template = cls.env['account.account.template'].create(account_vals)
account_vals.update({'company_id': cls.company.id})
cls.env['account.account'].create(account_vals)
return cls.env['account.tax.template']._load_records([{
'xml_id': tax_template_xmlid,
'values': {
'name': name,
'amount': amount,
'type_tax_use': 'none',
'active': active,
'chart_template_id': chart_template_id if chart_template_id else cls.chart_template.id,
'invoice_repartition_line_ids': [
Command.create({
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': [(6, 0, tag.ids)] if tag_name else None,
}),
Command.create({
'factor_percent': 100,
'account_id': account_template.id if account_data else None,
'repartition_type': 'tax',
}),
],
'refund_repartition_line_ids': [
Command.create({
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': [(6, 0, tag.ids)] if tag_name else None,
}),
Command.create({
'factor_percent': 100,
'account_id': account_template.id if account_data else None,
'repartition_type': 'tax',
}),
],
},
}])
@classmethod
def _create_fiscal_position_template(cls, fp_template_xmlid, fp_template_name, country_id):
return cls.env['account.fiscal.position.template']._load_records([{
'xml_id': fp_template_xmlid,
'values': {
'name': fp_template_name,
'chart_template_id': cls.chart_template.id,
'country_id': country_id,
'auto_apply': True,
},
}])
@classmethod
def _create_fiscal_position_tax_template(cls, fiscal_position_template, fiscal_position_tax_template_xmlid, tax_template_src, tax_template_dest):
return cls.env['account.fiscal.position.tax.template']._load_records([{
'xml_id': fiscal_position_tax_template_xmlid,
'values': {
'tax_src_id': tax_template_src.id,
'tax_dest_id': tax_template_dest.id,
'position_id': fiscal_position_template.id,
},
}])
def test_update_taxes_new_template(self):
""" Tests that adding a new tax template and a fiscal position tax template
creates this new tax and fiscal position line when updating
"""
tax_template_3 = self._create_tax_template('account.test_tax_3_template', 'Tax name 3', 3, tag_name='tag_name_3')
tax_template_4 = self._create_tax_template('account.test_tax_4_template', 'Tax name 4', 4, account_data={'name': 'account_name_4', 'code': 'TACT'})
self._create_fiscal_position_tax_template(self.fiscal_position_template, 'account.test_fiscal_position_tax_template', tax_template_3, tax_template_4)
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
taxes = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'in', [tax_template_3.name, tax_template_4.name]),
])
self.assertRecordValues(taxes, [
{'name': 'Tax name 3', 'amount': 3},
{'name': 'Tax name 4', 'amount': 4},
])
self.assertEqual(taxes.invoice_repartition_line_ids.tag_ids.name, 'tag_name_3')
self.assertEqual(taxes.invoice_repartition_line_ids.account_id.name, 'account_name_4')
self.assertRecordValues(self.fiscal_position.tax_ids.tax_src_id, [
{'name': 'Tax name 1'},
{'name': 'Tax name 3'},
])
self.assertRecordValues(self.fiscal_position.tax_ids.tax_dest_id, [
{'name': 'Tax name 2'},
{'name': 'Tax name 4'},
])
def test_update_taxes_existing_template_update(self):
""" When a template is close enough from the corresponding existing tax we want to update
that tax with the template values.
"""
self.tax_template_1.invoice_repartition_line_ids.tag_ids.name += " [DUP]"
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'like', f'%{self.tax_template_1.name}'),
])
# Check that tax was not recreated
self.assertEqual(len(tax), 1)
# Check that tags have been updated
self.assertEqual(tax.invoice_repartition_line_ids.tag_ids.name, self.tax_template_1.invoice_repartition_line_ids.tag_ids.name)
def test_update_taxes_existing_template_rounding_error(self):
"""
When a template is close enough from the corresponding existing tax but has a minor rounding error,
we still want to update that tax with the template values and not recreate it.
"""
# We compare up to the precision of the field, which is 4 decimals
self.tax_template_1.amount += 0.00001
self.tax_template_1.invoice_repartition_line_ids.tag_ids.name += " [DUP]"
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'like', f'%{self.tax_template_1.name}'),
])
# Check that tax was not recreated
self.assertEqual(len(tax), 1)
# Check that tags have been updated
self.assertEqual(tax.invoice_repartition_line_ids.tag_ids.name, self.tax_template_1.invoice_repartition_line_ids.tag_ids.name)
def test_update_taxes_existing_template_recreation(self):
""" When a template is too different from the corresponding existing tax we want to recreate
a new taxes from template.
"""
# We increment the amount so the template gets slightly different from the
# corresponding tax and triggers recreation
old_tax_name = self.tax_template_1.name
old_tax_amount = self.tax_template_1.amount
self.tax_template_1.name = "Tax name 1 modified"
self.tax_template_1.amount += 1
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
# Check that old tax has not been changed
old_tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', '=', old_tax_name),
], limit=1)
self.assertEqual(old_tax[0].amount, old_tax_amount)
# Check that new tax has been recreated
tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', '=', self.tax_template_1.name),
], limit=1)
self.assertEqual(tax[0].amount, self.tax_template_1.amount)
def test_update_taxes_remove_fiscal_position_from_tax(self):
""" Tests that when we remove the tax from the fiscal position mapping it is not
recreated after update of taxes.
"""
self.fiscal_position.tax_ids.unlink()
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
self.assertEqual(len(self.fiscal_position.tax_ids), 0)
def test_update_taxes_conflict_name(self):
""" When recreating a tax during update a conflict name can occur since
we need to respect unique constraint on (name, company_id, type_tax_use, tax_scope).
To do so, the old tax needs to be prefixed with '[old] '.
"""
# We increment the amount so the template gets slightly different from the
# corresponding tax and triggers recreation
old_amount = self.tax_template_1.amount
self.tax_template_1.amount += 1
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
taxes_from_template_1 = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'like', f"%{self.tax_template_1.name}"),
])
self.assertRecordValues(taxes_from_template_1, [
{'name': f"[old] {self.tax_template_1.name}", 'amount': old_amount},
{'name': f"{self.tax_template_1.name}", 'amount': self.tax_template_1.amount},
])
def test_update_taxes_multi_company(self):
""" In a multi-company environment all companies should be correctly updated."""
company_2 = self.env['res.company'].create({
'name': 'TestCompany2',
'country_id': self.env.ref('base.us').id,
'account_fiscal_country_id': self.env.ref('base.us').id,
})
self.chart_template.try_loading(company=company_2, install_demo=False)
# triggers recreation of taxes related to template 1
self.tax_template_1.amount += 1
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
taxes_from_template_1 = self.env['account.tax'].search([
('name', 'like', f"%{self.tax_template_1.name}"),
('company_id', 'in', [self.company.id, company_2.id]),
])
# we should have 4 records: 2 companies * (1 original tax + 1 recreated tax)
self.assertEqual(len(taxes_from_template_1), 4)
def test_message_to_accountants(self):
""" When we duplicate a tax because it was too different from the existing one we send
a message to accountant advisors. This message should only be sent to advisors
and not to regular users.
"""
# create 1 normal user, 2 accountants managers
accountant_manager_group = self.env.ref('account.group_account_manager')
advisor_users = self.env['res.users'].create([{
'name': 'AccountAdvisorTest1',
'login': 'aat1',
'password': 'aat1aat1',
'groups_id': [(4, accountant_manager_group.id)],
}, {
'name': 'AccountAdvisorTest2',
'login': 'aat2',
'password': 'aat2aat2',
'groups_id': [(4, accountant_manager_group.id)],
}])
normal_user = self.env['res.users'].create([{
'name': 'AccountUserTest1',
'login': 'aut1',
'password': 'aut1aut1',
'groups_id': [(4, self.env.ref('account.group_account_user').id)],
}])
# create situation where we need to recreate the tax during update to get notification(s) sent
self.tax_template_1.amount += 1
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
# accountants received the message
self.assertEqual(self.env['mail.message'].search_count([
('partner_ids', 'in', advisor_users.partner_id.ids),
('body', 'like', f"%{self.tax_template_1.name}%"), # we look for taxes' name that have been sent in the message's body
]), 1)
# normal user didn't
self.assertEqual(self.env['mail.message'].search_count([
('partner_ids', 'in', normal_user.partner_id.ids),
('body', 'like', f"%{self.tax_template_1.name}%"), # we look for taxes' name that have been sent in the message's body
]), 0)
def test_update_taxes_foreign_taxes(self):
""" When taxes are instantiated through the fiscal position system (in multivat),
its taxes should also be updated.
"""
country_test = self.env['res.country'].create({
'name': 'Country Test',
'code': 'ZZ',
})
chart_template_xmlid_test = 'l10n_test2.test_chart_template_xmlid_2'
chart_template_test = self.env['account.chart.template']._load_records([{
'xml_id': chart_template_xmlid_test,
'values': {
'name': 'Test Chart Template ZZ',
'currency_id': self.env.ref('base.EUR').id,
'bank_account_code_prefix': 1000,
'cash_account_code_prefix': 2000,
'transfer_account_code_prefix': 3000,
'country_id': country_test.id,
}
}])
self._create_tax_template('account.test_tax_test_template', 'Tax name 1 TEST', 10, chart_template_id=chart_template_test.id)
self.env['account.tax.template']._try_instantiating_foreign_taxes(country_test, self.company)
self._create_tax_template('account.test_tax_test_template2', 'Tax name 2 TEST', 15, chart_template_id=chart_template_test.id)
update_taxes_from_templates(self.env.cr, chart_template_xmlid_test)
tax_test_model_data = self.env['ir.model.data'].search([
('name', '=', f'{self.company.id}_test_tax_test_template2'),
('model', '=', 'account.tax'),
])
self.assertEqual(len(tax_test_model_data), 1, "Taxes should have been created even if the chart_template is installed through fiscal position system.")
def test_update_taxes_chart_template_country_check(self):
""" We can't update taxes that don't match the chart_template's country. """
self.company.chart_template_id.country_id = self.env.ref('base.lu')
# Generic chart_template is now (16.0+) in US so we also need to set fiscal country elsewhere for this test to fail as expected
self.company.account_fiscal_country_id = self.env.ref('base.lu')
# We provoke one recreation and one update
self.tax_template_1.amount += 1
self.tax_template_2.invoice_repartition_line_ids.tag_ids.name = 'tag_name_2_modified'
with self.assertRaises(ValidationError):
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
def test_update_taxes_fiscal_country_check(self):
""" If there is no country set on chart_template, the taxes can only be updated if
their country matches the fiscal country. """
self.chart_template.country_id = None
country_lu = self.env.ref('base.lu')
self.company.account_fiscal_country_id = country_lu
self.tax_template_1.amount += 1
self.tax_template_2.invoice_repartition_line_ids.tag_ids.name = 'tag_name_2_modified'
with self.assertRaises(ValidationError):
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
def test_update_taxes_children_tax_ids(self):
""" Ensures children_tax_ids are correctly generated when updating taxes with
amount_type='group'.
"""
# Both parent and its two children should be created.
group_tax_name = 'Group Tax name 1 TEST'
self._create_group_tax_template('account.test_group_tax_test_template', group_tax_name, chart_template_id=self.chart_template.id)
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
parent_tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', '=', group_tax_name),
])
children_taxes = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'like', f'{group_tax_name}_%'),
])
self.assertEqual(len(parent_tax), 1, "The parent tax should have been created.")
self.assertEqual(len(children_taxes), 2, "Two children should have been created.")
self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids, "The parent and its children taxes should be linked together.")
# Parent exists - only the two children should be created.
children_taxes.unlink()
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
children_taxes = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'like', f'{group_tax_name}_%'),
])
self.assertEqual(len(children_taxes), 2, "Two children should be re-created.")
self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids,
"The parent and its children taxes should be linked together.")
# Children exist - only the parent should be created.
parent_tax.unlink()
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
parent_tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', '=', group_tax_name),
])
self.assertEqual(len(parent_tax), 1, "The parent tax should have been re-created.")
self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids,
"The parent and its children taxes should be linked together.")
def test_update_taxes_children_tax_ids_inactive(self):
""" Ensure tax templates are correctly generated when updating taxes with children taxes,
even if templates are inactive.
"""
group_tax_name = 'Group Tax name 1 inactive TEST'
self._create_group_tax_template('account.test_group_tax_test_template_inactive', group_tax_name, chart_template_id=self.chart_template.id, active=False)
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
parent_tax = self.env['account.tax'].with_context(active_test=False).search([
('company_id', '=', self.company.id),
('name', '=', group_tax_name),
])
children_taxes = self.env['account.tax'].with_context(active_test=False).search([
('company_id', '=', self.company.id),
('name', 'like', f'{group_tax_name}_%'),
])
self.assertEqual(len(parent_tax), 1, "The parent tax should have been created, even if it is inactive.")
self.assertFalse(parent_tax.active, "The parent tax should be inactive.")
self.assertEqual(len(children_taxes), 2, "Two children should have been created, even if they are inactive.")
self.assertEqual(children_taxes.mapped('active'), [False] * 2, "Children taxes should be inactive.")

View file

@ -0,0 +1,7 @@
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('external_l10n', '-at_install', 'post_install', '-standard', 'external')
class TestDownloadXsds(TransactionCase):
def test_download_xsds(self):
self.env['ir.attachment'].action_download_xsd_files()

View file

@ -0,0 +1,967 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
from odoo import fields, Command
@tagged('post_install', '-at_install')
class TestAccountEarlyPaymentDiscount(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# Payment Terms
cls.early_pay_10_percents_10_days = cls.env['account.payment.term'].create({
'name': '10% discount if paid within 10 days',
'company_id': cls.company_data['company'].id,
'line_ids': [Command.create({
'value': 'balance',
'days': 0,
'discount_percentage': 10,
'discount_days': 10
})]
})
cls.early_pay_mixed_5_10 = cls.env['account.payment.term'].create({
'name': '5 percent discount on 50% of the amount, 10% on the balance, if payed within 10 days',
'company_id': cls.company_data['company'].id,
'line_ids': [
Command.create({
'value': 'percent',
'value_amount': 50,
'days': 30,
'discount_percentage': 5,
'discount_days': 10
}),
Command.create({
'value': 'balance',
'days': 30,
'discount_percentage': 10,
'discount_days': 10
})],
})
def assert_tax_totals(self, document, expected_values):
main_keys_to_ignore = {
'formatted_amount_total', 'formatted_amount_untaxed', 'display_tax_base', 'subtotals_order'}
group_keys_to_ignore = {'group_key', 'tax_group_id', 'tax_group_name',
'formatted_tax_group_amount', 'formatted_tax_group_base_amount'}
subtotals_keys_to_ignore = {'formatted_amount'}
to_compare = document.copy()
for key in main_keys_to_ignore:
del to_compare[key]
for key in group_keys_to_ignore:
for groups in to_compare['groups_by_subtotal'].values():
for group in groups:
del group[key]
for key in subtotals_keys_to_ignore:
for subtotal in to_compare['subtotals']:
del subtotal[key]
self.assertEqual(to_compare, expected_values)
# ========================== Tests Payment Terms ==========================
def test_early_payment_end_date(self):
inv_1200_10_percents_discount_no_tax = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [Command.create({
'name': 'line', 'price_unit': 1200.0, 'tax_ids': []
})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
for line in inv_1200_10_percents_discount_no_tax.line_ids:
if line.display_type == 'payment_term':
self.assertEqual(
line.discount_date,
fields.Date.from_string('2019-01-11') or False
)
# ========================== Tests Taxes Amounts =============================
def test_fixed_tax_amount_discounted_payment_mixed(self):
self.env.company.early_pay_discount_computation = 'mixed'
fixed_tax = self.env['account.tax'].create({
'name': 'Test 0.05',
'amount_type': 'fixed',
'amount': 0.05,
})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [Command.create({
'name': 'line',
'price_unit': 1000.0,
'tax_ids': [Command.set(self.product_a.taxes_id.ids + fixed_tax.ids)],
})],
'invoice_payment_term_id': self.early_pay_mixed_5_10.id,
})
self.assertInvoiceValues(invoice, [
# pylint: disable=bad-whitespace
{'display_type': 'epd', 'balance': -75.0},
{'display_type': 'epd', 'balance': 75.0},
{'display_type': 'product', 'balance': -1000.0},
{'display_type': 'tax', 'balance': -150},
{'display_type': 'tax', 'balance': 11.25},
{'display_type': 'tax', 'balance': -0.05},
{'display_type': 'payment_term', 'balance': 569.4},
{'display_type': 'payment_term', 'balance': 569.4},
], {
'amount_untaxed': 1000.0,
'amount_tax': 138.8,
'amount_total': 1138.8,
})
# ========================== Tests Payment Register ==========================
def test_register_discounted_payment_on_single_invoice(self):
self.company_data['company'].early_pay_discount_computation = 'included'
out_invoice_1 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 1000.0, 'tax_ids': []})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
out_invoice_1.action_post()
active_ids = out_invoice_1.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2017-01-01',
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -1000.0},
{
'account_id': self.env.company['account_journal_early_pay_discount_loss_account_id'].id,
'amount_currency': 100.0,
},
{'amount_currency': 900.0},
])
def test_register_discounted_payment_on_single_invoice_with_fixed_tax(self):
self.company_data['company'].early_pay_discount_computation = 'included'
fixed_tax = self.env['account.tax'].create({
'name': 'Test 0.05',
'amount_type': 'fixed',
'amount': 0.05,
'type_tax_use': 'purchase',
})
inv = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [Command.create({
'name': 'line',
'price_unit': 1500.0,
'tax_ids': [Command.set(self.product_a.supplier_taxes_id.ids + fixed_tax.ids)]
})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
inv.action_post()
active_ids = inv.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2017-01-01',
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -1552.55, 'tax_tag_invert': False},
{'amount_currency': -150.0, 'tax_tag_invert': True},
{'amount_currency': -22.5, 'tax_tag_invert': True},
{'amount_currency': 1725.05, 'tax_tag_invert': False},
])
def test_register_discounted_payment_on_single_invoice_with_tax(self):
self.company_data['company'].early_pay_discount_computation = 'included'
inv_1500_10_percents_discount_tax_incl_15_percents_tax = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [Command.create({'name': 'line', 'price_unit': 1500.0, 'tax_ids': [Command.set(self.product_a.supplier_taxes_id.ids)]})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
inv_1500_10_percents_discount_tax_incl_15_percents_tax.action_post()
active_ids = inv_1500_10_percents_discount_tax_incl_15_percents_tax.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2017-01-01',
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -1552.5, 'tax_tag_invert': False},
{'amount_currency': -150.0, 'tax_tag_invert': True},
{'amount_currency': -22.5, 'tax_tag_invert': True},
{'amount_currency': 1725.0, 'tax_tag_invert': False},
])
def test_register_discounted_payment_on_single_out_invoice_with_tax(self):
self.env.company.early_pay_discount_computation = 'included'
inv_1500_10_percents_discount_tax_incl_15_percents_tax = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [Command.create({'name': 'line', 'price_unit': 1500.0, 'tax_ids': [Command.set(self.product_a.taxes_id.ids)]})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
inv_1500_10_percents_discount_tax_incl_15_percents_tax.action_post()
active_ids = inv_1500_10_percents_discount_tax_incl_15_percents_tax.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2017-01-01',
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -1725.0, 'tax_tag_invert': False},
{'amount_currency': 22.5, 'tax_tag_invert': False},
{'amount_currency': 150.0, 'tax_tag_invert': False},
{'amount_currency': 1552.5, 'tax_tag_invert': False},
])
def test_register_discounted_payment_multi_line_discount(self):
self.company_data['company'].early_pay_discount_computation = 'included'
inv_mixed_lines_discount_and_no_discount = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({'name': 'line', 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.supplier_taxes_id.ids)]}),
Command.create({'name': 'line', 'price_unit': 2000.0, 'tax_ids': None})
],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
inv_mixed_lines_discount_and_no_discount.action_post()
active_ids = inv_mixed_lines_discount_and_no_discount.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2017-01-01',
'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -2835.0, 'tax_tag_invert': False},
{'amount_currency': -200.0, 'tax_tag_invert': False},
{'amount_currency': -100.0, 'tax_tag_invert': True},
{'amount_currency': -15.0, 'tax_tag_invert': True},
{'amount_currency': 3150.0, 'tax_tag_invert': False},
])
def test_register_discounted_payment_multi_line_multi_discount(self):
self.company_data['company'].early_pay_discount_computation = 'included'
inv_mixed_lines_multi_discount = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({'name': 'line', 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.supplier_taxes_id.ids)]}),
Command.create({'name': 'line', 'price_unit': 2000.0, 'tax_ids': None})
],
'invoice_payment_term_id': self.early_pay_mixed_5_10.id,
})
inv_mixed_lines_multi_discount.action_post()
active_ids = inv_mixed_lines_multi_discount.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move',
active_ids=active_ids).create({
'payment_date': '2019-01-01',
'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -2913.75},
{'amount_currency': -150.0},
{'amount_currency': -75},
{'amount_currency': -11.25},
{'amount_currency': 3150.0},
])
def test_register_discounted_payment_multi_line_multi_discount_tax_excluded(self):
self.company_data['company'].early_pay_discount_computation = 'excluded'
inv_mixed_lines_multi_discount = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({'name': 'line', 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.supplier_taxes_id.ids)]}),
Command.create({'name': 'line', 'price_unit': 2000.0, 'tax_ids': None})
],
'invoice_payment_term_id': self.early_pay_mixed_5_10.id,
})
inv_mixed_lines_multi_discount.action_post()
active_ids = inv_mixed_lines_multi_discount.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move',
active_ids=active_ids).create({
'payment_date': '2019-01-01',
'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -2925.00},
{'amount_currency': -225},
{'amount_currency': 3150.0},
])
def test_register_discounted_payment_multi_line_multi_discount_tax_mixed(self):
self.env.company.early_pay_discount_computation = 'mixed'
inv_mixed_lines_multi_discount = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({'name': 'line', 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.supplier_taxes_id.ids)]}),
Command.create({'name': 'line', 'price_unit': 2000.0, 'tax_ids': None})
],
'invoice_payment_term_id': self.early_pay_mixed_5_10.id,
})
inv_mixed_lines_multi_discount.action_post()
active_ids = inv_mixed_lines_multi_discount.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2019-01-01', 'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -2913.75},
{'amount_currency': -225.0},
{'amount_currency': 3138.75},
])
def test_register_discounted_payment_multi_line_multi_discount_tax_mixed_too_late(self):
self.env.company.early_pay_discount_computation = 'mixed'
inv_mixed_lines_multi_discount = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({'name': 'line', 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.supplier_taxes_id.ids)]}),
Command.create({'name': 'line', 'price_unit': 2000.0, 'tax_ids': None})
],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
inv_mixed_lines_multi_discount.action_post()
active_ids = inv_mixed_lines_multi_discount.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2019-01-31', 'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -3135.00},
{'amount_currency': 3135.00},
])
def test_register_payment_batch_included(self):
self.env.company.early_pay_discount_computation = 'included'
out_invoice_1 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 1000.0, 'tax_ids': []})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
out_invoice_2 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 2000.0, 'tax_ids': []})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
(out_invoice_1 + out_invoice_2).action_post()
active_ids = (out_invoice_1 + out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2019-01-01', 'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -3000.0},
{'amount_currency': 300.0},
{'amount_currency': 2700},
])
def test_register_payment_batch_excluded(self):
self.env.company.early_pay_discount_computation = 'excluded'
out_invoice_1 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.taxes_id.ids)]})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
out_invoice_2 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 2000.0, 'tax_ids': []})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
(out_invoice_1 + out_invoice_2).action_post()
active_ids = (out_invoice_1 + out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2019-01-01', 'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -3150.0},
{'amount_currency': 300.0},
{'amount_currency': 2850},
])
def test_register_payment_batch_mixed(self):
self.env.company.early_pay_discount_computation = 'mixed'
out_invoice_1 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.taxes_id.ids)]})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
out_invoice_2 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 2000.0, 'tax_ids': []})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
(out_invoice_1 + out_invoice_2).action_post()
active_ids = (out_invoice_1 + out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2019-01-01', 'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -3135.0},
{'amount_currency': 300.0},
{'amount_currency': 2835.0},
])
def test_register_payment_batch_mixed_one_too_late(self):
self.env.company.early_pay_discount_computation = 'mixed'
out_invoice_1 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 1000.0, 'tax_ids': [Command.set(self.product_a.taxes_id.ids)]})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
out_invoice_2 = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2019-01-01',
'invoice_date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 2000.0, 'tax_ids': []})],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
(out_invoice_1 + out_invoice_2).action_post()
active_ids = (out_invoice_1 + out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2019-01-01', 'group_payment': True
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -3135.0},
{'amount_currency': 200.0},
{'amount_currency': 2935.0},
])
def test_mixed_epd_with_draft_invoice(self):
self.env.company.early_pay_discount_computation = 'mixed'
tax = self.env['account.tax'].create({
'name': 'WonderTax',
'amount': 10,
})
with Form(self.env['account.move'].with_context(default_move_type='out_invoice')) as invoice:
invoice.partner_id = self.partner_a
invoice.invoice_date = fields.Date.from_string('2022-02-21')
invoice.invoice_payment_term_id = self.early_pay_10_percents_10_days
with invoice.invoice_line_ids.new() as line_form:
line_form.product_id = self.product_a
line_form.price_unit = 1000
line_form.quantity = 1
line_form.tax_ids.clear()
line_form.tax_ids.add(tax)
self.assert_tax_totals(invoice._values['tax_totals'], {
'amount_untaxed': 1000,
'amount_total': 1090,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_amount': 90,
'tax_group_base_amount': 900,
'hide_base_amount': False,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 1000,
}
],
})
def test_intracomm_bill_with_early_payment_included(self):
self.env.company.early_pay_discount_computation = 'included'
tax_tags = self.env['account.account.tag'].create({
'name': f'tax_tag_{i}',
'applicability': 'taxes',
'country_id': self.env.company.account_fiscal_country_id.id,
} for i in range(6))
intracomm_tax = self.env['account.tax'].create({
'name': 'tax20',
'amount_type': 'percent',
'amount': 20,
'type_tax_use': 'purchase',
'invoice_repartition_line_ids': [
# pylint: disable=bad-whitespace
Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[0].ids)]}),
Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[1].ids)]}),
Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[2].ids)]}),
],
'refund_repartition_line_ids': [
# pylint: disable=bad-whitespace
Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[3].ids)]}),
Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[4].ids)]}),
Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[5].ids)]}),
],
})
early_payment_term = self.env['account.payment.term'].create({
'name': "early_payment_term",
'company_id': self.company_data['company'].id,
'line_ids': [
Command.create({
'value': 'balance',
'days': 30,
'discount_percentage': 2,
'discount_days': 7,
}),
],
})
bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_payment_term_id': early_payment_term.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({
'name': 'line',
'price_unit': 1000.0,
'tax_ids': [Command.set(intracomm_tax.ids)],
}),
],
})
bill.action_post()
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=bill.ids)\
.create({'payment_date': '2019-01-01'})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# pylint: disable=bad-whitespace
{'amount_currency': -980.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False},
{'amount_currency': -20.0, 'tax_ids': intracomm_tax.ids, 'tax_tag_ids': tax_tags[3].ids, 'tax_tag_invert': True},
{'amount_currency': -4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[4].ids, 'tax_tag_invert': True},
{'amount_currency': 4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[5].ids, 'tax_tag_invert': True},
{'amount_currency': 1000.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False},
])
def test_mixed_early_discount_with_tag_on_tax_base_line(self):
"""
Ensure that early payment discount line grouping works properly when
using a tax that adds tax tags to its base line.
"""
self.env.company.early_pay_discount_computation = 'mixed'
tax_tag = self.env['account.account.tag'].create({
'name': 'tax_tag',
'applicability': 'taxes',
'country_id': self.env.company.account_fiscal_country_id.id,
})
tax_21 = self.env['account.tax'].create({
'name': "tax_21",
'amount': 21,
'invoice_repartition_line_ids': [
Command.create({
'factor_percent': 100,
'repartition_type': 'base',
'tag_ids': [Command.set(tax_tag.ids)],
}),
Command.create({
'factor_percent': 100, 'repartition_type': 'tax',
}),
],
'refund_repartition_line_ids': [
Command.create({
'factor_percent': 100, 'repartition_type': 'base',
}),
Command.create({
'factor_percent': 100, 'repartition_type': 'tax',
}),
],
})
bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
bill.write({
'invoice_line_ids': [
Command.create({
'name': 'line1',
'price_unit': 1000.0,
'tax_ids': [Command.set(tax_21.ids)],
}),
],
})
bill.write({
'invoice_line_ids': [Command.create({
'name': 'line2',
'price_unit': 1000.0,
'tax_ids': [Command.set(tax_21.ids)],
})],
})
epd_lines = bill.line_ids.filtered(lambda line: line.display_type == 'epd')
self.assertRecordValues(epd_lines.sorted('balance'), [
{'balance': -200.0},
{'balance': 200.0},
])
def test_mixed_epd_with_tax_included(self):
self.company_data['company'].early_pay_discount_computation = 'mixed'
early_pay_2_percents_10_days = self.env['account.payment.term'].create({
'name': '2% discount if paid within 10 days',
'company_id': self.company_data['company'].id,
'line_ids': [Command.create({
'value': 'balance',
'days': 0,
'discount_percentage': 2,
'discount_days': 10
})]
})
tax = self.env['account.tax'].create({
'name': 'Tax 21% included',
'amount': 21,
'price_include': True,
})
with Form(self.env['account.move'].with_context(default_move_type='out_invoice')) as invoice:
invoice.partner_id = self.partner_a
invoice.invoice_date = fields.Date.from_string('2022-02-21')
invoice.invoice_payment_term_id = early_pay_2_percents_10_days
with invoice.invoice_line_ids.new() as line_form:
line_form.product_id = self.product_a
line_form.price_unit = 121
line_form.quantity = 1
line_form.tax_ids.clear()
line_form.tax_ids.add(tax)
self.assert_tax_totals(invoice._values['tax_totals'], {
'amount_untaxed': 100,
'amount_total': 120.58,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_amount': 20.58,
'tax_group_base_amount': 98,
'hide_base_amount': False,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 100,
}
],
})
def test_mixed_epd_with_tax_no_duplication(self):
self.env.company.early_pay_discount_computation = 'mixed'
inv = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({'name': 'line', 'price_unit': 100.0, 'tax_ids': [Command.set(self.product_a.taxes_id.ids)]}),
],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
self.assertEqual(len(inv.line_ids), 6) # 1 prod, 1 tax, 2 epd, 1 epd tax discount, 1 payment terms
inv.write({'invoice_payment_term_id': self.pay_terms_a.id})
self.assertEqual(len(inv.line_ids), 3) # 1 prod, 1 tax, 1 payment terms
inv.write({'invoice_payment_term_id': self.early_pay_10_percents_10_days.id})
self.assertEqual(len(inv.line_ids), 6)
def test_mixed_epd_with_tax_deleted_line(self):
self.env.company.early_pay_discount_computation = 'mixed'
tax_a = self.env['account.tax'].create({
'name': 'Test A',
'amount': 10,
})
tax_b = self.env['account.tax'].create({
'name': 'Test B',
'amount': 15,
})
inv = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [
Command.create({'name': 'line', 'price_unit': 100.0, 'tax_ids': [Command.set(tax_a.ids)]}),
Command.create({'name': 'line2', 'price_unit': 100.0, 'tax_ids': [Command.set(tax_b.ids)]}),
],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
self.assertEqual(len(inv.line_ids), 10) # 2 prod, 2 tax, 3 epd, 2 epd tax discount, 1 payment terms
inv.invoice_line_ids[1].unlink()
self.assertEqual(len(inv.line_ids), 6) # 1 prod, 1 tax, 2 epd, 1 epd tax discount, 1 payment terms
self.assertEqual(inv.amount_tax, 9.00) # $100.0 @ 10% tax (-10% epd)
def test_mixed_epd_with_rounding_issue(self):
"""
Ensure epd line will not unbalance the invoice
"""
self.env.company.early_pay_discount_computation = 'mixed'
tax_6 = self.env['account.tax'].create({
'name': '6%',
'amount': 6,
})
tax_12 = self.env['account.tax'].create({
'name': '12%',
'amount': 12,
})
tax_136 = self.env['account.tax'].create({
'name': '136',
'amount': 0.136,
'amount_type': 'fixed',
'include_base_amount': True,
})
tax_176 = self.env['account.tax'].create({
'name': '176',
'amount': 0.176,
'amount_type': 'fixed',
'include_base_amount': True,
})
early_pay_1_percents_7_days = self.env['account.payment.term'].create({
'name': '1% discount if paid within 7 days',
'company_id': self.company_data['company'].id,
'line_ids': [Command.create({
'value': 'balance',
'days': 0,
'discount_percentage': 1,
'discount_days': 7,
})]
})
# The following vals will create a rounding issue
line_create_vals = [
(116, 6, tax_6),
(0.91, 350, tax_6),
(194.21, 1, tax_136 | tax_12),
(31.46, 5, tax_176 | tax_12)
]
# If invoice is not balanced the following create will fail
self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2022-02-21',
'invoice_payment_term_id': early_pay_1_percents_7_days.id,
'invoice_line_ids': [
Command.create({
'price_unit': price_unit,
'quantity': quantity,
'tax_ids': [Command.set(taxes.ids)]
}) for price_unit, quantity, taxes in line_create_vals
]
})
def test_mixed_epd_with_tax_refund(self):
"""
Ensure epd line are addeed to refunds
"""
self.env.company.early_pay_discount_computation = 'mixed'
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2022-02-21',
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
'invoice_line_ids': [
Command.create({
'price_unit': 100.0,
'quantity': 1,
'tax_ids': [Command.set(self.product_a.taxes_id.ids)],
})
]
})
invoice.action_post()
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({
'date': fields.Date.from_string('2017-01-01'),
'reason': 'no reason again',
'refund_method': 'cancel',
'journal_id': invoice.journal_id.id,
})
reversal = move_reversal.reverse_moves()
reverse_move = self.env['account.move'].browse(reversal['res_id'])
receivable_line = invoice.line_ids.filtered(lambda x: x.account_id.account_type == 'asset_receivable')
self.assertTrue(receivable_line.full_reconcile_id, "Invoice should be fully reconciled with the credit note")
self.assertRecordValues(reverse_move.line_ids.sorted('id'), [
{
'balance': 100.0,
'tax_base_amount': 0.0,
'display_type': 'product',
},
{
'balance': -10.0,
'tax_base_amount': 0.0,
'display_type': 'epd',
},
{
'balance': 10.0,
'tax_base_amount': 0.0,
'display_type': 'epd',
},
{
'balance': 15.0,
'tax_base_amount': 100.0,
'display_type': 'tax',
},
{
'balance': -1.5,
'tax_base_amount': -10.0,
'display_type': 'tax',
},
{
'balance': -receivable_line.balance,
'tax_base_amount': 0.0,
'display_type': 'payment_term',
},
])
def test_epd_entry_tag_invert_with_distinct_negative_invoice_line(self):
"""
`tax_tag_invert` should be the same for all Early Payment Discount lines of a single entry
"""
analytic_plan = self.env['account.analytic.plan'].create({
'name': 'existential plan',
})
analytic_account_a = self.env['account.analytic.account'].create({
'name': 'positive_account',
'plan_id': analytic_plan.id,
})
analytic_account_b = self.env['account.analytic.account'].create({
'name': 'negative_account',
'plan_id': analytic_plan.id,
})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-10',
'date': '2019-01-10',
'invoice_line_ids': [
Command.create({
'name': 'line',
'price_unit': 2000,
'tax_ids': self.tax_sale_a,
'analytic_distribution': {str(analytic_account_a.id): 100},
}),
Command.create({
'name': 'line',
'price_unit': -1500,
'tax_ids': self.tax_sale_a,
'analytic_distribution': {str(analytic_account_b.id): 100},
}),
],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
invoice.action_post()
bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_b.id,
'invoice_date': '2019-01-10',
'date': '2019-01-10',
'invoice_line_ids': [
Command.create({
'name': 'line',
'price_unit': 3000,
'tax_ids': self.tax_purchase_a,
'analytic_distribution': {str(analytic_account_a.id): 100},
}),
Command.create({
'name': 'line',
'price_unit': -2250,
'tax_ids': self.tax_purchase_a,
'analytic_distribution': {str(analytic_account_b.id): 100},
}),
],
'invoice_payment_term_id': self.early_pay_10_percents_10_days.id,
})
bill.action_post()
payments = self.env['account.payment.register'].with_context(
active_model='account.move',
active_ids=invoice.ids,
).create({
'payment_date': '2019-01-01',
})._create_payments()
for line in payments.line_ids.filtered(lambda line: line.tax_repartition_line_id or line.tax_ids):
self.assertFalse(line.tax_tag_invert)
payments = self.env['account.payment.register'].with_context(
active_model='account.move',
active_ids=bill.ids,
).create({
'payment_date': '2019-01-01',
})._create_payments()
for line in payments.line_ids.filtered(lambda line: line.tax_repartition_line_id or line.tax_ids):
self.assertTrue(line.tax_tag_invert)

View file

@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
from odoo.exceptions import ValidationError
class TestFiscalPosition(common.TransactionCase):
"""Tests for fiscal positions in auto apply (account.fiscal.position).
If a partner has a vat number, the fiscal positions with "vat_required=True"
are preferred.
"""
@classmethod
def setUpClass(cls):
super(TestFiscalPosition, cls).setUpClass()
cls.fp = cls.env['account.fiscal.position']
# reset any existing FP
cls.fp.search([]).write({'auto_apply': False})
cls.res_partner = cls.env['res.partner']
cls.be = be = cls.env.ref('base.be')
cls.fr = fr = cls.env.ref('base.fr')
cls.mx = mx = cls.env.ref('base.mx')
cls.eu = cls.env.ref('base.europe')
cls.nl = cls.env.ref('base.nl')
cls.us = cls.env.ref('base.us')
cls.state_fr = cls.env['res.country.state'].create(dict(
name="State",
code="ST",
country_id=fr.id))
cls.jc = cls.res_partner.create(dict(
name="JCVD",
vat="BE0477472701",
country_id=be.id))
cls.ben = cls.res_partner.create(dict(
name="BP",
country_id=be.id))
cls.george = cls.res_partner.create(dict(
name="George",
vat="BE0477472701",
country_id=fr.id))
cls.alberto = cls.res_partner.create(dict(
name="Alberto",
vat="BE0477472701",
country_id=mx.id))
cls.be_nat = cls.fp.create(dict(
name="BE-NAT",
auto_apply=True,
country_id=be.id,
vat_required=False,
sequence=10))
cls.fr_b2c = cls.fp.create(dict(
name="EU-VAT-FR-B2C",
auto_apply=True,
country_id=fr.id,
vat_required=False,
sequence=40))
cls.fr_b2b = cls.fp.create(dict(
name="EU-VAT-FR-B2B",
auto_apply=True,
country_id=fr.id,
vat_required=True,
sequence=50))
def test_10_fp_country(self):
def assert_fp(partner, expected_pos, message):
self.assertEqual(
self.fp._get_fiscal_position(partner).id,
expected_pos.id,
message)
george, jc, ben, alberto = self.george, self.jc, self.ben, self.alberto
# B2B has precedence over B2C for same country even when sequence gives lower precedence
self.assertGreater(self.fr_b2b.sequence, self.fr_b2c.sequence)
assert_fp(george, self.fr_b2b, "FR-B2B should have precedence over FR-B2C")
self.fr_b2b.auto_apply = False
assert_fp(george, self.fr_b2c, "FR-B2C should match now")
self.fr_b2b.auto_apply = True
# Create positions matching on Country Group and on NO country at all
self.eu_intra_b2b = self.fp.create(dict(
name="EU-INTRA B2B",
auto_apply=True,
country_group_id=self.eu.id,
vat_required=True,
sequence=20))
self.world = self.fp.create(dict(
name="WORLD-EXTRA",
auto_apply=True,
vat_required=False,
sequence=30))
# Country match has higher precedence than group match or sequence
self.assertGreater(self.fr_b2b.sequence, self.eu_intra_b2b.sequence)
assert_fp(george, self.fr_b2b, "FR-B2B should have precedence over EU-INTRA B2B")
# B2B has precedence regardless of country or group match
self.assertGreater(self.eu_intra_b2b.sequence, self.be_nat.sequence)
assert_fp(jc, self.eu_intra_b2b, "EU-INTRA B2B should match before BE-NAT")
# Lower sequence = higher precedence if country/group and VAT matches
self.assertFalse(ben.vat) # No VAT set
assert_fp(ben, self.be_nat, "BE-NAT should match before EU-INTRA due to lower sequence")
# Remove BE from EU group, now BE-NAT should be the fallback match before the wildcard WORLD
self.be.write({'country_group_ids': [(3, self.eu.id)]})
self.assertTrue(jc.vat) # VAT set
assert_fp(jc, self.be_nat, "BE-NAT should match as fallback even w/o VAT match")
# No country = wildcard match only if nothing else matches
self.assertTrue(alberto.vat) # with VAT
assert_fp(alberto, self.world, "WORLD-EXTRA should match anything else (1)")
alberto.vat = False # or without
assert_fp(alberto, self.world, "WORLD-EXTRA should match anything else (2)")
# Zip range
self.fr_b2b_zip100 = self.fr_b2b.copy(dict(zip_from=0, zip_to=5000, sequence=60))
george.zip = 6000
assert_fp(george, self.fr_b2b, "FR-B2B with wrong zip range should not match")
george.zip = 3000
assert_fp(george, self.fr_b2b_zip100, "FR-B2B with zip range should have precedence")
# States
self.fr_b2b_state = self.fr_b2b.copy(dict(state_ids=[(4, self.state_fr.id)], sequence=70))
george.state_id = self.state_fr
assert_fp(george, self.fr_b2b_zip100, "FR-B2B with zip should have precedence over states")
george.zip = False
assert_fp(george, self.fr_b2b_state, "FR-B2B with states should have precedence")
# Dedicated position has max precedence
george.property_account_position_id = self.be_nat
assert_fp(george, self.be_nat, "Forced position has max precedence")
def test_20_fp_one_tax_2m(self):
self.env.company.country_id = self.env.ref('base.us')
self.src_tax = self.env['account.tax'].create({'name': "SRC", 'amount': 0.0})
self.dst1_tax = self.env['account.tax'].create({'name': "DST1", 'amount': 0.0})
self.dst2_tax = self.env['account.tax'].create({'name': "DST2", 'amount': 0.0})
self.fp2m = self.fp.create({
'name': "FP-TAX2TAXES",
'tax_ids': [
(0,0,{
'tax_src_id': self.src_tax.id,
'tax_dest_id': self.dst1_tax.id
}),
(0,0,{
'tax_src_id': self.src_tax.id,
'tax_dest_id': self.dst2_tax.id
})
]
})
mapped_taxes = self.fp2m.map_tax(self.src_tax)
self.assertEqual(mapped_taxes, self.dst1_tax | self.dst2_tax)
def test_30_fp_delivery_address(self):
# Make sure the billing company is from Belgium (within the EU)
self.env.company.vat = 'BE0477472701'
self.env.company.country_id = self.be
# Reset any existing FP
self.env['account.fiscal.position'].search([]).auto_apply = False
# Create the fiscal positions
fp_be_nat = self.env['account.fiscal.position'].create({
'name': 'Régime National',
'auto_apply': True,
'country_id': self.be.id,
'vat_required': True,
'sequence': 10,
})
fp_eu_priv = self.env['account.fiscal.position'].create({
'name': 'EU privé',
'auto_apply': True,
'country_group_id': self.eu.id,
'vat_required': False,
'sequence': 20,
})
fp_eu_intra = self.env['account.fiscal.position'].create({
'name': 'Régime Intra-Communautaire',
'auto_apply': True,
'country_group_id': self.eu.id,
'vat_required': True,
'sequence': 30,
})
fp_eu_extra = self.env['account.fiscal.position'].create({
'name': 'Régime Extra-Communautaire',
'auto_apply': True,
'vat_required': False,
'sequence': 40,
})
# Create the partners
partner_be_vat = self.env['res.partner'].create({
'name': 'BE VAT',
'vat': 'BE0477472701',
'country_id': self.be.id,
})
partner_nl_vat = self.env['res.partner'].create({
'name': 'NL VAT',
'vat': 'NL123456782B90',
'country_id': self.nl.id,
})
partner_nl_no_vat = self.env['res.partner'].create({
'name': 'NL NO VAT',
'country_id': self.nl.id,
})
partner_us_no_vat = self.env['res.partner'].create({
'name': 'US NO VAT',
'country_id': self.us.id,
})
# Case : 1
# Billing (VAT/country) : BE/BE
# Delivery (VAT/country) : NL/NL
# Expected FP : Régime National
self.assertEqual(
self.env['account.fiscal.position']._get_fiscal_position(partner_be_vat, partner_nl_vat),
fp_be_nat
)
# Case : 2
# Billing (VAT/country) : NL/NL
# Delivery (VAT/country) : BE/BE
# Expected FP : Régime National
self.assertEqual(
self.env['account.fiscal.position']._get_fiscal_position(partner_nl_vat, partner_be_vat),
fp_be_nat
)
# Case : 3
# Billing (VAT/country) : BE/BE
# Delivery (VAT/country) : None/NL
# Expected FP : Régime National
self.assertEqual(
self.env['account.fiscal.position']._get_fiscal_position(partner_be_vat, partner_nl_no_vat),
fp_be_nat
)
# Case : 4
# Billing (VAT/country) : NL/NL
# Delivery (VAT/country) : NL/NL
# Expected FP : Régime Intra-Communautaire
self.assertEqual(
self.env['account.fiscal.position']._get_fiscal_position(partner_nl_vat, partner_nl_vat),
fp_eu_intra
)
# Case : 5
# Billing (VAT/country) : None/NL
# Delivery (VAT/country) : None/NL
# Expected FP : EU privé
self.assertEqual(
self.env['account.fiscal.position']._get_fiscal_position(partner_nl_no_vat, partner_nl_no_vat),
fp_eu_priv
)
# Case : 6
# Billing (VAT/country) : None/US
# Delivery (VAT/country) : None/US
# Expected FP : Régime Extra-Communautaire
self.assertEqual(
self.env['account.fiscal.position']._get_fiscal_position(partner_us_no_vat, partner_us_no_vat),
fp_eu_extra
)
def test_fiscal_position_constraint(self):
"""
Test fiscal position constraint by updating the record
- with only zip_from value
- with only zip_to value
- with both zip_from and zip_to values
"""
fiscal_position = self.fp.create({
'name': 'Test fiscal',
'auto_apply': True,
'country_id': self.be.id,
'vat_required': True,
'sequence': 10,
})
with self.assertRaises(ValidationError):
fiscal_position.write({
'zip_from': '123',
})
with self.assertRaises(ValidationError):
fiscal_position.write({
'zip_to': '456',
})
fiscal_position.write({
'zip_from': '123',
'zip_to': '456',
})
self.assertRecordValues(fiscal_position, [{
'name': 'Test fiscal',
'auto_apply': True,
'country_id': self.be.id,
'zip_from': '123',
'zip_to': '456',
}])

View file

@ -0,0 +1,920 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.fields import Command
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestTaxTotals(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.tax_group1 = cls.env['account.tax.group'].create({
'name': '1',
'sequence': 1
})
cls.tax_group2 = cls.env['account.tax.group'].create({
'name': '2',
'sequence': 2
})
cls.tax_group_sub1 = cls.env['account.tax.group'].create({
'name': 'subtotals 1',
'preceding_subtotal': "PRE GROUP 1",
'sequence': 3
})
cls.tax_group_sub2 = cls.env['account.tax.group'].create({
'name': 'subtotals 2',
'preceding_subtotal': "PRE GROUP 2",
'sequence': 4
})
cls.tax_group_sub3 = cls.env['account.tax.group'].create({
'name': 'subtotals 3',
'preceding_subtotal': "PRE GROUP 1", # same as sub1, on purpose
'sequence': 5
})
cls.tax_10 = cls.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
})
cls.tax_16 = cls.env['account.tax'].create({
'name': "tax_16",
'amount_type': 'percent',
'amount': 16.0,
})
cls.tax_53 = cls.env['account.tax'].create({
'name': "tax_53",
'amount_type': 'percent',
'amount': 53.0,
})
cls.tax_17a = cls.env['account.tax'].create({
'name': "tax_17a",
'amount_type': 'percent',
'amount': 17.0,
})
cls.tax_17b = cls.tax_17a.copy({'name': "tax_17b"})
def assertTaxTotals(self, document, expected_values):
main_keys_to_ignore = {'formatted_amount_total', 'formatted_amount_untaxed'}
group_keys_to_ignore = {'group_key', 'formatted_tax_group_amount', 'formatted_tax_group_base_amount', 'hide_base_amount'}
subtotals_keys_to_ignore = {'formatted_amount'}
to_compare = document.tax_totals
for key in main_keys_to_ignore:
del to_compare[key]
for key in group_keys_to_ignore:
for groups in to_compare['groups_by_subtotal'].values():
for group in groups:
del group[key]
for key in subtotals_keys_to_ignore:
for subtotal in to_compare['subtotals']:
del subtotal[key]
self.assertEqual(to_compare, expected_values)
def _create_document_for_tax_totals_test(self, lines_data):
""" Creates and returns a new record of a model defining a tax_totals
field and using the related widget.
By default, this function creates an invoice, but it is overridden in sale
and purchase to create respectively a sale.order or a purchase.order. This way,
we can test the invoice_tax_totals from both these models in the same way as
account.move's.
:param lines_data: a list of tuple (amount, taxes), where amount is a base amount,
and taxes a recordset of account.tax objects corresponding
to the taxes to apply on this amount. Each element of the list
corresponds to a line of the document (invoice line, PO line, SO line).
"""
invoice_lines_vals = [
(0, 0, {
'name': 'line',
'display_type': 'product',
'account_id': self.company_data['default_account_revenue'].id,
'price_unit': amount,
'tax_ids': [(6, 0, taxes.ids)],
})
for amount, taxes in lines_data]
return self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'invoice_line_ids': invoice_lines_vals,
})
def test_multiple_tax_lines(self):
tax_10 = self.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
'tax_group_id': self.tax_group1.id,
})
tax_20 = self.env['account.tax'].create({
'name': "tax_20",
'amount_type': 'percent',
'amount': 20.0,
'tax_group_id': self.tax_group2.id,
})
document = self._create_document_for_tax_totals_test([
(1000, tax_10 + tax_20),
(1000, tax_10),
(1000, tax_20),
])
self.assertTaxTotals(document, {
'amount_total': 3600,
'amount_untaxed': 3000,
'display_tax_base': True,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 200,
'tax_group_base_amount': 2000,
'tax_group_id': self.tax_group1.id,
},
{
'tax_group_name': self.tax_group2.name,
'tax_group_amount': 400,
'tax_group_base_amount': 2000,
'tax_group_id': self.tax_group2.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 3000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
# Same but both are sharing the same tax group.
tax_20.tax_group_id = self.tax_group1
document.invalidate_model(['tax_totals'])
self.assertTaxTotals(document, {
'amount_total': 3600,
'amount_untaxed': 3000,
'display_tax_base': False,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 600,
'tax_group_base_amount': 3000,
'tax_group_id': self.tax_group1.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 3000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
def test_zero_tax_lines(self):
tax_0 = self.env['account.tax'].create({
'name': "tax_0",
'amount_type': 'percent',
'amount': 0.0,
})
document = self._create_document_for_tax_totals_test([
(1000, tax_0),
])
self.assertTaxTotals(document, {
'amount_total': 1000,
'amount_untaxed': 1000,
'display_tax_base': False,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': tax_0.tax_group_id.name,
'tax_group_amount': 0,
'tax_group_base_amount': 1000,
'tax_group_id': tax_0.tax_group_id.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 1000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
def test_tax_affect_base_1(self):
tax_10 = self.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
'tax_group_id': self.tax_group1.id,
'price_include': True,
'include_base_amount': True,
})
tax_20 = self.env['account.tax'].create({
'name': "tax_20",
'amount_type': 'percent',
'amount': 20.0,
'tax_group_id': self.tax_group2.id,
})
document = self._create_document_for_tax_totals_test([
(1100, tax_10 + tax_20),
(1100, tax_10),
(1000, tax_20),
])
self.assertTaxTotals(document, {
'amount_total': 3620,
'amount_untaxed': 3000,
'display_tax_base': True,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 200,
'tax_group_base_amount': 2000,
'tax_group_id': self.tax_group1.id,
},
{
'tax_group_name': self.tax_group2.name,
'tax_group_amount': 420,
'tax_group_base_amount': 2100,
'tax_group_id': self.tax_group2.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 3000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
# Same but both are sharing the same tax group.
tax_20.tax_group_id = self.tax_group1
document.invalidate_model(['tax_totals'])
self.assertTaxTotals(document, {
'amount_total': 3620,
'amount_untaxed': 3000,
'display_tax_base': False,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 620,
'tax_group_base_amount': 3000,
'tax_group_id': self.tax_group1.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 3000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
def test_tax_affect_base_2(self):
tax_10 = self.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
'tax_group_id': self.tax_group1.id,
'include_base_amount': True,
'sequence': 2,
})
tax_20 = self.env['account.tax'].create({
'name': "tax_20",
'amount_type': 'percent',
'amount': 20.0,
'tax_group_id': self.tax_group1.id,
'sequence': 2,
})
tax_30 = self.env['account.tax'].create({
'name': "tax_30",
'amount_type': 'percent',
'amount': 30.0,
'tax_group_id': self.tax_group2.id,
'include_base_amount': True,
'sequence': 1,
})
document = self._create_document_for_tax_totals_test([
(1000, tax_10 + tax_20),
(1000, tax_30 + tax_10),
])
self.assertTaxTotals(document, {
'amount_total': 2750,
'amount_untaxed': 2000,
'display_tax_base': True,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 450,
'tax_group_base_amount': 2300,
'tax_group_id': self.tax_group1.id,
},
{
'tax_group_name': self.tax_group2.name,
'tax_group_amount': 300,
'tax_group_base_amount': 1000,
'tax_group_id': self.tax_group2.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 2000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
# Same but both are sharing the same tax group.
tax_30.tax_group_id = self.tax_group1
document.invalidate_model(['tax_totals'])
self.assertTaxTotals(document, {
'amount_total': 2750,
'amount_untaxed': 2000,
'display_tax_base': False,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 750,
'tax_group_base_amount': 2000,
'tax_group_id': self.tax_group1.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 2000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
def test_subtotals_basic(self):
tax_10 = self.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
'tax_group_id': self.tax_group_sub1.id,
})
tax_25 = self.env['account.tax'].create({
'name': "tax_25",
'amount_type': 'percent',
'amount': 25.0,
'tax_group_id': self.tax_group_sub2.id,
})
tax_42 = self.env['account.tax'].create({
'name': "tax_42",
'amount_type': 'percent',
'amount': 42.0,
'tax_group_id': self.tax_group1.id,
})
document = self._create_document_for_tax_totals_test([
(1000, tax_10),
(1000, tax_25),
(100, tax_42),
(200, tax_42 + tax_10 + tax_25),
])
self.assertTaxTotals(document, {
'amount_total': 2846,
'amount_untaxed': 2300,
'display_tax_base': True,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 126,
'tax_group_base_amount': 300,
'tax_group_id': self.tax_group1.id,
},
],
'PRE GROUP 1': [
{
'tax_group_name': self.tax_group_sub1.name,
'tax_group_amount': 120,
'tax_group_base_amount': 1200,
'tax_group_id': self.tax_group_sub1.id,
},
],
'PRE GROUP 2': [
{
'tax_group_name': self.tax_group_sub2.name,
'tax_group_amount': 300,
'tax_group_base_amount': 1200,
'tax_group_id': self.tax_group_sub2.id,
},
]
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 2300,
},
{
'name': "PRE GROUP 1",
'amount': 2426,
},
{
'name': "PRE GROUP 2",
'amount': 2546,
},
],
'subtotals_order': ["Untaxed Amount", "PRE GROUP 1", "PRE GROUP 2"],
})
def test_after_total_mix(self):
tax_10 = self.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
'tax_group_id': self.tax_group_sub3.id,
})
tax_25 = self.env['account.tax'].create({
'name': "tax_25",
'amount_type': 'percent',
'amount': -25.0,
'tax_group_id': self.tax_group_sub2.id,
})
tax_42 = self.env['account.tax'].create({
'name': "tax_42",
'amount_type': 'percent',
'amount': 42.0,
'tax_group_id': self.tax_group_sub1.id,
})
tax_30 = self.env['account.tax'].create({
'name': "tax_30",
'amount_type': 'percent',
'amount': 30.0,
'tax_group_id': self.tax_group1.id,
})
document = self._create_document_for_tax_totals_test([
(100, tax_10),
(100, tax_25 + tax_42 + tax_30),
(200, tax_10 + tax_25),
(1000, tax_30),
(100, tax_30 + tax_10)
])
self.assertTaxTotals(document, {
'amount_total': 1867,
'amount_untaxed': 1500,
'display_tax_base': True,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 360,
'tax_group_base_amount': 1200,
'tax_group_id': self.tax_group1.id,
},
],
'PRE GROUP 1': [
{
'tax_group_name': self.tax_group_sub1.name,
'tax_group_amount': 42,
'tax_group_base_amount': 100,
'tax_group_id': self.tax_group_sub1.id,
},
{
'tax_group_name': self.tax_group_sub3.name,
'tax_group_amount': 40,
'tax_group_base_amount': 400,
'tax_group_id': self.tax_group_sub3.id,
},
],
'PRE GROUP 2': [
{
'tax_group_name': self.tax_group_sub2.name,
'tax_group_amount': -75,
'tax_group_base_amount': 300,
'tax_group_id': self.tax_group_sub2.id,
},
],
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 1500,
},
{
'name': "PRE GROUP 1",
'amount': 1860,
},
{
'name': "PRE GROUP 2",
'amount': 1942,
},
],
'subtotals_order': ["Untaxed Amount", "PRE GROUP 1", "PRE GROUP 2"],
})
def test_discounted_tax(self):
tax_21_exempted = self.env['account.tax'].create({
'name': "tax_21_exempted",
'amount_type': 'group',
'amount': 2.0,
'tax_group_id': self.tax_group1.id,
'children_tax_ids': [
Command.create({
'name': "tax_exempt",
'amount_type': 'percent',
'amount': -2.0,
'include_base_amount': True,
'tax_group_id': self.tax_group_sub1.id,
'sequence': 1,
}),
Command.create({
'name': "tax_21",
'amount_type': 'percent',
'amount': 21.0,
'tax_group_id': self.tax_group_sub2.id,
'sequence': 2,
}),
Command.create({
'name': "tax_reapply",
'amount_type': 'percent',
'amount': 2.0,
'is_base_affected': False,
'tax_group_id': self.tax_group_sub3.id,
'sequence': 3,
}),
]
})
self.tax_group_sub1.preceding_subtotal = "Tax exemption"
self.tax_group_sub2.preceding_subtotal = "Tax application"
self.tax_group_sub3.preceding_subtotal = "Reapply amount"
document = self._create_document_for_tax_totals_test([
(1000 / 0.98, tax_21_exempted),
])
self.assertTaxTotals(document, {
'amount_total': 1230.41,
'amount_untaxed': 1020.41,
'display_tax_base': True,
'groups_by_subtotal': {
"Reapply amount": [{
'tax_group_name': self.tax_group_sub3.name,
'tax_group_amount': 20.41,
'tax_group_base_amount': 1020.41,
'tax_group_id': self.tax_group_sub3.id,
}],
"Tax application": [{
'tax_group_name': self.tax_group_sub2.name,
'tax_group_amount': 210.0,
'tax_group_base_amount': 1000.0,
'tax_group_id': self.tax_group_sub2.id,
}],
"Tax exemption": [{
'tax_group_name': self.tax_group_sub1.name,
'tax_group_amount': -20.41,
'tax_group_base_amount': 1020.41,
'tax_group_id': self.tax_group_sub1.id,
}],
},
'subtotals': [{
'name': "Tax exemption",
'amount': 1020.41,
}, {
'name': "Tax application",
'amount': 1000.00,
}, {
'name': "Reapply amount",
'amount': 1210.00,
}],
'subtotals_order': ["Tax exemption", "Tax application", "Reapply amount"],
})
def test_invoice_grouped_taxes_with_tax_group(self):
""" A tax of type group with a tax_group_id being the same as one of the children tax shouldn't affect the
result of the _prepare_tax_totals.
"""
tax_10_withheld = self.env['account.tax'].create({
'name': "tax_10_withheld",
'amount_type': 'group',
'tax_group_id': self.tax_group1.id,
'children_tax_ids': [
Command.create({
'name': "tax_withheld",
'amount_type': 'percent',
'amount': -47,
'tax_group_id': self.tax_group_sub1.id,
'sequence': 1,
}),
Command.create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10,
'tax_group_id': self.tax_group1.id,
'sequence': 2,
}),
]
})
self.tax_group_sub1.preceding_subtotal = "Tax withholding"
document = self._create_document_for_tax_totals_test([
(100, tax_10_withheld),
])
self.assertTaxTotals(document, {
'amount_total': 63,
'amount_untaxed': 100,
'display_tax_base': True,
'groups_by_subtotal': {
'Untaxed Amount': [{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 10,
'tax_group_base_amount': 100,
'tax_group_id': self.tax_group1.id,
}],
"Tax withholding": [{
'tax_group_name': self.tax_group_sub1.name,
'tax_group_amount': -47,
'tax_group_base_amount': 100,
'tax_group_id': self.tax_group_sub1.id,
}],
},
'subtotals': [{
'name': "Untaxed Amount",
'amount': 100,
}, {
'name': "Tax withholding",
'amount': 110,
}],
'subtotals_order': ["Untaxed Amount", "Tax withholding"],
})
def test_taxtotals_with_different_tax_rounding_methods(self):
def run_case(rounding_line, lines, expected_tax_group_amounts):
self.env.company.tax_calculation_rounding_method = rounding_line
document = self._create_document_for_tax_totals_test(lines)
tax_amounts = document.tax_totals['groups_by_subtotal']['Untaxed Amount']
if len(expected_tax_group_amounts) != len(tax_amounts):
self.fail("Wrong number of values to compare.")
for tax_amount, expected in zip(tax_amounts, expected_tax_group_amounts):
actual = tax_amount['tax_group_amount']
if document.currency_id.compare_amounts(actual, expected) != 0:
self.fail(f'{document.currency_id.round(actual)} != {expected}')
# one line, two taxes
lines = [
(100.41, self.tax_16 + self.tax_53),
]
run_case('round_per_line', lines, [69.29])
run_case('round_globally', lines, [69.29])
# two lines, different taxes
lines = [
(50.4, self.tax_17a),
(47.21, self.tax_17b),
]
run_case('round_per_line', lines, [16.60])
run_case('round_globally', lines, [16.60])
# two lines, same tax
lines = [
(50.4, self.tax_17a),
(47.21, self.tax_17a),
]
run_case('round_per_line', lines, [16.60])
run_case('round_globally', lines, [16.59])
lines = [
(54.45, self.tax_10),
(600, self.tax_10),
(-500, self.tax_10),
]
run_case('round_per_line', lines, [15.45])
run_case('round_globally', lines, [15.45])
def test_cash_rounding_amount_total_rounded(self):
tax_15 = self.env['account.tax'].create({
'name': "tax_15",
'amount_type': 'percent',
'tax_group_id': self.tax_group1.id,
'amount': 15.0,
})
tax_10 = self.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'tax_group_id': self.tax_group2.id,
'amount': 10.0,
})
cash_rounding_biggest_tax = self.env['account.cash.rounding'].create({
'name': 'biggest tax Rounding HALF-UP',
'rounding': 1,
'strategy': 'biggest_tax',
'rounding_method': 'HALF-UP',
})
cash_rounding_add_invoice_line = self.env['account.cash.rounding'].create({
'name': 'add invoice line Rounding HALF-UP',
'rounding': 1,
'strategy': 'add_invoice_line',
'profit_account_id': self.company_data['default_account_revenue'].id,
'loss_account_id': self.company_data['default_account_expense'].id,
'rounding_method': 'HALF-UP',
})
for move_type in ['out_invoice', 'in_invoice']:
move = self.env['account.move'].create({
'move_type': move_type,
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'invoice_line_ids': [
Command.create({
'name': 'line',
'display_type': 'product',
'price_unit': 378,
'tax_ids': [Command.set(tax_15.ids)],
}),
Command.create({
'name': 'line',
'display_type': 'product',
'price_unit': 100,
'tax_ids': [Command.set(tax_10.ids)],
})
],
})
move.invoice_cash_rounding_id = cash_rounding_biggest_tax
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][0]['tax_group_amount'], 57)
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][1]['tax_group_amount'], 10)
self.assertEqual(move.tax_totals['amount_total'], 545)
move.invoice_cash_rounding_id = cash_rounding_add_invoice_line
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][0]['tax_group_amount'], 56.7)
self.assertEqual(move.tax_totals['groups_by_subtotal']['Untaxed Amount'][1]['tax_group_amount'], 10)
self.assertEqual(move.tax_totals['rounding_amount'], 0.3)
self.assertEqual(move.tax_totals['amount_total'], 544.7)
self.assertEqual(move.tax_totals['amount_total_rounded'], 545)
def test_recompute_cash_rounding_lines(self):
# if rounding_method is changed then rounding shouldn't be recomputed in posted invoices
cash_rounding_add_invoice_line = self.env['account.cash.rounding'].create({
'name': 'Add invoice line Rounding UP',
'rounding': 1,
'strategy': 'add_invoice_line',
'profit_account_id': self.company_data['default_account_revenue'].id,
'loss_account_id': self.company_data['default_account_expense'].id,
'rounding_method': 'UP',
})
moves_rounding = {}
moves = self.env['account.move']
for move_type in ['out_invoice', 'in_invoice']:
move = self.env['account.move'].create({
'move_type': move_type,
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'invoice_cash_rounding_id': cash_rounding_add_invoice_line.id,
'invoice_line_ids': [
Command.create({
'name': 'line',
'display_type': 'product',
'price_unit': 99.5,
})
],
})
moves_rounding[move] = sum(move.line_ids.filtered(lambda line: line.display_type == 'rounding').mapped('balance'))
moves += move
moves.action_post()
cash_rounding_add_invoice_line.rounding_method = 'DOWN'
# check if rounding is recomputed
moves.to_check = True
for move in moves_rounding:
self.assertEqual(sum(move.line_ids.filtered(lambda line: line.display_type == 'rounding').mapped('balance')), moves_rounding[move])
def test_recompute_cash_rounding_lines_multi_company(self):
"""
Ensure that when a move is created with cash rounding that will add an invoice line, the cash rounding accounts
will be that of the move's company and not the user's company.
"""
cash_rounding_tbl = self.env['account.cash.rounding'].with_company(self.company_data["company"])
cash_rounding_add_invoice_line = cash_rounding_tbl.create({
'name': 'Add invoice line Rounding UP',
'rounding': 1,
'rounding_method': 'UP',
'strategy': 'add_invoice_line',
'profit_account_id': self.company_data['default_account_revenue'].id,
'loss_account_id': self.company_data['default_account_expense'].id,
})
cash_rounding_add_invoice_line.with_company(self.company_data_2["company"]).write({
'profit_account_id': self.company_data_2['default_account_revenue'].id,
'loss_account_id': self.company_data_2['default_account_expense'].id,
})
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'company_id': self.company_data_2['company'].id,
'invoice_cash_rounding_id': cash_rounding_add_invoice_line.id,
'invoice_line_ids': [
Command.create({
'name': 'line',
'display_type': 'product',
'price_unit': 99.5,
})
],
})
rounding_line_account = move.line_ids.filtered(lambda line: line.display_type == 'rounding').mapped('account_id')
self.assertEqual(rounding_line_account, self.company_data_2['default_account_revenue'])
def test_cash_rounding_amount_total_rounded_foreign_currency(self):
tax_15 = self.env['account.tax'].create({
'name': "tax_15",
'amount_type': 'percent',
'amount': 15.0,
})
cash_rounding = self.env['account.cash.rounding'].create({
'name': 'Rounding HALF-UP',
'rounding': 10,
'strategy': 'biggest_tax',
'rounding_method': 'HALF-UP',
})
self.env['res.currency.rate'].create({
'name': '2023-01-01',
'rate': 0.2,
'currency_id': self.currency_data['currency'].id,
'company_id': self.env.company.id,
})
for move_type in ['out_invoice', 'in_invoice']:
move = self.env['account.move'].create({
'move_type': move_type,
'partner_id': self.partner_a.id,
'invoice_date': '2023-01-01',
'currency_id': self.currency_data['currency'].id,
'invoice_line_ids': [
Command.create({
'name': 'line',
'display_type': 'product',
'price_unit': 100,
'tax_ids': [tax_15.id],
})
]
})
move.invoice_cash_rounding_id = cash_rounding
self.assertEqual(move.tax_totals['amount_total'], 120)

View file

@ -0,0 +1,818 @@
# -*- coding: utf-8 -*-
# pylint: disable=C0326
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
@tagged('post_install', '-at_install')
class TestInvoiceTaxes(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.company_data['company'].country_id = cls.env.ref('base.us')
cls.percent_tax_1 = cls.env['account.tax'].create({
'name': '21%',
'amount_type': 'percent',
'amount': 21,
'sequence': 10,
})
cls.percent_tax_1_incl = cls.env['account.tax'].create({
'name': '21% incl',
'amount_type': 'percent',
'amount': 21,
'price_include': True,
'include_base_amount': True,
'sequence': 20,
})
cls.percent_tax_2 = cls.env['account.tax'].create({
'name': '12%',
'amount_type': 'percent',
'amount': 12,
'sequence': 30,
})
cls.percent_tax_3_incl = cls.env['account.tax'].create({
'name': '5% incl',
'amount_type': 'percent',
'amount': 5,
'price_include': True,
'include_base_amount': True,
'sequence': 40,
})
cls.group_tax = cls.env['account.tax'].create({
'name': 'group 12% + 21%',
'amount_type': 'group',
'amount': 21,
'children_tax_ids': [
(4, cls.percent_tax_1_incl.id),
(4, cls.percent_tax_2.id)
],
'sequence': 40,
})
tax_report = cls.env['account.report'].create({
'name': "Tax report",
'country_id': cls.company_data['company'].country_id.id,
'column_ids': [
Command.create({
'name': "Balance",
'expression_label': 'balance',
}),
],
})
tax_report_line = cls.env['account.report.line'].create({
'name': 'test_tax_report_line',
'report_id': tax_report.id,
'sequence': 10,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'tax_tags',
'formula': 'test_tax_report_line',
}),
],
})
tax_tags = tax_report_line.expression_ids._get_matching_tags()
cls.tax_tag_pos, cls.tax_tag_neg = tax_tags.sorted('tax_negate')
base_report_line = cls.env['account.report.line'].create({
'name': 'base_test_tax_report_line',
'report_id': tax_report.id,
'sequence': 10,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'tax_tags',
'formula': 'base_test_tax_report_line',
}),
],
})
base_tags = base_report_line.expression_ids._get_matching_tags()
cls.base_tag_pos, cls.base_tag_neg = base_tags.sorted('tax_negate')
def _create_invoice(self, taxes_per_line, inv_type='out_invoice', currency_id=False, invoice_payment_term_id=False):
''' Create an invoice on the fly.
:param taxes_per_line: A list of tuple (price_unit, account.tax recordset)
'''
vals = {
'move_type': inv_type,
'partner_id': self.partner_a.id,
'invoice_line_ids': [(0, 0, {
'name': 'xxxx',
'quantity': 1,
'price_unit': amount,
'tax_ids': [(6, 0, taxes.ids)],
}) for amount, taxes in taxes_per_line],
}
if currency_id:
vals['currency_id'] = currency_id.id
if invoice_payment_term_id:
vals['invoice_payment_term_id'] = invoice_payment_term_id.id
return self.env['account.move'].create(vals)
def test_setting_tax_separately(self):
''' Test:
price_unit | Taxes
------------------
100 | 21%
Expected:
Tax | Taxes | Base | Amount
--------------------------------------------
21% | / | 100 | 21
'''
invoice = self._create_invoice([(100, self.env['account.tax'])])
invoice.invoice_line_ids[0].tax_ids = self.percent_tax_1
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [
{'name': self.percent_tax_1.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': []},
])
def test_one_tax_per_line(self):
''' Test:
price_unit | Taxes
------------------
100 | 21%
121 | 21% incl
100 | 12%
Expected:
Tax | Taxes | Base | Amount
--------------------------------------------
21% | / | 100 | 21
21% incl | / | 100 | 21
12% | / | 100 | 12
'''
invoice = self._create_invoice([
(100, self.percent_tax_1),
(121, self.percent_tax_1_incl),
(100, self.percent_tax_2),
])
invoice.action_post()
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted(lambda x: x.name), [
{'name': self.percent_tax_2.name, 'tax_base_amount': 100, 'balance': -12, 'tax_ids': []},
{'name': self.percent_tax_1.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': []},
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': []},
])
def test_affecting_base_amount(self):
''' Test:
price_unit | Taxes
------------------
121 | 21% incl, 12%
100 | 12%
Expected:
Tax | Taxes | Base | Amount
--------------------------------------------
21% incl | 12% | 100 | 21
12% | / | 121 | 14.52
12% | / | 100 | 12
'''
invoice = self._create_invoice([
(121, self.percent_tax_1_incl + self.percent_tax_2),
(100, self.percent_tax_2),
])
invoice.action_post()
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted(lambda x: -x.balance), [
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'balance': -21, 'tax_ids': [self.percent_tax_2.id]},
{'name': self.percent_tax_2.name, 'tax_base_amount': 221, 'balance': -26.52, 'tax_ids': []},
])
def test_group_of_taxes(self):
''' Test:
price_unit | Taxes
------------------
121 | 21% incl + 12%
100 | 12%
Expected:
Tax | Taxes | Base | Amount
--------------------------------------------
21% incl | / | 100 | 21
12% | 21% incl | 121 | 14.52
12% | / | 100 | 12
'''
invoice = self._create_invoice([
(121, self.group_tax),
(100, self.percent_tax_2),
])
invoice.action_post()
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted('balance'), [
{'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100.0, 'balance': -21.0, 'tax_ids': [self.percent_tax_2.id]},
{'name': self.percent_tax_2.name, 'tax_base_amount': 121.0, 'balance': -14.52, 'tax_ids': []},
{'name': self.percent_tax_2.name, 'tax_base_amount': 100.0, 'balance': -12.0, 'tax_ids': []},
])
def _create_tax_tag(self, tag_name):
return self.env['account.account.tag'].create({
'name': tag_name,
'applicability': 'taxes',
'country_id': self.env.company.country_id.id,
})
def test_tax_repartition(self):
inv_base_tag = self._create_tax_tag('invoice_base')
inv_tax_tag_10 = self._create_tax_tag('invoice_tax_10')
inv_tax_tag_90 = self._create_tax_tag('invoice_tax_90')
ref_base_tag = self._create_tax_tag('refund_base')
ref_tax_tag = self._create_tax_tag('refund_tax')
account_1 = self.env['account.account'].create({'name': 'test1', 'code': 'test1', 'account_type': 'asset_current'})
account_2 = self.env['account.account'].create({'name': 'test2', 'code': 'test2', 'account_type': 'asset_current'})
tax = self.env['account.tax'].create({
'name': "Tax with account",
'amount_type': 'fixed',
'type_tax_use': 'sale',
'amount': 42,
'invoice_repartition_line_ids': [
(0,0, {
'repartition_type': 'base',
'tag_ids': [(4, inv_base_tag.id, 0)],
}),
(0,0, {
'factor_percent': 10,
'repartition_type': 'tax',
'account_id': account_1.id,
'tag_ids': [(4, inv_tax_tag_10.id, 0)],
}),
(0,0, {
'factor_percent': 90,
'repartition_type': 'tax',
'account_id': account_2.id,
'tag_ids': [(4, inv_tax_tag_90.id, 0)],
}),
],
'refund_repartition_line_ids': [
(0,0, {
'repartition_type': 'base',
'tag_ids': [(4, ref_base_tag.id, 0)],
}),
(0,0, {
'factor_percent': 10,
'repartition_type': 'tax',
'tag_ids': [(4, ref_tax_tag.id, 0)],
}),
(0,0, {
'factor_percent': 90,
'repartition_type': 'tax',
'account_id': account_1.id,
'tag_ids': [(4, ref_tax_tag.id, 0)],
}),
],
})
# Test invoice repartition
invoice = self._create_invoice([(100, tax)], inv_type='out_invoice')
invoice.action_post()
self.assertEqual(len(invoice.line_ids), 4, "There should be 4 account move lines created for the invoice: payable, base and 2 tax lines")
inv_base_line = invoice.line_ids.filtered(lambda x: not x.tax_repartition_line_id and x.account_id.account_type != 'asset_receivable')
self.assertEqual(len(inv_base_line), 1, "There should be only one base line generated")
self.assertEqual(abs(inv_base_line.balance), 100, "Base amount should be 100")
self.assertEqual(inv_base_line.tax_tag_ids, inv_base_tag, "Base line should have received base tag")
inv_tax_lines = invoice.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
self.assertEqual(len(inv_tax_lines), 2, "There should be two tax lines, one for each repartition line.")
self.assertEqual(abs(inv_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 4.2, "Tax line on account 1 should amount to 4.2 (10% of 42)")
self.assertEqual(inv_tax_lines.filtered(lambda x: x.account_id == account_1).tax_tag_ids, inv_tax_tag_10, "Tax line on account 1 should have 10% tag")
self.assertAlmostEqual(abs(inv_tax_lines.filtered(lambda x: x.account_id == account_2).balance), 37.8, 2, "Tax line on account 2 should amount to 37.8 (90% of 42)")
self.assertEqual(inv_tax_lines.filtered(lambda x: x.account_id == account_2).tax_tag_ids, inv_tax_tag_90, "Tax line on account 2 should have 90% tag")
# Test refund repartition
refund = self._create_invoice([(100, tax)], inv_type='out_refund')
refund.action_post()
self.assertEqual(len(refund.line_ids), 4, "There should be 4 account move lines created for the refund: payable, base and 2 tax lines")
ref_base_line = refund.line_ids.filtered(lambda x: not x.tax_repartition_line_id and x.account_id.account_type != 'asset_receivable')
self.assertEqual(len(ref_base_line), 1, "There should be only one base line generated")
self.assertEqual(abs(ref_base_line.balance), 100, "Base amount should be 100")
self.assertEqual(ref_base_line.tax_tag_ids, ref_base_tag, "Base line should have received base tag")
ref_tax_lines = refund.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
self.assertEqual(len(ref_tax_lines), 2, "There should be two refund tax lines")
self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == ref_base_line.account_id).balance), 4.2, "Refund tax line on base account should amount to 4.2 (10% of 42)")
self.assertAlmostEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 37.8, 2, "Refund tax line on account 1 should amount to 37.8 (90% of 42)")
self.assertEqual(ref_tax_lines.mapped('tax_tag_ids'), ref_tax_tag, "Refund tax lines should have the right tag")
def test_division_tax(self):
'''
Test that when using division tax, with percentage amount
100% any change on price unit is correctly reflected on
the whole move.
Complete scenario:
- Create a division tax, 100% amount, included in price.
- Create an invoice, with only the mentioned tax
- Change price_unit of the aml
- Total price of the move should change as well
'''
sale_tax = self.env['account.tax'].create({
'name': 'tax',
'type_tax_use': 'sale',
'amount_type': 'division',
'amount': 100,
'price_include': True,
'include_base_amount': True,
})
invoice = self._create_invoice([(100, sale_tax)])
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'name': sale_tax.name,
'tax_base_amount': 0.0,
'balance': -100,
}])
# change price unit, everything should change as well
with Form(invoice) as invoice_form:
with invoice_form.invoice_line_ids.edit(0) as line_edit:
line_edit.price_unit = 200
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'name': sale_tax.name,
'tax_base_amount': 0.0,
'balance': -200,
}])
def test_misc_journal_entry_tax_tags_sale(self):
sale_tax = self.env['account.tax'].create({
'name': 'tax',
'type_tax_use': 'sale',
'amount_type': 'percent',
'amount': 10,
'invoice_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
'tag_ids': [(6, 0, self.base_tag_pos.ids)],
}),
(0, 0, {
'repartition_type': 'tax',
'tag_ids': [(6, 0, self.tax_tag_pos.ids)],
}),
],
'refund_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
'tag_ids': [(6, 0, self.base_tag_neg.ids)],
}),
(0, 0, {
'repartition_type': 'tax',
'tag_ids': [(6, 0, self.tax_tag_neg.ids)],
}),
],
})
inv_tax_rep_ln = sale_tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
ref_tax_rep_ln = sale_tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
# === Tax in debit ===
move_form = Form(self.env['account.move'], view='account.view_move_form')
move_form.ref = 'azerty'
# Debit base tax line.
with move_form.line_ids.new() as credit_line:
credit_line.name = 'debit_line_1'
credit_line.account_id = self.company_data['default_account_revenue']
credit_line.debit = 1000.0
credit_line.tax_ids.clear()
credit_line.tax_ids.add(sale_tax)
# Balance the journal entry.
with move_form.line_ids.new() as credit_line:
credit_line.name = 'balance'
credit_line.account_id = self.company_data['default_account_revenue']
credit_line.credit = 1100.0
move = move_form.save()
self.assertRecordValues(move.line_ids.sorted('balance'), [
{'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': ref_tax_rep_ln.id, 'tax_tag_invert': False},
{'balance': 1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
])
# === Tax in credit ===
move_form = Form(self.env['account.move'], view='account.view_move_form')
move_form.ref = 'azerty'
# Debit base tax line.
with move_form.line_ids.new() as credit_line:
credit_line.name = 'debit_line_1'
credit_line.account_id = self.company_data['default_account_revenue']
credit_line.credit = 1000.0
credit_line.tax_ids.clear()
credit_line.tax_ids.add(sale_tax)
# Balance the journal entry.
with move_form.line_ids.new() as debit_line:
debit_line.name = 'balance'
debit_line.account_id = self.company_data['default_account_revenue']
debit_line.debit = 1100.0
move = move_form.save()
self.assertRecordValues(move.line_ids.sorted('balance'), [
{'balance': -1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': True},
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': inv_tax_rep_ln.id, 'tax_tag_invert': True},
{'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
])
def test_misc_journal_entry_tax_tags_purchase(self):
purch_tax = self.env['account.tax'].create({
'name': 'tax',
'type_tax_use': 'purchase',
'amount_type': 'percent',
'amount': 10,
'invoice_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
'tag_ids': [(6, 0, self.base_tag_pos.ids)],
}),
(0, 0, {
'repartition_type': 'tax',
'tag_ids': [(6, 0, self.tax_tag_pos.ids)],
}),
],
'refund_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
'tag_ids': [(6, 0, self.base_tag_neg.ids)],
}),
(0, 0, {
'repartition_type': 'tax',
'tag_ids': [(6, 0, self.tax_tag_neg.ids)],
}),
],
})
inv_tax_rep_ln = purch_tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
ref_tax_rep_ln = purch_tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
# === Tax in debit ===
move_form = Form(self.env['account.move'])
move_form.ref = 'azerty'
# Debit base tax line.
with move_form.line_ids.new() as credit_line:
credit_line.name = 'debit_line_1'
credit_line.account_id = self.company_data['default_account_revenue']
credit_line.debit = 1000.0
credit_line.tax_ids.clear()
credit_line.tax_ids.add(purch_tax)
# Balance the journal entry.
with move_form.line_ids.new() as credit_line:
credit_line.name = 'balance'
credit_line.account_id = self.company_data['default_account_revenue']
credit_line.credit = 1100.0
move = move_form.save()
self.assertRecordValues(move.line_ids.sorted('balance'), [
{'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': inv_tax_rep_ln.id, 'tax_tag_invert': False},
{'balance': 1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
])
# === Tax in credit ===
move_form = Form(self.env['account.move'])
move_form.ref = 'azerty'
# Debit base tax line.
with move_form.line_ids.new() as credit_line:
credit_line.name = 'debit_line_1'
credit_line.account_id = self.company_data['default_account_revenue']
credit_line.credit = 1000.0
credit_line.tax_ids.clear()
credit_line.tax_ids.add(purch_tax)
# Balance the journal entry.
with move_form.line_ids.new() as debit_line:
debit_line.name = 'balance'
debit_line.account_id = self.company_data['default_account_revenue']
debit_line.debit = 1100.0
move = move_form.save()
self.assertRecordValues(move.line_ids.sorted('balance'), [
{'balance': -1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': True},
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': ref_tax_rep_ln.id, 'tax_tag_invert': True},
{'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False},
])
def test_misc_entry_tax_group_signs(self):
""" Tests sign inversion of the tags on misc operations made with tax
groups.
"""
def _create_group_of_taxes(tax_type):
# We use asymmetric tags between the child taxes to avoid shadowing errors
child1_sale_tax = self.env['account.tax'].create({
'sequence': 1,
'name': 'child1_%s' % tax_type,
'type_tax_use': 'none',
'amount_type': 'percent',
'amount': 5,
'invoice_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
'tag_ids': [(6, 0, self.base_tag_pos.ids)],
}),
(0, 0, {
'repartition_type': 'tax',
'tag_ids': [(6, 0, self.tax_tag_pos.ids)],
}),
],
'refund_repartition_line_ids': [
(0, 0, {'repartition_type': 'base'}),
(0, 0, {'repartition_type': 'tax'}),
],
})
child2_sale_tax = self.env['account.tax'].create({
'sequence': 2,
'name': 'child2_%s' % tax_type,
'type_tax_use': 'none',
'amount_type': 'percent',
'amount': 10,
'invoice_repartition_line_ids': [
(0, 0, {'repartition_type': 'base'}),
(0, 0, {'repartition_type': 'tax'}),
],
'refund_repartition_line_ids': [
(0, 0, {
'repartition_type': 'base',
'tag_ids': [(6, 0, self.base_tag_neg.ids)],
}),
(0, 0, {
'repartition_type': 'tax',
'tag_ids': [(6, 0, self.tax_tag_neg.ids)],
}),
],
})
return self.env['account.tax'].create({
'name': 'group_%s' % tax_type,
'type_tax_use': tax_type,
'amount_type': 'group',
'amount': 10,
'children_tax_ids':[(6,0,[child1_sale_tax.id, child2_sale_tax.id])]
})
def _create_misc_operation(tax, tax_field):
with Form(self.env['account.move'], view='account.view_move_form') as move_form:
for line_field in ('debit', 'credit'):
line_amount = tax_field == line_field and 1000 or 1150
with move_form.line_ids.new() as line_form:
line_form.name = '%s_line' % line_field
line_form.account_id = self.company_data['default_account_revenue']
line_form.debit = line_field == 'debit' and line_amount or 0
line_form.credit = line_field == 'credit' and line_amount or 0
if tax_field == line_field:
line_form.tax_ids.clear()
line_form.tax_ids.add(tax)
return move_form.save()
sale_group = _create_group_of_taxes('sale')
purchase_group = _create_group_of_taxes('purchase')
# Sale tax on debit: use refund repartition
debit_sale_move = _create_misc_operation(sale_group, 'debit')
self.assertRecordValues(debit_sale_move.line_ids.sorted('balance'), [
{'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
{'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000},
{'balance': 1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0},
])
# Sale tax on credit: use invoice repartition
credit_sale_move = _create_misc_operation(sale_group, 'credit')
self.assertRecordValues(credit_sale_move.line_ids.sorted('balance'), [
{'balance': -1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0},
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
{'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000},
{'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
])
# Purchase tax on debit: use invoice repartition
debit_purchase_move = _create_misc_operation(purchase_group, 'debit')
self.assertRecordValues(debit_purchase_move.line_ids.sorted('balance'), [
{'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
{'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000},
{'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
{'balance': 1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0},
])
# Purchase tax on credit: use refund repartition
credit_purchase_move = _create_misc_operation(purchase_group, 'credit')
self.assertRecordValues(credit_purchase_move.line_ids.sorted('balance'), [
{'balance': -1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0},
{'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000},
{'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000},
{'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0},
])
def test_tax_calculation_foreign_currency_large_quantity(self):
''' Test:
Foreign currency with rate of 1.1726 and tax of 21%
price_unit | Quantity | Taxes
------------------
2.82 | 20000 | 21% not incl
'''
self.env['res.currency.rate'].create({
'name': '2018-01-01',
'rate': 1.1726,
'currency_id': self.currency_data['currency'].id,
'company_id': self.env.company.id,
})
self.currency_data['currency'].rounding = 0.05
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_date': '2018-01-01',
'date': '2018-01-01',
'invoice_line_ids': [(0, 0, {
'name': 'xxxx',
'quantity': 20000,
'price_unit': 2.82,
'tax_ids': [(6, 0, self.percent_tax_1.ids)],
})]
})
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'tax_base_amount': 48098.24, # 20000 * 2.82 / 1.1726
'credit': 10100.63, # tax_base_amount * 0.21
}])
def test_ensure_no_unbalanced_entry(self):
''' Ensure to not create an unbalanced journal entry when saving. '''
self.env['res.currency.rate'].create({
'name': '2018-01-01',
'rate': 0.654065014,
'currency_id': self.currency_data['currency'].id,
'company_id': self.env.company.id,
})
self.currency_data['currency'].rounding = 0.05
invoice = self._create_invoice([
(5, self.percent_tax_3_incl),
(10, self.percent_tax_3_incl),
(50, self.percent_tax_3_incl),
], currency_id=self.currency_data['currency'], invoice_payment_term_id=self.pay_terms_a)
invoice.action_post()
def test_tax_calculation_multi_currency(self):
self.env['res.currency.rate'].create({
'name': '2018-01-01',
'rate': 0.273748,
'currency_id': self.currency_data['currency'].id,
'company_id': self.env.company.id,
})
self.currency_data['currency'].rounding = 0.01
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_date': '2018-01-01',
'date': '2018-01-01',
'invoice_line_ids': [(0, 0, {
'name': 'xxxx',
'quantity': 1,
'price_unit': 155.32,
'tax_ids': [(6, 0, self.percent_tax_1.ids)],
})]
})
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'tax_base_amount': 567.38, # 155.32 * 1 / (1 / 0.273748)
'balance': -119.16, # tax_base_amount * 0.21
}])
self.assertRecordValues(invoice.line_ids.filtered(lambda l: not l.name), [{
'balance': 686.54,
}])
with Form(invoice) as invoice_form:
invoice_form.currency_id = self.currency_data['currency']
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'tax_base_amount': 567.38,
'balance': -119.16,
}])
self.assertRecordValues(invoice.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable'), [{
'balance': 686.54,
}])
def test_tax_calculation_multi_currency_100_included_tax(self):
self.env['res.currency.rate'].create({
'name': '2018-01-01',
'rate': 0.273748,
'currency_id': self.currency_data['currency'].id,
'company_id': self.env.company.id,
})
self.currency_data['currency'].rounding = 0.01
tax = self.env['account.tax'].create({
'name': 'tax_100',
'amount_type': 'division',
'amount': 100,
'price_include': True,
})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_date': '2018-01-01',
'date': '2018-01-01',
'invoice_line_ids': [(0, 0, {
'name': 'xxxx',
'quantity': 1,
'price_unit': 100.00,
'tax_ids': [(6, 0, tax.ids)],
})]
})
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'tax_base_amount': 0.0,
'balance': -365.3, # 100 * (1 / 0.273748)
}])
def test_fixed_tax_with_zero_price(self):
fixed_tax = self.env['account.tax'].create({
'name': 'Test 5 fixed',
'amount_type': 'fixed',
'amount': 5,
})
invoice = self._create_invoice([
(0, fixed_tax),
])
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'credit': 5.0,
'debit': 0,
}])
invoice.invoice_line_ids.quantity = 2
self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
'credit': 10.0,
'debit': 0,
}])
def test_tax_line_amount_currency_modification_auto_balancing(self):
date = '2017-01-01'
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'date': date,
'partner_id': self.partner_a.id,
'invoice_date': date,
'currency_id': self.currency_data['currency'].id,
'invoice_payment_term_id': self.pay_terms_a.id,
'invoice_line_ids': [
(0, None, {
'name': self.product_a.name,
'product_id': self.product_a.id,
'product_uom_id': self.product_a.uom_id.id,
'quantity': 1.0,
'price_unit': 1000,
'tax_ids': self.product_a.taxes_id.ids,
}),
(0, None, {
'name': self.product_b.name,
'product_id': self.product_b.id,
'product_uom_id': self.product_b.uom_id.id,
'quantity': 1.0,
'price_unit': 200,
'tax_ids': self.product_b.taxes_id.ids,
}),
]
})
receivable_line = move.line_ids.filtered(lambda line: line.display_type == 'payment_term')
self.assertRecordValues(receivable_line, [
{'amount_currency': 1410.00, 'balance': 705.00},
])
# Modify the tax lines
tax_lines = move.line_ids.filtered(lambda line: line.display_type == 'tax').sorted('amount_currency')
self.assertRecordValues(tax_lines, [
{'amount_currency': -180.00, 'balance': -90.00},
{'amount_currency': -30.00, 'balance': -15.00},
])
tax_lines[0].amount_currency = -180.03
# The following line should not cause the move to become unbalanced; i.e. there should be no error
tax_lines[1].amount_currency = -29.99
self.assertRecordValues(receivable_line, [
{'amount_currency': 1410.02, 'balance': 705.02},
])

View file

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
import base64
import io
from PyPDF2 import PdfFileReader, PdfFileWriter
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.exceptions import RedirectWarning
from odoo.tools import pdf
from odoo.tests import tagged
from odoo.tools import file_open
@tagged('post_install', '-at_install')
class TestIrActionsReport(AccountTestInvoicingCommon):
def setUp(self):
super().setUp()
self.file = file_open('base/tests/minimal.pdf', 'rb').read()
self.minimal_reader_buffer = io.BytesIO(self.file)
self.minimal_pdf_reader = pdf.OdooPdfFileReader(self.minimal_reader_buffer)
def test_download_one_corrupted_pdf(self):
"""
PyPDF2 is not flawless. We can upload a PDF that can be previsualised but that cannot be merged by PyPDF2.
In the case of "Print Original Bills", we want to be able to download the pdf from the list view.
We test that, when selecting one record, it can be printed (downloaded) without error.
"""
attach_name = 'original_vendor_bill.pdf'
in_invoice_1 = self.env['account.move'].create({
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01'
})
in_invoice_1.message_main_attachment_id = self.env['ir.attachment'].create({
'datas': base64.b64encode(self.file),
'name': attach_name,
'mimetype': 'application/pdf',
'res_model': 'account.move',
'res_id': in_invoice_1.id,
})
test_record_report = self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account.action_account_original_vendor_bill', res_ids=in_invoice_1.id)
self.assertTrue(test_record_report, "The PDF should have been generated")
def test_download_with_encrypted_pdf(self):
"""
Same as test_download_one_corrupted_pdf
but for encrypted pdf with no password and set encryption type to 5 (not known by PyPDF2)
"""
attach_name = 'original_vendor_bill.pdf'
# we need to encrypt the file
with file_open('base/tests/minimal.pdf', 'rb') as pdf_file:
pdf_reader = PdfFileReader(pdf_file)
pdf_writer = PdfFileWriter()
for page_num in range(pdf_reader.getNumPages()):
pdf_writer.addPage(pdf_reader.getPage(page_num))
# Encrypt the PDF
pdf_writer.encrypt('', use_128bit=True)
# Get the binary
output_buffer = io.BytesIO()
pdf_writer.write(output_buffer)
encrypted_file = output_buffer.getvalue()
# we need to change the encryption value from 4 to 5 to simulate an encryption not used by PyPDF2
encrypt_start = encrypted_file.find(b'/Encrypt')
encrypt_end = encrypted_file.find(b'>>', encrypt_start)
encrypt_version = encrypted_file[encrypt_start: encrypt_end]
encrypted_file = encrypted_file.replace(encrypt_version, encrypt_version.replace(b'4', b'5'))
in_invoice_1 = self.env['account.move'].create({
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01'
})
in_invoice_1.message_main_attachment_id = self.env['ir.attachment'].create({
'datas': base64.b64encode(encrypted_file),
'name': attach_name,
'mimetype': 'application/pdf',
'res_model': 'account.move',
'res_id': in_invoice_1.id,
})
test_record_report = self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account.action_account_original_vendor_bill', res_ids=in_invoice_1.id)
self.assertTrue(test_record_report, "The PDF should have been generated")
in_invoice_2 = in_invoice_1.copy()
in_invoice_2.message_main_attachment_id = self.env['ir.attachment'].create({
'datas': base64.b64encode(self.file),
'name': attach_name,
'mimetype': 'application/pdf',
'res_model': 'account.move',
'res_id': in_invoice_2.id,
})
# trying to merge with a corrupted attachment should not work
with self.assertRaises(RedirectWarning):
self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf('account.action_account_original_vendor_bill', res_ids=[in_invoice_1.id, in_invoice_2.id])

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.test_mail.tests.common import TestMailCommon
from odoo.tests import Form
from odoo.tests.common import tagged
@tagged('post_install', '-at_install')
class TestTracking(AccountTestInvoicingCommon, TestMailCommon):
def test_aml_change_tracking(self):
""" tests that the field_groups is correctly set """
account_move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 200.0})]
})
account_move.action_post()
account_move.button_draft()
old_value = account_move.invoice_line_ids.account_id
with Form(account_move) as account_move_form:
with account_move_form.invoice_line_ids.edit(0) as line_form:
line_form.account_id = self.company_data['default_account_assets']
new_value = account_move.invoice_line_ids.account_id
self.flush_tracking()
self.assertTracking(account_move.message_ids, [
('account_id', 'many2one', old_value, new_value),
])
tracking_value = account_move.message_ids.sudo().tracking_value_ids
tracking_value._compute_field_groups()
self.assertEqual(tracking_value.field_groups, False, "There is no group on account.move.line.account_id")

View file

@ -0,0 +1,421 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.exceptions import ValidationError
from odoo.tests import tagged
from odoo import fields, Command
from odoo.tests.common import Form
@tagged('post_install', '-at_install')
class TestAccountPaymentTerms(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.pay_term_today = cls.env['account.payment.term'].create({
'name': 'Today',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 0,
}),
],
})
cls.pay_term_next_month_on_the_15 = cls.env['account.payment.term'].create({
'name': 'Next month on the 15th',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 0,
'end_month': True,
'days_after': 15,
}),
],
})
cls.pay_term_last_day_of_month = cls.env['account.payment.term'].create({
'name': 'Last Day of month',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 0,
'end_month': True
}),
],
})
cls.pay_term_first_day_next_month = cls.env['account.payment.term'].create({
'name': 'First day next month',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 0,
'end_month': True,
'days_after': 1,
}),
],
})
cls.pay_term_net_30_days = cls.env['account.payment.term'].create({
'name': 'Net 30 days',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 30,
}),
],
})
cls.pay_term_30_days_end_of_month = cls.env['account.payment.term'].create({
'name': '30 days end of month',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 30,
'end_month': True
}),
],
})
cls.pay_term_1_month_end_of_month = cls.env['account.payment.term'].create({
'name': '1 month, end of month',
'line_ids': [
(0, 0, {
'value': 'balance',
'months': 1,
'days': 0,
'end_month': True
}),
],
})
cls.pay_term_30_days_end_of_month_the_10 = cls.env['account.payment.term'].create({
'name': '30 days end of month the 10th',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 30,
'end_month': True,
'days_after': 10,
}),
],
})
cls.pay_term_90_days_end_of_month_the_10 = cls.env['account.payment.term'].create({
'name': '90 days end of month the 10',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 90,
'end_month': True,
'days_after': 10,
}),
],
})
cls.pay_term_3_months_end_of_month_the_10 = cls.env['account.payment.term'].create({
'name': '3 months end of month the 10',
'line_ids': [
(0, 0, {
'value': 'balance',
'months': 3,
'end_month': True,
'days_after': 10,
}),
],
})
cls.pay_term_end_month_on_the_30th = cls.env['account.payment.term'].create({
'name': 'End of month, the 30th',
'line_ids': [
(0, 0, {
'value': 'balance',
'end_month': True,
'days_after': 30,
}),
],
})
cls.pay_term_1_month_15_days_end_month_45_days = cls.env['account.payment.term'].create({
'name': '1 month, 15 days, end month, 45 days',
'line_ids': [
(0, 0, {
'value': 'balance',
'months': 1,
'days': 15,
'end_month': True,
'days_after': 45,
}),
],
})
cls.pay_term_next_10_of_the_month = cls.env['account.payment.term'].create({
'name': 'Next 10th of the month',
'line_ids': [
(0, 0, {
'value': 'balance',
'months': 0,
'days': -10,
'end_month': True,
'days_after': 10,
}),
],
})
cls.invoice = cls.init_invoice('out_refund', products=cls.product_a+cls.product_b)
def assertPaymentTerm(self, pay_term, invoice_date, dates):
with Form(self.invoice) as move_form:
move_form.invoice_payment_term_id = pay_term
move_form.invoice_date = invoice_date
self.assertEqual(
self.invoice.line_ids.filtered(
lambda l: l.account_id == self.company_data['default_account_receivable']
).mapped('date_maturity'),
[fields.Date.from_string(date) for date in dates],
)
def test_payment_term(self):
self.assertPaymentTerm(self.pay_term_today, '2019-01-01', ['2019-01-01'])
self.assertPaymentTerm(self.pay_term_today, '2019-01-15', ['2019-01-15'])
self.assertPaymentTerm(self.pay_term_today, '2019-01-31', ['2019-01-31'])
self.assertPaymentTerm(self.pay_term_next_month_on_the_15, '2019-01-01', ['2019-02-15'])
self.assertPaymentTerm(self.pay_term_next_month_on_the_15, '2019-01-15', ['2019-02-15'])
self.assertPaymentTerm(self.pay_term_next_month_on_the_15, '2019-01-31', ['2019-02-15'])
self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-01', ['2019-01-31'])
self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-15', ['2019-01-31'])
self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-31', ['2019-01-31'])
self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-01', ['2019-02-01'])
self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-15', ['2019-02-01'])
self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-31', ['2019-02-01'])
self.assertPaymentTerm(self.pay_term_net_30_days, '2022-01-01', ['2022-01-31'])
self.assertPaymentTerm(self.pay_term_net_30_days, '2022-01-15', ['2022-02-14'])
self.assertPaymentTerm(self.pay_term_net_30_days, '2022-01-31', ['2022-03-02'])
self.assertPaymentTerm(self.pay_term_30_days_end_of_month, '2022-01-01', ['2022-01-31'])
self.assertPaymentTerm(self.pay_term_30_days_end_of_month, '2022-01-15', ['2022-02-28'])
self.assertPaymentTerm(self.pay_term_30_days_end_of_month, '2022-01-31', ['2022-03-31'])
self.assertPaymentTerm(self.pay_term_1_month_end_of_month, '2022-01-01', ['2022-02-28'])
self.assertPaymentTerm(self.pay_term_1_month_end_of_month, '2022-01-15', ['2022-02-28'])
self.assertPaymentTerm(self.pay_term_1_month_end_of_month, '2022-01-31', ['2022-02-28'])
self.assertPaymentTerm(self.pay_term_30_days_end_of_month_the_10, '2022-01-01', ['2022-02-10'])
self.assertPaymentTerm(self.pay_term_30_days_end_of_month_the_10, '2022-01-15', ['2022-03-10'])
self.assertPaymentTerm(self.pay_term_30_days_end_of_month_the_10, '2022-01-31', ['2022-04-10'])
self.assertPaymentTerm(self.pay_term_90_days_end_of_month_the_10, '2022-01-01', ['2022-05-10'])
self.assertPaymentTerm(self.pay_term_90_days_end_of_month_the_10, '2022-01-15', ['2022-05-10'])
self.assertPaymentTerm(self.pay_term_90_days_end_of_month_the_10, '2022-01-31', ['2022-06-10'])
self.assertPaymentTerm(self.pay_term_3_months_end_of_month_the_10, '2022-01-01', ['2022-05-10'])
self.assertPaymentTerm(self.pay_term_3_months_end_of_month_the_10, '2022-01-15', ['2022-05-10'])
self.assertPaymentTerm(self.pay_term_3_months_end_of_month_the_10, '2022-01-31', ['2022-05-10'])
self.assertPaymentTerm(self.pay_term_1_month_15_days_end_month_45_days, '2022-01-01', ['2022-04-14'])
self.assertPaymentTerm(self.pay_term_1_month_15_days_end_month_45_days, '2022-01-15', ['2022-05-15'])
self.assertPaymentTerm(self.pay_term_1_month_15_days_end_month_45_days, '2022-01-31', ['2022-05-15'])
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-01', ['2022-01-10'])
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-09', ['2022-01-10'])
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-10', ['2022-01-10'])
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-15', ['2022-02-10'])
self.assertPaymentTerm(self.pay_term_next_10_of_the_month, '2022-01-31', ['2022-02-10'])
def test_payment_term_compute_method(self):
def assert_payment_term_values(expected_values_list):
res = pay_term._compute_terms(
fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company,
150, 150, 1, 1000, 1000,
)
self.assertEqual(len(res), len(expected_values_list))
for values, (company_amount, discount_balance) in zip(res, expected_values_list):
self.assertDictEqual(
{
'company_amount': values['company_amount'],
'discount_balance': values['discount_balance'],
},
{
'company_amount': company_amount,
'discount_balance': discount_balance,
},
)
pay_term = self.env['account.payment.term'].create({
'name': "turlututu",
'line_ids': [
Command.create({
'value': 'percent',
'value_amount': 10,
'days': 2,
'discount_percentage': 10,
'discount_days': 1,
}),
Command.create({
'value': 'percent',
'value_amount': 20,
'days': 4,
'discount_percentage': 20,
'discount_days': 3,
}),
Command.create({
'value': 'percent',
'value_amount': 20,
'days': 6,
}),
Command.create({
'value': 'balance',
'days': 8,
'discount_percentage': 20,
'discount_days': 7,
}),
],
})
self.env.company.early_pay_discount_computation = 'included'
assert_payment_term_values([
(115.0, 103.5),
(230.0, 184.0),
(230.0, 0.0),
(575.0, 460.0),
])
self.env.company.early_pay_discount_computation = 'excluded'
assert_payment_term_values([
(115.0, 105.0),
(230.0, 190.0),
(230.0, 0.0),
(575.0, 475.0),
])
def test_payment_term_compute_method_cash_rounding(self):
"""Test that the payment terms are computed correctly in case we apply cash rounding.
We check the amounts in document and company currency.
We check that the cash rounding does not change the totals in document or company curreny.
"""
def assert_payment_term_values(expected_values_list):
foreign_currency = self.currency_data['currency']
rate = self.env['res.currency']._get_conversion_rate(foreign_currency, self.env.company.currency_id, self.env.company, '2017-01-01')
self.assertEqual(rate, 0.5)
res = pay_term._compute_terms(
fields.Date.from_string('2017-01-01'), foreign_currency, self.env.company,
75, 150, 1, 359.18, 718.35, cash_rounding=self.cash_rounding_a
)
self.assertEqual(len(res), len(expected_values_list))
keys = ['company_amount', 'discount_balance', 'foreign_amount', 'discount_amount_currency']
for index, (values, expected_values) in enumerate(zip(res, expected_values_list)):
for key in keys:
with self.subTest(index=index, key=key):
self.assertAlmostEqual(values[key], expected_values[key])
total_company_amount = sum(value['company_amount'] for value in res)
total_foreign_amount = sum(value['foreign_amount'] for value in res)
self.assertAlmostEqual(total_company_amount, 434.18)
self.assertAlmostEqual(total_foreign_amount, 868.35)
pay_term = self.env['account.payment.term'].create({
'name': "turlututu",
'line_ids': [
Command.create({
'value': 'percent',
'value_amount': 10,
'days': 2,
'discount_percentage': 10,
'discount_days': 1,
}),
Command.create({
'value': 'percent',
'value_amount': 20,
'days': 4,
'discount_percentage': 20,
'discount_days': 3,
}),
Command.create({
'value': 'percent',
'value_amount': 20,
'days': 6,
}),
Command.create({
'value': 'balance',
'days': 8,
'discount_percentage': 20,
'discount_days': 7,
}),
],
})
with self.subTest(test='included'):
self.env.company.early_pay_discount_computation = 'included'
assert_payment_term_values([
{
'company_amount': 43.43,
'discount_balance': 39.11,
'foreign_amount': 86.85,
'discount_amount_currency': 78.20,
},
{
'company_amount': 86.86,
'discount_balance': 69.51,
'foreign_amount': 173.70,
'discount_amount_currency': 139.00,
},
{
'company_amount': 86.86,
'discount_balance': 0,
'foreign_amount': 173.70,
'discount_amount_currency': 0.00,
},
{
'company_amount': 217.03,
'discount_balance': 173.63,
'foreign_amount': 434.10,
'discount_amount_currency': 347.30,
},
])
with self.subTest(test='excluded'):
self.env.company.early_pay_discount_computation = 'excluded'
assert_payment_term_values([
{
'company_amount': 43.43,
'discount_balance': 39.86,
'foreign_amount': 86.85,
'discount_amount_currency': 79.70,
},
{
'company_amount': 86.86,
'discount_balance': 72.51,
'foreign_amount': 173.70,
'discount_amount_currency': 145.00,
},
{
'company_amount': 86.86,
'discount_balance': 0,
'foreign_amount': 173.70,
'discount_amount_currency': 0.00,
},
{
'company_amount': 217.03,
'discount_balance': 181.13,
'foreign_amount': 434.10,
'discount_amount_currency': 362.30,
},
])
def test_payment_term_multi_company(self):
"""
Ensure that the payment term is determined by `move.company_id` rather than `user.company_id`.
OdooBot has `res.company(1)` set as the default company. The test checks that the payment term correctly reflects
the company associated with the move, independent of the user's default company.
"""
user_company, other_company = self.company_data_2.get('company'), self.company_data.get('company')
self.env.user.write({
'company_ids': [user_company.id, other_company.id],
'company_id': user_company.id,
})
self.pay_terms_a.company_id = user_company
self.partner_a.with_company(user_company).property_payment_term_id = self.pay_terms_a
self.partner_a.with_company(other_company).property_payment_term_id = False
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'company_id': other_company.id
})
self.assertFalse(invoice.invoice_payment_term_id)

View file

@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
from odoo.tests.common import tagged
import json
from odoo import http
from odoo.tools import mute_logger
@tagged('post_install', '-at_install')
class TestPortalAttachment(AccountTestInvoicingHttpCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.out_invoice = cls.env['account.move'].with_context(tracking_disable=True).create({
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': '2019-05-01',
'date': '2019-05-01',
'invoice_line_ids': [
(0, 0, {'name': 'line1', 'price_unit': 100.0}),
],
})
cls.invoice_base_url = cls.out_invoice.get_base_url()
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_01_portal_attachment(self):
"""Test the portal chatter attachment route."""
self.partner_a.write({ # ensure an email for message_post
'email': 'partner.a@test.example.com',
})
self.authenticate(None, None)
# Test public user can't create attachment without token of document
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "new attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
},
files=[('file', ('test.txt', b'test', 'plain/text'))],
)
self.assertEqual(res.status_code, 400)
self.assertIn("you do not have the rights", res.text)
# Test public user can create attachment with token
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "new attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
'access_token': self.out_invoice._portal_ensure_token(),
},
files=[('file', ('test.txt', b'test', 'plain/text'))],
)
self.assertEqual(res.status_code, 200)
create_res = json.loads(res.content.decode('utf-8'))
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
# Test created attachment is private
res_binary = self.url_open('/web/content/%d' % create_res['id'])
self.assertEqual(res_binary.status_code, 404)
# Test created access_token is working
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
self.assertEqual(res_binary.status_code, 200)
# Test mimetype is neutered as non-admin
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "new attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
'access_token': self.out_invoice._portal_ensure_token(),
},
files=[('file', ('test.svg', b'<svg></svg>', 'image/svg+xml'))],
)
self.assertEqual(res.status_code, 200)
create_res = json.loads(res.content.decode('utf-8'))
self.assertEqual(create_res['mimetype'], 'text/plain')
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
self.assertEqual(res_binary.headers['Content-Type'], 'text/plain; charset=utf-8')
self.assertEqual(res_binary.content, b'<svg></svg>')
res_image = self.url_open('/web/image/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
self.assertEqual(res_image.headers['Content-Type'], 'application/octet-stream')
self.assertEqual(res_image.content, b'<svg></svg>')
# Test attachment can't be removed without valid token
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': create_res['id'],
'access_token': "wrong",
},
},
)
self.assertEqual(res.status_code, 200)
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
self.assertIn("you do not have the rights", res.text)
# Test attachment can be removed with token if "pending" state
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': create_res['id'],
'access_token': create_res['access_token'],
},
},
)
self.assertEqual(res.status_code, 200)
remove_res = json.loads(res.content.decode('utf-8'))['result']
self.assertFalse(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
self.assertTrue(remove_res is True)
# Test attachment can't be removed if not "pending" state
attachment = self.env['ir.attachment'].create({
'name': 'an attachment',
'access_token': self.env['ir.attachment']._generate_access_token(),
})
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': attachment.id,
'access_token': attachment.access_token,
},
},
)
self.assertEqual(res.status_code, 200)
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', attachment.id)]))
self.assertIn("not in a pending state", res.text)
# Test attachment can't be removed if attached to a message
attachment.write({
'res_model': 'mail.compose.message',
'res_id': 0,
})
attachment.flush_recordset()
message = self.env['mail.message'].create({
'attachment_ids': [(6, 0, attachment.ids)],
})
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': attachment.id,
'access_token': attachment.access_token,
},
},
)
self.assertEqual(res.status_code, 200)
self.assertTrue(attachment.exists())
self.assertIn("it is linked to a message", res.text)
message.sudo().unlink()
# Test attachment can't be associated if no attachment token.
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': ['false'],
'csrf_token': http.Request.csrf_token(self),
},
},
)
self.assertEqual(res.status_code, 200)
self.assertIn("The attachment %s does not exist or you do not have the rights to access it." % attachment.id, res.text)
# Test attachment can't be associated if no main document token
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.Request.csrf_token(self),
},
},
)
self.assertEqual(res.status_code, 200)
self.assertIn("You are not allowed to access 'Journal Entry' (account.move) records.", res.text)
# Test attachment can't be associated if not "pending" state
self.assertFalse(self.out_invoice.message_ids)
attachment.write({'res_model': 'model'})
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.Request.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
self.out_invoice.invalidate_recordset(['message_ids'])
self.assertEqual(len(self.out_invoice.message_ids), 1)
self.assertEqual(self.out_invoice.message_ids.body, "<p>test message 1</p>")
self.assertFalse(self.out_invoice.message_ids.attachment_ids)
# Test attachment can't be associated if not correct user
attachment.write({'res_model': 'mail.compose.message'})
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 2",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.Request.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
self.out_invoice.invalidate_recordset(['message_ids'])
self.assertEqual(len(self.out_invoice.message_ids), 2)
self.assertEqual(self.out_invoice.message_ids[0].author_id, self.partner_a)
self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 2</p>")
self.assertEqual(self.out_invoice.message_ids[0].email_from, self.partner_a.email_formatted)
self.assertFalse(self.out_invoice.message_ids.attachment_ids)
# Test attachment can be associated if all good (complete flow)
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "final attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
'access_token': self.out_invoice._portal_ensure_token(),
},
files=[('file', ('test.txt', b'test', 'plain/text'))],
)
self.assertEqual(res.status_code, 200)
create_res = json.loads(res.content.decode('utf-8'))
self.assertEqual(create_res['name'], "final attachment")
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 3",
'attachment_ids': [create_res['id']],
'attachment_tokens': [create_res['access_token']],
'csrf_token': http.Request.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
self.out_invoice.invalidate_recordset(['message_ids'])
self.assertEqual(len(self.out_invoice.message_ids), 3)
self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 3</p>")
self.assertEqual(len(self.out_invoice.message_ids[0].attachment_ids), 1)

View file

@ -0,0 +1,37 @@
from .common import AccountTestInvoicingCommon
from odoo.tests.common import Form, tagged, new_test_user
from odoo import Command
@tagged("post_install", "-at_install")
class AccountProductCase(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref)
cls.internal_user = new_test_user(
cls.env, login="internal_user", groups="base.group_user"
)
def test_internal_user_can_read_product_with_tax_and_tags(self):
"""Internal users need read access to products, no matter their taxes."""
# Add a tag to product_a's default tax
self.company_data["company"].country_id = self.env.ref("base.us")
tax_line_tag = self.env["account.account.tag"].create(
{
"name": "Tax tag",
"applicability": "taxes",
"country_id": self.company_data["company"].country_id.id,
}
)
repartition_lines = (
self.product_a.taxes_id.invoice_repartition_line_ids
| self.product_a.taxes_id.refund_repartition_line_ids
).filtered_domain([("repartition_type", "=", "tax")])
repartition_lines.write({"tag_ids": [Command.link(tax_line_tag.id)]})
# Check that internal user can read product_a
with Form(
self.product_a.with_user(self.internal_user).with_context(lang="en_US")
) as form_a:
# The tax string itself is not very important here; we just check
# it has a value and we can read it, so there were no access errors
self.assertTrue(form_a.tax_string)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,837 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.tests.common import Form, TransactionCase
from odoo import fields, api, SUPERUSER_ID, Command
from odoo.exceptions import ValidationError, UserError
from odoo.tools import mute_logger
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from functools import reduce
import json
import psycopg2
from unittest.mock import patch
class TestSequenceMixinCommon(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.company_data['company'].write({'fiscalyear_last_day': "31", 'fiscalyear_last_month': "3"})
cls.test_move = cls.create_move()
@classmethod
def create_move(cls, date=None, journal=None, name=None, post=False):
move = cls.env['account.move'].create({
'move_type': 'entry',
'date': date or '2016-01-01',
'line_ids': [
(0, None, {
'name': 'line',
'account_id': cls.company_data['default_account_revenue'].id,
}),
]
})
if journal:
move.name = False
move.journal_id = journal
if name:
move.name = name
if post:
move.action_post()
return move
@tagged('post_install', '-at_install')
class TestSequenceMixin(TestSequenceMixinCommon):
def assertNameAtDate(self, date, name):
test = self.create_move(date=date)
test.action_post()
self.assertEqual(test.name, name)
return test
def set_sequence(self, date, name):
return self.create_move(date=date, name=name)._post(soft=False)
def test_sequence_change_date(self):
"""Change the sequence when we change the date iff it has never been posted."""
# Check setup
self.assertEqual(self.test_move.state, 'draft')
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
self.assertEqual(fields.Date.to_string(self.test_move.date), '2016-01-01')
# Never posetd, the number must change if we change the date
self.test_move.date = '2020-02-02'
self.assertEqual(self.test_move.name, 'MISC/2020/02/0001')
# We don't recompute user's input when posting
self.test_move.name = 'MyMISC/2020/0000001'
self.test_move.action_post()
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
# Has been posted, and it doesn't change anymore
self.test_move.button_draft()
self.test_move.date = '2020-01-02'
self.test_move.action_post()
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
def test_sequence_change_date_with_quick_edit_mode(self):
"""
Test the sequence update behavior when changing the date of a move in quick edit mode.
The sequence should only be recalculated if a value (year or month) utilized in the sequence is modified.
"""
self.env.company.quick_edit_mode = "out_and_in_invoices"
self.env.company.fiscalyear_last_day = 30
self.env.company.fiscalyear_last_month = '12'
bill = self.env['account.move'].create({
'partner_id': 1,
'move_type': 'in_invoice',
'date': '2016-01-01',
'line_ids': [
Command.create({
'name': 'line',
'account_id': self.company_data['default_account_revenue'].id,
}),
]
})
bill = bill.copy({'date': '2016-02-01'})
self.assertEqual(bill.name, 'BILL/2016/02/0001')
with Form(bill) as bill_form:
bill_form.date = '2016-02-02'
self.assertEqual(bill_form.name, 'BILL/2016/02/0001')
bill_form.date = '2016-03-01'
self.assertEqual(bill_form.name, 'BILL/2016/03/0001')
bill_form.date = '2017-01-01'
self.assertEqual(bill_form.name, 'BILL/2017/01/0001')
invoice = self.env['account.move'].create({
'partner_id': 1,
'move_type': 'out_invoice',
'date': '2016-01-01',
'line_ids': [
Command.create({
'name': 'line',
'account_id': self.company_data['default_account_revenue'].id,
}),
]
})
self.assertEqual(invoice.name, 'INV/2016/00001')
with Form(invoice) as invoice_form:
invoice_form.date = '2016-01-02'
self.assertEqual(invoice_form.name, 'INV/2016/00001')
invoice_form.date = '2016-02-02'
self.assertEqual(invoice_form.name, 'INV/2016/00001')
invoice_form.date = '2017-01-01'
self.assertEqual(invoice_form.name, 'INV/2017/00001')
def test_sequence_empty_editable_with_quick_edit_mode(self):
""" Ensure the names of all but the first moves in a period are empty and editable in quick edit mode """
self.env.company.quick_edit_mode = 'in_invoices'
bill_1 = self.env['account.move'].create({
'partner_id': 1,
'move_type': 'in_invoice',
'date': '2016-01-01',
'line_ids': [
Command.create({
'name': 'line',
'account_id': self.company_data['default_account_revenue'].id,
}),
]
})
# First move in a period gets a name
self.assertEqual(bill_1.name, 'BILL/2016/01/0001')
bill_2 = bill_1.copy({'date': '2016-01-02'})
with Form(bill_2) as bill_2_form:
# Subsequent moves in the same period get an empty editable name in draft mode
self.assertFalse(bill_2_form.name)
bill_2_form.name = 'BILL/2016/01/0002'
self.assertEqual(bill_2_form.name, 'BILL/2016/01/0002')
bill_3 = bill_1.copy({'date': '2016-01-03'})
bill_4 = bill_1.copy({'date': '2016-01-04'})
(bill_3 + bill_4).date = fields.Date.from_string('2016-02-01')
# Same works with updating multiple moves
with Form(bill_3) as bill_3_form:
self.assertEqual(bill_3_form.name, 'BILL/2016/02/0001')
with Form(bill_4) as bill_4_form:
self.assertFalse(bill_4_form.name)
bill_4_form.name = 'BILL/2016/02/0002'
self.assertEqual(bill_4_form.name, 'BILL/2016/02/0002')
def test_sequence_draft_change_date(self):
# When a draft entry is added to an empty period, it should get a name.
# When a draft entry with a name is moved to a period already having entries, its name should be reset to '/'.
new_move = self.test_move.copy({'date': '2016-02-01'})
new_multiple_move_1 = self.test_move.copy({'date': '2016-03-01'})
new_multiple_move_2 = self.test_move.copy({'date': '2016-04-01'})
new_moves = new_multiple_move_1 + new_multiple_move_2
# Empty period, so a name should be set
self.assertEqual(new_move.name, 'MISC/2016/02/0001')
self.assertEqual(new_multiple_move_1.name, 'MISC/2016/03/0001')
self.assertEqual(new_multiple_move_2.name, 'MISC/2016/04/0001')
# Move to an existing period with another move in it
new_move.date = fields.Date.to_date('2016-01-10')
new_moves.date = fields.Date.to_date('2016-01-15')
# Not an empty period, so names should be reset to '/' (draft)
self.assertEqual(new_move.name, '/')
self.assertEqual(new_multiple_move_1.name, '/')
self.assertEqual(new_multiple_move_2.name, '/')
# Move back to a period with no moves in it
new_move.date = fields.Date.to_date('2016-02-01')
new_moves.date = fields.Date.to_date('2016-03-01')
# All moves in the previously empty periods should be given a name instead of `/`
self.assertEqual(new_move.name, 'MISC/2016/02/0001')
self.assertEqual(new_multiple_move_1.name, 'MISC/2016/03/0001')
# Since this is the second one in the same period, it should remain `/`
self.assertEqual(new_multiple_move_2.name, '/')
# Move both moves back to different periods, both with already moves in it.
new_multiple_move_1.date = fields.Date.to_date('2016-01-10')
new_multiple_move_2.date = fields.Date.to_date('2016-02-10')
# Moves are not in empty periods, so names should be set to '/' (draft)
self.assertEqual(new_multiple_move_1.name, '/')
self.assertEqual(new_multiple_move_2.name, '/')
# Change the journal of the last two moves (empty)
journal = self.env['account.journal'].create({
'name': 'awesome journal',
'type': 'general',
'code': 'AJ',
})
new_moves.journal_id = journal
# Both moves should be assigned a name, since no moves are in the journal and they are in different periods.
self.assertEqual(new_multiple_move_1.name, 'AJ/2016/01/0001')
self.assertEqual(new_multiple_move_2.name, 'AJ/2016/02/0001')
# When the date is removed in the form view, the name should not recompute
with Form(new_multiple_move_1) as move_form:
move_form.date = False
self.assertEqual(new_multiple_move_1.name, 'AJ/2016/01/0001')
move_form.date = fields.Date.to_date('2016-01-10')
def test_sequence_draft_first_of_period(self):
"""
| Step | Move | Action | Date | Name |
| ---- | ---- | ----------- | ---------- | ----------- |
| 1 | `A` | Add | 2023-02-01 | `2023/02/0001` |
| 2 | `B` | Add | 2023-02-02 | `/` |
| 3 | `B` | Post | 2023-02-02 | `2023/02/0002` |
| 4 | `A` | Cancel | 2023-02-01 | `2023/02/0001` | -> Assert
"""
move_a = self.test_move.copy({'date': '2023-02-01'})
self.assertEqual(move_a.name, 'MISC/2023/02/0001')
move_b = self.test_move.copy({'date': '2023-02-02'})
self.assertEqual(move_b.name, '/')
move_b.action_post()
self.assertEqual(move_b.name, 'MISC/2023/02/0002')
move_a.button_cancel()
self.assertEqual(move_a.name, 'MISC/2023/02/0001')
def test_journal_sequence(self):
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
self.test_move.action_post()
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
copy1 = self.create_move(date=self.test_move.date)
self.assertEqual(copy1.name, '/')
copy1.action_post()
self.assertEqual(copy1.name, 'MISC/2016/01/0002')
copy2 = self.create_move(date=self.test_move.date)
new_journal = self.test_move.journal_id.copy()
new_journal.code = "MISC2"
copy2.journal_id = new_journal
self.assertEqual(copy2.name, 'MISC2/2016/01/0001')
with Form(copy2) as move_form: # It is editable in the form
with mute_logger('odoo.tests.common.onchange'):
move_form.name = 'MyMISC/2016/0001'
self.assertIn(
'The sequence will restart at 1 at the start of every year',
move_form._perform_onchange(['name'])['warning']['message'],
)
move_form.journal_id = self.test_move.journal_id
self.assertEqual(move_form.name, '/')
move_form.journal_id = new_journal
self.assertEqual(move_form.name, 'MISC2/2016/01/0001')
with mute_logger('odoo.tests.common.onchange'):
move_form.name = 'MyMISC/2016/0001'
self.assertIn(
'The sequence will restart at 1 at the start of every year',
move_form._perform_onchange(['name'])['warning']['message'],
)
copy2.action_post()
self.assertEqual(copy2.name, 'MyMISC/2016/0001')
copy3 = self.create_move(date=copy2.date, journal=new_journal)
self.assertEqual(copy3.name, '/')
with self.assertRaises(AssertionError):
with Form(copy2) as move_form: # It is not editable in the form
move_form.name = 'MyMISC/2016/0002'
copy3.action_post()
self.assertEqual(copy3.name, 'MyMISC/2016/0002')
copy3.name = 'MISC2/2016/00002'
copy4 = self.create_move(date=copy2.date, journal=new_journal)
copy4.action_post()
self.assertEqual(copy4.name, 'MISC2/2016/00003')
copy5 = self.create_move(date=copy2.date, journal=new_journal)
copy5.date = '2021-02-02'
copy5.action_post()
self.assertEqual(copy5.name, 'MISC2/2021/00001')
copy5.name = 'N\'importe quoi?'
copy6 = self.create_move(date=copy5.date, journal=new_journal)
copy6.action_post()
self.assertEqual(copy6.name, 'N\'importe quoi?1')
def test_journal_sequence_format(self):
"""Test different format of sequences and what it becomes on another period"""
sequences = [
('JRNL/2016/00001', 'JRNL/2016/00002', 'JRNL/2016/00003', 'JRNL/2017/00001'),
('JRNL/2015-2016/00001', 'JRNL/2015-2016/00002', 'JRNL/2016-2017/00001', 'JRNL/2016-2017/00002'),
('JRNL/2015-16/00001', 'JRNL/2015-16/00002', 'JRNL/2016-17/00001', 'JRNL/2016-17/00002'),
('JRNL/15-16/00001', 'JRNL/15-16/00002', 'JRNL/16-17/00001', 'JRNL/16-17/00002'),
('1234567', '1234568', '1234569', '1234570'),
('20190910', '20190911', '20190912', '20190913'),
('2016-0910', '2016-0911', '2016-0912', '2017-0001'),
('201603-10', '201603-11', '201604-01', '201703-01'),
('16-03-10', '16-03-11', '16-04-01', '17-03-01'),
('2016-10', '2016-11', '2016-12', '2017-01'),
('045-001-000002', '045-001-000003', '045-001-000004', '045-001-000005'),
('JRNL/2016/00001suffix', 'JRNL/2016/00002suffix', 'JRNL/2016/00003suffix', 'JRNL/2017/00001suffix'),
]
init_move = self.create_move(date='2016-03-12')
next_move = self.create_move(date='2016-03-12')
next_move_month = self.create_move(date='2016-04-12')
next_move_year = self.create_move(date='2017-03-12')
next_moves = (next_move + next_move_month + next_move_year)
next_moves.action_post()
for sequence_init, sequence_next, sequence_next_month, sequence_next_year in sequences:
init_move.name = sequence_init
next_moves.name = False
next_moves._compute_name()
self.assertEqual(
[next_move.name, next_move_month.name, next_move_year.name],
[sequence_next, sequence_next_month, sequence_next_year],
)
def test_journal_next_sequence(self):
"""Sequences behave correctly even when there is not enough padding."""
prefix = "TEST_ORDER/2016/"
self.test_move.name = f"{prefix}1"
for c in range(2, 25):
copy = self.create_move(date=self.test_move.date)
copy.name = "/"
copy.action_post()
self.assertEqual(copy.name, f"{prefix}{c}")
def test_journal_sequence_multiple_type(self):
"""Domain is computed accordingly to different types."""
entry, entry2, invoice, invoice2, refund, refund2 = (
self.create_move(date='2016-01-01')
for i in range(6)
)
(invoice + invoice2 + refund + refund2).write({
'journal_id': self.company_data['default_journal_sale'].id,
'partner_id': 1,
'invoice_date': '2016-01-01',
})
(invoice + invoice2).move_type = 'out_invoice'
(refund + refund2).move_type = 'out_refund'
all_moves = (entry + entry2 + invoice + invoice2 + refund + refund2)
all_moves.name = False
all_moves.action_post()
self.assertEqual(entry.name, 'MISC/2016/01/0002')
self.assertEqual(entry2.name, 'MISC/2016/01/0003')
self.assertEqual(invoice.name, 'INV/2016/00001')
self.assertEqual(invoice2.name, 'INV/2016/00002')
self.assertEqual(refund.name, 'RINV/2016/00001')
self.assertEqual(refund2.name, 'RINV/2016/00002')
def test_journal_sequence_groupby_compute(self):
"""The grouping optimization is correctly done."""
# Setup two journals with a sequence that resets yearly
journals = self.env['account.journal'].create([{
'name': f'Journal{i}',
'code': f'J{i}',
'type': 'general',
} for i in range(2)])
account = self.env['account.account'].search([], limit=1)
moves = self.env['account.move'].create([{
'journal_id': journals[i].id,
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
'date': '2010-01-01',
} for i in range(2)])._post()
for i in range(2):
moves[i].name = f'J{i}/2010/00001'
# Check that the moves are correctly batched
moves = self.env['account.move'].create([{
'journal_id': journals[journal_index].id,
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
'date': f'2010-{month}-01',
} for journal_index, month in [(1, 1), (0, 1), (1, 2), (1, 1)]])._post()
self.assertEqual(
moves.mapped('name'),
['J1/2010/00002', 'J0/2010/00002', 'J1/2010/00004', 'J1/2010/00003'],
)
journals[0].code = 'OLD'
journals.flush_recordset()
journal_same_code = self.env['account.journal'].create([{
'name': 'Journal0',
'code': 'J0',
'type': 'general',
}])
moves = (
self.create_move(date='2010-01-01', journal=journal_same_code, name='J0/2010/00001')
+ self.create_move(date='2010-01-01', journal=journal_same_code)
+ self.create_move(date='2010-01-01', journal=journal_same_code)
+ self.create_move(date='2010-01-01', journal=journals[0])
)._post()
self.assertEqual(
moves.mapped('name'),
['J0/2010/00001', 'J0/2010/00002', 'J0/2010/00003', 'J0/2010/00003'],
)
def test_journal_override_sequence_regex(self):
"""There is a possibility to override the regex and change the order of the paramters."""
self.create_move(date='2020-01-01', name='00000876-G 0002/2020')
next_move = self.create_move(date='2020-01-01')
next_move.action_post()
self.assertEqual(next_move.name, '00000876-G 0002/2021') # Wait, I didn't want this!
next_move.button_draft()
next_move.name = False
next_move.journal_id.sequence_override_regex = r'^(?P<seq>\d*)(?P<suffix1>.*?)(?P<year>(\d{4})?)(?P<suffix2>)$'
next_move.action_post()
self.assertEqual(next_move.name, '00000877-G 0002/2020') # Pfew, better!
next_move = self.create_move(date='2020-01-01')
next_move.action_post()
self.assertEqual(next_move.name, '00000878-G 0002/2020')
next_move = self.create_move(date='2017-05-02')
next_move.action_post()
self.assertEqual(next_move.name, '00000001-G 0002/2017')
def test_journal_override_sequence_regex_year(self):
"""Override the sequence regex with a year syntax not matching the draft invoice name"""
move = self.create_move(date='2020-01-01')
move.journal_id.sequence_override_regex = (
'^'
r'(?P<prefix1>.*?)'
r'(?P<year>(?:(?<=\D)|(?<=^))\d{4})?'
r'(?P<prefix2>(?<=\d{4}).*?)?'
r'(?P<seq>\d{0,9})'
r'(?P<suffix>\D*?)'
'$'
)
# check if the default year_range regex is not used
next_move = self.create_move(date='2020-01-01', name='MISC/2020/21/00001')
next_move.action_post()
self.assertEqual(next_move.name, 'MISC/2020/21/00001')
# check the next sequence
next_move = self.create_move(date='2020-01-01')
next_move.action_post()
self.assertEqual(next_move.name, 'MISC/2020/21/00002')
# check for another year
next_move = self.create_move(date='2021-01-01')
next_move.action_post()
self.assertEqual(next_move.name, 'MISC/2021/21/00001')
# check if year is correctly extracted
with self.assertRaises(ValidationError):
self.create_move(date='2022-01-01', name='MISC/2021/22/00001') # year does not match
self.create_move(date='2022-01-01', name='MISC/2022/22/00001') # fix the year in the name
def test_journal_sequence_ordering(self):
"""Entries are correctly sorted when posting multiple at once."""
self.test_move.name = 'XMISC/2016/00001'
copies = reduce((lambda x, y: x+y), [
self.create_move(date=self.test_move.date)
for i in range(6)
])
copies[0].date = '2019-03-05'
copies[1].date = '2019-03-06'
copies[2].date = '2019-03-07'
copies[3].date = '2019-03-04'
copies[4].date = '2019-03-05'
copies[5].date = '2019-03-05'
# that entry is actualy the first one of the period, so it already has a name
# set it to '/' so that it is recomputed at post to be ordered correctly.
copies[0].name = '/'
copies.action_post()
# Ordered by date
self.assertEqual(copies[0].name, 'XMISC/2019/00002')
self.assertEqual(copies[1].name, 'XMISC/2019/00005')
self.assertEqual(copies[2].name, 'XMISC/2019/00006')
self.assertEqual(copies[3].name, 'XMISC/2019/00001')
self.assertEqual(copies[4].name, 'XMISC/2019/00003')
self.assertEqual(copies[5].name, 'XMISC/2019/00004')
# Can't have twice the same name
with self.assertRaises(psycopg2.DatabaseError), mute_logger('odoo.sql_db'), self.env.cr.savepoint():
copies[0].name = 'XMISC/2019/00001'
# Lets remove the order by date
copies[0].name = 'XMISC/2019/10001'
copies[1].name = 'XMISC/2019/10002'
copies[2].name = 'XMISC/2019/10003'
copies[3].name = 'XMISC/2019/10004'
copies[4].name = 'XMISC/2019/10005'
copies[5].name = 'XMISC/2019/10006'
copies[4].button_draft()
copies[4].with_context(force_delete=True).unlink()
copies[5].button_draft()
wizard = Form(self.env['account.resequence.wizard'].with_context(
active_ids=set(copies.ids) - set(copies[4].ids),
active_model='account.move'),
)
new_values = json.loads(wizard.new_values)
self.assertEqual(new_values[str(copies[0].id)]['new_by_date'], 'XMISC/2019/10002')
self.assertEqual(new_values[str(copies[0].id)]['new_by_name'], 'XMISC/2019/10001')
self.assertEqual(new_values[str(copies[1].id)]['new_by_date'], 'XMISC/2019/10004')
self.assertEqual(new_values[str(copies[1].id)]['new_by_name'], 'XMISC/2019/10002')
self.assertEqual(new_values[str(copies[2].id)]['new_by_date'], 'XMISC/2019/10005')
self.assertEqual(new_values[str(copies[2].id)]['new_by_name'], 'XMISC/2019/10003')
self.assertEqual(new_values[str(copies[3].id)]['new_by_date'], 'XMISC/2019/10001')
self.assertEqual(new_values[str(copies[3].id)]['new_by_name'], 'XMISC/2019/10004')
self.assertEqual(new_values[str(copies[5].id)]['new_by_date'], 'XMISC/2019/10003')
self.assertEqual(new_values[str(copies[5].id)]['new_by_name'], 'XMISC/2019/10005')
wizard.save().resequence()
self.assertEqual(copies[3].state, 'posted')
self.assertEqual(copies[5].name, 'XMISC/2019/10005')
self.assertEqual(copies[5].state, 'draft')
def test_journal_resequence_in_between_2_years_pattern(self):
"""Resequence XMISC/2023-2024/00001 into XMISC/23-24/00001."""
self.test_move.name = 'XMISC/2015-2016/00001'
invoices = (
self.create_move(date="2023-03-01", post=True)
+ self.create_move(date="2023-03-02", post=True)
+ self.create_move(date="2023-03-03", post=True)
+ self.create_move(date="2023-04-01", post=True)
+ self.create_move(date="2023-04-02", post=True)
)
self.assertRecordValues(invoices, (
{'name': 'XMISC/2022-2023/00001', 'state': 'posted'},
{'name': 'XMISC/2022-2023/00002', 'state': 'posted'},
{'name': 'XMISC/2022-2023/00003', 'state': 'posted'},
{'name': 'XMISC/2023-2024/00001', 'state': 'posted'},
{'name': 'XMISC/2023-2024/00002', 'state': 'posted'},
))
# Call the resequence wizard and change the sequence to XMISC/22-23/00001
# By default the sequence order should be kept
resequence_wizard = Form(self.env['account.resequence.wizard'].with_context(active_ids=invoices.ids, active_model='account.move'))
resequence_wizard.first_name = "XMISC/22-23/00001"
new_values = json.loads(resequence_wizard.new_values)
# Ensure consistencies of sequence displayed in the UI
self.assertEqual(new_values[str(invoices[0].id)]['new_by_name'], 'XMISC/22-23/00001')
self.assertEqual(new_values[str(invoices[1].id)]['new_by_name'], 'XMISC/22-23/00002')
self.assertEqual(new_values[str(invoices[2].id)]['new_by_name'], 'XMISC/22-23/00003')
self.assertEqual(new_values[str(invoices[3].id)]['new_by_name'], 'XMISC/23-24/00001')
self.assertEqual(new_values[str(invoices[4].id)]['new_by_name'], 'XMISC/23-24/00002')
resequence_wizard.save().resequence()
# Ensure the resequencing gave the same result as what was expected
self.assertRecordValues(invoices, (
{'name': 'XMISC/22-23/00001', 'state': 'posted'},
{'name': 'XMISC/22-23/00002', 'state': 'posted'},
{'name': 'XMISC/22-23/00003', 'state': 'posted'},
{'name': 'XMISC/23-24/00001', 'state': 'posted'},
{'name': 'XMISC/23-24/00002', 'state': 'posted'},
))
def test_sequence_get_more_specific(self):
"""There is the ability to change the format (i.e. from yearly to montlhy)."""
# Start with a continuous sequence
self.test_move.name = 'MISC/00001'
# Change the prefix to reset every year starting in 2017
new_year = self.set_sequence(self.test_move.date + relativedelta(years=1), 'MISC/2017/00001')
# Change the prefix to reset every month starting in February 2017
new_month = self.set_sequence(new_year.date + relativedelta(months=1), 'MISC/2017/02/00001')
self.assertNameAtDate(self.test_move.date, 'MISC/00002') # Keep the old prefix in 2016
self.assertNameAtDate(new_year.date, 'MISC/2017/00002') # Keep the new prefix in 2017
self.assertNameAtDate(new_month.date, 'MISC/2017/02/00002') # Keep the new prefix in February 2017
# Go fiscal year in March
# This will break the prefix of 2017 set previously and we will use the fiscal year prefix as of now
start_fiscal = self.set_sequence(new_year.date + relativedelta(months=2), 'MISC/2016-2017/00001')
self.assertNameAtDate(self.test_move.date, 'MISC/00003') # Keep the old prefix in 2016
self.assertNameAtDate(new_year.date, 'MISC/2016-2017/00002') # Prefix in January 2017 changed!
self.assertNameAtDate(new_month.date, 'MISC/2017/02/00003') # Keep the new prefix in February 2017
self.assertNameAtDate(start_fiscal.date, 'MISC/2016-2017/00003') # Keep the new prefix in March 2017
# Change the prefix to never reset (again) year starting in 2018 (Please don't do that)
reset_never = self.set_sequence(self.test_move.date + relativedelta(years=2), 'MISC/00100')
self.assertNameAtDate(reset_never.date, 'MISC/00101') # Keep the new prefix in 2018
def test_fiscal_vs_monthly(self):
"""Monthly sequence has priority over 2 digit financial year sequence but can be overridden."""
self.set_sequence('2101-02-01', 'MISC/01-02/00001')
move = self.assertNameAtDate('2101-03-01', 'MISC/01-03/00001')
move.journal_id.sequence_override_regex = move._sequence_year_range_regex
move.name = 'MISC/00-01/00001'
self.assertNameAtDate('2101-03-01', 'MISC/00-01/00002')
def test_resequence_clash(self):
"""Resequence doesn't clash when it uses a name set in the same batch
but that will be overriden later."""
moves = self.env['account.move']
for i in range(3):
moves += self.create_move(name=str(i))
moves.action_post()
mistake = moves[1]
mistake.button_draft()
mistake.posted_before = False
mistake.with_context(force_delete=True).unlink()
moves -= mistake
self.env['account.resequence.wizard'].create({
'move_ids': moves.ids,
'first_name': '2',
}).resequence()
@freeze_time('2021-10-01 00:00:00')
def test_change_journal_on_first_account_move(self):
"""Changing the journal on the first move is allowed"""
journal = self.env['account.journal'].create({
'name': 'awesome journal',
'type': 'general',
'code': 'AJ',
})
move = self.env['account.move'].create({})
self.assertEqual(move.name, 'MISC/2021/10/0001')
with Form(move) as move_form:
move_form.journal_id = journal
self.assertEqual(move.name, 'AJ/2021/10/0001')
def test_sequence_move_name_related_field_well_computed(self):
AccountMove = type(self.env['account.move'])
_compute_name = AccountMove._compute_name
def _flushing_compute_name(self):
self.env['account.move.line'].flush_model(fnames=['move_name'])
_compute_name(self)
payments = self.env['account.payment'].create([{
'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'customer',
'partner_id': self.partner_a.id,
'amount': 500,
}] * 2)
with patch.object(AccountMove, '_compute_name', _flushing_compute_name):
payments.action_post()
for move in payments.move_id:
self.assertRecordValues(move.line_ids, [{'move_name': move.name}] * len(move.line_ids))
def test_resequence_payment_and_non_payment_without_payment_sequence(self):
"""Resequence wizard could be open for different move type if the payment sequence is set to False on the journal."""
journal = self.company_data['default_journal_bank'].copy({'payment_sequence': False})
bsl = self.env['account.bank.statement.line'].create({'name': 'test', 'amount': 100, 'journal_id': journal.id})
payment = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_id': self.partner_a.id,
'amount': 100,
'journal_id': journal.id,
})
payment.action_post()
wizard = Form(self.env['account.resequence.wizard'].with_context(
active_ids=(payment.move_id + bsl.move_id).ids,
active_model='account.move'),
)
wizard.save().resequence()
self.assertTrue(wizard)
def test_change_same_journal_not_change_sequence(self):
"""Changing the journal to the same journal should not change the sequence."""
# On first move it's always have same value
self.create_move(date='2025-10-17', post=True)
move2 = self.create_move(date='2025-10-17', post=True)
# we need to create another move to higer the sequence
self.create_move(date='2025-10-17', post=True)
move2.journal_id = move2.journal_id
self.assertEqual(move2.name, 'MISC/2025/10/0002')
@tagged('post_install', '-at_install')
class TestSequenceMixinDeletion(TestSequenceMixinCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
journal = cls.env['account.journal'].create({
'name': 'Test sequences - deletion',
'code': 'SEQDEL',
'type': 'general',
})
cls.move_1_1 = cls.create_move('2021-01-01', journal, name='TOTO/2021/01/0001', post=True)
cls.move_1_2 = cls.create_move('2021-01-02', journal, post=True)
cls.move_1_3 = cls.create_move('2021-01-03', journal, post=True)
cls.move_2_1 = cls.create_move('2021-02-01', journal, post=True)
cls.move_draft = cls.create_move('2021-02-02', journal, post=False)
cls.move_2_2 = cls.create_move('2021-02-03', journal, post=True)
cls.move_3_1 = cls.create_move('2021-02-10', journal, name='TURLUTUTU/21/02/001', post=True)
def test_sequence_deletion_1(self):
"""The last element of a sequence chain should always be deletable if in draft state.
Trying to delete another part of the chain shouldn't work.
"""
# A draft move without any name can always be deleted.
self.move_draft.unlink()
# The last element of each sequence chain should allow deletion.
# Everything should be deletable if we follow this order (a bit randomized on purpose)
for move in (self.move_1_3, self.move_1_2, self.move_3_1, self.move_2_2, self.move_2_1, self.move_1_1):
move.button_draft()
move.unlink()
def test_sequence_deletion_2(self):
"""Can delete in batch."""
all_moves = (self.move_1_3 + self.move_1_2 + self.move_3_1 + self.move_2_2 + self.move_2_1 + self.move_1_1)
all_moves.button_draft()
all_moves.unlink()
@tagged('post_install', '-at_install')
class TestSequenceMixinConcurrency(TransactionCase):
def setUp(self):
super().setUp()
with self.env.registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
journal = env['account.journal'].create({
'name': 'concurency_test',
'code': 'CT',
'type': 'general',
})
account = env['account.account'].create({
'code': 'CT',
'name': 'CT',
'account_type': 'asset_fixed',
})
moves = env['account.move'].create([{
'journal_id': journal.id,
'date': fields.Date.from_string('2016-01-01'),
'line_ids': [(0, 0, {'name': 'name', 'account_id': account.id})]
}] * 3)
moves.name = '/'
moves[0].action_post()
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', '/'])
env.cr.commit()
self.data = {
'move_ids': moves.ids,
'account_id': account.id,
'journal_id': journal.id,
'envs': [
api.Environment(self.env.registry.cursor(), SUPERUSER_ID, {}),
api.Environment(self.env.registry.cursor(), SUPERUSER_ID, {}),
api.Environment(self.env.registry.cursor(), SUPERUSER_ID, {}),
],
}
self.addCleanup(self.cleanUp)
def cleanUp(self):
with self.env.registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
moves = env['account.move'].browse(self.data['move_ids'])
moves.button_draft()
moves.posted_before = False
moves.unlink()
journal = env['account.journal'].browse(self.data['journal_id'])
journal.unlink()
account = env['account.account'].browse(self.data['account_id'])
account.unlink()
env.cr.commit()
for env in self.data['envs']:
env.cr.close()
def test_sequence_concurency(self):
"""Computing the same name in concurent transactions is not allowed."""
env0, env1, env2 = self.data['envs']
# start the transactions here on cr1 to simulate concurency with cr2
env1.cr.execute('SELECT 1')
# post in cr2
move = env2['account.move'].browse(self.data['move_ids'][1])
move.action_post()
env2.cr.commit()
# try to post in cr1, the retry sould find the right number
move = env1['account.move'].browse(self.data['move_ids'][2])
move.action_post()
env1.cr.commit()
# check the values
moves = env0['account.move'].browse(self.data['move_ids'])
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', 'CT/2016/01/0002', 'CT/2016/01/0003'])
def test_sequence_concurency_no_useless_lock(self):
"""Do not lock needlessly when the sequence is not computed"""
env0, env1, env2 = self.data['envs']
# start the transactions here on cr1 to simulate concurency with cr2
env1.cr.execute('SELECT 1')
# get the last sequence in cr1 (for instance opening a form view)
move = env2['account.move'].browse(self.data['move_ids'][1])
move.highest_name
env2.cr.commit()
# post in cr1, should work even though cr2 read values
move = env1['account.move'].browse(self.data['move_ids'][2])
move.action_post()
env1.cr.commit()
# check the values
moves = env0['account.move'].browse(self.data['move_ids'])
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', 'CT/2016/01/0002'])

View file

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.exceptions import UserError
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestSettings(AccountTestInvoicingCommon):
def test_switch_taxB2B_taxB2C(self):
"""
Since having users both in the tax B2B and tax B2C groups raise,
modifications of the settings must be done in the right order;
otherwise it is impossible to change the settings.
"""
# at each setting change, all users should be removed from one group and added to the other
# so picking an arbitrary witness should be equivalent to checking that everything worked.
config = self.env['res.config.settings'].create({})
self.switch_tax_settings(config)
def switch_tax_settings(self, config):
config.show_line_subtotals_tax_selection = "tax_excluded"
config.flush_recordset()
config.execute()
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False)
config.show_line_subtotals_tax_selection = "tax_included"
config.flush_recordset()
config.execute()
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), False)
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), True)
config.show_line_subtotals_tax_selection = "tax_excluded"
config.flush_recordset()
config.execute()
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False)
def test_switch_taxB2B_taxB2C_multicompany(self):
"""
Contrarily to the (apparently reasonable) assumption that adding users
to group and removing them was symmetrical, it may not be the case
if one is done in SQL and the other via the ORM.
Because the latter automatically takes into account record rules that
might make some users invisible.
This one is identical to the previous, except that we do the actions
with a non-superuser user, and in a new company with one user in common
with another company which has a different taxB2X setting.
"""
user = self.env.ref('base.user_admin')
company = self.env['res.company'].create({'name': 'oobO'})
user.write({'company_ids': [(4, company.id)], 'company_id': company.id})
Settings = self.env['res.config.settings'].with_user(user.id)
config = Settings.create({})
self.switch_tax_settings(config)
def test_switch_company_currency(self):
"""
A user should not be able to switch the currency of another company
when that company already has posted account move lines.
"""
# Create company A (user's company)
company_a = self.env['res.company'].create({
'name': "Company A",
})
# Get company B from test setup
company_b = self.company_data['company']
# Create a purchase journal for company B
journal = self.env['account.journal'].create({
'name': "Vendor Bills Journal",
'code': "VEND",
'type': "purchase",
'company_id': company_b.id,
'currency_id': company_b.currency_id.id,
})
# Create an invoice for company B
invoice = self.env['account.move'].create({
'move_type': "in_invoice",
'company_id': company_b.id,
'journal_id': journal.id,
})
invoice.currency_id = self.env.ref('base.EUR').id
# Add a line to the invoice using an expense account
self.env['account.move.line'].create({
'move_id': invoice.id,
'account_id': self.company_data["default_account_expense"].id,
'name': "Test Invoice Line",
'company_id': company_b.id,
})
# Create a user that only belongs to company A
user = self.env['res.users'].create({
'name': "User A",
'login': "user_a@example.com",
'email': "user_a@example.com",
'company_id': company_a.id,
'company_ids': [Command.set([company_a.id])],
'groups_id': [Command.set([
self.env.ref('base.group_system').id,
self.env.ref('base.group_erp_manager').id,
self.env.ref('account.group_account_user').id,
])],
})
# Try to change company B's currency as user A (should raise UserError)
user_env = self.env(user=user)
with self.assertRaises(UserError):
user_env['res.company'].browse(company_b.id).write({
'currency_id': self.env.ref('base.EUR').id,
})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,314 @@
# -*- coding: utf-8 -*-
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TaxReportTest(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.test_country_1 = cls.env['res.country'].create({
'name': "The Old World",
'code': 'YY',
})
cls.test_country_2 = cls.env['res.country'].create({
'name': "The Principality of Zeon",
'code': 'ZZ',
})
cls.tax_report_1 = cls.env['account.report'].create({
'name': "Tax report 1",
'country_id': cls.test_country_1.id,
'column_ids': [
Command.create({
'name': "Balance",
'expression_label': 'balance',
}),
],
})
cls.tax_report_line_1_1 = cls._create_basic_tax_report_line(cls.tax_report_1, "Line 01", '01')
cls.tax_report_line_1_2 = cls._create_basic_tax_report_line(cls.tax_report_1, "Line 02", '02')
cls.tax_report_line_1_3 = cls._create_basic_tax_report_line(cls.tax_report_1, "Line 03", '03')
cls.tax_report_line_1_4 = cls._create_basic_tax_report_line(cls.tax_report_1, "Line 04", '04')
cls.tax_report_line_1_5 = cls._create_basic_tax_report_line(cls.tax_report_1, "Line 05", '05')
cls.tax_report_line_1_55 = cls._create_basic_tax_report_line(cls.tax_report_1, "Line 55", '55')
cls.tax_report_line_1_6 = cls._create_basic_tax_report_line(cls.tax_report_1, "Line 100", '100')
cls.tax_report_2 = cls.env['account.report'].create({
'name': "Tax report 2",
'country_id': cls.test_country_1.id,
'column_ids': [
Command.create({
'name': "Balance",
'expression_label': 'balance',
}),
],
})
cls.tax_report_line_2_1 = cls._create_basic_tax_report_line(cls.tax_report_2, "Line 01, but in report 2", '01')
cls.tax_report_line_2_2 = cls._create_basic_tax_report_line(cls.tax_report_2, "Line 02, but in report 2", '02')
cls.tax_report_line_2_42 = cls._create_basic_tax_report_line(cls.tax_report_2, "Line 42", '42')
cls.tax_report_line_2_6 = cls._create_basic_tax_report_line(cls.tax_report_2, "Line 100, but in report 2", '100')
@classmethod
def _create_basic_tax_report_line(cls, report, line_name, tag_name):
return cls.env['account.report.line'].create({
'name': f"[{tag_name}] {line_name}",
'report_id': report.id,
'sequence': max(report.mapped('line_ids.sequence') or [0]) + 1,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'tax_tags',
'formula': tag_name,
}),
],
})
def _get_tax_tags(self, country, tag_name=None, active_test=True):
domain = [('country_id', '=', country.id), ('applicability', '=', 'taxes')]
if tag_name:
domain.append(('name', 'like', '_' + tag_name))
return self.env['account.account.tag'].with_context(active_test=active_test).search(domain)
def test_create_shared_tags(self):
self.assertEqual(len(self._get_tax_tags(self.test_country_1, tag_name='01')), 2, "tax_tags expressions created for reports within the same countries using the same formula should create a single pair of tags.")
def test_add_expression(self):
""" Adding a tax_tags expression creates new tags.
"""
tags_before = self._get_tax_tags(self.test_country_1)
self._create_basic_tax_report_line(self.tax_report_1, "new tax_tags line", 'tournicoti')
tags_after = self._get_tax_tags(self.test_country_1)
self.assertEqual(len(tags_after), len(tags_before) + 2, "Two tags should have been created, +tournicoti and -tournicoti.")
def test_write_single_line_tagname_not_shared(self):
""" Writing on the formula of a tax_tags expression should overwrite the name of the existing tags if they are not used in other formulas.
"""
start_tags = self._get_tax_tags(self.test_country_1)
original_tag_name = self.tax_report_line_1_55.expression_ids.formula
original_tags = self.tax_report_line_1_55.expression_ids._get_matching_tags()
self.tax_report_line_1_55.expression_ids.formula = 'Mille sabords !'
new_tags = self.tax_report_line_1_55.expression_ids._get_matching_tags()
self.assertEqual(len(self._get_tax_tags(self.test_country_1, tag_name=original_tag_name)), 0, "The original formula of the expression should not correspond to any tag anymore.")
self.assertEqual(original_tags, new_tags, "The expression should still be linked to the same tags.")
self.assertEqual(len(self._get_tax_tags(self.test_country_1)), len(start_tags), "No new tag should have been created.")
def test_write_single_line_tagname_shared(self):
""" Writing on the formula of a tax_tags expression should create new tags if the formula was shared.
"""
start_tags = self._get_tax_tags(self.test_country_1)
original_tag_name = self.tax_report_line_1_1.expression_ids.formula
original_tags = self.tax_report_line_1_1.expression_ids._get_matching_tags()
self.tax_report_line_1_1.expression_ids.formula = 'Bulldozers à réaction !'
new_tags = self.tax_report_line_1_1.expression_ids._get_matching_tags()
self.assertEqual(self._get_tax_tags(self.test_country_1, tag_name=original_tag_name), original_tags, "The original tags should be unchanged")
self.assertEqual(len(self._get_tax_tags(self.test_country_1)), len(start_tags) + 2, "A + and - tag should have been created")
self.assertNotEqual(original_tags, new_tags, "New tags should have been assigned to the expression")
def test_write_multi_no_change(self):
""" Rewriting the formula of a tax_tags expression to the same value shouldn't do anything
"""
tags_before = self._get_tax_tags(self.test_country_1)
(self.tax_report_line_1_1 + self.tax_report_line_2_1).expression_ids.write({'formula': '01'})
tags_after = self._get_tax_tags(self.test_country_1)
self.assertEqual(tags_before, tags_after, "Re-assigning the same formula to a tax_tags expression should keep the same tags.")
def test_edit_multi_line_tagname_all_different_new(self):
""" Writing a new, common formula on expressions with distinct formulas should create a single pair of new + and - tag, and not
delete any of the previously-set tags (those can be archived by the user if he wants to hide them, but this way we don't loose previous
history in case we need to revert the change).
"""
lines = self.tax_report_line_1_1 + self.tax_report_line_2_2 + self.tax_report_line_2_42
tags_before = self._get_tax_tags(self.test_country_1)
lines.expression_ids.write({'formula': 'crabe'})
tags_after = self._get_tax_tags(self.test_country_1)
self.assertEqual(len(tags_before) + 2, len(tags_after), "Only two distinct tags should have been created.")
line_1_1_tags = self.tax_report_line_1_1.expression_ids._get_matching_tags()
line_2_2_tags = self.tax_report_line_2_2.expression_ids._get_matching_tags()
line_2_42_tags = self.tax_report_line_2_42.expression_ids._get_matching_tags()
self.assertTrue(line_1_1_tags == line_2_2_tags == line_2_42_tags, "The impacted expressions should now all share the same tags.")
def test_tax_report_change_country(self):
""" Tests that duplicating and modifying the country of a tax report works as intended
(countries wanting to use the tax report of another country need that).
"""
# Copy our first report
country_1_tags_before_copy = self._get_tax_tags(self.test_country_1)
copied_report_1 = self.tax_report_1.copy()
country_1_tags_after_copy = self._get_tax_tags(self.test_country_1)
self.assertEqual(country_1_tags_before_copy, country_1_tags_after_copy, "Report duplication should not create or remove any tag")
# Assign another country to one of the copies
country_2_tags_before_change = self._get_tax_tags(self.test_country_2)
copied_report_1.country_id = self.test_country_2
country_2_tags_after_change = self._get_tax_tags(self.test_country_2)
country_1_tags_after_change = self._get_tax_tags(self.test_country_1)
self.assertEqual(country_1_tags_after_change, country_1_tags_after_copy, "Modifying the country should not have changed the tags in the original country.")
self.assertEqual(len(country_2_tags_after_change), len(country_2_tags_before_change) + 2 * len(copied_report_1.line_ids), "Modifying the country should have created a new + and - tag in the new country for each tax_tags expression of the report.")
for original, copy in zip(self.tax_report_1.line_ids, copied_report_1.line_ids):
original_tags = original.expression_ids._get_matching_tags()
copy_tags = copy.expression_ids._get_matching_tags()
self.assertNotEqual(original_tags, copy_tags, "Tags matched by original and copied expressions should now be different.")
self.assertEqual(set(original_tags.mapped('name')), set(copy_tags.mapped('name')), "Tags matched by original and copied expression should have the same names.")
self.assertNotEqual(original_tags.country_id, copy_tags.country_id, "Tags matched by original and copied expression should have different countries.")
# Directly change the country of a report without copying it first (some of its tags are shared, but not all)
original_report_2_tags = {line: line.expression_ids._get_matching_tags() for line in self.tax_report_2.line_ids}
self.tax_report_2.country_id = self.test_country_2
for line in self.tax_report_2.line_ids:
line_tags = line.expression_ids._get_matching_tags()
if line == self.tax_report_line_2_42:
# This line is the only one of the report not sharing its tags
self.assertEqual(line_tags, original_report_2_tags[line], "The tax_tags expressions not sharing their tags with any other report should keep the same tags when the country of their report is changed.")
else:
# Tags already exist since 'copied_report_1' belongs to 'test_country_2'
for tag in line_tags:
self.assertIn(tag, country_2_tags_after_change, "The tax_tags expressions sharing their tags with other report should not receive new tags since they already exist.")
def test_unlink_report_line_tags_used_by_amls(self):
"""
Deletion of a report line whose tags are still referenced by an aml should archive tags and not delete them.
"""
tag_name = "55b"
tax_report_line = self._create_basic_tax_report_line(self.tax_report_1, "Line 55 bis", tag_name)
test_tag = tax_report_line.expression_ids._get_matching_tags("+")
test_tax = self.env['account.tax'].create({
'name': "Test tax",
'amount_type': 'percent',
'amount': 25,
'country_id': self.tax_report_1.country_id.id,
'type_tax_use': 'sale',
'invoice_repartition_line_ids': [
(0, 0, {'factor_percent': 100, 'repartition_type': 'base'}),
(0, 0, {
'factor_percent': 100,
'repartition_type': 'tax',
'tag_ids': [Command.link(test_tag.id)],
}),
],
'refund_repartition_line_ids': [
(0, 0, {'factor_percent': 100, 'repartition_type': 'base'}),
(0, 0, {'factor_percent': 100, 'repartition_type': 'tax'}),
],
})
# Make sure the fiscal country allows using this tax directly
self.env.company.account_fiscal_country_id = self.test_country_1
test_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '1992-12-22',
'invoice_line_ids': [
(0, 0, {'quantity': 1, 'price_unit': 42, 'tax_ids': [Command.set([test_tax.id])]}),
],
})
test_invoice.action_post()
tax_report_line.unlink()
tags_after = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
# only the +tag_name should be kept (and archived), -tag_name should be unlinked
self.assertEqual(tags_after.mapped('tax_negate'), [False], "Unlinking a tax_tags expression should keep the tag if it was used on move lines, and unlink it otherwise.")
self.assertEqual(tags_after.mapped('active'), [False], "Unlinking a tax_tags expression should archive the tag if it was used on move lines, and unlink it otherwise.")
self.assertEqual(len(test_tax.invoice_repartition_line_ids.tag_ids), 0, "After a tag is archived it shouldn't be on tax repartition lines.")
def test_unlink_report_line_tags_used_by_other_expression(self):
"""
Deletion of a report line whose tags are still referenced in other expression should not delete nor archive tags.
"""
tag_name = self.tax_report_line_1_1.expression_ids.formula # tag "O1" is used in both line 1.1 and line 2.1
tags_before = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
tags_archived_before = tags_before.filtered(lambda tag: not tag.active)
self.tax_report_line_1_1.unlink()
tags_after = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
tags_archived_after = tags_after.filtered(lambda tag: not tag.active)
self.assertEqual(len(tags_after), len(tags_before), "Unlinking a report expression whose tags are used by another expression should not delete them.")
self.assertEqual(len(tags_archived_after), len(tags_archived_before), "Unlinking a report expression whose tags are used by another expression should not archive them.")
def test_tag_recreation_archived(self):
"""
In a situation where we have only one of the two (+ and -) sign that exist
we want only the missing sign to be re-created if we try to reuse the same tag name.
(We can get into this state when only one of the signs were used by aml: then we archived it and deleted the complement.)
"""
tag_name = self.tax_report_line_1_55.expression_ids.formula
tags_before = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
tags_before[0].unlink() # we unlink one and archive the other, doesn't matter which one
tags_before[1].active = False
self._create_basic_tax_report_line(self.tax_report_1, "Line 55 bis", tag_name)
tags_after = self._get_tax_tags(self.test_country_1, tag_name=tag_name, active_test=False)
self.assertEqual(len(tags_after), 2, "When creating a tax report line with an archived tag and it's complement doesn't exist, it should be re-created.")
self.assertEqual(tags_after.mapped('name'), ['+' + tag_name, '-' + tag_name], "After creating a tax report line with an archived tag and when its complement doesn't exist, both a negative and a positive tag should be created.")
def test_change_engine_without_formula(self):
aggregation_line = self.env['account.report.line'].create({
'name': "Je ne mange pas de graines !!!",
'report_id': self.tax_report_1.id,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'aggregation',
'formula': 'Dudu',
}),
],
})
tags_before = self._get_tax_tags(self.test_country_1, tag_name='Dudu')
self.assertFalse(tags_before, "The tags shouldn't exist yet")
aggregation_line.expression_ids.engine = 'tax_tags'
tags_after = self._get_tax_tags(self.test_country_1, tag_name='Dudu')
self.assertEqual(len(tags_after), 2, "Changing the engine should have created tags")
self.assertEqual(tags_after.mapped('name'), ['-Dudu', '+Dudu'])
def test_change_engine_shared_tags(self):
aggregation_line = self.env['account.report.line'].create({
'name': "Je ne mange pas de graines !!!",
'report_id': self.tax_report_1.id,
'expression_ids': [
Command.create({
'label': 'balance',
'engine': 'aggregation',
'formula': 'Dudu',
}),
],
})
tags_before = self._get_tax_tags(self.test_country_1, tag_name='01')
self.assertEqual(len(tags_before), 2, "The tags should already exist because of another expression")
aggregation_line.expression_ids.write({'engine': 'tax_tags', 'formula': '01'})
tags_after = self._get_tax_tags(self.test_country_1, tag_name='01')
self.assertEqual(tags_after, tags_before, "No new tag should have been created")
def test_change_formula_multiple_fields(self):
tags_before = self._get_tax_tags(self.test_country_1, tag_name='Buny')
self.assertFalse(tags_before, "The tags shouldn't exist yet")
tags_to_rename = self._get_tax_tags(self.test_country_1, tag_name='55')
self.tax_report_line_1_55.expression_ids.write({
'engine': 'tax_tags', # Same value as before
'formula': 'Buny',
})
tags_after = self._get_tax_tags(self.test_country_1, tag_name='Buny')
self.assertEqual(len(tags_after), 2, "Changing the formula should have renamed the tags")
self.assertEqual(tags_after.mapped('name'), ['-Buny', '+Buny'])
self.assertEqual(tags_after, tags_to_rename, "Changing the formula should have renamed the tags")

View file

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('post_install', '-at_install')
class AccountingTestTemplConsistency(TransactionCase):
'''Test the templates consistency between some objects like account.account when account.account.template.
'''
def get_model_fields(self, model, extra_domain=None):
# Retrieve fields to compare
domain = [
('model', '=', model),
('state', '=', 'base'),
('related', '=', False),
('compute', '=', False),
('store', '=', True),
]
if extra_domain:
domain += extra_domain
return self.env['ir.model.fields'].search(domain)
def check_fields_consistency(self, model_from, model_to, exceptions=None):
'''Check the consistency of fields from one model to another by comparing if all fields
in the model_from are present in the model_to.
:param model_from: The model to compare.
:param model_to: The compared model.
:param exceptions: Not copied model's fields.
'''
extra_domain = [('name', 'not in', exceptions)] if exceptions else []
from_fields = self.get_model_fields(model_from, extra_domain=extra_domain).filtered_domain([('modules', '=', 'account')])
to_fields_set = set([f.name for f in self.get_model_fields(model_to)])
for field in from_fields:
assert field.name in to_fields_set,\
'Missing field "%s" from "%s" in model "%s".' % (field.name, model_from, model_to)
def test_account_account_fields(self):
'''Test fields consistency for ('account.account', 'account.account.template')
'''
self.check_fields_consistency(
'account.account.template', 'account.account', exceptions=['chart_template_id', 'nocreate'])
self.check_fields_consistency(
'account.account', 'account.account.template', exceptions=['company_id', 'deprecated', 'opening_debit', 'opening_credit', 'allowed_journal_ids', 'group_id', 'root_id', 'is_off_balance', 'non_trade', 'include_initial_balance', 'internal_group'])
def test_account_tax_fields(self):
'''Test fields consistency for ('account.tax', 'account.tax.template')
'''
self.check_fields_consistency('account.tax.template', 'account.tax', exceptions=['chart_template_id'])
self.check_fields_consistency('account.tax', 'account.tax.template', exceptions=['company_id', 'country_id', 'real_amount'])
self.check_fields_consistency('account.tax.repartition.line.template', 'account.tax.repartition.line', exceptions=['plus_report_expression_ids', 'minus_report_expression_ids'])
self.check_fields_consistency('account.tax.repartition.line', 'account.tax.repartition.line.template', exceptions=['tag_ids', 'country_id', 'company_id', 'sequence'])
def test_fiscal_position_fields(self):
'''Test fields consistency for ('account.fiscal.position', 'account.fiscal.position.template')
'''
#main
self.check_fields_consistency('account.fiscal.position.template', 'account.fiscal.position', exceptions=['chart_template_id'])
self.check_fields_consistency('account.fiscal.position', 'account.fiscal.position.template', exceptions=['active', 'company_id', 'states_count', 'foreign_vat'])
#taxes
self.check_fields_consistency('account.fiscal.position.tax.template', 'account.fiscal.position.tax')
self.check_fields_consistency('account.fiscal.position.tax', 'account.fiscal.position.tax.template')
#accounts
self.check_fields_consistency('account.fiscal.position.account.template', 'account.fiscal.position.account')
self.check_fields_consistency('account.fiscal.position.account', 'account.fiscal.position.account.template')
def test_reconcile_model_fields(self):
'''Test fields consistency for ('account.reconcile.model', 'account.reconcile.model.template')
'''
self.check_fields_consistency('account.reconcile.model.template', 'account.reconcile.model', exceptions=['chart_template_id'])
# exclude fields from inherited 'mail.thread'
mail_thread_fields = [field.name for field in self.get_model_fields('mail.thread')]
self.check_fields_consistency(
'account.reconcile.model',
'account.reconcile.model.template',
exceptions=mail_thread_fields + ['active', 'company_id', 'past_months_limit', 'partner_mapping_line_ids'],
)
# lines
self.check_fields_consistency('account.reconcile.model.line.template', 'account.reconcile.model.line', exceptions=['chart_template_id'])
self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution', 'amount'])
def test_account_group_fields(self):
'''Test fields consistency for ('account.group', 'account.group.template')
'''
self.check_fields_consistency('account.group', 'account.group.template', exceptions=['company_id', 'parent_path'])
self.check_fields_consistency('account.group.template', 'account.group', exceptions=['chart_template_id'])

View file

@ -0,0 +1,70 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
@odoo.tests.tagged('post_install_l10n', 'post_install', '-at_install')
class TestUi(AccountTestInvoicingCommon, odoo.tests.HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
all_moves = cls.env['account.move'].search([('move_type', '!=', 'entry')])
all_moves.button_draft()
all_moves.with_context(force_delete=True).unlink()
# In case of latam impacting multiple countries, disable the required fields manually.
if 'l10n_latam_use_documents' in cls.env['account.journal']._fields:
cls.env['account.journal']\
.search([('company_id', '=', cls.env.company.id), ('type', '=', 'purchase')])\
.write({'l10n_latam_use_documents': False})
def test_01_account_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.ref('base.user_admin').write({
'company_id': self.env.company.id,
'company_ids': [(4, self.env.company.id)],
})
self.env.company.write({
'country_id': None, # Also resets account_fiscal_country_id
'account_sale_tax_id': None,
'account_purchase_tax_id': None,
})
account_with_taxes = self.env['account.account'].search([('tax_ids', '!=', False), ('company_id', '=', self.env.company.id)])
account_with_taxes.write({
'tax_ids': [Command.clear()],
})
self.start_tour("/web", 'account_tour', login="admin")
def test_01_account_tax_groups_tour(self):
self.env.ref('base.user_admin').write({
'company_id': self.env.company.id,
'company_ids': [(4, self.env.company.id)],
})
self.env['res.partner'].create({
'name': 'Account Tax Group Partner',
'email': 'azure.Interior24@example.com',
})
product = self.env['product.product'].create({
'name': 'Account Tax Group Product',
'standard_price': 600.0,
'list_price': 147.0,
'detailed_type': 'consu',
})
new_tax = self.env['account.tax'].create({
'name': '10% Tour Tax',
'type_tax_use': 'purchase',
'amount_type': 'percent',
'amount': 10,
})
product.supplier_taxes_id = new_tax
self.start_tour("/web", 'account_tax_group', login="admin")

View file

@ -0,0 +1,562 @@
# -*- coding: utf-8 -*-
from freezegun import freeze_time
from odoo import fields, Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
import time
@tagged('post_install', '-at_install')
class TestTransferWizard(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.company = cls.company_data['company']
cls.receivable_account = cls.company_data['default_account_receivable']
cls.payable_account = cls.company_data['default_account_payable']
cls.accounts = cls.env['account.account'].search([('reconcile', '=', False), ('company_id', '=', cls.company.id)], limit=5)
cls.journal = cls.company_data['default_journal_misc']
# Set rate for base currency to 1
cls.env['res.currency.rate'].search([('company_id', '=', cls.company.id), ('currency_id', '=', cls.company.currency_id.id)]).write({'rate': 1})
# Create test currencies
cls.test_currency_1 = cls.env['res.currency'].create({
'name': "PMK",
'symbol':'P',
})
cls.test_currency_2 = cls.env['res.currency'].create({
'name': "toto",
'symbol':'To',
})
cls.test_currency_3 = cls.env['res.currency'].create({
'name': "titi",
'symbol':'Ti',
})
# Create test rates
cls.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-' + '01' + '-01',
'rate': 0.5,
'currency_id': cls.test_currency_1.id,
'company_id': cls.company.id
})
cls.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-' + '01' + '-01',
'rate': 2,
'currency_id': cls.test_currency_2.id,
'company_id': cls.company.id
})
cls.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-' + '01' + '-01',
'rate': 10,
'currency_id': cls.test_currency_3.id,
'company_id': cls.company.id
})
# Create an account using a foreign currency
cls.test_currency_account = cls.env['account.account'].create({
'name': 'test destination account',
'code': 'test.dest.acc',
'account_type': 'asset_current',
'currency_id': cls.test_currency_3.id,
})
# Create test account.move
cls.move_1 = cls.env['account.move'].create({
'journal_id': cls.journal.id,
'line_ids': [
(0, 0, {
'name': "test1_1",
'account_id': cls.receivable_account.id,
'debit': 500,
}),
(0, 0, {
'name': "test1_2",
'account_id': cls.accounts[0].id,
'credit': 500,
}),
(0, 0, {
'name': "test1_3",
'account_id': cls.accounts[0].id,
'debit': 800,
'partner_id': cls.partner_a.id,
}),
(0, 0, {
'name': "test1_4",
'account_id': cls.accounts[1].id,
'credit': 500,
}),
(0, 0, {
'name': "test1_5",
'account_id': cls.accounts[2].id,
'credit': 300,
'partner_id': cls.partner_a.id,
}),
(0, 0, {
'name': "test1_6",
'account_id': cls.accounts[0].id,
'debit': 270,
'currency_id': cls.test_currency_1.id,
'amount_currency': 540,
}),
(0, 0, {
'name': "test1_7",
'account_id': cls.accounts[1].id,
'credit': 140,
}),
(0, 0, {
'name': "test1_8",
'account_id': cls.accounts[2].id,
'credit': 160,
}),
(0, 0, {
'name': "test1_9",
'account_id': cls.accounts[2].id,
'debit': 30,
'currency_id': cls.test_currency_2.id,
'amount_currency': 15,
}),
]
})
cls.move_1.action_post()
cls.move_2 = cls.env['account.move'].create({
'journal_id': cls.journal.id,
'line_ids': [
(0, 0, {
'name': "test2_1",
'account_id': cls.accounts[1].id,
'debit': 400,
}),
(0, 0, {
'name': "test2_2",
'account_id': cls.payable_account.id,
'credit': 400,
}),
(0, 0, {
'name': "test2_3",
'account_id': cls.accounts[3].id,
'debit': 250,
'partner_id': cls.partner_a.id,
}),
(0, 0, {
'name': "test2_4",
'account_id': cls.accounts[1].id,
'debit': 480,
'partner_id': cls.partner_b.id,
}),
(0, 0, {
'name': "test2_5",
'account_id': cls.accounts[2].id,
'credit': 730,
'partner_id': cls.partner_a.id,
}),
(0, 0, {
'name': "test2_6",
'account_id': cls.accounts[2].id,
'credit': 412,
'partner_id': cls.partner_a.id,
'currency_id': cls.test_currency_2.id,
'amount_currency': -633,
}),
(0, 0, {
'name': "test2_7",
'account_id': cls.accounts[1].id,
'debit': 572,
}),
(0, 0, {
'name': "test2_8",
'account_id': cls.accounts[2].id,
'credit': 100,
'partner_id': cls.partner_a.id,
'currency_id': cls.test_currency_2.id,
'amount_currency': -123,
}),
(0, 0, {
'name': "test2_9",
'account_id': cls.accounts[2].id,
'credit': 60,
'partner_id': cls.partner_a.id,
'currency_id': cls.test_currency_1.id,
'amount_currency': -10,
}),
]
})
cls.move_2.action_post()
analytic_plan_1, analytic_plan_2 = cls.env['account.analytic.plan'].create([
{'name': 'Plan Test 1', 'company_id': False},
{'name': 'Plan Test 2', 'company_id': False},
])
cls.analytic_account_1, cls.analytic_account_2 = cls.env['account.analytic.account'].create([
{
'name': 'test_analytic_account_1',
'plan_id': analytic_plan_1.id,
'code': 'TESTEUH1'
},
{
'name': 'test_analytic_account_2',
'plan_id': analytic_plan_2.id,
'code': 'TESTEUH2'
},
])
@freeze_time('2024-03-13')
def test_transfer_default_tax(self):
""" Make sure default taxes on accounts are not computed on transfer moves
"""
account_with_tax = self.env['account.account'].create({
'name': 'Auto Taxed',
'code': 'autotaxed',
'account_type': 'expense',
'tax_ids': [Command.link(self.company_data['default_tax_purchase'].id)],
})
expense_accrual_account = self.env['account.account'].create({
'name': 'Accrual Expense Account',
'code': '234567',
'account_type': 'expense',
'reconcile': True,
})
move_with_tax = self.env['account.move'].create({
'journal_id': self.journal.id,
'line_ids': [
Command.create({
'account_id': account_with_tax.id,
'balance': 400,
}),
Command.create({
'account_id': self.payable_account.id,
'balance': -460,
}),
]
})
move_with_tax.action_post()
self.assertRecordValues(move_with_tax.line_ids, [
{'balance': 400, 'account_id': account_with_tax.id},
{'balance': -460, 'account_id': self.payable_account.id},
{'balance': 60, 'account_id': self.company_data['default_account_tax_purchase'].id},
])
# Open the transfer wizard
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': move_with_tax.line_ids[0].ids}
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_period'
wizard_form.date = '2019-05-01'
wizard_form.journal_id = self.company_data['default_journal_misc']
wizard_form.expense_accrual_account = expense_accrual_account
wizard = wizard_form.save()
# Create the adjustment moves.
wizard_res = wizard.do_action()
# Check that the adjustment moves only contain the expense account and not the linked taxes.
created_moves = self.env['account.move'].browse(wizard_res['domain'][0][2])
self.assertRecordValues(created_moves[0].line_ids, [
{'balance': 400, 'account_id': account_with_tax.id},
{'balance': -400, 'account_id': expense_accrual_account.id},
])
self.assertRecordValues(created_moves[1].line_ids, [
{'balance': -400, 'account_id': account_with_tax.id},
{'balance': 400, 'account_id': expense_accrual_account.id},
])
def test_transfer_wizard_reconcile(self):
""" Tests reconciliation when doing a transfer with the wizard
"""
active_move_lines = (self.move_1 + self.move_2).mapped('line_ids').filtered(lambda x: x.account_id.account_type in ('asset_receivable', 'liability_payable'))
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.receivable_account
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
payable_transfer = transfer_move.line_ids.filtered(lambda x: x.account_id == self.payable_account)
receivable_transfer = transfer_move.line_ids.filtered(lambda x: x.account_id == self.receivable_account)
self.assertTrue(payable_transfer.reconciled, "Payable line of the transfer move should be fully reconciled")
self.assertAlmostEqual(self.move_1.line_ids.filtered(lambda x: x.account_id == self.receivable_account).amount_residual, 100, self.company.currency_id.decimal_places, "Receivable line of the original move should be partially reconciled, and still have a residual amount of 100 (500 - 400 from payable account)")
self.assertTrue(self.move_2.line_ids.filtered(lambda x: x.account_id == self.payable_account).reconciled, "Payable line of the original move should be fully reconciled")
self.assertAlmostEqual(receivable_transfer.amount_residual, 0, self.company.currency_id.decimal_places, "Receivable line from the transfer move should have nothing left to reconcile")
self.assertAlmostEqual(payable_transfer.debit, 400, self.company.currency_id.decimal_places, "400 should have been debited from payable account to apply the transfer")
self.assertAlmostEqual(receivable_transfer.credit, 400, self.company.currency_id.decimal_places, "400 should have been credited to receivable account to apply the transfer")
def test_transfer_wizard_grouping(self):
""" Tests grouping (by account and partner) when doing a transfer with the wizard
"""
active_move_lines = (self.move_1 + self.move_2).mapped('line_ids').filtered(lambda x: x.name in ('test1_3', 'test1_4', 'test1_5', 'test2_3', 'test2_4', 'test2_5', 'test2_6', 'test2_8'))
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.accounts[4]
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
groups = {}
for line in transfer_move.line_ids:
key = (line.account_id, line.partner_id or None, line.currency_id)
self.assertFalse(groups.get(key), "There should be only one line per (account, partner, currency) group in the transfer move.")
groups[key] = line
self.assertAlmostEqual(groups[(self.accounts[0], self.partner_a, self.company_data['currency'])].balance, -800, self.company.currency_id.decimal_places)
self.assertAlmostEqual(groups[(self.accounts[1], None, self.company_data['currency'])].balance, 500, self.company.currency_id.decimal_places)
self.assertAlmostEqual(groups[(self.accounts[1], self.partner_b, self.company_data['currency'])].balance, -480, self.company.currency_id.decimal_places)
self.assertAlmostEqual(groups[(self.accounts[2], self.partner_a, self.company_data['currency'])].balance, 1030, self.company.currency_id.decimal_places)
self.assertAlmostEqual(groups[(self.accounts[2], self.partner_a, self.test_currency_2)].balance, 512, self.company.currency_id.decimal_places)
self.assertAlmostEqual(groups[(self.accounts[3], self.partner_a, self.company_data['currency'])].balance, -250, self.company.currency_id.decimal_places)
def test_transfer_wizard_currency_conversion(self):
""" Tests multi currency use of the transfer wizard, checking the conversion
is propperly done when using a destination account with a currency_id set.
"""
active_move_lines = self.move_1.mapped('line_ids').filtered(lambda x: x.name in ('test1_6', 'test1_9'))
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.test_currency_account
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
destination_line = transfer_move.line_ids.filtered(lambda x: x.account_id == self.test_currency_account)
self.assertEqual(destination_line.currency_id, self.test_currency_3, "Transferring to an account with a currency set should keep this currency on the transfer line.")
self.assertAlmostEqual(destination_line.amount_currency, 3000, self.company.currency_id.decimal_places, "Transferring two lines with different currencies (and the same partner) on an account with a currency set should convert the balance of these lines into this account's currency (here (270 + 30) * 10 = 3000)")
def test_transfer_wizard_no_currency_conversion(self):
""" Tests multi currency use of the transfer wizard, verifying that
currency amounts are kept on distinct lines when transferring to an
account without any currency specified.
"""
active_move_lines = self.move_2.mapped('line_ids').filtered(lambda x: x.name in ('test2_9', 'test2_6', 'test2_8'))
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.receivable_account
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
destination_lines = transfer_move.line_ids.filtered(lambda x: x.account_id == self.receivable_account)
self.assertEqual(len(destination_lines), 2, "Two lines should have been created on destination account: one for each currency (the lines with same partner and currency should have been aggregated)")
self.assertAlmostEqual(destination_lines.filtered(lambda x: x.currency_id == self.test_currency_1).amount_currency, -10, self.test_currency_1.decimal_places)
self.assertAlmostEqual(destination_lines.filtered(lambda x: x.currency_id == self.test_currency_2).amount_currency, -756, self.test_currency_2.decimal_places)
def test_period_change_lock_date(self):
""" Test that the period change wizard correctly handles the lock date: if the original entry is dated
before the lock date, the adjustment entry is created on the first end of month after the lock date.
"""
# Set up accrual accounts
self.company_data['company'].expense_accrual_account_id = self.env['account.account'].create({
'name': 'Expense Accrual Account',
'code': '113226',
'account_type': 'asset_prepayments',
'reconcile': True,
})
self.company_data['company'].revenue_accrual_account_id = self.env['account.account'].create({
'name': 'Revenue Accrual Account',
'code': '226113',
'account_type': 'liability_current',
'reconcile': True,
})
# Create a move before the lock date
move = self.env['account.move'].create({
'journal_id': self.company_data['default_journal_sale'].id,
'date': '2019-01-01',
'line_ids': [
Command.create({'account_id': self.accounts[0].id, 'debit': 1000, }),
Command.create({'account_id': self.accounts[0].id, 'credit': 1000, }),
]
})
move.action_post()
# Set the lock date
move.company_id.write({'period_lock_date': '2019-02-28', 'fiscalyear_lock_date': '2019-02-28'})
# Open the transfer wizard at a date after the lock date
wizard = self.env['account.automatic.entry.wizard'] \
.with_context(active_model='account.move.line', active_ids=move.line_ids[0].ids) \
.create({
'action': 'change_period',
'date': '2019-05-01',
'journal_id': self.company_data['default_journal_misc'].id,
})
# Check that the 'The date is being set prior to the user lock date' message appears.
self.assertRecordValues(wizard, [{
'lock_date_message': 'The date is being set prior to the user lock date 02/28/2019. '
'The Journal Entry will be accounted on 03/31/2019 upon posting.'
}])
# Create the adjustment move.
wizard_res = wizard.do_action()
# Check that the adjustment move was created on the first end of month after the lock date.
created_moves = self.env['account.move'].browse(wizard_res['domain'][0][2])
adjustment_move = created_moves[1] # There are 2 created moves; the adjustment move is the second one.
self.assertRecordValues(adjustment_move, [{'date': fields.Date.to_date('2019-03-31')}])
def test_period_change_tax_lock_date(self):
""" If there is only a tax lock date, we should be able to proceed with the flow"""
move = self.env['account.move'].create({
'journal_id': self.company_data['default_journal_sale'].id,
'date': '2019-01-01',
'line_ids': [
# Base Tax line
Command.create({
'debit': 0.0,
'credit': 100.0,
'account_id': self.company_data['default_account_revenue'].id,
'tax_ids': [(6, 0, self.tax_sale_a.ids)],
}),
# Tax line
Command.create({
'debit': 0.0,
'credit': 15.0,
'account_id': self.accounts[0].id,
}),
# Receivable line
Command.create({
'debit': 115,
'credit': 0.0,
'account_id': self.receivable_account.id,
}),
]
})
move.action_post()
# Set the tax lock date
move.company_id.write({'tax_lock_date': '2019-02-28'})
# Open the transfer wizard at a date after the lock date
wizard = self.env['account.automatic.entry.wizard'] \
.with_context(active_model='account.move.line', active_ids=move.line_ids[0].ids) \
.create({
'action': 'change_period',
'date': '2019-05-01',
'journal_id': self.company_data['default_journal_misc'].id,
})
# Check that there is no lock message
self.assertRecordValues(wizard, [{
'lock_date_message': False,
}])
def test_transfer_wizard_amount_currency_is_zero(self):
""" Tests that the transfer wizard create a transfer move when the amount_currency is zero.
"""
move = self.env['account.move'].create({
'journal_id': self.company_data['default_journal_misc'].id,
'date': '2019-01-01',
'line_ids': [
Command.create({'account_id': self.accounts[2].id, 'currency_id': self.company.currency_id.id, 'amount_currency': 1000, 'debit': 1000, }),
Command.create({'account_id': self.receivable_account.id, 'currency_id': self.test_currency_1.id, 'amount_currency': 0, 'credit': 1000, }),
]
})
move.action_post()
active_move_lines = move.line_ids.filtered(lambda line: line.account_id.id == self.receivable_account.id)
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.accounts[0]
wizard_form.journal_id = self.company_data['default_journal_misc']
wizard = wizard_form.save()
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
source_line = transfer_move.line_ids.filtered(lambda x: x.account_id == self.receivable_account)
destination_line = transfer_move.line_ids.filtered(lambda x: x.account_id == self.accounts[0])
self.assertRecordValues(source_line, [
{'account_id': self.receivable_account.id, 'amount_currency': 0.0, 'currency_id': self.test_currency_1.id, 'balance': 1000}
])
self.assertRecordValues(destination_line, [
{'account_id': self.accounts[0].id, 'amount_currency': 0.0, 'currency_id': self.test_currency_1.id, 'balance': -1000}
])
def test_transfer_wizard_analytic(self):
""" Tests that the analytic distribution is transmitted when doing a transfer with the wizard """
invoice = self.env['account.move'].create([
{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2017-01-01',
'journal_id': self.company_data['default_journal_sale'].id,
'invoice_line_ids': [
Command.create({
'quantity': 1,
'price_unit': 1000.0,
'account_id': self.company_data['default_account_revenue'].id,
'analytic_distribution': {self.analytic_account_1.id: 100},
}),
Command.create({
'quantity': 1,
'price_unit': 2000.0,
'account_id': self.company_data['default_account_revenue'].id,
'analytic_distribution': {self.analytic_account_1.id: 50, self.analytic_account_2.id: 50},
}),
Command.create({
'quantity': 1,
'price_unit': 1000.0,
'account_id': self.company_data['default_account_revenue'].id,
'analytic_distribution': False,
}),
],
}
])
invoice.action_post()
wizard = self.env['account.automatic.entry.wizard'].with_context(
active_model='account.move.line',
active_ids=invoice.invoice_line_ids.ids
).create({
'action': 'change_account',
'date': '2018-01-01',
'journal_id': self.journal.id,
'destination_account_id': self.receivable_account.id,
})
transfer_move = self.env['account.move'].browse(wizard.do_action()['res_id'])
self.assertRecordValues(transfer_move.line_ids, [
{'balance': -4000, 'analytic_distribution': {str(self.analytic_account_1.id): 50, str(self.analytic_account_2.id): 25}},
{'balance': 1000, 'analytic_distribution': {str(self.analytic_account_1.id): 100}},
{'balance': 2000, 'analytic_distribution': {str(self.analytic_account_1.id): 50, str(self.analytic_account_2.id): 50}},
{'balance': 1000, 'analytic_distribution': False},
])