odoo_utility_scripts/import_fixed_assets.py

251 lines
9.3 KiB
Python

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 Inventaris',
'name': 'Peralatan Dapur',
'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
acq_date = row[7]
if isinstance(acq_date, datetime):
acq_date = acq_date.date()
if not isinstance(acq_date, date):
continue
if acq_date > CUTOFF_DATE:
if skipped_count < 5: # Limit detailed skip logs
print(f"Skipping '{col_name}': Acquired {acq_date} (After Cutoff)")
skipped_count += 1
continue
original_value = row[8]
if not isinstance(original_value, (int, float)):
original_value = 0.0
accum_depr_dec31 = row[17]
if not isinstance(accum_depr_dec31, (int, float)):
accum_depr_dec31 = 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'
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
# Determine total duration in months
period_multiple = int(model.method_period) # 1 or 12
total_months = model.method_number * period_multiple
end_date = acq_date + relativedelta(months=total_months)
if end_date <= CUTOFF_DATE:
# Asset should be fully depreciated by Cutoff
accum_depr_oct31 = accum_depr_dec31
# Sanity check: Should ideally be equal to original_value, but use Excel data.
else:
# Asset is still active or naturally finishes after Cutoff
# We need to back-calculate from Dec 31
monthly_depr = original_value / total_months if total_months > 0 else 0
# Adjustment for Nov, Dec (2 months)
adjustment = 2 * monthly_depr
accum_depr_oct31 = accum_depr_dec31 - adjustment
# Clamp values
if accum_depr_oct31 < 0:
accum_depr_oct31 = 0.0
if accum_depr_oct31 > original_value:
accum_depr_oct31 = original_value
vals = {
'name': col_name,
'asset_code': col_1, # Added Asset Code
'original_value': original_value,
'acquisition_date': acq_date,
'model_id': model.id,
'method': model.method,
'method_number': model.method_number,
'method_period': model.method_period,
'prorata_computation_type': model.prorata_computation_type,
'already_depreciated_amount_import': accum_depr_oct31,
'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)
# 9. Compute Board and Set to Running
asset.compute_depreciation_board()
# 10. Clean up Historical Moves
# To strictly preserve Opening Balance, we must NOT have any draft moves
# dated before or on the Cutoff Date. We assume the 'already_depreciated_amount_import'
# covers the past. Any "catch-up" entries generated by Odoo for the past look like
# expenses in prior years, which affects Retained Earnings. We delete them.
historical_moves = asset.depreciation_move_ids.filtered(lambda m: m.date <= CUTOFF_DATE)
if historical_moves:
print(f" Removing {len(historical_moves)} historical catch-up moves for {asset.name}...")
historical_moves.unlink()
asset.write({'state': 'open'})
print(f"Imported: {asset.name} | Val: {original_value} | Oct31: {accum_depr_oct31:,.2f}")
count += 1
print(f"\nTotal Assets Imported: {count}")
if __name__ == "__main__":
main()