first commit

This commit is contained in:
Suherdy Yacob 2026-02-04 11:37:09 +07:00
commit 624d6034b1
29 changed files with 759 additions and 0 deletions

2
__init__.py Normal file
View File

@ -0,0 +1,2 @@
from . import models
from . import wizard

27
__manifest__.py Normal file
View File

@ -0,0 +1,27 @@
{
'name': 'GA Asset Management',
'version': '17.0.1.0.0',
'category': 'Operations',
'summary': 'Asset Management for General Affair Team',
'description': """
This module provides a simplified interface for General Affair team to manage fixed assets.
Features:
- Simplified Asset Views
- Auto-generated Asset Code based on Product Barcode
- Asset Transfer Wizard
""",
'author': 'Antigravity',
'depends': ['base', 'account_asset', 'asset_code_field', 'product', 'stock', 'purchase'],
'data': [
'security/ga_asset_security.xml',
'security/ir.model.access.csv',
'data/ir_sequence_data.xml',
'views/account_asset_views.xml',
'views/stock_picking_views.xml',
'views/product_template_views.xml',
'wizard/asset_transfer_views.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}

Binary file not shown.

13
data/ir_sequence_data.xml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sequence for GA Service -->
<record id="seq_ga_asset_code" model="ir.sequence">
<field name="name">Asset Code</field>
<field name="code">ga.asset.code</field>
<field name="prefix">/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

5
models/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from . import account_asset
from . import stock_move
from . import stock_picking
from . import account_move
from . import product_template

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

62
models/account_asset.py Normal file
View File

@ -0,0 +1,62 @@
from odoo import models, fields, api
import datetime
class AccountAsset(models.Model):
_inherit = 'account.asset'
product_id = fields.Many2one('product.product', string='Product')
description = fields.Text(string='Description')
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('asset_code'):
product_id = vals.get('product_id')
sequence = self.env['ir.sequence'].next_by_code('ga.asset.code') or '00000'
year = datetime.datetime.now().year
prefix = 'AST'
if product_id:
product = self.env['product.product'].browse(product_id)
if product.barcode:
prefix = product.barcode
# Format: [Barcode/AST]/[Year]/[Sequence]
# Note: Sequence defined in XML has prefix '/' so we just append it
# Wait, sequence next_by_code returns the full string including prefix/suffix if defined
# My XML defines prefix '/' and padding 5. So it returns '/00001'
# Let's construct manually to control the format exactly as requested:
# [Barcode]/[Year]/[Sequence_Number]
# If I use next_by_code, I get what is configured.
# To get just number, checking implementation... standard next_by_code returns full string.
# Let's simple use sequence for the number part only.
# I will change sequence prefix to empty in XML or just use it here.
# Actually, standard way is to have sequence configured with year but here we have dynamic prefix (Product Barcode).
# Revised Logic:
# 1. Get raw sequence number? No, next_by_code gives formatted string.
# Let's use a sequence with NO prefix in XML, and build format here.
# Re-reading XML I created: prefix='/'
# So `self.env['ir.sequence'].next_by_code('ga.asset.code')` returns `/00001`.
vals['asset_code'] = f"{prefix}/{year}{sequence}"
return super(AccountAsset, self).create(vals_list)
def action_open_transfer_wizard(self):
self.ensure_one()
return {
'name': 'Transfer Asset',
'type': 'ir.actions.act_window',
'res_model': 'ga.asset.transfer.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_asset_id': self.id,
'default_current_company_id': self.company_id.id,
}
}

71
models/account_move.py Normal file
View File

@ -0,0 +1,71 @@
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def _auto_create_asset(self):
# We need to intercept standard behavior if we can link to an existing asset from Stock Move
# This method iterates over self (invoices) and creates assets.
# We can run the standard method, BUT standard method creates new assets.
# We want to PREVENT creating new assets if one was already created by Stock Picking.
# The standard method _auto_create_asset loops over invoice lines and creates assets.
# It doesn't seem to have a hook to check for existing assets easily.
# However, we can check if the invoice line is linked to a PO line, and that PO line linked to a Stock Move.
# Strategy:
# 1. Let standard create assets? No, duplicate.
# 2. Override completely? risky for maintenance.
# 3. Pre-process: if we find a link, set the asset_id on the move_line?
# If `move_line.asset_ids` is set, `_auto_create_asset` might skip?
# Checking standard code:
# `and not move_line.asset_ids` -> YES! definition at line 188 of account_move.py.
# So strategy is:
# Before calling super, try to find existing asset and link it to move_line.
for move in self:
if move.is_invoice():
for line in move.invoice_line_ids:
# check purchase line
if line.purchase_line_id:
# Find related stock moves
# purchase_line_id.move_ids returns stock moves
related_stock_moves = line.purchase_line_id.move_ids.filtered(lambda m: m.state == 'done' and m.asset_id)
if related_stock_moves:
# Link the first found asset
asset = related_stock_moves[0].asset_id
line.asset_ids = [(4, asset.id)]
# Also update asset value if needed?
# Usually creation from Stock might lack price if Price Unit was 0 or estimate.
# But let's assume standard behavior: Link is enough.
# Log connection
asset.message_post(body=f"Linked to Vendor Bill: {move.name}")
# Call super to generate new assets (if not linked above)
created_assets = super(AccountMove, self)._auto_create_asset()
# Post-process: Ensure Product ID is set and Asset Code is correct
for asset in created_assets:
# key: use sudo() to allow system to correct data even if user has no write access to asset
# (e.g. Bill Clerk creating asset but not managing it)
if not asset.product_id and asset.original_move_line_ids:
# Find product from the first line
# (Standard logic usually groups lines by account/product context, so one asset usually comes from one product type)
product = asset.original_move_line_ids[0].product_id
if product:
asset.sudo().product_id = product.id
# Fix Asset Code if it defaults to AST but product has barcode
if asset.asset_code and asset.asset_code.startswith('AST') and product.barcode:
# Replace 'AST' prefix with Barcode
# Format is expected to be AST/YYYY/SEQ
new_code = asset.asset_code.replace('AST', product.barcode, 1)
asset.sudo().asset_code = new_code
return created_assets

View File

@ -0,0 +1,26 @@
from odoo import models, fields, api
class ProductTemplate(models.Model):
_inherit = 'product.template'
asset_count = fields.Integer(string='Assets', compute='_compute_asset_count')
def _compute_asset_count(self):
# account.asset has product_id which links to product.product
# We need to count assets for all variants of this template
for product in self:
# Domain: product_id.product_tmpl_id = product.id
product.asset_count = self.env['account.asset'].search_count([
('product_id.product_tmpl_id', '=', product.id)
])
def action_view_assets(self):
self.ensure_one()
return {
'name': 'Assets',
'type': 'ir.actions.act_window',
'res_model': 'account.asset',
'view_mode': 'tree,form',
'domain': [('product_id.product_tmpl_id', '=', self.id)],
'context': {'default_product_id': self.product_variant_id.id},
}

7
models/stock_move.py Normal file
View File

@ -0,0 +1,7 @@
from odoo import models, fields
class StockMove(models.Model):
_inherit = 'stock.move'
asset_code = fields.Char(string='Asset Code', copy=False)
asset_id = fields.Many2one('account.asset', string='Created Asset', copy=False)

114
models/stock_picking.py Normal file
View File

@ -0,0 +1,114 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import datetime
class StockPicking(models.Model):
_inherit = 'stock.picking'
def action_generate_asset_codes(self):
self.ensure_one()
for move in self.move_ids_without_package:
# Only generate if product creates asset (optional check, but good practice)
# However, user wants to manually decide, so we generate for all lines that look like assets?
# Let's assume user triggers this button for asset receipts.
# We can check if product has an asset model set in accounting?
# Or just blindly generate if missing?
# Re-using logic from account.asset.create:
if not move.asset_code and move.product_id:
sequence = self.env['ir.sequence'].next_by_code('ga.asset.code') or '00000'
year = datetime.datetime.now().year
# Logic from AccountAsset create: [Barcode or AST]/[Year]/[Sequence]
prefix = move.product_id.barcode or 'AST'
# Note: Sequence next_by_code returns '/0000X' as configured in XML
move.asset_code = f"{prefix}/{year}{sequence}"
has_asset_moves = fields.Boolean(compute='_compute_has_asset_moves', store=True)
@api.depends('move_ids.product_id', 'picking_type_code')
def _compute_has_asset_moves(self):
for pick in self:
has_asset = False
if pick.picking_type_code == 'incoming':
for move in pick.move_ids_without_package:
# Check if product is configured to create assets
# Logic: Check Product's Expense Account or Category's Expense Account
account = move.product_id.property_account_expense_id or move.product_id.categ_id.property_account_expense_categ_id
if account and account.create_asset != 'no':
has_asset = True
break
pick.has_asset_moves = has_asset
def button_validate(self):
# Validation Check
for pick in self:
if pick.has_asset_moves:
for move in pick.move_ids_without_package:
account = move.product_id.property_account_expense_id or move.product_id.categ_id.property_account_expense_categ_id
if account and account.create_asset != 'no' and not move.asset_code:
raise UserError(_(
"Asset Code is missing for product '%s'. \n"
"Please click 'Generate Asset Codes' before validating.",
move.product_id.name
))
return super(StockPicking, self).button_validate()
def _action_done(self):
res = super(StockPicking, self)._action_done()
# After validation, create assets for moves with asset_code
for pick in self:
for move in pick.move_ids_without_package:
if move.asset_code and not move.asset_id:
# Get Account to check configuration
account = move.product_id.property_account_expense_id or move.product_id.categ_id.property_account_expense_categ_id
# Prepare basic values
vals_base = {
'name': move.product_id.name,
'product_id': move.product_id.id,
'acquisition_date': fields.Date.today(),
'company_id': move.company_id.id,
'state': 'draft',
}
# Add Model if configured on Account
if account and account.asset_model:
vals_base['model_id'] = account.asset_model.id
qty = int(move.quantity)
# CASE 1: Multiple Assets per Line
if account and account.multiple_assets_per_line and qty > 1:
# 1st Asset uses the pre-generated code
vals = vals_base.copy()
vals['asset_code'] = move.asset_code
vals['original_value'] = move.price_unit # Per unit value
asset = self.env['account.asset'].create(vals)
move.asset_id = asset.id # Link first one to move
# Create remaining assets
for i in range(qty - 1):
vals_extra = vals_base.copy()
vals_extra['original_value'] = move.price_unit
# Generate new code for extra assets
sequence = self.env['ir.sequence'].next_by_code('ga.asset.code') or '00000'
year = datetime.datetime.now().year
prefix = move.product_id.barcode or 'AST'
vals_extra['asset_code'] = f"{prefix}/{year}{sequence}"
self.env['account.asset'].create(vals_extra)
# CASE 2: Single Assset (default)
else:
vals = vals_base.copy()
vals['asset_code'] = move.asset_code
vals['original_value'] = move.price_unit * move.quantity # Total Value
asset = self.env['account.asset'].create(vals)
move.asset_id = asset.id
return res

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="module_category_ga_asset" model="ir.module.category">
<field name="name">General Affair Assets</field>
<field name="description">Manage General Affair Assets</field>
<field name="sequence">10</field>
</record>
<record id="group_ga_asset_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_category_ga_asset"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_ga_asset_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_category_ga_asset"/>
<field name="implied_ids" eval="[(4, ref('group_ga_asset_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ga_asset_transfer_wizard_user,ga.asset.transfer.wizard.user,model_ga_asset_transfer_wizard,group_ga_asset_user,1,1,1,0
access_ga_asset_transfer_wizard_manager,ga.asset.transfer.wizard.manager,model_ga_asset_transfer_wizard,group_ga_asset_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ga_asset_transfer_wizard_user ga.asset.transfer.wizard.user model_ga_asset_transfer_wizard group_ga_asset_user 1 1 1 0
3 access_ga_asset_transfer_wizard_manager ga.asset.transfer.wizard.manager model_ga_asset_transfer_wizard group_ga_asset_manager 1 1 1 1

BIN
static/description/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import test_ga_asset

131
tests/test_ga_asset.py Normal file
View File

@ -0,0 +1,131 @@
from odoo.tests.common import TransactionCase
from datetime import datetime
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestGAAssetManagement(TransactionCase):
def setUp(self):
super(TestGAAssetManagement, self).setUp()
self.Asset = self.env['account.asset']
self.Product = self.env['product.product']
self.Wizard = self.env['ga.asset.transfer.wizard']
self.Company = self.env['res.company']
self.StockPicking = self.env['stock.picking']
self.PurchaseOrder = self.env['purchase.order']
self.AccountMove = self.env['account.move']
# Create a product with barcode
self.product_with_barcode = self.Product.create({
'name': 'Test Product Barcode',
'barcode': '123456789',
'type': 'product', # Storable generic
})
# Create a product without barcode
self.product_no_barcode = self.Product.create({
'name': 'Test Product No Barcode',
'type': 'product',
})
self.company_a = self.env.user.company_id
self.company_b = self.Company.create({'name': 'Company B'})
self.partner = self.env['res.partner'].create({'name': 'Vendor A'})
def test_asset_creation_with_barcode(self):
"""Test asset code generation with product barcode"""
asset = self.Asset.create({
'name': 'Asset Barcode',
'product_id': self.product_with_barcode.id,
'original_value': 1000,
'acquisition_date': '2025-01-01',
'company_id': self.company_a.id,
})
year = datetime.now().year
self.assertTrue(asset.asset_code.startswith(f"123456789/{year}"))
def test_asset_creation_no_barcode(self):
"""Test asset code generation without product barcode (fallback to AST)"""
asset = self.Asset.create({
'name': 'Asset No Barcode',
'product_id': self.product_no_barcode.id,
'original_value': 1000,
'acquisition_date': '2025-01-01',
'company_id': self.company_a.id,
})
year = datetime.now().year
self.assertTrue(asset.asset_code.startswith(f"AST/{year}"))
def test_asset_transfer(self):
"""Test asset transfer wizard"""
asset = self.Asset.create({
'name': 'Asset Transfer',
'original_value': 1000,
'acquisition_date': '2025-01-01',
'company_id': self.company_a.id,
})
wizard = self.Wizard.create({
'asset_id': asset.id,
'current_company_id': self.company_a.id,
'target_company_id': self.company_b.id,
'note': 'Moving to Company B'
})
wizard.action_transfer()
self.assertEqual(asset.company_id, self.company_b)
def test_receipt_to_asset_flow(self):
"""Test PO -> Receipt -> Asset Gen -> Validation -> Asset Created -> Bill -> Link"""
# 1. Create PO
po = self.PurchaseOrder.create({
'partner_id': self.partner.id,
'order_line': [(0, 0, {
'product_id': self.product_with_barcode.id,
'name': 'Laptop',
'product_qty': 1.0,
'price_unit': 1500.0,
})]
})
po.button_confirm()
# 2. Get Receipt
picking = po.picking_ids[0]
self.assertEqual(picking.state, 'assigned')
# 3. Generate Asset Codes
picking.action_generate_asset_codes()
move = picking.move_ids_without_package[0]
year = datetime.now().year
self.assertTrue(move.asset_code.startswith(f"123456789/{year}"))
# 4. Validate Receipt
picking.button_validate()
self.assertEqual(picking.state, 'done')
# 5. Check Asset Creation
self.assertTrue(move.asset_id, "Asset should be linked to Stock Move")
asset = move.asset_id
self.assertEqual(asset.asset_code, move.asset_code)
self.assertEqual(asset.state, 'draft')
# 6. Create Vendor Bill from PO
action = po.action_create_invoice()
invoice = self.AccountMove.browse(action['res_id'])
# 7. Post Bill (Triggers _auto_create_asset which we overrode)
invoice.action_post()
# 8. Verify Link (Invoice Line should be linked to the SAME asset)
inv_line = invoice.invoice_line_ids[0]
# inv_line.asset_ids is Many2many
self.assertIn(asset, inv_line.asset_ids, "Bill line should be linked to the existing asset from Receipt")
# Ensure no duplicate asset created
assets_for_this_product = self.Asset.search([('product_id', '=', self.product_with_barcode.id)])
# We might have other tests creating assets, so filter by code
assets_for_this_code = self.Asset.search([('asset_code', '=', move.asset_code)])
self.assertEqual(len(assets_for_this_code), 1, "Should be exactly one asset for this code")

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- GA Asset Tree View -->
<record id="view_ga_account_asset_tree" model="ir.ui.view">
<field name="name">ga.account.asset.tree</field>
<field name="model">account.asset</field>
<field name="arch" type="xml">
<tree string="Fixed Assets" sample="1">
<field name="asset_code"/>
<field name="name"/>
<field name="product_id" optional="show"/>
<field name="acquisition_date"/>
<field name="original_value"/>
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'open'" decoration-danger="state == 'close'"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- GA Asset Form View -->
<record id="view_ga_account_asset_form" model="ir.ui.view">
<field name="name">ga.account.asset.form</field>
<field name="model">account.asset</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<form string="Fixed Asset">
<header>
<button name="action_open_transfer_wizard" string="Transfer Asset" type="object" class="oe_highlight" invisible="state == 'model'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,open,close"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Laptop"/>
</h1>
</div>
<group>
<group>
<field name="product_id"/>
<field name="asset_code" readonly="1"/>
<field name="acquisition_date"/>
</group>
<group>
<field name="original_value"/>
<field name="model_id" string="Asset Model"/>
<field name="company_id" groups="base.group_multi_company" readonly="1"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Depreciation Board" invisible="1">
<!-- Hiding depreciation details for GA as per requirement to simplify -->
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_ga_asset_management" model="ir.actions.act_window">
<field name="name">Assets</field>
<field name="res_model">account.asset</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_ga_account_asset_tree"/>
<field name="search_view_id" ref="account_asset.view_account_asset_search"/>
<field name="domain">[('state', '!=', 'model')]</field> <!-- Filter out Asset Models -->
<field name="context">{'default_state': 'draft'}</field>
</record>
<record id="action_ga_asset_tree_view_link" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_ga_account_asset_tree"/>
<field name="act_window_id" ref="action_ga_asset_management"/>
</record>
<record id="action_ga_asset_form_view_link" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="view_ga_account_asset_form"/>
<field name="act_window_id" ref="action_ga_asset_management"/>
</record>
<!-- Menu Items -->
<menuitem id="menu_ga_root" name="General Affair" web_icon="ga_asset_management,static/description/icon.png" sequence="10" groups="ga_asset_management.group_ga_asset_user"/>
<menuitem id="menu_ga_asset_management"
name="Assets"
parent="menu_ga_root"
action="action_ga_asset_management"
sequence="1"/>
</odoo>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_form_view_inherit_ga_asset" model="ir.ui.view">
<field name="name">product.template.form.inherit.ga.asset</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button" name="action_view_assets" type="object" icon="fa-building-o">
<field string="Assets" name="asset_count" widget="statinfo"/>
</button>
</div>
</field>
</record>
</odoo>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_picking_form_inherit_ga_asset" model="ir.ui.view">
<field name="name">stock.picking.form.inherit.ga.asset</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='button_validate']" position="before">
<button name="action_generate_asset_codes"
string="Generate Asset Codes"
type="object"
class="oe_highlight"
invisible="state != 'assigned'"
groups="ga_asset_management.group_ga_asset_user"/>
</xpath>
<xpath expr="//field[@name='move_ids_without_package']/tree/field[@name='product_id']" position="after">
<field name="asset_code" optional="show"/>
<field name="asset_id" optional="hide" readonly="1"/>
</xpath>
</field>
</record>
<!-- Action for Asset Receipts -->
<record id="action_asset_receipts" model="ir.actions.act_window">
<field name="name">Pending Asset Receipts</field>
<field name="res_model">stock.picking</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('picking_type_code', '=', 'incoming'), ('state', 'not in', ('done', 'cancel')), ('has_asset_moves', '=', True)]</field>
<field name="context">{'create': False}</field>
</record>
<!-- Add Menu under General Affair > Assets -->
<!-- Assuming the parent menu 'menu_ga_asset_root' is defined in account_asset_views.xml -->
<menuitem id="menu_asset_receipts"
name="Pending Asset Receipts"
parent="menu_ga_root"
action="action_asset_receipts"
sequence="20"
groups="ga_asset_management.group_ga_asset_user"/>
</odoo>

