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:
Suherdy Yacob 2026-01-23 08:23:01 +07:00
parent b1de181c59
commit 450977a31f
3 changed files with 244 additions and 47 deletions

View File

@ -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,

View File

@ -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,
'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,23 +224,17 @@ 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}")

214
test_depreciation_logic.py Normal file
View 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()