first commit
This commit is contained in:
commit
359b1a2278
31
README_IMPORT.md
Normal file
31
README_IMPORT.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Fixed Asset Import Script
|
||||
|
||||
This script imports fixed assets from `Fixed Asset Kipas.xlsx` into Odoo 17 database `kipasdbclone5`.
|
||||
|
||||
## Features
|
||||
- **Avoids Double Posting**: Sets assets to 'Running' (Open) state manually, bypassing the Journal Entry creation for Asset Recognition.
|
||||
- **Depreciation Calculation**: Adjusts the "Accumulated Depreciation Per Dec 31" from Excel to "Per Oct 31" (Opening Balance Date) by subtracting 2 months of depreciation.
|
||||
- **Model Handling**: Automatically maps categories. Creates `Peralatan Dapur` model if missing (copying from `Peralatan Inventaris`).
|
||||
- **Cutoff Date**: Skips assets acquired after Oct 31, 2025.
|
||||
|
||||
## Usage
|
||||
Run the script using the Odoo virtual environment:
|
||||
```bash
|
||||
/home/suherdy/Pythoncode/odoo17/.venv/bin/python /home/suherdy/Pythoncode/odoo17/scripts/import_fixed_assets.py
|
||||
```
|
||||
|
||||
## Logic Details
|
||||
1. **Accumulated Depreciation**:
|
||||
- Uses Excel column R (Accum Depr Dec 31).
|
||||
- `Accum Oct 31 = Accum Dec 31 - (2 * Monthly Depreciation)`.
|
||||
- If asset is fully depreciated (End Date <= Oct 31), uses `Accum Dec 31` as is.
|
||||
2. **State**:
|
||||
- Assets are created in `draft`.
|
||||
- `compute_depreciation_board()` is called to generate Draft moves for future depreciation (subtracted by imported amount).
|
||||
- `state` is manually Set to `open`.
|
||||
- Result: No historical moves posted. Future moves are Draft (to be posted by cron/user). No Asset Recognition entry.
|
||||
|
||||
## Prerequisites
|
||||
- `openpyxl`
|
||||
- `python-dateutil`
|
||||
- Odoo configuration file at `../odoo.conf`
|
||||
77
delete_all_assets.py
Normal file
77
delete_all_assets.py
Normal file
@ -0,0 +1,77 @@
|
||||
import sys
|
||||
import os
|
||||
# ---------------- CONFIGURATION ----------------
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
os.chdir(PROJECT_ROOT)
|
||||
|
||||
ODOO_PATH = os.path.join(PROJECT_ROOT, 'odoo')
|
||||
CONF_FILE = os.path.join(PROJECT_ROOT, 'odoo.conf')
|
||||
DB_NAME = 'kipasdbclone5'
|
||||
|
||||
if ODOO_PATH not in sys.path:
|
||||
sys.path.append(ODOO_PATH)
|
||||
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
def delete_assets():
|
||||
print(f"Initializing Odoo Environment for database: {DB_NAME}...")
|
||||
try:
|
||||
odoo.tools.config.parse_config(['-c', CONF_FILE])
|
||||
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.")
|
||||
|
||||
# 1. Search for non-model assets
|
||||
assets = env['account.asset'].search([('state', '!=', 'model')])
|
||||
print(f"Found {len(assets)} assets to delete.")
|
||||
|
||||
if not assets:
|
||||
print("No assets to delete.")
|
||||
return
|
||||
|
||||
try:
|
||||
# 2. Find and Delete Linked Depreciation Moves
|
||||
moves = assets.mapped('depreciation_move_ids')
|
||||
posted_moves = moves.filtered(lambda m: m.state == 'posted')
|
||||
|
||||
if posted_moves:
|
||||
print(f"Resetting {len(posted_moves)} posted moves to draft...")
|
||||
posted_moves.button_draft()
|
||||
|
||||
if moves:
|
||||
print(f"Deleting {len(moves)} depreciation moves...")
|
||||
moves.unlink() # Deleting moves first cleans up the relation
|
||||
|
||||
# 3. Reset Assets to Draft
|
||||
# Some assets might be 'open' or 'close' or 'cancelled'.
|
||||
# To delete, they often need to be in draft or cancelled state depending on logic,
|
||||
# but unlink() in Odoo 17 account_asset usually checks if they are NOT in open/paused/close.
|
||||
# So we must write state = draft.
|
||||
|
||||
# Check for assets that are not draft
|
||||
non_draft_assets = assets.filtered(lambda a: a.state != 'draft')
|
||||
if non_draft_assets:
|
||||
print(f"Setting {len(non_draft_assets)} assets to draft state...")
|
||||
non_draft_assets.write({'state': 'draft'})
|
||||
|
||||
# 4. Delete Assets
|
||||
print(f"Deleting {len(assets)} assets...")
|
||||
assets.unlink()
|
||||
|
||||
cr.commit()
|
||||
print("Successfully deleted all assets and related entries.")
|
||||
|
||||
except Exception as e:
|
||||
cr.rollback()
|
||||
print(f"An error occurred. Transaction rolled back.\nError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
delete_assets()
|
||||
235
import_fixed_assets.py
Normal file
235
import_fixed_assets.py
Normal file
@ -0,0 +1,235 @@
|
||||
import sys
|
||||
import os
|
||||
import openpyxl
|
||||
from datetime import datetime, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
# ---------------- CONFIGURATION ----------------
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
# Change CWD to Project Root so relative paths in odoo.conf work
|
||||
os.chdir(PROJECT_ROOT)
|
||||
|
||||
ODOO_PATH = os.path.join(PROJECT_ROOT, 'odoo')
|
||||
CONF_FILE = os.path.join(PROJECT_ROOT, 'odoo.conf')
|
||||
EXCEL_FILE = os.path.join(PROJECT_ROOT, 'Fixed Asset Kipas.xlsx')
|
||||
DB_NAME = 'kipasdbclone5'
|
||||
CUTOFF_DATE = date(2025, 10, 31)
|
||||
|
||||
if ODOO_PATH not in sys.path:
|
||||
sys.path.append(ODOO_PATH)
|
||||
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
def import_assets():
|
||||
print(f"Initializing Odoo Environment for database: {DB_NAME}...")
|
||||
try:
|
||||
odoo.tools.config.parse_config(['-c', CONF_FILE])
|
||||
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)
|
||||
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):
|
||||
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__":
|
||||
import_assets()
|
||||
32
list_asset_models.py
Normal file
32
list_asset_models.py
Normal file
@ -0,0 +1,32 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
ODOO_PATH = os.path.join(PROJECT_ROOT, 'odoo')
|
||||
CONF_FILE = os.path.join(PROJECT_ROOT, 'odoo.conf')
|
||||
DB_NAME = 'kipasdbclone5'
|
||||
|
||||
if ODOO_PATH not in sys.path:
|
||||
sys.path.append(ODOO_PATH)
|
||||
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
def list_models():
|
||||
try:
|
||||
odoo.tools.config.parse_config(['-c', CONF_FILE])
|
||||
registry = odoo.registry(DB_NAME)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
models = env['account.asset'].search([('state', '=', 'model')])
|
||||
print(f"Found {len(models)} Asset Models:")
|
||||
for m in models:
|
||||
print(f"- Name: '{m.name}' | Method: {m.method} | Duration: {m.method_number} {m.method_period}")
|
||||
print(f" Accounts: Asset={m.account_asset_id.code}, Depr={m.account_depreciation_id.code}, Exp={m.account_depreciation_expense_id.code}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
list_models()
|
||||
75
post_depreciation.py
Normal file
75
post_depreciation.py
Normal file
@ -0,0 +1,75 @@
|
||||
import sys
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
# ---------------- CONFIGURATION ----------------
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
# Change CWD to Project Root so relative paths in odoo.conf work
|
||||
os.chdir(PROJECT_ROOT)
|
||||
|
||||
ODOO_PATH = os.path.join(PROJECT_ROOT, 'odoo')
|
||||
CONF_FILE = os.path.join(PROJECT_ROOT, 'odoo.conf')
|
||||
DB_NAME = 'kipasdbclone5'
|
||||
POST_UP_TO_DATE = date(2026, 1, 21) # User specified "now"
|
||||
|
||||
if ODOO_PATH not in sys.path:
|
||||
sys.path.append(ODOO_PATH)
|
||||
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
def post_depreciation():
|
||||
print(f"Initializing Odoo Environment for database: {DB_NAME}...")
|
||||
try:
|
||||
odoo.tools.config.parse_config(['-c', CONF_FILE])
|
||||
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.")
|
||||
|
||||
# Search for Draft Depreciation MOVES (account.move)
|
||||
# Linked to an asset, in draft state, date <= target date
|
||||
moves_to_post = env['account.move'].search([
|
||||
('asset_id', '!=', False),
|
||||
('state', '=', 'draft'),
|
||||
('date', '<=', POST_UP_TO_DATE)
|
||||
])
|
||||
|
||||
if not moves_to_post:
|
||||
print(f"No draft depreciation entries found on or before {POST_UP_TO_DATE}.")
|
||||
return
|
||||
|
||||
print(f"Found {len(moves_to_post)} moves to post up to {POST_UP_TO_DATE}.")
|
||||
|
||||
# Group by date for nicer logging
|
||||
moves_by_date = {}
|
||||
for move in moves_to_post:
|
||||
d = move.date
|
||||
if d not in moves_by_date:
|
||||
moves_by_date[d] = []
|
||||
moves_by_date[d].append(move)
|
||||
|
||||
count = 0
|
||||
try:
|
||||
for d in sorted(moves_by_date.keys()):
|
||||
batch = moves_by_date[d]
|
||||
batch_moves = env['account.move'].browse([m.id for m in batch])
|
||||
print(f"Posting {len(batch)} entries for date {d}...")
|
||||
batch_moves.action_post()
|
||||
count += len(batch)
|
||||
|
||||
cr.commit()
|
||||
print(f"Successfully posted {count} depreciation entries.")
|
||||
|
||||
except Exception as e:
|
||||
cr.rollback()
|
||||
print(f"An error occurred. Transaction rolled back.\nError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
post_depreciation()
|
||||
Loading…
Reference in New Issue
Block a user