1008 lines
38 KiB
Python
1008 lines
38 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from freezegun import freeze_time
|
|
from datetime import timedelta
|
|
|
|
from odoo import fields
|
|
from odoo.fields import Command
|
|
from odoo.tests import Form, tagged
|
|
from odoo.tools import float_compare, mute_logger, float_round
|
|
|
|
from odoo.addons.sale.tests.common import SaleCommon
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestSalePrices(SaleCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.discount = 10 # %
|
|
|
|
# Needed when run without demo data
|
|
# s.t. taxes creation doesn't fail
|
|
belgium = cls.env.ref('base.be')
|
|
cls.env.company.account_fiscal_country_id = belgium
|
|
for model in ('account.tax', 'account.tax.group'):
|
|
cls.env.add_to_compute(
|
|
cls.env[model]._fields['country_id'],
|
|
cls.env[model].search([('company_id', '=', cls.env.company.id)]),
|
|
)
|
|
|
|
def _create_discount_pricelist_rule(self, **additional_values):
|
|
return self.env['product.pricelist.item'].create({
|
|
'pricelist_id': self.pricelist.id,
|
|
'compute_price': 'percentage',
|
|
'percent_price': self.discount,
|
|
**additional_values,
|
|
})
|
|
|
|
def test_pricelist_minimal_qty(self):
|
|
""" Verify the quantity and uom are correctly provided to the pricelist API"""
|
|
pricelist_rule = self._create_discount_pricelist_rule(
|
|
min_quantity=4.0,
|
|
)
|
|
product_price = self.product.lst_price
|
|
product_dozen_price = product_price * 12
|
|
discount = 1 - self.discount/100
|
|
|
|
self.empty_order.order_line = [
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 3.0,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 4.0,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 5.0,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 1.0,
|
|
'product_uom': self.uom_dozen.id,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 0.4,
|
|
'product_uom': self.uom_dozen.id,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 0.3,
|
|
'product_uom': self.uom_dozen.id,
|
|
})
|
|
]
|
|
|
|
discounted_lines = self.empty_order.order_line.filtered('pricelist_item_id')
|
|
self.assertEqual(discounted_lines, self.empty_order.order_line[1:5])
|
|
self.assertEqual(discounted_lines.pricelist_item_id, pricelist_rule)
|
|
self.assertTrue(all(not line.discount for line in self.empty_order.order_line))
|
|
self.assertEqual(
|
|
discounted_lines.mapped('price_unit'),
|
|
[
|
|
product_price*discount,
|
|
product_price*discount,
|
|
product_dozen_price*discount,
|
|
product_dozen_price*discount
|
|
]
|
|
)
|
|
|
|
self.pricelist.discount_policy = 'without_discount'
|
|
self.empty_order._recompute_prices()
|
|
self.assertEqual(
|
|
discounted_lines.mapped('price_unit'),
|
|
[product_price, product_price, product_dozen_price, product_dozen_price])
|
|
self.assertEqual(discounted_lines.mapped('discount'), [self.discount]*len(discounted_lines))
|
|
|
|
def test_pricelist_dates(self):
|
|
""" Verify the order date is correctly provided to the pricelist API"""
|
|
today = fields.Datetime.today()
|
|
tomorrow = today + timedelta(days=1)
|
|
|
|
pricelist_rule = self._create_discount_pricelist_rule(
|
|
date_start=today - timedelta(hours=1),
|
|
date_end=today + timedelta(hours=23),
|
|
)
|
|
|
|
with freeze_time(today):
|
|
# Create an order today, add line today, rule active today works
|
|
self.empty_order.date_order = today
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
|
|
self.assertEqual(order_line.pricelist_item_id, pricelist_rule)
|
|
self.assertEqual(
|
|
order_line.price_unit,
|
|
self.product.lst_price * (1 - self.discount / 100.0))
|
|
self.assertEqual(order_line.discount, 0.0)
|
|
|
|
# Create an order tomorrow, add line today, rule active today doesn't work
|
|
self.empty_order.date_order = tomorrow
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
|
|
self.assertFalse(order_line.pricelist_item_id)
|
|
self.assertEqual(order_line.price_unit, self.product.lst_price)
|
|
self.assertEqual(order_line.discount, 0.0)
|
|
|
|
with freeze_time(tomorrow):
|
|
# Create an order tomorrow, add line tomorrow, rule active today doesn't work
|
|
self.empty_order.date_order = tomorrow
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
|
|
self.assertFalse(order_line.pricelist_item_id)
|
|
self.assertEqual(order_line.price_unit, self.product.lst_price)
|
|
self.assertEqual(order_line.discount, 0.0)
|
|
|
|
# Create an order today, add line tomorrow, rule active today works
|
|
self.empty_order.date_order = today
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
|
|
self.assertEqual(order_line.pricelist_item_id, pricelist_rule)
|
|
self.assertEqual(
|
|
order_line.price_unit,
|
|
self.product.lst_price * (1 - self.discount / 100.0))
|
|
self.assertEqual(order_line.discount, 0.0)
|
|
|
|
self.assertEqual(
|
|
self.empty_order.amount_untaxed,
|
|
self.product.lst_price * 3.8) # Discount of 10% on 2 of the 4 sol
|
|
|
|
def test_pricelist_product_context(self):
|
|
""" Verify that the product attributes extra prices are correctly considered """
|
|
no_variant_attribute = self.env['product.attribute'].create({
|
|
'name': 'No Variant Test Attribute',
|
|
'create_variant': 'no_variant',
|
|
'value_ids': [
|
|
Command.create({'name': 'A'}),
|
|
Command.create({'name': 'B'}),
|
|
Command.create({'name': 'C'}),
|
|
],
|
|
})
|
|
product_template = self.env['product.template'].create({
|
|
'name': 'Test Template with no_variant attributes',
|
|
'categ_id': self.product_category.id,
|
|
'attribute_line_ids': [
|
|
Command.create({
|
|
'attribute_id': no_variant_attribute.id,
|
|
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
|
|
}),
|
|
],
|
|
'list_price': 75.0,
|
|
'taxes_id': False,
|
|
})
|
|
|
|
# Specify an extra_price on a variant
|
|
ptavs = product_template.attribute_line_ids.product_template_value_ids
|
|
ptavs[0].price_extra = 5.0
|
|
ptavs[2].price_extra = 25.0
|
|
|
|
self.empty_order.order_line = [
|
|
Command.create({
|
|
'product_id': product_template.product_variant_id.id,
|
|
'product_no_variant_attribute_value_ids': [Command.link(ptav.id)]
|
|
})
|
|
for ptav in ptavs
|
|
]
|
|
|
|
order_lines = self.empty_order.order_line
|
|
self.assertEqual(order_lines[0].price_unit, 80.0)
|
|
self.assertEqual(order_lines[1].price_unit, 75.0)
|
|
self.assertEqual(order_lines[2].price_unit, 100.0)
|
|
|
|
def test_no_pricelist_rules(self):
|
|
"""Check currencies and uom conversions when no pricelist rule is available"""
|
|
# UoM Conversion
|
|
# Selling dozens => price_unit = 12*price by unit
|
|
self.empty_order.order_line = [
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom': self.uom_dozen.id,
|
|
'product_uom_qty': 2.0,
|
|
}),
|
|
]
|
|
self.assertEqual(self.empty_order.order_line.price_unit, 240.0)
|
|
|
|
other_currency = self._enable_currency('EUR')
|
|
pricelist_in_other_curr = self.env['product.pricelist'].create({
|
|
'name': 'Test Pricelist (EUR)',
|
|
'currency_id': other_currency.id,
|
|
})
|
|
with freeze_time('2022-08-19'):
|
|
self.env['res.currency.rate'].create({
|
|
'name': fields.Date.today(),
|
|
'rate': 2.0,
|
|
'currency_id': other_currency.id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
order_in_other_currency = self.env['sale.order'].create({
|
|
'partner_id': self.partner.id,
|
|
'pricelist_id': pricelist_in_other_curr.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom': self.uom_dozen.id,
|
|
'product_uom_qty': 2.0,
|
|
}),
|
|
]
|
|
})
|
|
# 20.0 (product price) * 24.0 (2 dozens) * 2.0 (price rate USD -> EUR)
|
|
self.assertEqual(order_in_other_currency.amount_total, 960.0)
|
|
|
|
def test_negative_discounts(self):
|
|
"""aka surcharges"""
|
|
self.discount = -10
|
|
rule = self._create_discount_pricelist_rule()
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
self.assertEqual(order_line.price_unit, 22.0)
|
|
self.assertEqual(order_line.pricelist_item_id, rule)
|
|
|
|
# Even when the discount is supposed to be shown
|
|
# Surcharges shouldn't be shown to the user
|
|
self.pricelist.discount_policy = 'without_discount'
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
self.assertEqual(order_line.price_unit, 22.0)
|
|
self.assertEqual(order_line.pricelist_item_id, rule)
|
|
|
|
def test_pricelist_based_on_another(self):
|
|
""" Test price and discount are correctly applied with a pricelist based on an other one"""
|
|
self.product.lst_price = 100
|
|
|
|
base_pricelist = self.env['product.pricelist'].create({
|
|
'name': 'First pricelist',
|
|
'discount_policy': 'without_discount',
|
|
'item_ids': [Command.create({
|
|
'compute_price': 'percentage',
|
|
'base': 'list_price',
|
|
'percent_price': 10,
|
|
'applied_on': '3_global',
|
|
'name': 'First discount',
|
|
})],
|
|
})
|
|
|
|
self.pricelist.write({
|
|
'discount_policy': 'without_discount',
|
|
'item_ids': [Command.create({
|
|
'compute_price': 'formula',
|
|
'base': 'pricelist',
|
|
'base_pricelist_id': base_pricelist.id,
|
|
'price_discount': 10,
|
|
'applied_on': '3_global',
|
|
'name': 'Second discount',
|
|
})],
|
|
})
|
|
|
|
self.empty_order.write({
|
|
'date_order': '2018-07-11',
|
|
})
|
|
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
|
|
self.assertEqual(order_line.pricelist_item_id, self.pricelist.item_ids)
|
|
self.assertEqual(order_line.price_subtotal, 81, "Second pricelist rule not applied")
|
|
self.assertEqual(order_line.discount, 19, "Second discount not applied")
|
|
|
|
def test_pricelist_with_another_currency(self):
|
|
""" Test prices are correctly applied with a pricelist with another currency"""
|
|
self.product.lst_price = 100
|
|
|
|
currency_eur = self._enable_currency('EUR')
|
|
self.env['res.currency.rate'].create({
|
|
'name': '2018-07-11',
|
|
'rate': 2.0,
|
|
'currency_id': currency_eur.id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
with mute_logger('odoo.models.unlink'):
|
|
self.env['res.currency.rate'].search(
|
|
[('currency_id', '=', self.env.company.currency_id.id)]
|
|
).unlink()
|
|
new_uom = self.env['uom.uom'].create({
|
|
'name': '10 units',
|
|
'factor_inv': 10,
|
|
'uom_type': 'bigger',
|
|
'rounding': 1.0,
|
|
'category_id': self.uom_unit.category_id.id,
|
|
})
|
|
|
|
# This pricelist doesn't show the discount
|
|
pricelist_eur = self.env['product.pricelist'].create({
|
|
'name': 'First pricelist',
|
|
'currency_id': currency_eur.id,
|
|
'discount_policy': 'with_discount',
|
|
'item_ids': [Command.create({
|
|
'compute_price': 'percentage',
|
|
'base': 'list_price',
|
|
'percent_price': 10,
|
|
'applied_on': '3_global',
|
|
'name': 'First discount'
|
|
})],
|
|
})
|
|
|
|
self.empty_order.write({
|
|
'date_order': '2018-07-12',
|
|
'pricelist_id': pricelist_eur.id,
|
|
})
|
|
|
|
order_line = self.env['sale.order.line'].create({
|
|
'order_id': self.empty_order.id,
|
|
'product_id': self.product.id,
|
|
})
|
|
|
|
# force compute uom and prices
|
|
self.assertEqual(order_line.price_unit, 180, "First pricelist rule not applied")
|
|
order_line.product_uom = new_uom
|
|
self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied")
|
|
|
|
def test_multi_currency_discount(self):
|
|
"""Verify the currency used for pricelist price & discount computation."""
|
|
product_1 = self.consumable_product
|
|
product_2 = self.service_product
|
|
|
|
# Make sure the company is in USD
|
|
main_company = self.env.ref('base.main_company')
|
|
main_curr = main_company.currency_id
|
|
current_curr = self.env.company.currency_id # USD
|
|
other_curr = self._enable_currency('EUR')
|
|
# main_company.currency_id = other_curr # product.currency_id when no company_id set
|
|
other_company = self.env['res.company'].create({
|
|
'name': 'Test',
|
|
'currency_id': other_curr.id
|
|
})
|
|
user_in_other_company = self.env['res.users'].create({
|
|
'company_id': other_company.id,
|
|
'company_ids': [Command.set([other_company.id])],
|
|
'name': 'E.T',
|
|
'login': 'hohoho',
|
|
})
|
|
with mute_logger('odoo.models.unlink'):
|
|
self.env['res.currency.rate'].search([]).unlink()
|
|
self.env['res.currency.rate'].create({
|
|
'name': '2010-01-01',
|
|
'rate': 2.0,
|
|
'currency_id': main_curr.id,
|
|
'company_id': False,
|
|
})
|
|
|
|
product_1.company_id = False
|
|
product_2.company_id = False
|
|
|
|
self.assertEqual(product_1.currency_id, main_curr)
|
|
self.assertEqual(product_2.currency_id, main_curr)
|
|
self.assertEqual(product_1.cost_currency_id, current_curr)
|
|
self.assertEqual(product_2.cost_currency_id, current_curr)
|
|
|
|
product_1_ctxt = product_1.with_user(user_in_other_company)
|
|
product_2_ctxt = product_2.with_user(user_in_other_company)
|
|
self.assertEqual(product_1_ctxt.currency_id, main_curr)
|
|
self.assertEqual(product_2_ctxt.currency_id, main_curr)
|
|
self.assertEqual(product_1_ctxt.cost_currency_id, other_curr)
|
|
self.assertEqual(product_2_ctxt.cost_currency_id, other_curr)
|
|
|
|
product_1.lst_price = 100.0
|
|
product_2_ctxt.standard_price = 10.0 # cost is company_dependent
|
|
|
|
pricelist = self.env['product.pricelist'].create({
|
|
'name': 'Test multi-currency',
|
|
'discount_policy': 'without_discount',
|
|
'currency_id': other_curr.id,
|
|
'item_ids': [
|
|
Command.create({
|
|
'base': 'list_price',
|
|
'product_id': product_1.id,
|
|
'compute_price': 'percentage',
|
|
'percent_price': 20,
|
|
}),
|
|
Command.create({
|
|
'base': 'standard_price',
|
|
'product_id': product_2.id,
|
|
'compute_price': 'percentage',
|
|
'percent_price': 10,
|
|
})
|
|
]
|
|
})
|
|
|
|
# Create a SO in the other company
|
|
##################################
|
|
# product_currency = main_company.currency_id when no company_id on the product
|
|
|
|
# CASE 1:
|
|
# company currency = so currency
|
|
# product_1.currency != so currency
|
|
# product_2.cost_currency_id = so currency
|
|
sales_order = product_1_ctxt.with_context(mail_notrack=True, mail_create_nolog=True).env['sale.order'].create({
|
|
'partner_id': self.env.user.partner_id.id,
|
|
'pricelist_id': pricelist.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': product_1.id,
|
|
'product_uom_qty': 1.0
|
|
}),
|
|
Command.create({
|
|
'product_id': product_2.id,
|
|
'product_uom_qty': 1.0
|
|
})
|
|
]
|
|
})
|
|
|
|
so_line_1 = sales_order.order_line[0]
|
|
so_line_2 = sales_order.order_line[1]
|
|
self.assertEqual(so_line_1.discount, 20)
|
|
self.assertEqual(so_line_1.price_unit, 50.0)
|
|
self.assertEqual(so_line_2.discount, 10)
|
|
self.assertEqual(so_line_2.price_unit, 10)
|
|
|
|
# CASE 2
|
|
# company currency != so currency
|
|
# product_1.currency == so currency
|
|
# product_2.cost_currency_id != so currency
|
|
pricelist.currency_id = main_curr
|
|
sales_order = product_1_ctxt.with_context(mail_notrack=True, mail_create_nolog=True).env['sale.order'].create({
|
|
'partner_id': self.env.user.partner_id.id,
|
|
'pricelist_id': pricelist.id,
|
|
'order_line': [
|
|
# Verify discount is considered in create hack
|
|
Command.create({
|
|
'product_id': product_1.id,
|
|
'product_uom_qty': 1.0
|
|
}),
|
|
Command.create({
|
|
'product_id': product_2.id,
|
|
'product_uom_qty': 1.0
|
|
})
|
|
]
|
|
})
|
|
|
|
so_line_1 = sales_order.order_line[0]
|
|
so_line_2 = sales_order.order_line[1]
|
|
self.assertEqual(so_line_1.discount, 20)
|
|
self.assertEqual(so_line_1.price_unit, 100.0)
|
|
self.assertEqual(so_line_2.discount, 10)
|
|
self.assertEqual(so_line_2.price_unit, 20)
|
|
|
|
def test_update_prices(self):
|
|
"""Test prices recomputation on SO's.
|
|
|
|
`_recompute_prices` is shown as a button to update
|
|
prices when the pricelist was changed.
|
|
"""
|
|
sale_order = self.sale_order
|
|
so_amount = sale_order.amount_total
|
|
start_so_amount = so_amount
|
|
sale_order._recompute_prices()
|
|
self.assertEqual(
|
|
sale_order.amount_total, so_amount,
|
|
"Updating the prices of an unmodified SO shouldn't modify the amounts")
|
|
|
|
pricelist = sale_order.pricelist_id
|
|
pricelist.item_ids = [
|
|
fields.Command.create({
|
|
'percent_price': 5.0,
|
|
'compute_price': 'percentage'
|
|
})
|
|
]
|
|
pricelist.discount_policy = "without_discount"
|
|
sale_order._recompute_prices()
|
|
|
|
self.assertTrue(all(line.discount == 5 for line in sale_order.order_line))
|
|
self.assertEqual(sale_order.amount_undiscounted, so_amount)
|
|
self.assertEqual(sale_order.amount_total, 0.95*so_amount)
|
|
|
|
pricelist.discount_policy = "with_discount"
|
|
sale_order._recompute_prices()
|
|
|
|
self.assertTrue(all(line.discount == 0 for line in sale_order.order_line))
|
|
self.assertEqual(sale_order.amount_undiscounted, so_amount)
|
|
self.assertEqual(sale_order.amount_total, 0.95*so_amount)
|
|
|
|
# Test taking off the pricelist
|
|
sale_order.pricelist_id = False
|
|
sale_order._recompute_prices()
|
|
|
|
self.assertTrue(all(line.discount == 0 for line in sale_order.order_line))
|
|
self.assertEqual(sale_order.amount_undiscounted, so_amount)
|
|
self.assertEqual(
|
|
sale_order.amount_total, start_so_amount,
|
|
"The SO amount without pricelist should be the same than with an empty pricelist"
|
|
)
|
|
|
|
# Taxes tests:
|
|
# We do not rely on accounting common on purpose to avoid
|
|
# all the useless setup not needed here.
|
|
# If you need the accounting common (journals, ...), use/make another test class
|
|
|
|
def test_sale_tax_mapping(self):
|
|
tax_a, tax_b = self.env['account.tax'].create([{
|
|
'name': 'Test tax A',
|
|
'type_tax_use': 'sale',
|
|
'price_include': True,
|
|
'amount': 15.0,
|
|
}, {
|
|
'name': 'Test tax B',
|
|
'type_tax_use': 'sale',
|
|
'amount': 6.0,
|
|
}])
|
|
|
|
country_belgium = self.env['res.country'].search([
|
|
('name', '=', 'Belgium'),
|
|
], limit=1)
|
|
fiscal_pos = self.env['account.fiscal.position'].create({
|
|
'name': 'Test Fiscal Position',
|
|
'auto_apply': True,
|
|
'country_id': country_belgium.id,
|
|
'tax_ids': [Command.create({
|
|
'tax_src_id': tax_a.id,
|
|
'tax_dest_id': tax_b.id
|
|
})]
|
|
})
|
|
|
|
# setting up partner:
|
|
self.partner.country_id = country_belgium
|
|
|
|
self.product.write({
|
|
'lst_price': 115,
|
|
'taxes_id': [Command.set(tax_a.ids)]
|
|
})
|
|
|
|
self.pricelist.write({
|
|
'discount_policy': 'without_discount',
|
|
'item_ids': [Command.create({
|
|
'applied_on': '3_global',
|
|
'compute_price': 'percentage',
|
|
'percent_price': 54,
|
|
})]
|
|
})
|
|
|
|
# creating SO
|
|
self.empty_order.write({
|
|
'fiscal_position_id': fiscal_pos.id,
|
|
'order_line': [Command.create({
|
|
'product_id': self.product.id,
|
|
})],
|
|
})
|
|
|
|
# Update Prices
|
|
self.empty_order._recompute_prices()
|
|
|
|
# Check that the discount displayed is the correct one
|
|
self.assertEqual(
|
|
self.empty_order.order_line.discount, 54,
|
|
"Wrong discount computed for specified product & pricelist"
|
|
)
|
|
# Additional to check for overall consistency
|
|
self.assertEqual(
|
|
self.empty_order.order_line.price_unit, 100,
|
|
"Wrong unit price computed for specified product & pricelist"
|
|
)
|
|
self.assertEqual(
|
|
self.empty_order.order_line.price_subtotal, 46,
|
|
"Wrong subtotal price computed for specified product & pricelist"
|
|
)
|
|
self.assertEqual(
|
|
self.empty_order.order_line.tax_id.id, tax_b.id,
|
|
"Wrong tax applied for specified product & pricelist"
|
|
)
|
|
|
|
def test_fiscalposition_application(self):
|
|
"""Test application of a fiscal position mapping
|
|
price included to price included tax
|
|
"""
|
|
# If test is run without demo data
|
|
# pricelists are not automatically enabled
|
|
self._enable_pricelists()
|
|
pricelist = self.pricelist
|
|
partner = self.partner
|
|
|
|
(
|
|
tax_fixed_incl,
|
|
tax_fixed_excl,
|
|
tax_include_src,
|
|
tax_include_dst,
|
|
tax_exclude_src,
|
|
tax_exclude_dst,
|
|
) = self.env['account.tax'].create([{
|
|
'name': "fixed include",
|
|
'amount': '10.00',
|
|
'amount_type': 'fixed',
|
|
'price_include': True,
|
|
}, {
|
|
'name': "fixed exclude",
|
|
'amount': '10.00',
|
|
'amount_type': 'fixed',
|
|
'price_include': False,
|
|
}, {
|
|
'name': "Include 21%",
|
|
'amount': 21.00,
|
|
'amount_type': 'percent',
|
|
'price_include': True,
|
|
}, {
|
|
'name': "Include 6%",
|
|
'amount': 6.00,
|
|
'amount_type': 'percent',
|
|
'price_include': True,
|
|
}, {
|
|
'name': "Exclude 15%",
|
|
'amount': 15.00,
|
|
'amount_type': 'percent',
|
|
'price_include': False,
|
|
}, {
|
|
'name': "Exclude 21%",
|
|
'amount': 21.00,
|
|
'amount_type': 'percent',
|
|
'price_include': False,
|
|
}])
|
|
|
|
(
|
|
product_tmpl_a,
|
|
product_tmpl_b,
|
|
product_tmpl_c,
|
|
product_tmpl_d,
|
|
) = self.env['product.template'].create([{
|
|
'name': "Voiture",
|
|
'list_price': 121,
|
|
'taxes_id': [Command.set([tax_include_src.id])]
|
|
}, {
|
|
'name': "Voiture",
|
|
'list_price': 100,
|
|
'taxes_id': [Command.set([tax_exclude_src.id])]
|
|
}, {
|
|
'name': "Voiture",
|
|
'list_price': 100,
|
|
'taxes_id': [Command.set([tax_fixed_incl.id, tax_exclude_src.id])]
|
|
}, {
|
|
'name': "Voiture",
|
|
'list_price': 100,
|
|
'taxes_id': [Command.set([tax_fixed_excl.id, tax_include_src.id])]
|
|
}])
|
|
|
|
(
|
|
fpos_incl_incl,
|
|
fpos_excl_incl,
|
|
fpos_incl_excl,
|
|
fpos_excl_excl,
|
|
) = self.env['account.fiscal.position'].create([{
|
|
'name': "incl -> incl",
|
|
'sequence': 1,
|
|
'tax_ids': [Command.create({
|
|
'tax_src_id': tax_include_src.id,
|
|
'tax_dest_id': tax_include_dst.id,
|
|
})]
|
|
}, {
|
|
'name': "excl -> incl",
|
|
'sequence': 2,
|
|
'tax_ids': [Command.create({
|
|
'tax_src_id': tax_exclude_src.id,
|
|
'tax_dest_id': tax_include_dst.id,
|
|
})]
|
|
}, {
|
|
'name': "incl -> excl",
|
|
'sequence': 3,
|
|
'tax_ids': [Command.create({
|
|
'tax_src_id': tax_include_src.id,
|
|
'tax_dest_id': tax_exclude_dst.id,
|
|
})]
|
|
}, {
|
|
'name': "excl -> excp",
|
|
'sequence': 4,
|
|
'tax_ids': [Command.create({
|
|
'tax_src_id': tax_exclude_src.id,
|
|
'tax_dest_id': tax_exclude_dst.id,
|
|
})]
|
|
}])
|
|
|
|
# Create the SO with one SO line and apply a pricelist and fiscal position on it
|
|
# Then check if price unit and price subtotal matches the expected values
|
|
|
|
SaleOrder = self.env['sale.order']
|
|
|
|
# Test Mapping included to included
|
|
order_form = Form(SaleOrder)
|
|
order_form.partner_id = partner
|
|
order_form.pricelist_id = pricelist
|
|
order_form.fiscal_position_id = fpos_incl_incl
|
|
with order_form.order_line.new() as line:
|
|
line.name = product_tmpl_a.product_variant_id.name
|
|
line.product_id = product_tmpl_a.product_variant_id
|
|
line.product_uom_qty = 1.0
|
|
sale_order = order_form.save()
|
|
self.assertRecordValues(sale_order.order_line, [{'price_unit': 106, 'price_subtotal': 100}])
|
|
|
|
# Test Mapping excluded to included
|
|
order_form = Form(SaleOrder)
|
|
order_form.partner_id = partner
|
|
order_form.pricelist_id = pricelist
|
|
order_form.fiscal_position_id = fpos_excl_incl
|
|
with order_form.order_line.new() as line:
|
|
line.name = product_tmpl_b.product_variant_id.name
|
|
line.product_id = product_tmpl_b.product_variant_id
|
|
line.product_uom_qty = 1.0
|
|
sale_order = order_form.save()
|
|
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 94.34}])
|
|
|
|
# Test Mapping included to excluded
|
|
order_form = Form(SaleOrder)
|
|
order_form.partner_id = partner
|
|
order_form.pricelist_id = pricelist
|
|
order_form.fiscal_position_id = fpos_incl_excl
|
|
with order_form.order_line.new() as line:
|
|
line.name = product_tmpl_a.product_variant_id.name
|
|
line.product_id = product_tmpl_a.product_variant_id
|
|
line.product_uom_qty = 1.0
|
|
sale_order = order_form.save()
|
|
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 100}])
|
|
|
|
# Test Mapping excluded to excluded
|
|
order_form = Form(SaleOrder)
|
|
order_form.partner_id = partner
|
|
order_form.pricelist_id = pricelist
|
|
order_form.fiscal_position_id = fpos_excl_excl
|
|
with order_form.order_line.new() as line:
|
|
line.name = product_tmpl_b.product_variant_id.name
|
|
line.product_id = product_tmpl_b.product_variant_id
|
|
line.product_uom_qty = 1.0
|
|
sale_order = order_form.save()
|
|
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 100}])
|
|
|
|
# Test Mapping (included,excluded) to (included, included)
|
|
order_form = Form(SaleOrder)
|
|
order_form.partner_id = partner
|
|
order_form.pricelist_id = pricelist
|
|
order_form.fiscal_position_id = fpos_excl_incl
|
|
with order_form.order_line.new() as line:
|
|
line.name = product_tmpl_c.product_variant_id.name
|
|
line.product_id = product_tmpl_c.product_variant_id
|
|
line.product_uom_qty = 1.0
|
|
sale_order = order_form.save()
|
|
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 84.91}])
|
|
|
|
# Test Mapping (excluded,included) to (excluded, excluded)
|
|
order_form = Form(SaleOrder)
|
|
order_form.partner_id = partner
|
|
order_form.pricelist_id = pricelist
|
|
order_form.fiscal_position_id = fpos_incl_excl
|
|
with order_form.order_line.new() as line:
|
|
line.name = product_tmpl_d.product_variant_id.name
|
|
line.product_id = product_tmpl_d.product_variant_id
|
|
line.product_uom_qty = 1.0
|
|
sale_order = order_form.save()
|
|
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 100}])
|
|
|
|
def test_so_tax_mapping(self):
|
|
order = self.empty_order
|
|
|
|
tax_include, tax_exclude = self.env['account.tax'].create([{
|
|
'name': 'Include Tax',
|
|
'amount': '21.00',
|
|
'price_include': True,
|
|
'type_tax_use': 'sale',
|
|
}, {
|
|
'name': 'Exclude Tax',
|
|
'amount': '0.00',
|
|
'type_tax_use': 'sale',
|
|
}])
|
|
|
|
self.product.write({
|
|
'list_price': 121,
|
|
'taxes_id': [Command.set(tax_include.ids)]
|
|
})
|
|
|
|
fpos = self.env['account.fiscal.position'].create({
|
|
'name': 'Test Fiscal Position',
|
|
'sequence': 1,
|
|
'tax_ids': [Command.create({
|
|
'tax_src_id': tax_include.id,
|
|
'tax_dest_id': tax_exclude.id,
|
|
})],
|
|
})
|
|
|
|
order.write({
|
|
'fiscal_position_id': fpos.id,
|
|
'order_line': [Command.create({
|
|
'product_id': self.product.id,
|
|
})]
|
|
})
|
|
|
|
# Check the unit price of SO line
|
|
self.assertEqual(
|
|
100, order.order_line[0].price_unit,
|
|
"The included tax must be subtracted to the price")
|
|
|
|
def test_free_product_and_price_include_fixed_tax(self):
|
|
""" Check that fixed tax include are correctly computed while the price_unit is 0 """
|
|
taxes = self.env['account.tax'].create([{
|
|
'name': 'BEBAT 0.05',
|
|
'type_tax_use': 'sale',
|
|
'amount_type': 'fixed',
|
|
'amount': 0.05,
|
|
'price_include': True,
|
|
'include_base_amount': True,
|
|
}, {
|
|
'name': 'Recupel 0.25',
|
|
'type_tax_use': 'sale',
|
|
'amount_type': 'fixed',
|
|
'amount': 0.25,
|
|
'price_include': True,
|
|
'include_base_amount': True,
|
|
}])
|
|
order = self.empty_order
|
|
order.order_line = [Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 1,
|
|
'price_unit': 0.0,
|
|
'tax_id': [
|
|
Command.set(taxes.ids),
|
|
],
|
|
})]
|
|
|
|
self.assertRecordValues(order.order_line, [{
|
|
'price_tax': 0.3,
|
|
'price_subtotal': -0.3,
|
|
'price_total': 0.0,
|
|
}])
|
|
self.assertRecordValues(order, [{
|
|
'amount_untaxed': -0.30,
|
|
'amount_tax': 0.30,
|
|
'amount_total': 0.0,
|
|
}])
|
|
|
|
def test_sale_with_taxes(self):
|
|
""" Test SO with taxes applied on its lines and check subtotal applied on its lines and total applied on the SO """
|
|
tax_include, tax_exclude = self.env['account.tax'].create([{
|
|
'name': 'Tax with price include',
|
|
'amount': 10,
|
|
'price_include': True
|
|
}, {
|
|
'name': 'Tax with no price include',
|
|
'amount': 10,
|
|
}])
|
|
|
|
# Apply taxes on the sale order lines
|
|
self.sale_order.order_line[0].write({'tax_id': [Command.link(tax_include.id)]})
|
|
self.sale_order.order_line[1].write({'tax_id': [Command.link(tax_exclude.id)]})
|
|
|
|
for line in self.sale_order.order_line:
|
|
if line.tax_id.price_include:
|
|
price = line.price_unit * line.product_uom_qty - line.price_tax
|
|
else:
|
|
price = line.price_unit * line.product_uom_qty
|
|
|
|
self.assertEqual(float_compare(line.price_subtotal, price, precision_digits=2), 0)
|
|
|
|
self.assertAlmostEqual(
|
|
self.sale_order.amount_total,
|
|
self.sale_order.amount_untaxed + self.sale_order.amount_tax,
|
|
places=2)
|
|
|
|
def test_discount_and_untaxed_subtotal(self):
|
|
"""When adding a discount on a SO line, this test ensures that the untaxed amount to invoice is
|
|
equal to the untaxed subtotal"""
|
|
self.product.invoice_policy = 'delivery'
|
|
order = self.empty_order
|
|
|
|
order.order_line = [Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 38,
|
|
'price_unit': 541.26,
|
|
'discount': 2.00,
|
|
})]
|
|
order.action_confirm()
|
|
line = order.order_line
|
|
self.assertEqual(line.untaxed_amount_to_invoice, 0)
|
|
|
|
line.qty_delivered = 38
|
|
# (541.26 - 0.02 * 541.26) * 38 = 20156.5224 ~= 20156.52
|
|
self.assertEqual(line.price_subtotal, 20156.52)
|
|
self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal)
|
|
|
|
# Same with an included-in-price tax
|
|
order = order.copy()
|
|
line = order.order_line
|
|
line.tax_id = [Command.create({
|
|
'name': 'Super Tax',
|
|
'amount_type': 'percent',
|
|
'amount': 15.0,
|
|
'price_include': True,
|
|
})]
|
|
order.action_confirm()
|
|
self.assertEqual(line.untaxed_amount_to_invoice, 0)
|
|
|
|
line.qty_delivered = 38
|
|
# (541,26 / 1,15) * ,98 * 38 = 17527,410782609 ~= 17527.41
|
|
self.assertEqual(line.price_subtotal, 17527.41)
|
|
self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal)
|
|
|
|
def test_discount_and_amount_undiscounted(self):
|
|
"""When adding a discount on a SO line, this test ensures that amount undiscounted is
|
|
consistent with the used tax"""
|
|
order = self.empty_order
|
|
|
|
order.order_line = [Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': 1,
|
|
'price_unit': 100.0,
|
|
'discount': 1.00,
|
|
})]
|
|
order.action_confirm()
|
|
order_line = order.order_line
|
|
|
|
# test discount and qty 1
|
|
self.assertEqual(order.amount_undiscounted, 100.0)
|
|
self.assertEqual(order_line.price_subtotal, 99.0)
|
|
|
|
# more quantity 1 -> 3
|
|
order_line.write({
|
|
'product_uom_qty': 3.0,
|
|
'price_unit': 100.0,
|
|
})
|
|
order.invalidate_recordset(['amount_undiscounted'])
|
|
|
|
self.assertEqual(order.amount_undiscounted, 300.0)
|
|
self.assertEqual(order_line.price_subtotal, 297.0)
|
|
|
|
# undiscounted
|
|
order_line.discount = 0.0
|
|
self.assertEqual(order_line.price_subtotal, 300.0)
|
|
self.assertEqual(order.amount_undiscounted, 300.0)
|
|
|
|
# Same with an included-in-price tax
|
|
order = order.copy()
|
|
line = order.order_line
|
|
line.tax_id = [Command.create({
|
|
'name': 'Super Tax',
|
|
'amount_type': 'percent',
|
|
'amount': 10.0,
|
|
'price_include': True,
|
|
})]
|
|
line.discount = 50.0
|
|
order.action_confirm()
|
|
|
|
# 300 with 10% incl tax -> 272.72 total tax excluded without discount
|
|
# 136.36 price tax excluded with discount applied
|
|
self.assertEqual(order.amount_undiscounted, 272.72)
|
|
self.assertEqual(line.price_subtotal, 136.36)
|
|
|
|
def test_product_quantity_rounding(self):
|
|
"""When adding a sale order line, product quantity should be rounded
|
|
according to decimal precision"""
|
|
order = self.empty_order
|
|
|
|
product_uom_qty = 0.333333
|
|
order.order_line = [Command.create({
|
|
'product_id': self.product.id,
|
|
'product_uom_qty': product_uom_qty,
|
|
'price_unit': 75.0,
|
|
})]
|
|
order.action_confirm()
|
|
line = order.order_line
|
|
quantity_precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
self.assertEqual(
|
|
line.product_uom_qty, float_round(product_uom_qty, precision_digits=quantity_precision))
|
|
expected_price_subtotal = line.currency_id.round(
|
|
line.price_unit * float_round(product_uom_qty, precision_digits=quantity_precision))
|
|
self.assertAlmostEqual(line.price_subtotal, expected_price_subtotal)
|
|
self.assertEqual(order.amount_total, order.tax_totals.get('amount_total'))
|