feat: Implement asset transfer functionality with a new transfer log, in_transit status for assets, and make depreciation fields readonly.

This commit is contained in:
Suherdy Yacob 2026-02-06 15:12:00 +07:00
parent e60e85c30f
commit 5ec64ef131
12 changed files with 327 additions and 92 deletions

View File

@ -11,7 +11,7 @@
- Asset Transfer Wizard - Asset Transfer Wizard
""", """,
'author': 'Antigravity', 'author': 'Antigravity',
'depends': ['base', 'account_asset', 'asset_code_field', 'product', 'stock', 'purchase'], 'depends': ['base', 'account_asset', 'asset_code_field', 'product', 'stock', 'purchase', 'hr'],
'data': [ 'data': [
'security/ga_asset_security.xml', 'security/ga_asset_security.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
@ -19,6 +19,7 @@
'views/account_asset_views.xml', 'views/account_asset_views.xml',
'views/stock_picking_views.xml', 'views/stock_picking_views.xml',
'views/product_template_views.xml', 'views/product_template_views.xml',
'views/asset_transfer_log_views.xml',
'wizard/asset_transfer_views.xml', 'wizard/asset_transfer_views.xml',
], ],
'installable': True, 'installable': True,

View File

@ -3,3 +3,4 @@ from . import stock_move
from . import stock_picking from . import stock_picking
from . import account_move from . import account_move
from . import product_template from . import product_template
from . import ga_asset_transfer_log

View File

@ -7,6 +7,10 @@ class AccountAsset(models.Model):
product_id = fields.Many2one('product.product', string='Product') product_id = fields.Many2one('product.product', string='Product')
description = fields.Text(string='Description') description = fields.Text(string='Description')
location_id = fields.Many2one('stock.location', string='Location', domain="[('company_id', '=', company_id)]")
employee_id = fields.Many2one('hr.employee', string='Employee', domain="[('company_id', '=', company_id)]")
in_transit = fields.Boolean(string='In Transit', default=False, help="Asset is currently being transferred between companies.")
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
for vals in vals_list: for vals in vals_list:
@ -45,6 +49,34 @@ class AccountAsset(models.Model):
vals['asset_code'] = f"{prefix}/{year}{sequence}" vals['asset_code'] = f"{prefix}/{year}{sequence}"
# Remove original_value if it is 0.0 to allow computation from lines later
if 'original_value' in vals and vals['original_value'] == 0.0:
del vals['original_value']
# Populate accounting fields from Asset Model if not already set
if vals.get('model_id'):
model = self.env['account.asset'].browse(vals['model_id'])
if model:
if not vals.get('method'):
vals['method'] = model.method
if not vals.get('method_number'):
vals['method_number'] = model.method_number
if not vals.get('method_period'):
vals['method_period'] = model.method_period
if not vals.get('prorata_computation_type'):
vals['prorata_computation_type'] = model.prorata_computation_type
if not vals.get('account_asset_id'):
vals['account_asset_id'] = model.account_asset_id.id
if not vals.get('account_depreciation_id'):
vals['account_depreciation_id'] = model.account_depreciation_id.id
if not vals.get('account_depreciation_expense_id'):
vals['account_depreciation_expense_id'] = model.account_depreciation_expense_id.id
if not vals.get('journal_id'):
vals['journal_id'] = model.journal_id.id
if not vals.get('analytic_distribution') and model.analytic_distribution:
vals['analytic_distribution'] = model.analytic_distribution
return super(AccountAsset, self).create(vals_list) return super(AccountAsset, self).create(vals_list)
def action_open_transfer_wizard(self): def action_open_transfer_wizard(self):

View File

