Compare commits
4 Commits
9794970429
...
3efef7efc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3efef7efc7 | ||
|
|
92f445eb8f | ||
|
|
892c37e2b0 | ||
|
|
a6ef7e0c2a |
@@ -1,3 +1,4 @@
|
||||
from . import models
|
||||
from . import services
|
||||
from . import controllers
|
||||
from . import wizards
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'Fusion Accounting Assets',
|
||||
'version': '19.0.1.0.26',
|
||||
'version': '19.0.1.0.30',
|
||||
'category': 'Accounting/Accounting',
|
||||
'summary': 'AI-augmented asset management with depreciation schedules.',
|
||||
'description': """
|
||||
@@ -34,6 +34,10 @@ menu hides; the engine + AI tools remain available for the chat.
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/cron.xml',
|
||||
'wizards/create_asset_wizard_views.xml',
|
||||
'wizards/disposal_wizard_views.xml',
|
||||
'wizards/partial_sale_wizard_views.xml',
|
||||
'wizards/depreciation_run_wizard_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
|
||||
@@ -9,3 +9,7 @@ access_fusion_asset_disposal_user,fusion.asset.disposal.user,model_fusion_asset_
|
||||
access_fusion_asset_disposal_admin,fusion.asset.disposal.admin,model_fusion_asset_disposal,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||
access_fusion_asset_anomaly_user,fusion.asset.anomaly.user,model_fusion_asset_anomaly,base.group_user,1,0,0,0
|
||||
access_fusion_asset_anomaly_admin,fusion.asset.anomaly.admin,model_fusion_asset_anomaly,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||
access_fusion_create_asset_wizard_user,fusion.create.asset.wizard.user,model_fusion_create_asset_wizard,base.group_user,1,1,1,0
|
||||
access_fusion_disposal_wizard_user,fusion.disposal.wizard.user,model_fusion_disposal_wizard,base.group_user,1,1,1,0
|
||||
access_fusion_partial_sale_wizard_user,fusion.partial.sale.wizard.user,model_fusion_partial_sale_wizard,base.group_user,1,1,1,0
|
||||
access_fusion_depreciation_run_wizard_user,fusion.depreciation.run.wizard.user,model_fusion_depreciation_run_wizard,base.group_user,1,1,1,0
|
||||
|
||||
|
@@ -19,3 +19,7 @@ from . import test_engine_property
|
||||
from . import test_method_integration
|
||||
from . import test_asset_book_values_mv
|
||||
from . import test_performance_benchmarks
|
||||
from . import test_create_asset_wizard
|
||||
from . import test_disposal_wizard
|
||||
from . import test_partial_sale_wizard
|
||||
from . import test_depreciation_run_wizard
|
||||
|
||||
62
fusion_accounting_assets/tests/test_create_asset_wizard.py
Normal file
62
fusion_accounting_assets/tests/test_create_asset_wizard.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from datetime import date
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestCreateAssetWizard(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env['ir.config_parameter'].sudo().search([
|
||||
('key', 'in', ['fusion_accounting.provider.asset_useful_life',
|
||||
'fusion_accounting.provider.default'])
|
||||
]).unlink()
|
||||
|
||||
def test_create_minimal_asset(self):
|
||||
wizard = self.env['fusion.create.asset.wizard'].create({
|
||||
'name': 'Test Asset',
|
||||
'cost': 5000,
|
||||
'method': 'straight_line',
|
||||
'useful_life_years': 5,
|
||||
'acquisition_date': date(2026, 1, 1),
|
||||
'source_invoice_line_id': False,
|
||||
})
|
||||
action = wizard.action_create_asset()
|
||||
self.assertEqual(action['res_model'], 'fusion.asset')
|
||||
asset = self.env['fusion.asset'].browse(action['res_id'])
|
||||
self.assertEqual(asset.name, 'Test Asset')
|
||||
self.assertEqual(asset.cost, 5000)
|
||||
|
||||
def test_ai_suggest_fills_fields(self):
|
||||
wizard = self.env['fusion.create.asset.wizard'].create({
|
||||
'name': 'Dell laptop',
|
||||
'cost': 2000,
|
||||
'method': 'straight_line',
|
||||
'useful_life_years': 5,
|
||||
'acquisition_date': date(2026, 1, 1),
|
||||
})
|
||||
wizard.action_ai_suggest()
|
||||
self.assertEqual(wizard.ai_suggested_years, 4)
|
||||
self.assertEqual(wizard.useful_life_years, 4)
|
||||
|
||||
def test_category_onchange_pre_fills(self):
|
||||
category = self.env['fusion.asset.category'].create({
|
||||
'name': 'Test Category',
|
||||
'method': 'declining_balance',
|
||||
'useful_life_years': 7,
|
||||
'declining_rate_pct': 25.0,
|
||||
'salvage_value_pct': 10.0,
|
||||
})
|
||||
wizard = self.env['fusion.create.asset.wizard'].new({
|
||||
'name': 'Test', 'cost': 10000,
|
||||
'method': 'straight_line', 'useful_life_years': 5,
|
||||
'acquisition_date': date(2026, 1, 1),
|
||||
'category_id': category.id,
|
||||
})
|
||||
wizard._onchange_category_id()
|
||||
self.assertEqual(wizard.method, 'declining_balance')
|
||||
self.assertEqual(wizard.useful_life_years, 7)
|
||||
self.assertEqual(wizard.declining_rate_pct, 25.0)
|
||||
self.assertAlmostEqual(wizard.salvage_value, 1000, places=2)
|
||||
@@ -0,0 +1,43 @@
|
||||
from datetime import date
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestDepreciationRunWizard(TransactionCase):
|
||||
|
||||
def test_run_all_running_posts_due_periods(self):
|
||||
for amt in [3000, 5000]:
|
||||
asset = self.env['fusion.asset'].create({
|
||||
'name': f'Run Test {amt}', 'cost': amt,
|
||||
'acquisition_date': date(2026, 1, 1),
|
||||
'in_service_date': date(2026, 1, 1),
|
||||
'method': 'straight_line', 'useful_life_years': 3,
|
||||
})
|
||||
self.env['fusion.asset.engine'].compute_depreciation_schedule(asset)
|
||||
asset.action_set_running()
|
||||
wizard = self.env['fusion.depreciation.run.wizard'].create({
|
||||
'period_date': date(2030, 12, 31),
|
||||
'state_filter': 'all_running',
|
||||
})
|
||||
wizard.action_run()
|
||||
self.assertEqual(wizard.state, 'done')
|
||||
self.assertGreater(wizard.posted_count, 0)
|
||||
|
||||
def test_run_selected_posts_only_selected(self):
|
||||
asset = self.env['fusion.asset'].create({
|
||||
'name': 'Selected Test', 'cost': 1000,
|
||||
'acquisition_date': date(2026, 1, 1),
|
||||
'in_service_date': date(2026, 1, 1),
|
||||
'method': 'straight_line', 'useful_life_years': 3,
|
||||
})
|
||||
self.env['fusion.asset.engine'].compute_depreciation_schedule(asset)
|
||||
asset.action_set_running()
|
||||
wizard = self.env['fusion.depreciation.run.wizard'].create({
|
||||
'period_date': date(2030, 12, 31),
|
||||
'state_filter': 'selected',
|
||||
'asset_ids': [(6, 0, [asset.id])],
|
||||
})
|
||||
wizard.action_run()
|
||||
self.assertEqual(wizard.state, 'done')
|
||||
50
fusion_accounting_assets/tests/test_disposal_wizard.py
Normal file
50
fusion_accounting_assets/tests/test_disposal_wizard.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from datetime import date
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestDisposalWizard(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.asset = self.env['fusion.asset'].create({
|
||||
'name': 'Disposal Test Asset',
|
||||
'cost': 6000,
|
||||
'acquisition_date': date(2026, 1, 1),
|
||||
'in_service_date': date(2026, 1, 1),
|
||||
'method': 'straight_line', 'useful_life_years': 3,
|
||||
})
|
||||
self.env['fusion.asset.engine'].compute_depreciation_schedule(self.asset)
|
||||
self.asset.action_set_running()
|
||||
|
||||
def test_default_loads_active_asset(self):
|
||||
wizard = self.env['fusion.disposal.wizard'].with_context(
|
||||
active_model='fusion.asset', active_id=self.asset.id,
|
||||
).create({})
|
||||
self.assertEqual(wizard.asset_id, self.asset)
|
||||
|
||||
def test_action_dispose_marks_asset_disposed(self):
|
||||
wizard = self.env['fusion.disposal.wizard'].create({
|
||||
'asset_id': self.asset.id,
|
||||
'disposal_type': 'sale',
|
||||
'sale_amount': 4000,
|
||||
'disposal_date': date(2026, 6, 1),
|
||||
})
|
||||
wizard.action_dispose()
|
||||
self.asset.invalidate_recordset(['state'])
|
||||
self.assertEqual(self.asset.state, 'disposed')
|
||||
|
||||
def test_compute_gain_loss_sale(self):
|
||||
wizard = self.env['fusion.disposal.wizard'].create({
|
||||
'asset_id': self.asset.id,
|
||||
'disposal_type': 'sale',
|
||||
'sale_amount': 7000,
|
||||
})
|
||||
wizard._compute_gain_loss()
|
||||
self.assertAlmostEqual(
|
||||
wizard.estimated_gain_loss,
|
||||
7000 - self.asset.book_value,
|
||||
places=2,
|
||||
)
|
||||
48
fusion_accounting_assets/tests/test_partial_sale_wizard.py
Normal file
48
fusion_accounting_assets/tests/test_partial_sale_wizard.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from datetime import date
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestPartialSaleWizard(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.asset = self.env['fusion.asset'].create({
|
||||
'name': 'Partial Sale Test',
|
||||
'cost': 10000,
|
||||
'acquisition_date': date(2026, 1, 1),
|
||||
'in_service_date': date(2026, 1, 1),
|
||||
'method': 'straight_line', 'useful_life_years': 5,
|
||||
})
|
||||
self.env['fusion.asset.engine'].compute_depreciation_schedule(self.asset)
|
||||
self.asset.action_set_running()
|
||||
|
||||
def test_partial_sell_30pct_creates_child(self):
|
||||
wizard = self.env['fusion.partial.sale.wizard'].create({
|
||||
'asset_id': self.asset.id,
|
||||
'sold_pct': 30.0, 'sold_amount': 4000,
|
||||
'sale_date': date(2026, 6, 1),
|
||||
})
|
||||
wizard.action_partial_sell()
|
||||
self.asset.invalidate_recordset(['cost'])
|
||||
self.assertAlmostEqual(self.asset.cost, 7000, places=2)
|
||||
|
||||
def test_invalid_pct_raises(self):
|
||||
wizard = self.env['fusion.partial.sale.wizard'].create({
|
||||
'asset_id': self.asset.id,
|
||||
'sold_pct': 0, 'sold_amount': 100,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
wizard.action_partial_sell()
|
||||
|
||||
def test_compute_estimated_gain_loss(self):
|
||||
wizard = self.env['fusion.partial.sale.wizard'].new({
|
||||
'asset_id': self.asset.id,
|
||||
'sold_pct': 30.0, 'sold_amount': 4000,
|
||||
})
|
||||
wizard._compute_sold_cost()
|
||||
self.assertAlmostEqual(wizard.estimated_sold_cost, 3000, places=2)
|
||||
self.assertAlmostEqual(wizard.estimated_gain_loss, 1000, places=2)
|
||||
@@ -0,0 +1,4 @@
|
||||
from . import create_asset_wizard
|
||||
from . import disposal_wizard
|
||||
from . import partial_sale_wizard
|
||||
from . import depreciation_run_wizard
|
||||
|
||||
133
fusion_accounting_assets/wizards/create_asset_wizard.py
Normal file
133
fusion_accounting_assets/wizards/create_asset_wizard.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Create-asset-from-invoice-line wizard.
|
||||
|
||||
Reads an account.move.line as the source, pre-fills name/cost/category,
|
||||
and optionally calls the AI useful-life predictor for suggestions."""
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from ..services.useful_life_predictor import predict_useful_life
|
||||
|
||||
|
||||
class FusionCreateAssetWizard(models.TransientModel):
|
||||
_name = "fusion.create.asset.wizard"
|
||||
_description = "Create Fusion Asset from Invoice Line"
|
||||
|
||||
source_invoice_line_id = fields.Many2one(
|
||||
'account.move.line', string='Source Invoice Line',
|
||||
default=lambda self: self._default_source_line(),
|
||||
)
|
||||
name = fields.Char(required=True)
|
||||
cost = fields.Monetary(required=True)
|
||||
salvage_value = fields.Monetary(default=0.0)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', required=True,
|
||||
default=lambda self: self.env.company.currency_id,
|
||||
)
|
||||
category_id = fields.Many2one('fusion.asset.category')
|
||||
method = fields.Selection([
|
||||
('straight_line', 'Straight Line'),
|
||||
('declining_balance', 'Declining Balance'),
|
||||
('units_of_production', 'Units of Production'),
|
||||
], required=True, default='straight_line')
|
||||
useful_life_years = fields.Integer(default=5)
|
||||
declining_rate_pct = fields.Float(default=20.0)
|
||||
acquisition_date = fields.Date(required=True, default=fields.Date.today)
|
||||
in_service_date = fields.Date(default=fields.Date.today)
|
||||
|
||||
ai_suggested_years = fields.Integer(readonly=True)
|
||||
ai_suggested_method = fields.Char(readonly=True)
|
||||
ai_rationale = fields.Text(readonly=True)
|
||||
ai_confidence = fields.Float(readonly=True)
|
||||
|
||||
@api.model
|
||||
def _default_source_line(self):
|
||||
ctx = self.env.context
|
||||
if ctx.get('active_model') == 'account.move.line':
|
||||
return ctx.get('active_id')
|
||||
return False
|
||||
|
||||
@api.onchange('source_invoice_line_id')
|
||||
def _onchange_source_invoice_line_id(self):
|
||||
if not self.source_invoice_line_id:
|
||||
return
|
||||
line = self.source_invoice_line_id
|
||||
if not self.name:
|
||||
self.name = line.name or line.move_id.name or 'New Asset'
|
||||
if not self.cost:
|
||||
self.cost = abs(line.balance) if line.balance else (line.price_unit * line.quantity)
|
||||
if line.currency_id and not self.currency_id:
|
||||
self.currency_id = line.currency_id
|
||||
|
||||
@api.onchange('category_id')
|
||||
def _onchange_category_id(self):
|
||||
if self.category_id:
|
||||
self.method = self.category_id.method
|
||||
self.useful_life_years = self.category_id.useful_life_years
|
||||
self.declining_rate_pct = self.category_id.declining_rate_pct
|
||||
if self.category_id.salvage_value_pct and self.cost:
|
||||
self.salvage_value = round(
|
||||
self.cost * self.category_id.salvage_value_pct / 100, 2)
|
||||
|
||||
def action_ai_suggest(self):
|
||||
"""Call AI useful-life predictor."""
|
||||
self.ensure_one()
|
||||
if not self.name and not self.source_invoice_line_id:
|
||||
raise UserError(_("Need a name or source invoice line first."))
|
||||
description = self.name
|
||||
if self.source_invoice_line_id and self.source_invoice_line_id.name:
|
||||
description = self.source_invoice_line_id.name
|
||||
partner_name = None
|
||||
if self.source_invoice_line_id and self.source_invoice_line_id.partner_id:
|
||||
partner_name = self.source_invoice_line_id.partner_id.name
|
||||
|
||||
suggestion = predict_useful_life(
|
||||
self.env, description=description,
|
||||
amount=self.cost, partner_name=partner_name,
|
||||
)
|
||||
self.write({
|
||||
'ai_suggested_years': suggestion.get('useful_life_years'),
|
||||
'ai_suggested_method': suggestion.get('depreciation_method'),
|
||||
'ai_rationale': suggestion.get('rationale'),
|
||||
'ai_confidence': suggestion.get('confidence'),
|
||||
'useful_life_years': suggestion.get('useful_life_years'),
|
||||
'method': suggestion.get('depreciation_method'),
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_create_asset(self):
|
||||
"""Create the fusion.asset record + link to source invoice line."""
|
||||
self.ensure_one()
|
||||
if not self.cost:
|
||||
raise UserError(_("Cost is required."))
|
||||
Asset = self.env['fusion.asset']
|
||||
asset = Asset.create({
|
||||
'name': self.name,
|
||||
'cost': self.cost,
|
||||
'salvage_value': self.salvage_value,
|
||||
'currency_id': self.currency_id.id,
|
||||
'category_id': self.category_id.id if self.category_id else False,
|
||||
'method': self.method,
|
||||
'useful_life_years': self.useful_life_years,
|
||||
'declining_rate_pct': self.declining_rate_pct,
|
||||
'acquisition_date': self.acquisition_date,
|
||||
'in_service_date': self.in_service_date,
|
||||
'source_invoice_line_id': self.source_invoice_line_id.id if self.source_invoice_line_id else False,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
if self.source_invoice_line_id:
|
||||
self.source_invoice_line_id.fusion_asset_id = asset.id
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.asset',
|
||||
'res_id': asset.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_fusion_create_asset_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.create.asset.wizard.form</field>
|
||||
<field name="model">fusion.create.asset.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Fixed Asset">
|
||||
<group>
|
||||
<group string="Basics">
|
||||
<field name="name"/>
|
||||
<field name="source_invoice_line_id"
|
||||
options="{'no_create': True}"
|
||||
readonly="source_invoice_line_id"/>
|
||||
<field name="cost"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="salvage_value"/>
|
||||
<field name="category_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group string="Depreciation">
|
||||
<field name="method"/>
|
||||
<field name="useful_life_years"
|
||||
invisible="method == 'units_of_production'"/>
|
||||
<field name="declining_rate_pct"
|
||||
invisible="method != 'declining_balance'"/>
|
||||
<field name="acquisition_date"/>
|
||||
<field name="in_service_date"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="AI Suggestion" invisible="not ai_suggested_years">
|
||||
<field name="ai_suggested_years" readonly="1"/>
|
||||
<field name="ai_suggested_method" readonly="1"/>
|
||||
<field name="ai_rationale" readonly="1"/>
|
||||
<field name="ai_confidence" readonly="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_ai_suggest" type="object"
|
||||
string="AI Suggest" class="btn-secondary"/>
|
||||
<button name="action_create_asset" type="object"
|
||||
string="Create Asset" class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_create_asset_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Create Asset from Invoice</field>
|
||||
<field name="res_model">fusion.create.asset.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="account.model_account_move_line"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
</odoo>
|
||||
72
fusion_accounting_assets/wizards/depreciation_run_wizard.py
Normal file
72
fusion_accounting_assets/wizards/depreciation_run_wizard.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Manual depreciation run wizard.
|
||||
|
||||
Operator picks a period_date and the wizard posts all running assets'
|
||||
unposted lines whose scheduled_date <= period_date."""
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FusionDepreciationRunWizard(models.TransientModel):
|
||||
_name = "fusion.depreciation.run.wizard"
|
||||
_description = "Manual Depreciation Run Wizard"
|
||||
|
||||
period_date = fields.Date(
|
||||
required=True, default=fields.Date.today,
|
||||
help="Post all unposted lines whose scheduled_date is on or before this date.",
|
||||
)
|
||||
state_filter = fields.Selection([
|
||||
('all_running', 'All Running Assets'),
|
||||
('selected', 'Selected Asset(s) Only'),
|
||||
], default='all_running', required=True)
|
||||
asset_ids = fields.Many2many(
|
||||
'fusion.asset', domain=[('state', '=', 'running')],
|
||||
)
|
||||
|
||||
state = fields.Selection(
|
||||
[('draft', 'Draft'), ('done', 'Done')], default='draft',
|
||||
)
|
||||
posted_count = fields.Integer(readonly=True)
|
||||
skipped_count = fields.Integer(readonly=True)
|
||||
error_count = fields.Integer(readonly=True)
|
||||
summary = fields.Text(readonly=True)
|
||||
|
||||
def action_run(self):
|
||||
self.ensure_one()
|
||||
if self.state_filter == 'all_running':
|
||||
assets = self.env['fusion.asset'].search([
|
||||
('state', '=', 'running'),
|
||||
('company_id', '=', self.env.company.id),
|
||||
])
|
||||
else:
|
||||
assets = self.asset_ids
|
||||
|
||||
engine = self.env['fusion.asset.engine']
|
||||
posted = 0
|
||||
skipped = 0
|
||||
errors = []
|
||||
for asset in assets:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
result = engine.post_depreciation_entry(
|
||||
asset, period_date=self.period_date,
|
||||
)
|
||||
posted += result.get('posted_count', 0)
|
||||
if result.get('posted_count', 0) == 0:
|
||||
skipped += 1
|
||||
except Exception as e: # noqa: BLE001
|
||||
errors.append(f"{asset.name}: {e}")
|
||||
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'posted_count': posted,
|
||||
'skipped_count': skipped,
|
||||
'error_count': len(errors),
|
||||
'summary': '\n'.join(errors[:20]) if errors else False,
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_fusion_depreciation_run_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.depreciation.run.wizard.form</field>
|
||||
<field name="model">fusion.depreciation.run.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Run Depreciation">
|
||||
<group invisible="state == 'done'">
|
||||
<field name="period_date"/>
|
||||
<field name="state_filter" widget="radio"/>
|
||||
<field name="asset_ids" widget="many2many_tags"
|
||||
invisible="state_filter != 'selected'"/>
|
||||
</group>
|
||||
<group invisible="state != 'done'" string="Results">
|
||||
<field name="posted_count"/>
|
||||
<field name="skipped_count"/>
|
||||
<field name="error_count"/>
|
||||
<field name="summary"/>
|
||||
</group>
|
||||
<field name="state" invisible="1"/>
|
||||
<footer>
|
||||
<button name="action_run" type="object" string="Run"
|
||||
class="btn-primary" invisible="state == 'done'"/>
|
||||
<button special="cancel" string="Close"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_depreciation_run_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Run Depreciation</field>
|
||||
<field name="res_model">fusion.depreciation.run.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
65
fusion_accounting_assets/wizards/disposal_wizard.py
Normal file
65
fusion_accounting_assets/wizards/disposal_wizard.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Asset disposal wizard (sale, scrap, donation, lost)."""
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class FusionDisposalWizard(models.TransientModel):
|
||||
_name = "fusion.disposal.wizard"
|
||||
_description = "Asset Disposal Wizard"
|
||||
|
||||
asset_id = fields.Many2one(
|
||||
'fusion.asset', required=True,
|
||||
default=lambda self: self._default_asset(),
|
||||
)
|
||||
company_id = fields.Many2one(related='asset_id.company_id')
|
||||
currency_id = fields.Many2one(related='asset_id.currency_id')
|
||||
book_value = fields.Monetary(related='asset_id.book_value', readonly=True)
|
||||
|
||||
disposal_type = fields.Selection([
|
||||
('sale', 'Sale'),
|
||||
('scrap', 'Scrap'),
|
||||
('donation', 'Donation'),
|
||||
('lost', 'Lost / Stolen'),
|
||||
], required=True, default='sale')
|
||||
disposal_date = fields.Date(required=True, default=fields.Date.today)
|
||||
sale_amount = fields.Monetary(default=0.0)
|
||||
sale_partner_id = fields.Many2one('res.partner')
|
||||
notes = fields.Text()
|
||||
|
||||
estimated_gain_loss = fields.Monetary(compute='_compute_gain_loss')
|
||||
|
||||
@api.model
|
||||
def _default_asset(self):
|
||||
ctx = self.env.context
|
||||
if ctx.get('active_model') == 'fusion.asset':
|
||||
return ctx.get('active_id')
|
||||
return False
|
||||
|
||||
@api.depends('sale_amount', 'book_value', 'disposal_type')
|
||||
def _compute_gain_loss(self):
|
||||
for w in self:
|
||||
if w.disposal_type == 'sale':
|
||||
w.estimated_gain_loss = w.sale_amount - w.book_value
|
||||
else:
|
||||
w.estimated_gain_loss = -w.book_value
|
||||
|
||||
def action_dispose(self):
|
||||
self.ensure_one()
|
||||
if self.asset_id.state == 'disposed':
|
||||
raise UserError(_("Asset already disposed."))
|
||||
partner = self.sale_partner_id if self.disposal_type == 'sale' else None
|
||||
self.env['fusion.asset.engine'].dispose_asset(
|
||||
self.asset_id,
|
||||
sale_amount=self.sale_amount if self.disposal_type == 'sale' else 0,
|
||||
sale_date=self.disposal_date,
|
||||
sale_partner=partner,
|
||||
disposal_type=self.disposal_type,
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.asset',
|
||||
'res_id': self.asset_id.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
39
fusion_accounting_assets/wizards/disposal_wizard_views.xml
Normal file
39
fusion_accounting_assets/wizards/disposal_wizard_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_fusion_disposal_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.disposal.wizard.form</field>
|
||||
<field name="model">fusion.disposal.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Dispose Asset">
|
||||
<group>
|
||||
<field name="asset_id" options="{'no_create': True}" readonly="1"/>
|
||||
<field name="book_value" readonly="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="disposal_type"/>
|
||||
<field name="disposal_date"/>
|
||||
<field name="sale_amount" invisible="disposal_type != 'sale'"/>
|
||||
<field name="sale_partner_id" invisible="disposal_type != 'sale'"/>
|
||||
<field name="estimated_gain_loss" readonly="1"/>
|
||||
<field name="notes"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_dispose" type="object"
|
||||
string="Confirm Disposal" class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_disposal_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Dispose Asset</field>
|
||||
<field name="res_model">fusion.disposal.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="model_fusion_asset"/>
|
||||
<field name="binding_view_types">form,list</field>
|
||||
</record>
|
||||
</odoo>
|
||||
67
fusion_accounting_assets/wizards/partial_sale_wizard.py
Normal file
67
fusion_accounting_assets/wizards/partial_sale_wizard.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Partial sale wizard (sell a portion of an asset).
|
||||
|
||||
Splits the asset into a child (the sold portion) and disposes the child;
|
||||
parent retains remaining cost + salvage."""
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class FusionPartialSaleWizard(models.TransientModel):
|
||||
_name = "fusion.partial.sale.wizard"
|
||||
_description = "Asset Partial Sale Wizard"
|
||||
|
||||
asset_id = fields.Many2one(
|
||||
'fusion.asset', required=True,
|
||||
default=lambda self: self._default_asset(),
|
||||
)
|
||||
company_id = fields.Many2one(related='asset_id.company_id')
|
||||
currency_id = fields.Many2one(related='asset_id.currency_id')
|
||||
cost = fields.Monetary(related='asset_id.cost', readonly=True)
|
||||
book_value = fields.Monetary(related='asset_id.book_value', readonly=True)
|
||||
|
||||
sold_pct = fields.Float(
|
||||
string='% of cost being sold', default=30.0,
|
||||
help="Percentage of original cost attributed to the sold portion.",
|
||||
)
|
||||
sold_amount = fields.Monetary(string='Sale Amount', required=True)
|
||||
sale_date = fields.Date(required=True, default=fields.Date.today)
|
||||
sale_partner_id = fields.Many2one('res.partner')
|
||||
|
||||
estimated_sold_cost = fields.Monetary(compute='_compute_sold_cost')
|
||||
estimated_gain_loss = fields.Monetary(compute='_compute_sold_cost')
|
||||
|
||||
@api.model
|
||||
def _default_asset(self):
|
||||
ctx = self.env.context
|
||||
if ctx.get('active_model') == 'fusion.asset':
|
||||
return ctx.get('active_id')
|
||||
return False
|
||||
|
||||
@api.depends('sold_pct', 'sold_amount', 'cost')
|
||||
def _compute_sold_cost(self):
|
||||
for w in self:
|
||||
w.estimated_sold_cost = round(w.cost * (w.sold_pct or 0) / 100, 2)
|
||||
w.estimated_gain_loss = w.sold_amount - w.estimated_sold_cost
|
||||
|
||||
def action_partial_sell(self):
|
||||
self.ensure_one()
|
||||
if not (0 < self.sold_pct < 100):
|
||||
raise UserError(_("sold_pct must be strictly between 0 and 100."))
|
||||
if self.asset_id.state == 'disposed':
|
||||
raise UserError(_("Asset already disposed."))
|
||||
|
||||
result = self.env['fusion.asset.engine'].partial_sale(
|
||||
self.asset_id,
|
||||
sold_amount=self.sold_amount,
|
||||
sold_qty=self.sold_pct / 100,
|
||||
sale_date=self.sale_date,
|
||||
sale_partner=self.sale_partner_id,
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.asset',
|
||||
'res_id': result['parent_asset_id'],
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_fusion_partial_sale_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.partial.sale.wizard.form</field>
|
||||
<field name="model">fusion.partial.sale.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Partial Sale">
|
||||
<group>
|
||||
<field name="asset_id" readonly="1" options="{'no_create': True}"/>
|
||||
<field name="cost" readonly="1"/>
|
||||
<field name="book_value" readonly="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="sold_pct"/>
|
||||
<field name="estimated_sold_cost" readonly="1"/>
|
||||
<field name="sold_amount"/>
|
||||
<field name="estimated_gain_loss" readonly="1"/>
|
||||
<field name="sale_date"/>
|
||||
<field name="sale_partner_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_partial_sell" type="object"
|
||||
string="Confirm Partial Sale" class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_partial_sale_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Partial Sale</field>
|
||||
<field name="res_model">fusion.partial.sale.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user