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': fallback.method,
|
||||||
'method_number': fallback.method_number,
|
'method_number': fallback.method_number,
|
||||||
'method_period': fallback.method_period,
|
'method_period': fallback.method_period,
|
||||||
'prorata_computation_type': fallback.prorata_computation_type,
|
'prorata_computation_type': 'constant_periods',
|
||||||
'method_progress_factor': fallback.method_progress_factor,
|
'method_progress_factor': fallback.method_progress_factor,
|
||||||
'account_asset_id': fallback.account_asset_id.id,
|
'account_asset_id': fallback.account_asset_id.id,
|
||||||
'account_depreciation_id': fallback.account_depreciation_id.id,
|
'account_depreciation_id': fallback.account_depreciation_id.id,
|
||||||
@ -118,6 +118,9 @@ def ensure_models(env):
|
|||||||
'prorata_computation_type': 'constant_periods',
|
'prorata_computation_type': 'constant_periods',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Ensure 'constant_periods' even if inherited from fallback
|
||||||
|
vals['prorata_computation_type'] = 'constant_periods'
|
||||||
|
|
||||||
env['account.asset'].create(vals)
|
env['account.asset'].create(vals)
|
||||||
print(f"Created model '{config['name']}'.")
|
print(f"Created model '{config['name']}'.")
|
||||||
|
|
||||||
@ -222,11 +225,12 @@ def process_import(env, excel_file):
|
|||||||
'asset_code': col_1, # Added Asset Code
|
'asset_code': col_1, # Added Asset Code
|
||||||
'original_value': original_value,
|
'original_value': original_value,
|
||||||
'acquisition_date': acquisition_date,
|
'acquisition_date': acquisition_date,
|
||||||
|
'prorata_date': acquisition_date.replace(day=1), # FORCE FIRST OF MONTH
|
||||||
'model_id': model.id,
|
'model_id': model.id,
|
||||||
'method': model.method,
|
'method': model.method,
|
||||||
'method_number': effective_method_number,
|
'method_number': effective_method_number,
|
||||||
'method_period': model.method_period,
|
'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,
|
'already_depreciated_amount_import': accum_depr_oct31,
|
||||||
'state': 'draft',
|
'state': 'draft',
|
||||||
'account_asset_id': model.account_asset_id.id,
|
'account_asset_id': model.account_asset_id.id,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from dateutil.relativedelta import relativedelta
|
|||||||
CUTOFF_DATE = date(2025, 10, 31)
|
CUTOFF_DATE = date(2025, 10, 31)
|
||||||
|
|
||||||
def main():
|
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("odoo_bin_path", help="Path to odoo-bin executable")
|
||||||
parser.add_argument("conf_path", help="Path to odoo.conf")
|
parser.add_argument("conf_path", help="Path to odoo.conf")
|
||||||
parser.add_argument("excel_path", help="Path to the Excel file")
|
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.")
|
print(f"Error: Could not import 'odoo' module from {odoo_root}. Make sure odoo-bin path is correct.")
|
||||||
sys.exit(1)
|
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:
|
try:
|
||||||
odoo.tools.config.parse_config(['-c', conf_path])
|
odoo.tools.config.parse_config(['-c', conf_path])
|
||||||
registry = odoo.registry(db_name)
|
registry = odoo.registry(db_name)
|
||||||
@ -78,6 +78,8 @@ def ensure_models(env):
|
|||||||
for key, config in missing_models.items():
|
for key, config in missing_models.items():
|
||||||
exists = env['account.asset'].search([('state', '=', 'model'), ('name', '=ilike', config['name'])], limit=1)
|
exists = env['account.asset'].search([('state', '=', 'model'), ('name', '=ilike', config['name'])], limit=1)
|
||||||
if exists:
|
if exists:
|
||||||
|
# Update existing models to 'none' prorata as well
|
||||||
|
exists.write({'prorata_computation_type': 'none'})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"Creating missing model '{config['name']}'...")
|
print(f"Creating missing model '{config['name']}'...")
|
||||||
@ -94,7 +96,7 @@ def ensure_models(env):
|
|||||||
'method': fallback.method,
|
'method': fallback.method,
|
||||||
'method_number': fallback.method_number,
|
'method_number': fallback.method_number,
|
||||||
'method_period': fallback.method_period,
|
'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,
|
'method_progress_factor': fallback.method_progress_factor,
|
||||||
'account_asset_id': fallback.account_asset_id.id,
|
'account_asset_id': fallback.account_asset_id.id,
|
||||||
'account_depreciation_id': fallback.account_depreciation_id.id,
|
'account_depreciation_id': fallback.account_depreciation_id.id,
|
||||||
@ -115,9 +117,12 @@ def ensure_models(env):
|
|||||||
'method': 'linear',
|
'method': 'linear',
|
||||||
'method_number': config['method_number'],
|
'method_number': config['method_number'],
|
||||||
'method_period': config['method_period'],
|
'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)
|
env['account.asset'].create(vals)
|
||||||
print(f"Created model '{config['name']}'.")
|
print(f"Created model '{config['name']}'.")
|
||||||
|
|
||||||
@ -183,11 +188,14 @@ def process_import(env, excel_file):
|
|||||||
continue
|
continue
|
||||||
models_cache[model_name_search] = model
|
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
|
# Determine total duration in months
|
||||||
DURATION_OVERRIDES = {
|
DURATION_OVERRIDES = {
|
||||||
'RENOVASI BANGUNAN': 60,
|
'RENOVASI BANGUNAN': 60,
|
||||||
'PATENT & MERK': 120,
|
'PATENT & MERK': 60,
|
||||||
'PERALATAN DAPUR': 60,
|
'PERALATAN DAPUR': 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,40 +203,17 @@ def process_import(env, excel_file):
|
|||||||
if current_category_name in DURATION_OVERRIDES:
|
if current_category_name in DURATION_OVERRIDES:
|
||||||
effective_method_number = DURATION_OVERRIDES[current_category_name]
|
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 = {
|
vals = {
|
||||||
'name': col_name,
|
'name': col_name,
|
||||||
'asset_code': col_1,
|
'asset_code': col_1,
|
||||||
'original_value': original_value,
|
'original_value': original_value,
|
||||||
'acquisition_date': acquisition_date, # Keep original acquisition date
|
'acquisition_date': acquisition_date,
|
||||||
'model_id': model.id,
|
'model_id': model.id,
|
||||||
'method': model.method,
|
'method': model.method,
|
||||||
'method_number': effective_method_number, # Full duration
|
'method_number': effective_method_number,
|
||||||
'method_period': model.method_period,
|
'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',
|
'state': 'draft',
|
||||||
'account_asset_id': model.account_asset_id.id,
|
'account_asset_id': model.account_asset_id.id,
|
||||||
'account_depreciation_id': model.account_depreciation_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)
|
asset = env['account.asset'].create(vals)
|
||||||
|
|
||||||
# Compute the full depreciation board
|
|
||||||
asset.compute_depreciation_board()
|
asset.compute_depreciation_board()
|
||||||
|
|
||||||
# Remove historical depreciation moves up to cutoff date
|
# Clean up Historical Moves
|
||||||
historical_moves = asset.depreciation_move_ids.filtered(
|
historical_moves = asset.depreciation_move_ids.filtered(lambda m: m.date <= CUTOFF_DATE)
|
||||||
lambda m: m.date <= CUTOFF_DATE and m.state == 'draft'
|
|
||||||
)
|
|
||||||
if historical_moves:
|
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()
|
historical_moves.unlink()
|
||||||
|
|
||||||
asset.write({'state': 'open'})
|
asset.write({'state': 'open'})
|
||||||
|
|
||||||
# Calculate end date for verification
|
print(f"Imported: {asset.name} | Val: {original_value} | No Prorata | ImportAmt: 0")
|
||||||
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}")
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
print(f"\nTotal Assets Imported: {count}")
|
print(f"\nTotal Assets Imported: {count}")
|
||||||
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