From b1de181c596413b5c2cb646609f92c8f63e2621c Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 22 Jan 2026 16:28:23 +0700 Subject: [PATCH] create v2 import with different approach --- import_fixed_assets_v2.py | 264 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 import_fixed_assets_v2.py diff --git a/import_fixed_assets_v2.py b/import_fixed_assets_v2.py new file mode 100644 index 0000000..6f87253 --- /dev/null +++ b/import_fixed_assets_v2.py @@ -0,0 +1,264 @@ +import sys +import os +import argparse +import openpyxl +from datetime import datetime, date +from dateutil.relativedelta import relativedelta + +# Constants +CUTOFF_DATE = date(2025, 10, 31) + +def main(): + parser = argparse.ArgumentParser(description="Import Fixed Assets to Odoo") + 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") + 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) + excel_path = os.path.abspath(args.excel_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: + ensure_models(env) + process_import(env, excel_path) + cr.commit() + print("Changes committed to database.") + except Exception as e: + cr.rollback() + print(f"An error occurred during import. Transaction rolled back.\nError: {e}") + import traceback + traceback.print_exc() + +def ensure_models(env): + missing_models = { + 'PERALATAN DAPUR': { + 'fallback': 'Peralatan Dapur', + 'name': 'Peralatan Dapur', + 'method_number': 60, + 'method_period': '1' + }, + 'PATENT & MERK': { + 'fallback': 'Asset Tidak Berwujud', + 'name': 'Asset Tidak Berwujud', + 'method_number': 60, + 'method_period': '1' + } + } + + for key, config in missing_models.items(): + exists = env['account.asset'].search([('state', '=', 'model'), ('name', '=ilike', config['name'])], limit=1) + if exists: + continue + + print(f"Creating missing model '{config['name']}'...") + fallback = env['account.asset'].search([('state', '=', 'model'), ('name', '=ilike', config['fallback'])], limit=1) + + vals = { + 'name': config['name'], + 'state': 'model', + 'active': True, + } + + if fallback: + vals.update({ + 'method': fallback.method, + 'method_number': fallback.method_number, + 'method_period': fallback.method_period, + 'prorata_computation_type': fallback.prorata_computation_type, + 'method_progress_factor': fallback.method_progress_factor, + 'account_asset_id': fallback.account_asset_id.id, + 'account_depreciation_id': fallback.account_depreciation_id.id, + 'account_depreciation_expense_id': fallback.account_depreciation_expense_id.id, + 'journal_id': fallback.journal_id.id, + 'analytic_distribution': fallback.analytic_distribution, + }) + else: + any_model = env['account.asset'].search([('state', '=', 'model')], limit=1) + if any_model: + vals.update({ + 'account_asset_id': any_model.account_asset_id.id, + 'account_depreciation_id': any_model.account_depreciation_id.id, + 'account_depreciation_expense_id': any_model.account_depreciation_expense_id.id, + 'journal_id': any_model.journal_id.id, + }) + vals.update({ + 'method': 'linear', + 'method_number': config['method_number'], + 'method_period': config['method_period'], + 'prorata_computation_type': 'constant_periods', + }) + + env['account.asset'].create(vals) + print(f"Created model '{config['name']}'.") + +def process_import(env, excel_file): + print(f"Reading Excel file: {excel_file}...") + wb = openpyxl.load_workbook(excel_file, data_only=True) + ws = wb.active + + current_category_name = None + count = 0 + models_cache = {} + skipped_count = 0 + + for row_idx, row in enumerate(ws.iter_rows(min_row=10, values_only=True), start=10): + col_0 = row[0] + col_1 = row[1] + col_name = row[2] + + if col_0 and isinstance(col_0, str) and not col_1 and not col_name: + current_category_name = col_0.strip() + print(f"\n--- Category Detected: {current_category_name} ---") + continue + + if not col_name: + continue + + acquisition_date = row[7] + if isinstance(acquisition_date, datetime): + acquisition_date = acquisition_date.date() + + if not isinstance(acquisition_date, date): + continue + + # Check if asset is newer than cutoff + if acquisition_date > CUTOFF_DATE: + if skipped_count < 5: # Limit detailed skip logs + print(f"Skipping '{col_name}': Acquired {acquisition_date} (After Cutoff)") + skipped_count += 1 + continue + + original_value = row[13] # Column N Saldo Akhir + if not isinstance(original_value, (int, float)): + original_value = 0.0 + + if not current_category_name: + continue + + model_name_search = current_category_name + if current_category_name == 'PERALATAN DAPUR': + model_name_search = 'Peralatan Dapur' + elif current_category_name == 'PATENT & MERK': + model_name_search = 'Asset Tidak Berwujud' + + model = models_cache.get(model_name_search) + if not model: + model = env['account.asset'].search([ + ('state', '=', 'model'), + ('name', '=ilike', model_name_search) + ], limit=1) + + if not model: + print(f"SKIPPING: Asset Model '{model_name_search}' not found.") + continue + models_cache[model_name_search] = model + + # Calculate Logic - FIXED VERSION + # Determine total duration in months + DURATION_OVERRIDES = { + 'RENOVASI BANGUNAN': 60, + 'PATENT & MERK': 120, + 'PERALATAN DAPUR': 60, + } + + effective_method_number = model.method_number + 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 + 'model_id': model.id, + 'method': model.method, + 'method_number': effective_method_number, # Full duration + 'method_period': model.method_period, + '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) + + # 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' + ) + if historical_moves: + print(f" Removing {len(historical_moves)} historical moves 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}") + count += 1 + + print(f"\nTotal Assets Imported: {count}") + +if __name__ == "__main__": + main() \ No newline at end of file