334 lines
12 KiB
Python
334 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo.tests import TransactionCase
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class TestEdgeCases(TransactionCase):
|
|
"""
|
|
Unit tests for edge cases in vendor bill editable totals.
|
|
|
|
Tests cover:
|
|
- Zero quantity error handling (Requirement 3.1)
|
|
- Negative values for credit notes (Requirement 3.2)
|
|
- No taxes scenario (Requirement 3.3)
|
|
- Single tax calculation (Requirement 3.4)
|
|
- Multiple taxes calculation (Requirement 3.4)
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Create a vendor partner
|
|
self.vendor = self.env['res.partner'].create({
|
|
'name': 'Test Vendor',
|
|
'supplier_rank': 1,
|
|
})
|
|
|
|
# Create a product
|
|
self.product = self.env['res.product'].create({
|
|
'name': 'Test Product',
|
|
'type': 'service',
|
|
'list_price': 100.0,
|
|
})
|
|
|
|
# Create a vendor bill
|
|
self.vendor_bill = self.env['account.move'].create({
|
|
'move_type': 'in_invoice',
|
|
'partner_id': self.vendor.id,
|
|
'invoice_date': '2024-01-01',
|
|
})
|
|
|
|
# Create a vendor refund (credit note)
|
|
self.vendor_refund = self.env['account.move'].create({
|
|
'move_type': 'in_refund',
|
|
'partner_id': self.vendor.id,
|
|
'invoice_date': '2024-01-01',
|
|
})
|
|
|
|
# Create tax records for testing
|
|
# Single tax: 10%
|
|
self.tax_10 = self.env['account.tax'].create({
|
|
'name': 'Tax 10%',
|
|
'amount': 10.0,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'purchase',
|
|
'price_include': False,
|
|
})
|
|
|
|
# Another tax: 5%
|
|
self.tax_5 = self.env['account.tax'].create({
|
|
'name': 'Tax 5%',
|
|
'amount': 5.0,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'purchase',
|
|
'price_include': False,
|
|
})
|
|
|
|
# Tax-included tax: 15%
|
|
self.tax_15_included = self.env['account.tax'].create({
|
|
'name': 'Tax 15% Included',
|
|
'amount': 15.0,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'purchase',
|
|
'price_include': True,
|
|
})
|
|
|
|
# Get decimal precision for Product Price
|
|
self.precision = self.env['decimal.precision'].precision_get('Product Price')
|
|
|
|
# Test zero quantity error handling (Requirement 3.1)
|
|
|
|
def test_zero_quantity_price_subtotal_raises_error(self):
|
|
"""
|
|
Test that modifying price_subtotal with zero quantity raises UserError.
|
|
Requirement 3.1: Division by zero protection
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 0.0,
|
|
'price_unit': 100.0,
|
|
})
|
|
|
|
line.price_subtotal = 500.0
|
|
|
|
with self.assertRaises(UserError) as context:
|
|
line._onchange_price_subtotal()
|
|
|
|
self.assertIn('quantity must be greater than zero', str(context.exception))
|
|
|
|
def test_zero_quantity_price_total_raises_error(self):
|
|
"""
|
|
Test that modifying price_total with zero quantity raises UserError.
|
|
Requirement 3.1: Division by zero protection
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 0.0,
|
|
'price_unit': 100.0,
|
|
})
|
|
|
|
line.price_total = 550.0
|
|
|
|
with self.assertRaises(UserError) as context:
|
|
line._onchange_price_total()
|
|
|
|
self.assertIn('quantity must be greater than zero', str(context.exception))
|
|
|
|
# Test negative values (credit notes) (Requirement 3.2)
|
|
|
|
def test_negative_price_subtotal_credit_note(self):
|
|
"""
|
|
Test that negative price_subtotal values are handled correctly for credit notes.
|
|
Requirement 3.2: Accept and process negative values correctly
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_refund.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 5.0,
|
|
'price_unit': 100.0,
|
|
})
|
|
|
|
# Set negative price_subtotal (credit note scenario)
|
|
line.price_subtotal = -500.0
|
|
line._onchange_price_subtotal()
|
|
|
|
# Expected: price_unit = -500.0 / 5.0 = -100.0
|
|
expected_price_unit = -100.0
|
|
|
|
self.assertAlmostEqual(
|
|
line.price_unit,
|
|
expected_price_unit,
|
|
places=self.precision,
|
|
msg=f"Negative price_subtotal not handled correctly: expected {expected_price_unit}, got {line.price_unit}"
|
|
)
|
|
|
|
def test_negative_price_total_credit_note(self):
|
|
"""
|
|
Test that negative price_total values are handled correctly for credit notes.
|
|
Requirement 3.2: Accept and process negative values correctly
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_refund.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 5.0,
|
|
'price_unit': 100.0,
|
|
'tax_ids': [(6, 0, [self.tax_10.id])],
|
|
})
|
|
|
|
# Set negative price_total (credit note scenario with tax)
|
|
# If price_subtotal = -500, with 10% tax, price_total = -550
|
|
line.price_total = -550.0
|
|
line._onchange_price_total()
|
|
|
|
# Expected: price_subtotal = -550.0 / 1.10 = -500.0
|
|
# price_unit = -500.0 / 5.0 = -100.0
|
|
expected_price_unit = -100.0
|
|
|
|
self.assertAlmostEqual(
|
|
line.price_unit,
|
|
expected_price_unit,
|
|
places=self.precision,
|
|
msg=f"Negative price_total not handled correctly: expected {expected_price_unit}, got {line.price_unit}"
|
|
)
|
|
|
|
# Test no taxes scenario (Requirement 3.3)
|
|
|
|
def test_no_taxes_price_total_equals_subtotal(self):
|
|
"""
|
|
Test that when no taxes are configured, price_total is treated as price_subtotal.
|
|
Requirement 3.3: Handle no taxes scenario correctly
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 10.0,
|
|
'price_unit': 100.0,
|
|
# No tax_ids set
|
|
})
|
|
|
|
# Set price_total when no taxes are configured
|
|
line.price_total = 1000.0
|
|
line._onchange_price_total()
|
|
|
|
# Expected: price_unit = 1000.0 / 10.0 = 100.0
|
|
expected_price_unit = 100.0
|
|
|
|
self.assertAlmostEqual(
|
|
line.price_unit,
|
|
expected_price_unit,
|
|
places=self.precision,
|
|
msg=f"No taxes scenario not handled correctly: expected {expected_price_unit}, got {line.price_unit}"
|
|
)
|
|
|
|
# Test single tax calculation (Requirement 3.4)
|
|
|
|
def test_single_tax_calculation(self):
|
|
"""
|
|
Test that a single tax is correctly computed in price_total calculation.
|
|
Requirement 3.4: Correctly compute single tax effect
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 10.0,
|
|
'price_unit': 100.0,
|
|
'tax_ids': [(6, 0, [self.tax_10.id])],
|
|
})
|
|
|
|
# Set price_total with 10% tax
|
|
# If price_subtotal = 1000, with 10% tax, price_total = 1100
|
|
line.price_total = 1100.0
|
|
line._onchange_price_total()
|
|
|
|
# Expected: price_subtotal = 1100.0 / 1.10 = 1000.0
|
|
# price_unit = 1000.0 / 10.0 = 100.0
|
|
expected_price_unit = 100.0
|
|
|
|
self.assertAlmostEqual(
|
|
line.price_unit,
|
|
expected_price_unit,
|
|
places=self.precision,
|
|
msg=f"Single tax calculation failed: expected {expected_price_unit}, got {line.price_unit}"
|
|
)
|
|
|
|
def test_single_tax_included_calculation(self):
|
|
"""
|
|
Test that a single tax-included tax is correctly computed.
|
|
Requirement 3.4: Correctly compute single tax effect with price_include=True
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 10.0,
|
|
'price_unit': 100.0,
|
|
'tax_ids': [(6, 0, [self.tax_15_included.id])],
|
|
})
|
|
|
|
# Set price_total with 15% tax-included
|
|
# For tax-included, price_unit = price_total / quantity
|
|
line.price_total = 1150.0
|
|
line._onchange_price_total()
|
|
|
|
# Expected: price_unit = 1150.0 / 10.0 = 115.0
|
|
expected_price_unit = 115.0
|
|
|
|
self.assertAlmostEqual(
|
|
line.price_unit,
|
|
expected_price_unit,
|
|
places=self.precision,
|
|
msg=f"Single tax-included calculation failed: expected {expected_price_unit}, got {line.price_unit}"
|
|
)
|
|
|
|
# Test multiple taxes calculation (Requirement 3.4)
|
|
|
|
def test_multiple_taxes_calculation(self):
|
|
"""
|
|
Test that multiple taxes are correctly computed in price_total calculation.
|
|
Requirement 3.4: Correctly compute combined tax effect
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 10.0,
|
|
'price_unit': 100.0,
|
|
'tax_ids': [(6, 0, [self.tax_10.id, self.tax_5.id])],
|
|
})
|
|
|
|
# Set price_total with 10% + 5% = 15% combined tax
|
|
# If price_subtotal = 1000, with 15% tax, price_total = 1150
|
|
line.price_total = 1150.0
|
|
line._onchange_price_total()
|
|
|
|
# Expected: price_subtotal = 1150.0 / 1.15 = 1000.0
|
|
# price_unit = 1000.0 / 10.0 = 100.0
|
|
expected_price_unit = 100.0
|
|
|
|
self.assertAlmostEqual(
|
|
line.price_unit,
|
|
expected_price_unit,
|
|
places=self.precision,
|
|
msg=f"Multiple taxes calculation failed: expected {expected_price_unit}, got {line.price_unit}"
|
|
)
|
|
|
|
def test_multiple_taxes_with_different_amounts(self):
|
|
"""
|
|
Test multiple taxes with different amounts to verify correct computation.
|
|
Requirement 3.4: Correctly compute combined tax effect with various amounts
|
|
"""
|
|
line = self.env['account.move.line'].create({
|
|
'move_id': self.vendor_bill.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Test Line',
|
|
'quantity': 5.0,
|
|
'price_unit': 200.0,
|
|
'tax_ids': [(6, 0, [self.tax_10.id, self.tax_5.id])],
|
|
})
|
|
|
|
# Set price_total with 10% + 5% = 15% combined tax
|
|
# If price_subtotal = 1000 (5 * 200), with 15% tax, price_total = 1150
|
|
line.price_total = 1150.0
|
|
line._onchange_price_total()
|
|
|
|
# Expected: price_subtotal = 1150.0 / 1.15 = 1000.0
|
|
# price_unit = 1000.0 / 5.0 = 200.0
|
|
expected_price_unit = 200.0
|
|
|
|
self.assertAlmostEqual(
|
|
line.price_unit,
|
|
expected_price_unit,
|
|
places=self.precision,
|
|
msg=f"Multiple taxes with different amounts failed: expected {expected_price_unit}, got {line.price_unit}"
|
|
)
|