mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-21 05:02:07 +02:00
Initial commit: Accounting packages
This commit is contained in:
commit
4ef34c2317
2661 changed files with 1709616 additions and 0 deletions
45
odoo-bringout-oca-ocb-account/account/tests/__init__.py
Normal file
45
odoo-bringout-oca-ocb-account/account/tests/__init__.py
Normal 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
|
||||
778
odoo-bringout-oca-ocb-account/account/tests/common.py
Normal file
778
odoo-bringout-oca-ocb-account/account/tests/common.py
Normal 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
|
||||
)
|
||||
|
|
@ -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},
|
||||
])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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."
|
||||
)
|
||||
|
|
@ -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)
|
||||
1023
odoo-bringout-oca-ocb-account/account/tests/test_account_payment.py
Normal file
1023
odoo-bringout-oca-ocb-account/account/tests/test_account_payment.py
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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']
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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',
|
||||
}])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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},
|
||||
])
|
||||
|
|
@ -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])
|
||||
|
|
@ -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")
|
||||
421
odoo-bringout-oca-ocb-account/account/tests/test_payment_term.py
Normal file
421
odoo-bringout-oca-ocb-account/account/tests/test_payment_term.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
37
odoo-bringout-oca-ocb-account/account/tests/test_product.py
Normal file
37
odoo-bringout-oca-ocb-account/account/tests/test_product.py
Normal 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
|
|
@ -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'])
|
||||
118
odoo-bringout-oca-ocb-account/account/tests/test_settings.py
Normal file
118
odoo-bringout-oca-ocb-account/account/tests/test_settings.py
Normal 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,
|
||||
})
|
||||
1221
odoo-bringout-oca-ocb-account/account/tests/test_tax.py
Normal file
1221
odoo-bringout-oca-ocb-account/account/tests/test_tax.py
Normal file
File diff suppressed because it is too large
Load diff
314
odoo-bringout-oca-ocb-account/account/tests/test_tax_report.py
Normal file
314
odoo-bringout-oca-ocb-account/account/tests/test_tax_report.py
Normal 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")
|
||||
|
|
@ -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'])
|
||||
70
odoo-bringout-oca-ocb-account/account/tests/test_tour.py
Normal file
70
odoo-bringout-oca-ocb-account/account/tests/test_tour.py
Normal 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")
|
||||
|
|
@ -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},
|
||||
])
|
||||
Loading…
Add table
Add a link
Reference in a new issue