Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 578708f403 | |||
| 865695577c | |||
| d10ef65257 | |||
| d322793576 | |||
| 98ff30fff7 |
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,14 +1,15 @@
|
|||||||
# Python
|
# Python
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
__pycache__/
|
||||||
|
*.so
|
||||||
|
|
||||||
# Odoo
|
# Odoo
|
||||||
*.po
|
*.po~
|
||||||
.ipynb_checkpoints/
|
*.pot~
|
||||||
|
|
||||||
# Editors
|
# Editor / System
|
||||||
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
*~
|
||||||
|
|||||||
@ -223,7 +223,7 @@ python customaddons/vendor_bill_editable_totals/tests/run_integration_tests.py
|
|||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
- **Odoo Version:** 17.0
|
- **Odoo Version:** 19.0
|
||||||
- **Python Version:** 3.10+
|
- **Python Version:** 3.10+
|
||||||
- **Database:** PostgreSQL 12+
|
- **Database:** PostgreSQL 12+
|
||||||
- **License:** LGPL-3
|
- **License:** LGPL-3
|
||||||
@ -238,9 +238,9 @@ For issues, questions, or contributions, please contact your system administrato
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### Version 17.0.1.1.0
|
### Version 19.0.1.0.3
|
||||||
- Fix: Prevent incorrect revaluation entries (STJ) on Vendor Bills when creating Credit Notes (Reversal).
|
- Fix incorrect vendor bill line revaluation during credit note creation
|
||||||
- Fix: Restricted revaluation logic to strictly apply only when manually editing Vendor Bills, excluding system-triggered updates during reversals.
|
- Restrict logic to vendor bills (`in_invoice`) only
|
||||||
|
|
||||||
### Version 17.0.1.0.0
|
### Version 17.0.1.0.0
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Vendor Bill Editable Totals",
|
"name": "Vendor Bill Editable Totals",
|
||||||
"version": "17.0.1.0.2",
|
"version": "19.0.1.0.3",
|
||||||
"summary": "Enable direct editing of tax excluded and tax included amounts on vendor bill lines with automatic unit price recalculation",
|
"summary": "Enable direct editing of tax excluded and tax included amounts on vendor bill lines with automatic unit price recalculation",
|
||||||
"description": """
|
"description": """
|
||||||
Vendor Bill Editable Totals
|
Vendor Bill Editable Totals
|
||||||
@ -23,7 +23,9 @@
|
|||||||
"author": "Suherdy Yacob",
|
"author": "Suherdy Yacob",
|
||||||
"category": "Accounting",
|
"category": "Accounting",
|
||||||
"depends": [
|
"depends": [
|
||||||
"account"
|
"account",
|
||||||
|
"purchase",
|
||||||
|
"stock_account",
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
"views/account_move_views.xml",
|
"views/account_move_views.xml",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -1,53 +0,0 @@
|
|||||||
# SCRIPT TO DELETE INCORRECT VALUATION LAYER
|
|
||||||
# Run this in: ./odoo-bin shell -d <your_db>
|
|
||||||
|
|
||||||
def fix_valuation():
|
|
||||||
print("Searching for remaining candidate layers...")
|
|
||||||
|
|
||||||
# Broaden search to include the 420,000 one and any others similar
|
|
||||||
layer_domain = [
|
|
||||||
('product_id.name', 'ilike', 'Kotak Imlek Pink'),
|
|
||||||
('quantity', '=', 0),
|
|
||||||
('value', '!=', 0),
|
|
||||||
('description', 'like', 'Valuation correction%') # Safety check
|
|
||||||
]
|
|
||||||
|
|
||||||
layers = env['stock.valuation.layer'].search(layer_domain)
|
|
||||||
|
|
||||||
print(f"Found {len(layers)} remaining layers with 0 quantity and non-zero value.")
|
|
||||||
|
|
||||||
if not layers:
|
|
||||||
print("No more incorrect layers found!")
|
|
||||||
return
|
|
||||||
|
|
||||||
for layer in layers:
|
|
||||||
print(f"\n---------------------------------------------------")
|
|
||||||
print(f"Layer ID: {layer.id}")
|
|
||||||
print(f"Product: {layer.product_id.display_name}")
|
|
||||||
print(f"Description: {layer.description}")
|
|
||||||
print(f"Value: {layer.value}")
|
|
||||||
print(f"Linked Account Move: {layer.account_move_id.name if layer.account_move_id else 'None'}")
|
|
||||||
|
|
||||||
# We can just proceed to delete since we confirmed these are the bad ones
|
|
||||||
# But let's check values to be sure it matches the 420k one or similar
|
|
||||||
print(">>> DELETING <<<")
|
|
||||||
|
|
||||||
# UNLINK ACCOUNT MOVE
|
|
||||||
if layer.account_move_id:
|
|
||||||
print(f"Deleting associated account move: {layer.account_move_id.name}")
|
|
||||||
try:
|
|
||||||
if layer.account_move_id.state == 'posted':
|
|
||||||
layer.account_move_id.button_draft()
|
|
||||||
layer.account_move_id.unlink()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not delete account move: {e}")
|
|
||||||
|
|
||||||
# UNLINK LAYER
|
|
||||||
print("Deleting layer...")
|
|
||||||
layer.unlink()
|
|
||||||
print("Layer deleted successfully.")
|
|
||||||
|
|
||||||
env.cr.commit()
|
|
||||||
print("\nAll changes committed to database.")
|
|
||||||
|
|
||||||
fix_valuation()
|
|
||||||
Binary file not shown.
Binary file not shown.
@ -3,8 +3,7 @@
|
|||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
from odoo.tools import float_compare
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
|
|
||||||
class AccountMoveLine(models.Model):
|
class AccountMoveLine(models.Model):
|
||||||
@ -163,113 +162,7 @@ class AccountMoveLine(models.Model):
|
|||||||
if was_locked:
|
if was_locked:
|
||||||
po.button_done()
|
po.button_done()
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# INVENTORY VALUATION UPDATE (AVCO/FIFO FIX)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# If PO is received, updating price should update Stock Value
|
|
||||||
# COMMENTED OUT TO PREVENT INCORRECT "Revaluation" JOURNAL ENTRIES
|
|
||||||
# User request: "do not create revaluation journal only sync the change to PO"
|
|
||||||
|
|
||||||
# if po_line.state in ['purchase', 'done'] and po_line.product_id.type == 'product':
|
|
||||||
# for stock_move in po_line.move_ids.filtered(lambda m: m.state == 'done'):
|
|
||||||
# # Calculate Diff based on NEW Price (updated above)
|
|
||||||
# new_val = price_unit * stock_move.quantity
|
|
||||||
# # Current Value from SVLs
|
|
||||||
# current_val = sum(stock_move.stock_valuation_layer_ids.mapped('value'))
|
|
||||||
# diff = new_val - current_val
|
|
||||||
#
|
|
||||||
# currency = stock_move.company_id.currency_id
|
|
||||||
# if not currency.is_zero(diff):
|
|
||||||
# # 1. Create Correction SVL
|
|
||||||
# svl_vals = {
|
|
||||||
# 'company_id': stock_move.company_id.id,
|
|
||||||
# 'product_id': stock_move.product_id.id,
|
|
||||||
# 'description': _("Valuation correction from Vendor Bill %s") % line.move_id.name,
|
|
||||||
# 'value': diff,
|
|
||||||
# 'quantity': 0,
|
|
||||||
# 'stock_move_id': stock_move.id,
|
|
||||||
# }
|
|
||||||
# svl = self.env['stock.valuation.layer'].create(svl_vals)
|
|
||||||
#
|
|
||||||
# # Backdate SVL
|
|
||||||
# if stock_move.date:
|
|
||||||
# new_date = stock_move.date + timedelta(seconds=1)
|
|
||||||
# self.env.cr.execute("UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s", (new_date, svl.id))
|
|
||||||
#
|
|
||||||
# # 2. AVCO/FIFO Logic: Update Standard Price and Distribute Value
|
|
||||||
# product = stock_move.product_id
|
|
||||||
# if product.categ_id.property_cost_method in ['average', 'fifo'] and product.quantity_svl > 0:
|
|
||||||
# new_std_price = product.standard_price + (diff / product.quantity_svl)
|
|
||||||
# product.with_context(disable_auto_svl=True).sudo().write({'standard_price': new_std_price})
|
|
||||||
#
|
|
||||||
# remaining_svls = self.env['stock.valuation.layer'].search([
|
|
||||||
# ('product_id', '=', product.id),
|
|
||||||
# ('remaining_qty', '>', 0),
|
|
||||||
# ('company_id', '=', stock_move.company_id.id),
|
|
||||||
# ])
|
|
||||||
#
|
|
||||||
# if remaining_svls:
|
|
||||||
# remaining_qty_total = sum(remaining_svls.mapped('remaining_qty'))
|
|
||||||
# if remaining_qty_total > 0:
|
|
||||||
# remaining_value_to_distribute = diff
|
|
||||||
# remaining_value_unit_cost = remaining_value_to_distribute / remaining_qty_total
|
|
||||||
#
|
|
||||||
# for layer in remaining_svls:
|
|
||||||
# if float_compare(layer.remaining_qty, remaining_qty_total, precision_rounding=product.uom_id.rounding) >= 0:
|
|
||||||
# taken_remaining_value = remaining_value_to_distribute
|
|
||||||
# else:
|
|
||||||
# taken_remaining_value = remaining_value_unit_cost * layer.remaining_qty
|
|
||||||
#
|
|
||||||
# taken_remaining_value = stock_move.company_id.currency_id.round(taken_remaining_value)
|
|
||||||
# layer.sudo().write({'remaining_value': layer.remaining_value + taken_remaining_value})
|
|
||||||
#
|
|
||||||
# remaining_value_to_distribute -= taken_remaining_value
|
|
||||||
# remaining_qty_total -= layer.remaining_qty
|
|
||||||
#
|
|
||||||
# # 3. Create Accounting Entry
|
|
||||||
# if stock_move.product_id.categ_id.property_valuation == 'real_time':
|
|
||||||
# accounts = stock_move.product_id.product_tmpl_id.get_product_accounts()
|
|
||||||
# acc_expense = accounts.get('expense')
|
|
||||||
# acc_valuation = accounts.get('stock_valuation')
|
|
||||||
#
|
|
||||||
# if acc_expense and acc_valuation:
|
|
||||||
# if diff > 0:
|
|
||||||
# debit_acc = acc_valuation.id
|
|
||||||
# credit_acc = acc_expense.id
|
|
||||||
# amount = diff
|
|
||||||
# else:
|
|
||||||
# debit_acc = acc_expense.id
|
|
||||||
# credit_acc = acc_valuation.id
|
|
||||||
# amount = abs(diff)
|
|
||||||
#
|
|
||||||
# acc_date = stock_move.date.date() if stock_move.date else fields.Date.today()
|
|
||||||
#
|
|
||||||
# move_vals = {
|
|
||||||
# 'journal_id': accounts['stock_journal'].id,
|
|
||||||
# 'company_id': stock_move.company_id.id,
|
|
||||||
# 'ref': _("Revaluation for %s from Bill Edit") % stock_move.product_id.name,
|
|
||||||
# 'date': acc_date,
|
|
||||||
# 'move_type': 'entry',
|
|
||||||
# 'stock_valuation_layer_ids': [(6, 0, [svl.id])],
|
|
||||||
# 'line_ids': [
|
|
||||||
# (0, 0, {
|
|
||||||
# 'name': _("Valuation Correction - %s") % stock_move.product_id.name,
|
|
||||||
# 'account_id': debit_acc,
|
|
||||||
# 'debit': amount,
|
|
||||||
# 'credit': 0,
|
|
||||||
# 'product_id': stock_move.product_id.id,
|
|
||||||
# }),
|
|
||||||
# (0, 0, {
|
|
||||||
# 'name': _("Valuation Correction - %s") % stock_move.product_id.name,
|
|
||||||
# 'account_id': credit_acc,
|
|
||||||
# 'debit': 0,
|
|
||||||
# 'credit': amount,
|
|
||||||
# 'product_id': stock_move.product_id.id,
|
|
||||||
# })
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
# am = self.env['account.move'].create(move_vals)
|
|
||||||
# am._post()
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|||||||
@ -6,3 +6,4 @@ from . import test_price_total_property
|
|||||||
from . import test_edge_cases
|
from . import test_edge_cases
|
||||||
from . import test_view_configuration
|
from . import test_view_configuration
|
||||||
from . import test_integration
|
from . import test_integration
|
||||||
|
from . import test_no_revaluation
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
from odoo.tests import TransactionCase, tagged
|
|
||||||
|
|
||||||
@tagged('post_install', '-at_install')
|
|
||||||
class TestButtonVisibility(TransactionCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.vendor = self.env['res.partner'].create({'name': 'Test Vendor'})
|
|
||||||
self.product = self.env['product.product'].create({'name': 'Test Product', 'standard_price': 100})
|
|
||||||
|
|
||||||
def test_reset_to_draft_visibility(self):
|
|
||||||
# Find existing posted bills for the vendor
|
|
||||||
bills = self.env['account.move'].search([
|
|
||||||
('move_type', '=', 'in_invoice'),
|
|
||||||
('partner_id', '=', self.vendor.id),
|
|
||||||
('state', '=', 'posted')
|
|
||||||
])
|
|
||||||
|
|
||||||
print(f"\nFOUND {len(bills)} POSTED BILLS FOR VENDOR {self.vendor.name}")
|
|
||||||
for bill in bills:
|
|
||||||
print(f"\nChecking Bill {bill.name} (ID: {bill.id}):")
|
|
||||||
print(f" state: {bill.state}")
|
|
||||||
print(f" restrict_mode_hash_table: {bill.restrict_mode_hash_table}")
|
|
||||||
print(f" need_cancel_request: {bill.need_cancel_request}")
|
|
||||||
print(f" show_reset_to_draft_button: {bill.show_reset_to_draft_button}")
|
|
||||||
print(f" journal_id.restrict_mode_hash_table: {bill.journal_id.restrict_mode_hash_table}")
|
|
||||||
|
|
||||||
# Additional check for localization overrides if any
|
|
||||||
# (Printed above via logic)
|
|
||||||
|
|
||||||
if not bills:
|
|
||||||
print("No posted bills found to check. Creating one...")
|
|
||||||
# Create Bill
|
|
||||||
bill = self.env['account.move'].create({
|
|
||||||
'move_type': 'in_invoice',
|
|
||||||
'partner_id': self.vendor.id,
|
|
||||||
'invoice_date': '2024-01-01',
|
|
||||||
'line_ids': [(0, 0, {
|
|
||||||
'product_id': self.product.id,
|
|
||||||
'quantity': 1,
|
|
||||||
'price_unit': 100,
|
|
||||||
})]
|
|
||||||
})
|
|
||||||
|
|
||||||
# Post Bill
|
|
||||||
bill.action_post()
|
|
||||||
print(f"\nCREATED FRESH BILL {bill.name}:")
|
|
||||||
print(f" state: {bill.state}")
|
|
||||||
print(f" show_reset_to_draft_button: {bill.show_reset_to_draft_button}")
|
|
||||||
self.assertTrue(bill.show_reset_to_draft_button, "Fresh bill should have button visible")
|
|
||||||
@ -80,7 +80,7 @@ def test_price_subtotal_field_editable():
|
|||||||
|
|
||||||
# Find the xpath that modifies price_subtotal
|
# Find the xpath that modifies price_subtotal
|
||||||
xpath_elements = root.xpath(
|
xpath_elements = root.xpath(
|
||||||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']\"]"
|
"//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_subtotal']\"]"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not xpath_elements:
|
if not xpath_elements:
|
||||||
@ -132,7 +132,7 @@ def test_price_total_field_editable():
|
|||||||
|
|
||||||
# Find the xpath that modifies price_total
|
# Find the xpath that modifies price_total
|
||||||
xpath_elements = root.xpath(
|
xpath_elements = root.xpath(
|
||||||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_total']\"]"
|
"//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_total']\"]"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not xpath_elements:
|
if not xpath_elements:
|
||||||
@ -184,7 +184,7 @@ def test_fields_readonly_conditions():
|
|||||||
|
|
||||||
# Check price_subtotal attrs
|
# Check price_subtotal attrs
|
||||||
xpath_subtotal = root.xpath(
|
xpath_subtotal = root.xpath(
|
||||||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']\"]"
|
"//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_subtotal']\"]"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not xpath_subtotal:
|
if not xpath_subtotal:
|
||||||
@ -205,7 +205,7 @@ def test_fields_readonly_conditions():
|
|||||||
|
|
||||||
# Check price_total attrs
|
# Check price_total attrs
|
||||||
xpath_total = root.xpath(
|
xpath_total = root.xpath(
|
||||||
"//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_total']\"]"
|
"//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_total']\"]"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not xpath_total:
|
if not xpath_total:
|
||||||
|
|||||||
72
tests/test_no_revaluation.py
Normal file
72
tests/test_no_revaluation.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
from odoo.fields import Date
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install', 'repro_bug')
|
||||||
|
class TestNoRevaluation(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.env.company.currency_id = self.env.ref('base.USD')
|
||||||
|
|
||||||
|
# Create a product with automated valuation (AVCO)
|
||||||
|
self.product_category = self.env['product.category'].create({
|
||||||
|
'name': 'Test Auto Valuation',
|
||||||
|
'property_cost_method': 'average',
|
||||||
|
'property_valuation': 'real_time',
|
||||||
|
})
|
||||||
|
self.product = self.env['product.product'].create({
|
||||||
|
'name': 'Test Product',
|
||||||
|
'type': 'consu',
|
||||||
|
'is_storable': True,
|
||||||
|
'categ_id': self.product_category.id,
|
||||||
|
'standard_price': 100.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a vendor
|
||||||
|
self.vendor = self.env['res.partner'].create({'name': 'Test Vendor'})
|
||||||
|
|
||||||
|
def test_no_revaluation_on_bill_edit(self):
|
||||||
|
"""
|
||||||
|
Verify that editing a vendor bill line price does NOT trigger
|
||||||
|
inventory revaluation (SVL creation) or accounting entries.
|
||||||
|
"""
|
||||||
|
# 1. Create and Confirm PO
|
||||||
|
po = self.env['purchase.order'].create({
|
||||||
|
'partner_id': self.vendor.id,
|
||||||
|
'order_line': [(0, 0, {
|
||||||
|
'product_id': self.product.id,
|
||||||
|
'product_qty': 10.0,
|
||||||
|
'price_unit': 100.0,
|
||||||
|
})],
|
||||||
|
})
|
||||||
|
po.button_confirm()
|
||||||
|
|
||||||
|
# 2. Receive Products
|
||||||
|
picking = po.picking_ids[0]
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
# Verify initial SVL
|
||||||
|
svls = self.env['stock.valuation.layer'].search([('product_id', '=', self.product.id)])
|
||||||
|
self.assertEqual(len(svls), 1, "Should be 1 SVL for reception")
|
||||||
|
initial_svl_count = len(svls)
|
||||||
|
|
||||||
|
# 3. Create Vendor Bill
|
||||||
|
action = po.action_create_invoice()
|
||||||
|
bill = self.env['account.move'].browse(action['res_id'])
|
||||||
|
bill.invoice_date = Date.today()
|
||||||
|
|
||||||
|
# 4. Edit Vendor Bill Line Price (Simulate User Edit)
|
||||||
|
# Change price from 100 to 120
|
||||||
|
# This triggers the write method on account.move.line
|
||||||
|
line = bill.invoice_line_ids[0]
|
||||||
|
line.price_unit = 120.0 # This triggers the write method
|
||||||
|
|
||||||
|
# 5. Verify NO new SVL created
|
||||||
|
new_svls = self.env['stock.valuation.layer'].search([('product_id', '=', self.product.id)])
|
||||||
|
self.assertEqual(len(new_svls), initial_svl_count, "No new SVL should be created after bill edit")
|
||||||
|
|
||||||
|
# Verify standard price did NOT change (since no revaluation)
|
||||||
|
# Note: If revaluation logic was present, it would update standard_price
|
||||||
|
# However, since we expect NO revaluation, standard price might stay same or move based on other logic?
|
||||||
|
# The key is checking SVL count.
|
||||||
@ -1,98 +0,0 @@
|
|||||||
from odoo.tests import TransactionCase, tagged
|
|
||||||
from odoo.exceptions import UserError
|
|
||||||
from datetime import date, timedelta
|
|
||||||
|
|
||||||
@tagged('post_install', '-at_install')
|
|
||||||
class TestReproduction(TransactionCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.vendor = self.env['res.partner'].create({'name': 'Test Vendor'})
|
|
||||||
self.stock_journal = self.env['account.journal'].search([('type', '=', 'general')], limit=1)
|
|
||||||
|
|
||||||
# Create a product with AVCO/Automated valuation
|
|
||||||
self.product_category = self.env['product.category'].create({
|
|
||||||
'name': 'Test Category',
|
|
||||||
'property_cost_method': 'average',
|
|
||||||
'property_valuation': 'real_time',
|
|
||||||
})
|
|
||||||
self.product = self.env['product.product'].create({
|
|
||||||
'name': 'Test Product',
|
|
||||||
'type': 'product',
|
|
||||||
'categ_id': self.product_category.id,
|
|
||||||
'standard_price': 10.0,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Ensure accounts are set on category (simplified for test)
|
|
||||||
account_type = self.env.ref('account.data_account_type_expenses')
|
|
||||||
self.account_expense = self.env['account.account'].create({
|
|
||||||
'name': 'Expense',
|
|
||||||
'code': 'EXP001',
|
|
||||||
'account_type': account_type.id,
|
|
||||||
'reconcile': True,
|
|
||||||
})
|
|
||||||
self.account_valuation = self.env['account.account'].create({
|
|
||||||
'name': 'Stock Valuation',
|
|
||||||
'code': 'STK001',
|
|
||||||
'account_type': account_type.id,
|
|
||||||
'reconcile': True,
|
|
||||||
})
|
|
||||||
self.product_category.property_stock_account_input_categ_id = self.account_expense
|
|
||||||
self.product_category.property_stock_account_output_categ_id = self.account_expense
|
|
||||||
self.product_category.property_stock_valuation_account_id = self.account_valuation
|
|
||||||
self.product_category.property_stock_journal = self.stock_journal
|
|
||||||
|
|
||||||
def test_revaluation_journal_creation(self):
|
|
||||||
# 1. Create PO
|
|
||||||
po = self.env['purchase.order'].create({
|
|
||||||
'partner_id': self.vendor.id,
|
|
||||||
'order_line': [(0, 0, {
|
|
||||||
'product_id': self.product.id,
|
|
||||||
'product_qty': 10.0,
|
|
||||||
'price_unit': 10.0, # $100 total
|
|
||||||
})]
|
|
||||||
})
|
|
||||||
po.button_confirm()
|
|
||||||
|
|
||||||
# 2. Receive Goods
|
|
||||||
picking = po.picking_ids
|
|
||||||
picking.button_validate()
|
|
||||||
|
|
||||||
# 3. Create Bill
|
|
||||||
move_form = self.env['account.move'].create({
|
|
||||||
'move_type': 'in_invoice',
|
|
||||||
'partner_id': self.vendor.id,
|
|
||||||
'invoice_date': date.today(),
|
|
||||||
'purchase_vendor_bill_id': po.id,
|
|
||||||
'line_ids': [(0, 0, {
|
|
||||||
'product_id': self.product.id,
|
|
||||||
'quantity': 10.0,
|
|
||||||
'price_unit': 10.0,
|
|
||||||
'purchase_line_id': po.order_line.id
|
|
||||||
})]
|
|
||||||
})
|
|
||||||
|
|
||||||
# Simulate linking causing sync? Actually just creating the line linked to PO line should be enough
|
|
||||||
# But for test, we can just create the line manually linked to PO line as above
|
|
||||||
|
|
||||||
bill_line = move_form.line_ids.filtered(lambda l: l.product_id == self.product)
|
|
||||||
|
|
||||||
# 4. Modify Price (Trigger the issue)
|
|
||||||
# Edit price to 12.0
|
|
||||||
bill_line.with_context(active_model='account.move').write({'price_unit': 12.0})
|
|
||||||
|
|
||||||
# 5. Check for Revaluation Journal
|
|
||||||
# Search for account.move with 'Revaluation for' in ref
|
|
||||||
revaluation_moves = self.env['account.move'].search([
|
|
||||||
('ref', 'like', 'Revaluation for %s from Bill Edit' % self.product.name)
|
|
||||||
])
|
|
||||||
|
|
||||||
# Assert that it exists (Current bad behavior)
|
|
||||||
self.assertEqual(len(revaluation_moves), 0, "Revaluation journal should NOT exist after fix")
|
|
||||||
|
|
||||||
# 6. Check for Stock Valuation Layer
|
|
||||||
svls = self.env['stock.valuation.layer'].search([
|
|
||||||
('description', 'like', 'Valuation correction from Vendor Bill %')
|
|
||||||
])
|
|
||||||
self.assertEqual(len(svls), 0, "Stock Valuation Layer should NOT be created")
|
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class TestViewConfiguration(TransactionCase):
|
|||||||
arch = etree.fromstring(self.view.arch)
|
arch = etree.fromstring(self.view.arch)
|
||||||
|
|
||||||
# Find the xpath that modifies price_subtotal
|
# Find the xpath that modifies price_subtotal
|
||||||
xpath_elements = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']\"]")
|
xpath_elements = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_subtotal']\"]")
|
||||||
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
len(xpath_elements) > 0,
|
len(xpath_elements) > 0,
|
||||||
@ -80,7 +80,7 @@ class TestViewConfiguration(TransactionCase):
|
|||||||
arch = etree.fromstring(self.view.arch)
|
arch = etree.fromstring(self.view.arch)
|
||||||
|
|
||||||
# Find the xpath that modifies price_total
|
# Find the xpath that modifies price_total
|
||||||
xpath_elements = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_total']\"]")
|
xpath_elements = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_total']\"]")
|
||||||
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
len(xpath_elements) > 0,
|
len(xpath_elements) > 0,
|
||||||
@ -110,7 +110,7 @@ class TestViewConfiguration(TransactionCase):
|
|||||||
arch = etree.fromstring(self.view.arch)
|
arch = etree.fromstring(self.view.arch)
|
||||||
|
|
||||||
# Check price_subtotal attrs
|
# Check price_subtotal attrs
|
||||||
xpath_subtotal = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']\"]")
|
xpath_subtotal = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_subtotal']\"]")
|
||||||
self.assertTrue(len(xpath_subtotal) > 0, "price_subtotal xpath should exist")
|
self.assertTrue(len(xpath_subtotal) > 0, "price_subtotal xpath should exist")
|
||||||
|
|
||||||
attrs_subtotal = xpath_subtotal[0].xpath(".//attribute[@name='attrs']")
|
attrs_subtotal = xpath_subtotal[0].xpath(".//attribute[@name='attrs']")
|
||||||
@ -133,7 +133,7 @@ class TestViewConfiguration(TransactionCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check price_total attrs
|
# Check price_total attrs
|
||||||
xpath_total = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_total']\"]")
|
xpath_total = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_total']\"]")
|
||||||
self.assertTrue(len(xpath_total) > 0, "price_total xpath should exist")
|
self.assertTrue(len(xpath_total) > 0, "price_total xpath should exist")
|
||||||
|
|
||||||
attrs_total = xpath_total[0].xpath(".//attribute[@name='attrs']")
|
attrs_total = xpath_total[0].xpath(".//attribute[@name='attrs']")
|
||||||
@ -251,7 +251,7 @@ class TestViewConfiguration(TransactionCase):
|
|||||||
arch = etree.fromstring(self.view.arch)
|
arch = etree.fromstring(self.view.arch)
|
||||||
|
|
||||||
# Verify both fields have attrs that reference parent.state
|
# Verify both fields have attrs that reference parent.state
|
||||||
xpath_subtotal = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']\"]")
|
xpath_subtotal = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_subtotal']\"]")
|
||||||
attrs_subtotal = xpath_subtotal[0].xpath(".//attribute[@name='attrs']")
|
attrs_subtotal = xpath_subtotal[0].xpath(".//attribute[@name='attrs']")
|
||||||
attrs_text_subtotal = attrs_subtotal[0].text
|
attrs_text_subtotal = attrs_subtotal[0].text
|
||||||
|
|
||||||
@ -262,7 +262,7 @@ class TestViewConfiguration(TransactionCase):
|
|||||||
"Attrs should reference parent.state for price_subtotal"
|
"Attrs should reference parent.state for price_subtotal"
|
||||||
)
|
)
|
||||||
|
|
||||||
xpath_total = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/tree//field[@name='price_total']\"]")
|
xpath_total = arch.xpath("//xpath[@expr=\"//field[@name='invoice_line_ids']/list//field[@name='price_total']\"]")
|
||||||
attrs_total = xpath_total[0].xpath(".//attribute[@name='attrs']")
|
attrs_total = xpath_total[0].xpath(".//attribute[@name='attrs']")
|
||||||
attrs_text_total = attrs_total[0].text
|
attrs_text_total = attrs_total[0].text
|
||||||
|
|
||||||
|
|||||||
@ -8,15 +8,13 @@
|
|||||||
<field name="inherit_id" ref="account.view_move_form"/>
|
<field name="inherit_id" ref="account.view_move_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<!-- Make price_subtotal editable in vendor bill invoice lines -->
|
<!-- Make price_subtotal editable in vendor bill invoice lines -->
|
||||||
<xpath expr="//field[@name='invoice_line_ids']/tree//field[@name='price_subtotal']" position="attributes">
|
<xpath expr="//field[@name='invoice_line_ids']/list//field[@name='price_subtotal']" position="attributes">
|
||||||
<attribute name="readonly">0</attribute>
|
<attribute name="readonly">parent.state != 'draft'</attribute>
|
||||||
<attribute name="attrs">{'readonly': [('parent.state', '!=', 'draft')]}</attribute>
|
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
||||||
<!-- Make price_total editable in vendor bill invoice lines -->
|
<!-- Make price_total editable in vendor bill invoice lines -->
|
||||||
<xpath expr="//field[@name='invoice_line_ids']/tree//field[@name='price_total']" position="attributes">
|
<xpath expr="//field[@name='invoice_line_ids']/list//field[@name='price_total']" position="attributes">
|
||||||
<attribute name="readonly">0</attribute>
|
<attribute name="readonly">parent.state != 'draft'</attribute>
|
||||||
<attribute name="attrs">{'readonly': [('parent.state', '!=', 'draft')]}</attribute>
|
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user