1
wizard/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import asset_transfer_wizard

Binary file not shown.

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_ga_asset_transfer_wizard_form" model="ir.ui.view">
<field name="name">ga.asset.transfer.wizard.form</field>
<field name="model">ga.asset.transfer.wizard</field>
<field name="arch" type="xml">
<form string="Transfer Asset">
<group>
<field name="asset_id"/>
<field name="current_company_id" readonly="1"/>
<field name="target_company_id" domain="[('id', '!=', current_company_id)]"/>
<field name="note"/>
</group>
<footer>
<button name="action_transfer" string="Transfer" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,97 @@
from odoo import models, fields, api
class GaAssetTransferWizard(models.TransientModel):
_name = 'ga.asset.transfer.wizard'
_description = 'Asset Transfer Wizard'
asset_id = fields.Many2one('account.asset', string='Asset', required=True, readonly=True)
current_company_id = fields.Many2one('res.company', string='Current Company', required=True, readonly=True)
target_company_id = fields.Many2one('res.company', string='Target Company', required=True)
note = fields.Text(string='Transfer Note')
def action_transfer(self):
self.ensure_one()
if self.target_company_id and self.target_company_id != self.current_company_id:
old_asset = self.asset_id
# CASE 1: Asset is Draft or Model -> Simple Move
# No depreciation history to preserve, so we just move it.
if old_asset.state in ('draft', 'model'):
old_asset.sudo().write({'company_id': self.target_company_id.id})
if self.note:
old_asset.sudo().message_post(body=f"Asset transferred to {self.target_company_id.name}. Note: {self.note}")
return {
'name': 'Transferred Asset',
'type': 'ir.actions.act_window',
'res_model': 'account.asset',
'view_mode': 'form',
'res_id': old_asset.id,
'target': 'current',
}
# CASE 2: Asset is Running (Open/Parsed) -> Close and Clone
# We want to stop the old one and start fresh in new company
vals = {
'name': old_asset.name,
'product_id': old_asset.product_id.id,
'asset_code': old_asset.asset_code,
'original_value': old_asset.original_value,
'already_depreciated_amount_import': old_asset.original_value - old_asset.value_residual,
'acquisition_date': old_asset.acquisition_date,
'prorata_date': old_asset.prorata_date,
'method': old_asset.method,
'method_number': old_asset.method_number,
'method_period': old_asset.method_period,
'method_progress_factor': old_asset.method_progress_factor,
'prorata_computation_type': old_asset.prorata_computation_type,
'company_id': self.target_company_id.id,
'state': 'draft',
}
# Find Asset Model in Target Company to apply Accounts
if old_asset.product_id:
# Get Product with company context
product = old_asset.product_id.with_company(self.target_company_id)
account = product.property_account_expense_id or product.categ_id.property_account_expense_categ_id
if account and account.asset_model:
model = account.asset_model
vals['model_id'] = model.id
# Auto-populate Accounts from Model
vals['account_asset_id'] = model.account_asset_id.id
vals['account_depreciation_id'] = model.account_depreciation_id.id
vals['account_depreciation_expense_id'] = model.account_depreciation_expense_id.id
vals['journal_id'] = model.journal_id.id
# 2. Close the old asset WITHOUT generating Disposal Moves
# User request: "no need to use dispose method (gain/loss account)"
# We simply stop future depreciation and archive the old asset.
# NOTE: The Asset Value remains on the old company's Balance Sheet until manually adjusted/transferred.
# Cancel future draft moves (using sudo to bypass company rules if needed)
old_asset.depreciation_move_ids.filtered(lambda m: m.state == 'draft').with_context(force_delete=True).sudo().unlink()
# Close and Archive (using sudo to ensure we can modify the record even if we are switching context)
old_asset.sudo().write({
'state': 'close',
'active': False,
})
old_asset.sudo().message_post(body=f"Asset transferred to {self.target_company_id.name}. Auto-disposal skipped.")
# 3. Create the new asset in target company (using sudo to create in a company we might not be active in)
new_asset = self.env['account.asset'].sudo().create(vals)
new_asset.sudo().message_post(body=f"Asset transferred from {self.current_company_id.name}. Note: {self.note}")
# 4. Open the new asset
return {
'name': 'Transferred Asset',
'type': 'ir.actions.act_window',
'res_model': 'account.asset',
'view_mode': 'form',
'res_id': new_asset.id,
'target': 'current',
}
return {'type': 'ir.actions.act_window_close'}