313 lines
10 KiB
Python
313 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Standalone test runner for view configuration unit tests.
|
||
This allows running tests without the full Odoo test framework.
|
||
|
||
Tests verify:
|
||
- price_subtotal field is editable in vendor bill form
|
||
- price_total field is editable in vendor bill form
|
||
- Fields are readonly in non-vendor-bill contexts
|
||
|
||
Requirements tested: 1.1, 2.1, 5.2
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
from lxml import etree
|
||
|
||
|
||
def test_view_xml_structure():
|
||
"""
|
||
Test that the view XML file has the correct structure.
|
||
Requirement 5.2: Use proper view inheritance
|
||
"""
|
||
view_path = os.path.join(
|
||
os.path.dirname(__file__),
|
||
'../views/account_move_views.xml'
|
||
)
|
||
|
||
if not os.path.exists(view_path):
|
||
return False, f"View file not found at {view_path}"
|
||
|
||
try:
|
||
tree = etree.parse(view_path)
|
||
root = tree.getroot()
|
||
|
||
# Check that it's an Odoo XML file
|
||
if root.tag != 'odoo':
|
||
return False, f"Root tag should be 'odoo', got '{root.tag}'"
|
||
|
||
# Find the record element
|
||
records = root.xpath("//record[@id='view_move_form_editable_totals']")
|
||
if not records:
|
||
return False, "Could not find record with id 'view_move_form_editable_totals'"
|
||
|
||
record = records[0]
|
||
|
||
# Check model
|
||
if record.get('model') != 'ir.ui.view':
|
||
return False, f"Record model should be 'ir.ui.view', got '{record.get('model')}'"
|
||
|
||
# Check inherit_id field
|
||
inherit_fields = record.xpath(".//field[@name='inherit_id']")
|
||
if not inherit_fields:
|
||
return False, "Missing inherit_id field"
|
||
|
||
inherit_ref = inherit_fields[0].get('ref')
|
||
if inherit_ref != 'account.view_move_form':
|
||
return False, f"inherit_id should reference 'account.view_move_form', got '{inherit_ref}'"
|
||
|
||
return True, "View XML structure is correct"
|
||
|
||
except Exception as e:
|
||
return False, f"Error parsing XML: {e}"
|
||
|
||
|
||
def test_price_subtotal_field_editable():
|
||
"""
|
||
Test that price_subtotal field is made editable in the view.
|
||
Requirement 1.1: price_subtotal field should be editable in vendor bill form
|
||
"""
|
||
view_path = os.path.join(
|
||
os.path.dirname(__file__),
|
||
'../views/account_move_views.xml'
|
||
)
|
||
|
||
try:
|
||
tree = etree.parse(view_path)
|
||
root = tree.getroot()
|
||
|
||
# Find the xpath that modifies price_subtotal
|
||
xpath_elements = root.xpath(
|
||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']\"]"
|
||
)
|
||
|
||
if not xpath_elements:
|
||
return False, "XPath for price_subtotal field not found in view"
|
||
|
||
xpath_element = xpath_elements[0]
|
||
|
||
# Check that readonly attribute is set to 0
|
||
readonly_attrs = xpath_element.xpath(".//attribute[@name='readonly']")
|
||
|
||
if not readonly_attrs:
|
||
return False, "Readonly attribute not set for price_subtotal"
|
||
|
||
if readonly_attrs[0].text != '0':
|
||
return False, f"price_subtotal readonly should be '0', got '{readonly_attrs[0].text}'"
|
||
|
||
# Check that attrs attribute exists for conditional readonly
|
||
attrs_elements = xpath_element.xpath(".//attribute[@name='attrs']")
|
||
|
||
if not attrs_elements:
|
||
return False, "Attrs attribute not set for price_subtotal"
|
||
|
||
attrs_text = attrs_elements[0].text
|
||
if 'readonly' not in attrs_text:
|
||
return False, "Attrs should contain readonly condition"
|
||
|
||
if 'draft' not in attrs_text:
|
||
return False, "Attrs should reference draft state"
|
||
|
||
return True, "price_subtotal field is correctly configured as editable"
|
||
|
||
except Exception as e:
|
||
return False, f"Error checking price_subtotal field: {e}"
|
||
|
||
|
||
def test_price_total_field_editable():
|
||
"""
|
||
Test that price_total field is made editable in the view.
|
||
Requirement 2.1: price_total field should be editable in vendor bill form
|
||
"""
|
||
view_path = os.path.join(
|
||
os.path.dirname(__file__),
|
||
'../views/account_move_views.xml'
|
||
)
|
||
|
||
try:
|
||
tree = etree.parse(view_path)
|
||
root = tree.getroot()
|
||
|
||
# Find the xpath that modifies price_total
|
||
xpath_elements = root.xpath(
|
||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_total']\"]"
|
||
)
|
||
|
||
if not xpath_elements:
|
||
return False, "XPath for price_total field not found in view"
|
||
|
||
xpath_element = xpath_elements[0]
|
||
|
||
# Check that readonly attribute is set to 0
|
||
readonly_attrs = xpath_element.xpath(".//attribute[@name='readonly']")
|
||
|
||
if not readonly_attrs:
|
||
return False, "Readonly attribute not set for price_total"
|
||
|
||
if readonly_attrs[0].text != '0':
|
||
return False, f"price_total readonly should be '0', got '{readonly_attrs[0].text}'"
|
||
|
||
# Check that attrs attribute exists for conditional readonly
|
||
attrs_elements = xpath_element.xpath(".//attribute[@name='attrs']")
|
||
|
||
if not attrs_elements:
|
||
return False, "Attrs attribute not set for price_total"
|
||
|
||
attrs_text = attrs_elements[0].text
|
||
if 'readonly' not in attrs_text:
|
||
return False, "Attrs should contain readonly condition"
|
||
|
||
if 'draft' not in attrs_text:
|
||
return False, "Attrs should reference draft state"
|
||
|
||
return True, "price_total field is correctly configured as editable"
|
||
|
||
except Exception as e:
|
||
return False, f"Error checking price_total field: {e}"
|
||
|
||
|
||
def test_fields_readonly_conditions():
|
||
"""
|
||
Test that fields have attrs to make them readonly when state is not draft.
|
||
Requirement 5.2: Fields should be readonly in non-draft states
|
||
"""
|
||
view_path = os.path.join(
|
||
os.path.dirname(__file__),
|
||
'../views/account_move_views.xml'
|
||
)
|
||
|
||
try:
|
||
tree = etree.parse(view_path)
|
||
root = tree.getroot()
|
||
|
||
# Check price_subtotal attrs
|
||
xpath_subtotal = root.xpath(
|
||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']\"]"
|
||
)
|
||
|
||
if not xpath_subtotal:
|
||
return False, "price_subtotal xpath not found"
|
||
|
||
attrs_subtotal = xpath_subtotal[0].xpath(".//attribute[@name='attrs']")
|
||
|
||
if not attrs_subtotal:
|
||
return False, "Attrs not set for price_subtotal"
|
||
|
||
attrs_text = attrs_subtotal[0].text
|
||
|
||
if 'readonly' not in attrs_text:
|
||
return False, "price_subtotal attrs should contain readonly condition"
|
||
|
||
if 'parent.state' not in attrs_text:
|
||
return False, "price_subtotal attrs should reference parent.state"
|
||
|
||
# Check price_total attrs
|
||
xpath_total = root.xpath(
|
||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_total']\"]"
|
||
)
|
||
|
||
if not xpath_total:
|
||
return False, "price_total xpath not found"
|
||
|
||
attrs_total = xpath_total[0].xpath(".//attribute[@name='attrs']")
|
||
|
||
if not attrs_total:
|
||
return False, "Attrs not set for price_total"
|
||
|
||
attrs_text = attrs_total[0].text
|
||
|
||
if 'readonly' not in attrs_text:
|
||
return False, "price_total attrs should contain readonly condition"
|
||
|
||
if 'parent.state' not in attrs_text:
|
||
return False, "price_total attrs should reference parent.state"
|
||
|
||
return True, "Both fields have correct readonly conditions based on state"
|
||
|
||
except Exception as e:
|
||
return False, f"Error checking readonly conditions: {e}"
|
||
|
||
|
||
def test_view_inheritance_pattern():
|
||
"""
|
||
Test that the view follows Odoo's inheritance best practices.
|
||
Requirement 5.2: Use proper view inheritance
|
||
"""
|
||
view_path = os.path.join(
|
||
os.path.dirname(__file__),
|
||
'../views/account_move_views.xml'
|
||
)
|
||
|
||
try:
|
||
tree = etree.parse(view_path)
|
||
root = tree.getroot()
|
||
|
||
# Check that we're using xpath for modifications
|
||
xpaths = root.xpath("//xpath")
|
||
|
||
if len(xpaths) < 2:
|
||
return False, f"Expected at least 2 xpath elements, found {len(xpaths)}"
|
||
|
||
# Check that we're using position="attributes" for field modifications
|
||
for xpath in xpaths:
|
||
position = xpath.get('position')
|
||
if position != 'attributes':
|
||
return False, f"XPath should use position='attributes', got '{position}'"
|
||
|
||
# Check that we're using attribute elements
|
||
attributes = root.xpath("//attribute")
|
||
|
||
if len(attributes) < 4: # At least 2 fields × 2 attributes each
|
||
return False, f"Expected at least 4 attribute elements, found {len(attributes)}"
|
||
|
||
return True, "View follows proper inheritance patterns"
|
||
|
||
except Exception as e:
|
||
return False, f"Error checking inheritance pattern: {e}"
|
||
|
||
|
||
if __name__ == '__main__':
|
||
print("Running View Configuration Unit Tests for vendor-bill-editable-totals")
|
||
print("=" * 70)
|
||
|
||
tests = [
|
||
("View XML structure", test_view_xml_structure),
|
||
("price_subtotal field editable", test_price_subtotal_field_editable),
|
||
("price_total field editable", test_price_total_field_editable),
|
||
("Fields readonly conditions", test_fields_readonly_conditions),
|
||
("View inheritance pattern", test_view_inheritance_pattern),
|
||
]
|
||
|
||
passed = 0
|
||
failed = 0
|
||
|
||
for test_name, test_func in tests:
|
||
print(f"\nTest: {test_name}")
|
||
print("-" * 70)
|
||
try:
|
||
success, message = test_func()
|
||
if success:
|
||
print(f"✓ PASSED: {message}")
|
||
passed += 1
|
||
else:
|
||
print(f"✗ FAILED: {message}")
|
||
failed += 1
|
||
except Exception as e:
|
||
print(f"✗ FAILED with exception: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
failed += 1
|
||
|
||
print("\n" + "=" * 70)
|
||
print(f"Results: {passed} passed, {failed} failed out of {len(tests)} tests")
|
||
|
||
if failed == 0:
|
||
print("✓ All view configuration tests passed!")
|
||
sys.exit(0)
|
||
else:
|
||
print("✗ Some view configuration tests failed!")
|
||
sys.exit(1)
|
||
|