@ -0,0 +1,119 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class GaAssetTransferLog(models.Model):
_name = 'ga.asset.transfer.log'
_description = 'Asset Transfer Log'
_order = 'create_date desc'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='Transfer Reference', required=True, copy=False, readonly=True, default=lambda self: _('New'))
asset_id = fields.Many2one('account.asset', string='Asset', required=True, readonly=True, help="Asset being transferred.")
source_company_id = fields.Many2one('res.company', string='Source Company', required=True, readonly=True)
target_company_id = fields.Many2one('res.company', string='Target Company', required=True, readonly=True)
user_id = fields.Many2one('res.users', string='Requested By', required=True, readonly=True, default=lambda self: self.env.user)
transfer_date = fields.Date(string='Request Date', default=fields.Date.context_today, readonly=True)
note = fields.Text(string='Transfer Note', readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('transit', 'In Transit'),
('done', 'Done'),
('cancel', 'Cancelled')
], string='Status', default='draft', tracking=True, copy=False)
@api.model
def create(self, vals):
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('ga.asset.transfer.log') or _('New')
return super(GaAssetTransferLog, self).create(vals)
def action_validate(self):
self.ensure_one()
if self.state != 'transit':
raise UserError(_("You can only validate transfers that are currently In Transit."))
# Check if user is allowed to validate (must be in target company or have access)
# Assuming if they can see the button and access the record rules allow it, but let's double check company context
if self.env.company != self.target_company_id:
raise UserError(_("You must be logged into the Target Company (%s) to validate this transfer.", self.target_company_id.name))
# Perform the logic that was previously in the wizard
old_asset = self.asset_id
target_company = self.target_company_id
# Logic adapted from original wizard
if old_asset.state in ('draft', 'model'):
old_asset.sudo().write({'company_id': target_company.id})
old_asset.sudo().message_post(body=f"Asset transfer validated by {self.env.user.name}. Received in {target_company.name}.")
else:
# Running Asset -> Close and Clone
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': target_company.id,
'location_id': old_asset.location_id.id, # Copy location? Maybe not if it's new company. But user surely wants to update it later.
'employee_id': old_asset.employee_id.id, # Keep employee if moving with them?
'state': 'draft',
}
# Find Asset Model in Target Company
if old_asset.product_id:
product = old_asset.product_id.with_company(target_company)
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
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
# Close Old Asset
old_asset.depreciation_move_ids.filtered(lambda m: m.state == 'draft').with_context(force_delete=True).sudo().unlink()
old_asset.sudo().write({
'state': 'close',
'active': False,
'in_transit': False # No longer in transit
})
old_asset.sudo().message_post(body=f"Asset transfer validated. Closed and moved to {target_company.name}.")
# Create New Asset
new_asset = self.env['account.asset'].sudo().create(vals)
new_asset.sudo().message_post(body=f"Asset received from {self.source_company_id.name}. Transfer Ref: {self.name}")
self.write({'state': 'done'})
# Unlock old asset just in case (if it was simple move)
if old_asset.id: # If simple move, old_asset is still the active one
old_asset.sudo().write({'in_transit': False})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': _('Asset has been successfully transferred.'),
'sticky': False,
}
}
def action_cancel(self):
self.ensure_one()
if self.state == 'done':
raise UserError(_("You cannot cancel a completed transfer."))
self.asset_id.sudo().write({'in_transit': False})
self.write({'state': 'cancel'})

View File

@ -20,4 +20,10 @@
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/> <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record> </record>
<record id="rule_ga_asset_transfer_log_multi_company" model="ir.rule">
<field name="name">Asset Transfer Log Multi Company</field>
<field name="model_id" ref="model_ga_asset_transfer_log"/>
<field name="domain_force">['|', ('source_company_id', 'in', company_ids), ('target_company_id', 'in', company_ids)]</field>
</record>
</odoo> </odoo>

View File

@ -1,3 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 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_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 access_ga_asset_transfer_wizard_manager,ga.asset.transfer.wizard.manager,model_ga_asset_transfer_wizard,group_ga_asset_manager,1,1,1,1
access_ga_asset_transfer_log_user,ga.asset.transfer.log.user,model_ga_asset_transfer_log,group_ga_asset_user,1,1,1,0
access_ga_asset_transfer_log_manager,ga.asset.transfer.log.manager,model_ga_asset_transfer_log,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
4 access_ga_asset_transfer_log_user ga.asset.transfer.log.user model_ga_asset_transfer_log group_ga_asset_user 1 1 1 0
5 access_ga_asset_transfer_log_manager ga.asset.transfer.log.manager model_ga_asset_transfer_log group_ga_asset_manager 1 1 1 1

