feat: Restructure fixed asset import scripts, enforcing constant periods and first-of-month prorata dates, and adding a depreciation logic test.
This commit is contained in:
parent
b1de181c59
commit
450977a31f
@ -94,7 +94,7 @@ def ensure_models(env):
|
||||
'method': fallback.method,
|
||||
'method_number': fallback.method_number,
|
||||
'method_period': fallback.method_period,
|
||||
'prorata_computation_type': fallback.prorata_computation_type,
|
||||
'prorata_computation_type': 'constant_periods',
|
||||
'method_progress_factor': fallback.method_progress_factor,
|
||||
'account_asset_id': fallback.account_asset_id.id,
|
||||
'account_depreciation_id': fallback.account_depreciation_id.id,
|
||||
@ -118,6 +118,9 @@ def ensure_models(env):
|
||||
'prorata_computation_type': 'constant_periods',
|
||||
})
|
||||
|
||||
# Ensure 'constant_periods' even if inherited from fallback
|
||||
vals['prorata_computation_type'] = 'constant_periods'
|
||||
|
||||
env['account.asset'].create(vals)
|
||||
print(f"Created model '{config['name']}'.")
|
||||
|
||||
@ -222,11 +225,12 @@ def process_import(env, excel_file):
|
||||
'asset_code': col_1, # Added Asset Code
|
||||
'original_value': original_value,
|
||||
'acquisition_date': acquisition_date,
|
||||
'prorata_date': acquisition_date.replace(day=1), # FORCE FIRST OF MONTH
|
||||
'model_id': model.id,
|
||||
'method': model.method,
|
||||
'method_number': effective_method_number,
|
||||
'method_period': model.method_period,
|
||||
'prorata_computation_type': model.prorata_computation_type,
|
||||
'prorata_computation_type': 'constant_periods', # FORCE CONSTANT PERIODS
|
||||
'already_depreciated_amount_import': accum_depr_oct31,
|
||||
'state': 'draft',
|
||||
'account_asset_id': model.account_asset_id.id,
|
||||
|
||||
@ -9,7 +9,7 @@ from dateutil.relativedelta import relativedelta
|
||||
CUTOFF_DATE = date(2025, 10, 31)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Import Fixed Assets to Odoo")
|
||||
parser = argparse.ArgumentParser(description="Import Fixed Assets to Odoo (NO PRORATA Variant)")
|
||||
parser.add_argument("odoo_bin_path", help="Path to odoo-bin executable")
|
||||
parser.add_argument("conf_path", help="Path to odoo.conf")
|
||||
parser.add_argument("excel_path", help="Path to the Excel file")
|
||||
@ -37,7 +37,7 @@ def main():
|
||||
print(f"Error: Could not import 'odoo' module from {odoo_root}. Make sure odoo-bin path is correct.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Initializing Odoo Environment for database: {db_name}...")
|
||||
print(f"Initializing Odoo Environment for database: {db_name} (NO PRORATA Variant)...")
|
||||
try:
|
||||
odoo.tools.config.parse_config(['-c', conf_path])
|
||||
registry = odoo.registry(db_name)
|
||||
@ -78,6 +78,8 @@ def ensure_models(env):
|
||||
for key, config in missing_models.items():
|
||||
exists = env['account.asset'].search([('state', '=', 'model'), ('name', '=ilike', config['name'])], limit=1)
|
||||
if exists:
|
||||
# Update existing models to 'none' prorata as well
|
||||
exists.write({'prorata_computation_type': 'none'})
|
||||
continue
|
||||
|
||||
print(f"Creating missing model '{config['name']}'...")
|
||||
@ -94,7 +96,7 @@ def ensure_models(env):
|
||||
'method': fallback.method,
|
||||
'method_number': fallback.method_number,
|
||||
'method_period': fallback.method_period,
|
||||
'prorata_computation_type': fallback.prorata_computation_type,
|
||||
'prorata_computation_type': 'none', # NO PRORATA
|
||||
'method_progress_factor': fallback.method_progress_factor,
|
||||
'account_asset_id': fallback.account_asset_id.id,
|
||||
'account_depreciation_id': fallback.account_depreciation_id.id,
|
||||
@ -115,9 +117,12 @@ def ensure_models(env):
|
||||
'method': 'linear',
|
||||
'method_number': config['method_number'],
|
||||
'method_period': config['method_period'],
|
||||
'prorata_computation_type': 'constant_periods',
|
||||
'prorata_computation_type': 'none', # NO PRORATA
|
||||
})
|
||||
|
||||
# Ensure 'none' even if inherited from fallback
|
||||
vals['prorata_computation_type'] = 'none'
|
||||
|
||||
env['account.asset'].create(vals)
|
||||
print(f"Created model '{config['name']}'.")
|
||||
|
||||
@ -183,11 +188,14 @@ def process_import(env, excel_file):
|
||||
continue
|
||||
models_cache[model_name_search] = model
|
||||
|
||||
# Calculate Logic - FIXED VERSION
|
||||
# Ensure model is set to 'none' if it exists
|
||||
if model.prorata_computation_type != 'none':
|
||||
model.write({'prorata_computation_type': 'none'})
|
||||
|
||||
# Determine total duration in months
|
||||
DURATION_OVERRIDES = {
|
||||
'RENOVASI BANGUNAN': 60,
|
||||
'PATENT & MERK': 120,
|
||||
'PATENT & MERK': 60,
|
||||
'PERALATAN DAPUR': 60,
|
||||
}
|
||||
|
||||
@ -195,40 +203,17 @@ def process_import(env, excel_file):
|
||||
if current_category_name in DURATION_OVERRIDES:
|
||||
effective_method_number = DURATION_OVERRIDES[current_category_name]
|
||||
|
||||
period_multiple = int(model.method_period) # 1 or 12
|
||||
total_months = effective_method_number * period_multiple
|
||||
|
||||
# Calculate months passed from acquisition to CUTOFF_DATE
|
||||
delta = relativedelta(CUTOFF_DATE, acquisition_date)
|
||||
months_passed = delta.years * 12 + delta.months + (1 if delta.days > 0 else 0)
|
||||
|
||||
# Ensure months_passed does not exceed total_months
|
||||
months_passed = min(months_passed, total_months)
|
||||
|
||||
# Calculate accumulated depreciation up to CUTOFF_DATE
|
||||
monthly_depr = original_value / total_months if total_months > 0 else 0
|
||||
accum_depr_oct31 = monthly_depr * months_passed
|
||||
|
||||
# Clamp values
|
||||
if accum_depr_oct31 < 0:
|
||||
accum_depr_oct31 = 0.0
|
||||
if accum_depr_oct31 > original_value:
|
||||
accum_depr_oct31 = original_value
|
||||
|
||||
# SOLUTION: Don't use already_depreciated_amount_import
|
||||
# Instead, create the asset with the original acquisition date and full duration
|
||||
# Then manually create an opening balance entry
|
||||
|
||||
vals = {
|
||||
'name': col_name,
|
||||
'asset_code': col_1,
|
||||
'asset_code': col_1,
|
||||
'original_value': original_value,
|
||||
'acquisition_date': acquisition_date, # Keep original acquisition date
|
||||
'acquisition_date': acquisition_date,
|
||||
'model_id': model.id,
|
||||
'method': model.method,
|
||||
'method_number': effective_method_number, # Full duration
|
||||
'method_number': effective_method_number,
|
||||
'method_period': model.method_period,
|
||||
'prorata_computation_type': model.prorata_computation_type,
|
||||
'prorata_computation_type': 'none', # FORCE NO PRORATA
|
||||
'already_depreciated_amount_import': 0.0, # FORCE 0 AS REQUESTED
|
||||
'state': 'draft',
|
||||
'account_asset_id': model.account_asset_id.id,
|
||||
'account_depreciation_id': model.account_depreciation_id.id,
|
||||
@ -239,26 +224,20 @@ def process_import(env, excel_file):
|
||||
}
|
||||
|
||||
asset = env['account.asset'].create(vals)
|
||||
|
||||
# Compute the full depreciation board
|
||||
asset.compute_depreciation_board()
|
||||
|
||||
# Remove historical depreciation moves up to cutoff date
|
||||
historical_moves = asset.depreciation_move_ids.filtered(
|
||||
lambda m: m.date <= CUTOFF_DATE and m.state == 'draft'
|
||||
)
|
||||
# Clean up Historical Moves
|
||||
historical_moves = asset.depreciation_move_ids.filtered(lambda m: m.date <= CUTOFF_DATE)
|
||||
if historical_moves:
|
||||
print(f" Removing {len(historical_moves)} historical moves for {asset.name}")
|
||||
print(f" Removing {len(historical_moves)} historical moves before {CUTOFF_DATE} for {asset.name}...")
|
||||
historical_moves.unlink()
|
||||
|
||||
asset.write({'state': 'open'})
|
||||
|
||||
# Calculate end date for verification
|
||||
end_date = acquisition_date + relativedelta(months=total_months)
|
||||
print(f"Imported: {asset.name} | Val: {original_value:,.2f} | Accum: {accum_depr_oct31:,.2f} | End: {end_date}")
|
||||
print(f"Imported: {asset.name} | Val: {original_value} | No Prorata | ImportAmt: 0")
|
||||
count += 1
|
||||
|
||||
print(f"\nTotal Assets Imported: {count}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
214
test_depreciation_logic.py
Normal file
214
test_depreciation_logic.py
Normal file
@ -0,0 +1,214 @@
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from datetime import datetime, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Deep Test Depreciation Logic")
|
||||
parser.add_argument("odoo_bin_path", help="Path to odoo-bin executable")
|
||||
parser.add_argument("conf_path", help="Path to odoo.conf")
|
||||
parser.add_argument("db_name", help="Database name")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
odoo_bin_path = os.path.abspath(args.odoo_bin_path)
|
||||
conf_path = os.path.abspath(args.conf_path)
|
||||
db_name = args.db_name
|
||||
|
||||
# Add Odoo to sys.path
|
||||
odoo_root = os.path.dirname(odoo_bin_path)
|
||||
if odoo_root not in sys.path:
|
||||
sys.path.append(odoo_root)
|
||||
|
||||
# Change CWD to config directory to handle relative paths in config (like addons_path)
|
||||
os.chdir(os.path.dirname(conf_path))
|
||||
|
||||
try:
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
except ImportError:
|
||||
print(f"Error: Could not import 'odoo' module from {odoo_root}. Make sure odoo-bin path is correct.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Initializing Odoo Environment for database: {db_name}...")
|
||||
try:
|
||||
odoo.tools.config.parse_config(['-c', conf_path])
|
||||
registry = odoo.registry(db_name)
|
||||
except Exception as e:
|
||||
print(f"Error initializing Odoo: {e}")
|
||||
return
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
print("Connected to Odoo.")
|
||||
|
||||
try:
|
||||
test_different_scenarios(env)
|
||||
cr.commit()
|
||||
print("Test completed successfully.")
|
||||
except Exception as e:
|
||||
cr.rollback()
|
||||
print(f"Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_different_scenarios(env):
|
||||
# Find an asset model to use
|
||||
model = env['account.asset'].search([('state', '=', 'model')], limit=1)
|
||||
if not model:
|
||||
print("No asset model found!")
|
||||
return
|
||||
|
||||
print(f"Using asset model: {model.name}")
|
||||
print(f"Model settings: method={model.method}, method_number={model.method_number}, method_period={model.method_period}")
|
||||
|
||||
# Test different acquisition dates
|
||||
test_cases = [
|
||||
{
|
||||
'name': 'Future Acquisition (2025-11-01)',
|
||||
'acquisition_date': date(2025, 11, 1),
|
||||
'description': 'Asset acquired after cutoff date'
|
||||
},
|
||||
{
|
||||
'name': 'Recent Acquisition (2024-11-01)',
|
||||
'acquisition_date': date(2024, 11, 1),
|
||||
'description': 'Asset acquired before cutoff date'
|
||||
},
|
||||
{
|
||||
'name': 'Old Acquisition (2020-01-01)',
|
||||
'acquisition_date': date(2020, 1, 1),
|
||||
'description': 'Very old asset'
|
||||
}
|
||||
]
|
||||
|
||||
for i, test_case in enumerate(test_cases):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"TEST {i+1}: {test_case['name']}")
|
||||
print(f"Description: {test_case['description']}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
acquisition_date = test_case['acquisition_date']
|
||||
original_value = 48500000.0
|
||||
|
||||
vals = {
|
||||
'name': f'TEST Asset - {test_case["name"]}',
|
||||
'original_value': original_value,
|
||||
'acquisition_date': acquisition_date,
|
||||
'model_id': model.id,
|
||||
'method': model.method,
|
||||
'method_number': 60, # Force 60 months
|
||||
'method_period': '1', # Force monthly
|
||||
'prorata_computation_type': model.prorata_computation_type,
|
||||
'state': 'draft',
|
||||
'account_asset_id': model.account_asset_id.id,
|
||||
'account_depreciation_id': model.account_depreciation_id.id,
|
||||
'account_depreciation_expense_id': model.account_depreciation_expense_id.id,
|
||||
'journal_id': model.journal_id.id,
|
||||
'method_progress_factor': model.method_progress_factor,
|
||||
'analytic_distribution': model.analytic_distribution,
|
||||
}
|
||||
|
||||
asset = env['account.asset'].create(vals)
|
||||
|
||||
print(f"Created asset: {asset.name}")
|
||||
print(f" Acquisition Date: {asset.acquisition_date}")
|
||||
print(f" Method Number: {asset.method_number}")
|
||||
print(f" Method Period: {asset.method_period}")
|
||||
|
||||
# Compute depreciation board
|
||||
asset.compute_depreciation_board()
|
||||
|
||||
moves = asset.depreciation_move_ids.sorted('date')
|
||||
print(f" Moves created: {len(moves)}")
|
||||
|
||||
if moves:
|
||||
print(f" First move: {moves[0].date}")
|
||||
print(f" Last move: {moves[-1].date}")
|
||||
|
||||
# Calculate expected end date
|
||||
expected_end = acquisition_date + relativedelta(months=59) # 60 months = 0 to 59
|
||||
print(f" Expected last move around: {expected_end}")
|
||||
|
||||
# Check if complete
|
||||
if len(moves) >= 60:
|
||||
print(f" ✅ Complete: {len(moves)} moves")
|
||||
else:
|
||||
print(f" ❌ Incomplete: Only {len(moves)} moves (expected 60)")
|
||||
|
||||
# Show first few and last few moves
|
||||
print(f" First 3 moves:")
|
||||
for move in moves[:3]:
|
||||
amount = sum(line.debit for line in move.line_ids if line.account_id == asset.account_depreciation_expense_id)
|
||||
print(f" {move.date}: {amount:,.2f}")
|
||||
|
||||
print(f" Last 3 moves:")
|
||||
for move in moves[-3:]:
|
||||
amount = sum(line.debit for line in move.line_ids if line.account_id == asset.account_depreciation_expense_id)
|
||||
print(f" {move.date}: {amount:,.2f}")
|
||||
|
||||
# Cleanup
|
||||
asset.unlink()
|
||||
|
||||
# Test with different prorata settings
|
||||
print(f"\n{'='*60}")
|
||||
print(f"TEST: Different Prorata Settings")
|
||||
print(f"{'='*60}")
|
||||
|
||||
prorata_options = ['constant_periods', 'daily_computation', 'none']
|
||||
|
||||
for prorata in prorata_options:
|
||||
print(f"\nTesting prorata_computation_type: {prorata}")
|
||||
|
||||
vals = {
|
||||
'name': f'TEST Asset - Prorata {prorata}',
|
||||
'original_value': 48500000.0,
|
||||
'acquisition_date': date(2024, 11, 1),
|
||||
'model_id': model.id,
|
||||
'method': 'linear',
|
||||
'method_number': 60,
|
||||
'method_period': '1',
|
||||
'prorata_computation_type': prorata,
|
||||
'state': 'draft',
|
||||
'account_asset_id': model.account_asset_id.id,
|
||||
'account_depreciation_id': model.account_depreciation_id.id,
|
||||
'account_depreciation_expense_id': model.account_depreciation_expense_id.id,
|
||||
'journal_id': model.journal_id.id,
|
||||
'method_progress_factor': model.method_progress_factor,
|
||||
'analytic_distribution': model.analytic_distribution,
|
||||
}
|
||||
|
||||
asset = env['account.asset'].create(vals)
|
||||
asset.compute_depreciation_board()
|
||||
|
||||
moves = asset.depreciation_move_ids.sorted('date')
|
||||
print(f" Moves: {len(moves)}, Last: {moves[-1].date if moves else 'None'}, Prorata Date: {asset.prorata_date}")
|
||||
|
||||
asset.unlink()
|
||||
|
||||
# Manual Test for 'none' with forced date
|
||||
print(f"\nTesting prorata_computation_type: none (FORCED prorata_date)")
|
||||
vals = {
|
||||
'name': f'TEST Asset - Forced None',
|
||||
'original_value': 48500000.0,
|
||||
'acquisition_date': date(2024, 11, 1),
|
||||
'prorata_date': date(2024, 11, 1), # Explicitly set
|
||||
'model_id': model.id,
|
||||
'method': 'linear',
|
||||
'method_number': 60,
|
||||
'method_period': '1',
|
||||
'prorata_computation_type': 'none',
|
||||
'state': 'draft',
|
||||
'account_asset_id': model.account_asset_id.id,
|
||||
'account_depreciation_id': model.account_depreciation_id.id,
|
||||
'account_depreciation_expense_id': model.account_depreciation_expense_id.id,
|
||||
'journal_id': model.journal_id.id,
|
||||
}
|
||||
asset = env['account.asset'].create(vals)
|
||||
asset.compute_depreciation_board()
|
||||
moves = asset.depreciation_move_ids.sorted('date')
|
||||
print(f" Moves: {len(moves)}, Last: {moves[-1].date if moves else 'None'}, Prorata Date: {asset.prorata_date}")
|
||||
asset.unlink()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user