From 450977a31ff9f89dc3bedfac2b24ebf98d087a15 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 23 Jan 2026 08:23:01 +0700 Subject: [PATCH] feat: Restructure fixed asset import scripts, enforcing constant periods and first-of-month prorata dates, and adding a depreciation logic test. --- import_fixed_assets.py | 8 +- ...v2.py => import_fixed_assets_no_prorata.py | 69 ++---- test_depreciation_logic.py | 214 ++++++++++++++++++ 3 files changed, 244 insertions(+), 47 deletions(-) rename import_fixed_assets_v2.py => import_fixed_assets_no_prorata.py (79%) create mode 100644 test_depreciation_logic.py diff --git a/import_fixed_assets.py b/import_fixed_assets.py index 445817d..7f9a9a7 100644 --- a/import_fixed_assets.py +++ b/import_fixed_assets.py @@ -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, diff --git a/import_fixed_assets_v2.py b/import_fixed_assets_no_prorata.py similarity index 79% rename from import_fixed_assets_v2.py rename to import_fixed_assets_no_prorata.py index 6f87253..bd948f6 100644 --- a/import_fixed_assets_v2.py +++ b/import_fixed_assets_no_prorata.py @@ -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() \ No newline at end of file + main() diff --git a/test_depreciation_logic.py b/test_depreciation_logic.py new file mode 100644 index 0000000..4d2f8bd --- /dev/null +++ b/test_depreciation_logic.py @@ -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() \ No newline at end of file