View File

@ -26,7 +26,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Fixed Asset"> <form string="Fixed Asset">
<header> <header>
<button name="action_open_transfer_wizard" string="Transfer Asset" type="object" class="oe_highlight" invisible="state == 'model'"/> <button name="action_open_transfer_wizard" string="Transfer Asset" type="object" class="oe_highlight" invisible="state == 'model' or in_transit"/>
<field name="state" widget="statusbar" statusbar_visible="draft,open,close"/> <field name="state" widget="statusbar" statusbar_visible="draft,open,close"/>
</header> </header>
<sheet> <sheet>
@ -47,8 +47,23 @@
<field name="original_value"/> <field name="original_value"/>
<field name="model_id" string="Asset Model"/> <field name="model_id" string="Asset Model"/>
<field name="company_id" groups="base.group_multi_company" readonly="1"/> <field name="company_id" groups="base.group_multi_company" readonly="1"/>
<field name="location_id"/>
<field name="employee_id"/>
<field name="employee_id"/>
<field name="in_transit" invisible="1"/>
</group> </group>
</group> </group>
<group string="Depreciation Board">
<label for="method_number" string="Duration"/>
<div class="o_row">
<field name="method_number" nolabel="1" readonly="1" force_save="1"/>
<field name="method_period" nolabel="1" readonly="1" force_save="1"/>
</div>
<!-- Hidden fields for logic -->
<field name="method" invisible="1"/>
<field name="prorata_computation_type" invisible="1"/>
</group>
<!-- Accounting Group Removed as per request -->
<notebook> <notebook>
<page string="Description"> <page string="Description">
<field name="description"/> <field name="description"/>
@ -67,25 +82,15 @@
<field name="name">Assets</field> <field name="name">Assets</field>
<field name="res_model">account.asset</field> <field name="res_model">account.asset</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="view_id" ref="view_ga_account_asset_tree"/> <field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('view_ga_account_asset_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('view_ga_account_asset_form')})]"/>
<field name="search_view_id" ref="account_asset.view_account_asset_search"/> <field name="search_view_id" ref="account_asset.view_account_asset_search"/>
<field name="domain">[('state', '!=', 'model')]</field> <!-- Filter out Asset Models --> <field name="domain">[('state', '!=', 'model')]</field> <!-- Filter out Asset Models -->
<field name="context">{'default_state': 'draft'}</field> <field name="context">{'default_state': 'draft'}</field>
</record> </record>
<record id="action_ga_asset_tree_view_link" model="ir.actions.act_window.view"> <!-- Duplicate view links removed -->
<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 --> <!-- 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_root" name="General Affair" web_icon="ga_asset_management,static/description/icon.png" sequence="10" groups="ga_asset_management.group_ga_asset_user"/>
@ -96,4 +101,49 @@
action="action_ga_asset_management" action="action_ga_asset_management"
sequence="1"/> sequence="1"/>
<!-- Inheritance to modify Standard Account Asset Form -->
<record id="view_account_asset_form_inherit_ga" model="ir.ui.view">
<field name="name">account.asset.form.inherit.ga</field>
<field name="model">account.asset</field>
<field name="inherit_id" ref="account_asset.view_account_asset_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='main_page']//field[@name='method']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='method_number']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='method_period']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='prorata_computation_type']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='account_asset_id']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='account_depreciation_id']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='account_depreciation_expense_id']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='journal_id']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//page[@name='main_page']//field[@name='analytic_distribution']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="force_save">1</attribute>
</xpath>
</field>
</record>
</odoo> </odoo>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_ga_asset_transfer_log_tree" model="ir.ui.view">
<field name="name">ga.asset.transfer.log.tree</field>
<field name="model">ga.asset.transfer.log</field>
<field name="arch" type="xml">
<tree string="Asset Transfer Logs" decoration-info="state == 'transit'" decoration-muted="state == 'cancel'" decoration-success="state == 'done'">
<field name="name"/>
<field name="transfer_date"/>
<field name="asset_id"/>
<field name="source_company_id"/>
<field name="target_company_id"/>
<field name="user_id"/>
<field name="state" widget="badge" decoration-info="state == 'transit'" decoration-success="state == 'done'"/>
</tree>
</field>
</record>
<record id="view_ga_asset_transfer_log_form" model="ir.ui.view">
<field name="name">ga.asset.transfer.log.form</field>
<field name="model">ga.asset.transfer.log</field>
<field name="arch" type="xml">
<form string="Asset Transfer Log">
<header>
<button name="action_validate" string="Validate &amp; Receive" type="object" class="btn-primary" invisible="state != 'transit'"/>
<button name="action_cancel" string="Cancel" type="object" class="btn-secondary" invisible="state not in ('draft', 'transit')"/>
<field name="state" widget="statusbar" statusbar_visible="draft,transit,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="asset_id"/>
<field name="transfer_date"/>
<field name="user_id"/>
</group>
<group>
<field name="source_company_id"/>
<field name="target_company_id"/>
</group>
</group>
<field name="note" placeholder="Add a note..."/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<record id="action_ga_asset_transfer_log" model="ir.actions.act_window">
<field name="name">Pending Asset Receipts</field>
<field name="res_model">ga.asset.transfer.log</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('state', '=', 'transit')]</field> <!-- Focus on pending ones by default, or maybe all? -->
<field name="context">{'search_default_transit': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No pending asset transfers found
</p><p>
When an asset is transferred from another company, it will appear here for validation.
</p>
</field>
</record>
<menuitem id="menu_ga_asset_transfer_log"
name="Pending Asset Receipts"
parent="account_asset.menu_finance_config_assets"
action="action_ga_asset_transfer_log"
sequence="15"/>
<!-- Also add to main Asset menu root if needed, but config seems safe or better under 'Assets' main
Let's check where 'account_asset.menu_account_asset' is. Usually Accounting -> Assets -> Assets.
Wait, check existing menu structure.
-->
<!-- Removed Accounting menu as per request -->
</odoo>

View File

@ -12,86 +12,27 @@ class GaAssetTransferWizard(models.TransientModel):
def action_transfer(self): def action_transfer(self):
self.ensure_one() self.ensure_one()
if self.target_company_id and self.target_company_id != self.current_company_id: if self.target_company_id and self.target_company_id != self.current_company_id:
old_asset = self.asset_id # Create Transfer Log
transfer_log = self.env['ga.asset.transfer.log'].create({
# CASE 1: Asset is Draft or Model -> Simple Move 'asset_id': self.asset_id.id,
# No depreciation history to preserve, so we just move it. 'source_company_id': self.current_company_id.id,
if old_asset.state in ('draft', 'model'): 'target_company_id': self.target_company_id.id,
old_asset.sudo().write({'company_id': self.target_company_id.id}) 'note': self.note,
if self.note: 'state': 'transit',
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.") # Lock Asset
self.asset_id.sudo().write({'in_transit': True})
self.asset_id.message_post(body=f"Asset transfer initiated to {self.target_company_id.name}. Waiting for validation. Ref: {transfer_log.name}")
# 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 { return {
'name': 'Transferred Asset', 'type': 'ir.actions.client',
'type': 'ir.actions.act_window', 'tag': 'display_notification',
'res_model': 'account.asset', 'params': {
'view_mode': 'form', 'title': 'Transfer Initiated',
'res_id': new_asset.id, 'message': f"Asset transfer to {self.target_company_id.name} is now pending validation.",
'target': 'current', 'sticky': False,
}
} }
return {'type': 'ir.actions.act_window_close'} return {'type': 'ir.actions.act_window_close'}