commit 533b72d6c0671752c997c3a16609c2393578abed Author: liAnGjiA Date: Mon Mar 8 15:53:48 2021 +0800 init diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f17ad3d --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import wizard +from . import report diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..969ff15 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Odoo 14 Assets Management', + 'version': '14.0.2.6.0', + 'author': 'Odoo Mates, Odoo SA', + 'depends': ['account'], + 'description': """Manage assets owned by a company or a person. + Keeps track of depreciation's, and creates corresponding journal entries""", + 'summary': 'Odoo 14 Assets Management', + 'category': 'Accounting', + 'sequence': 10, + 'website': 'https://www.odoomates.tech', + 'license': 'LGPL-3', + 'images': ['static/description/assets.gif'], + 'data': [ + 'security/account_asset_security.xml', + 'security/ir.model.access.csv', + 'wizard/asset_depreciation_confirmation_wizard_views.xml', + 'wizard/asset_modify_views.xml', + 'views/account_asset_views.xml', + 'views/account_invoice_views.xml', + 'views/account_asset_templates.xml', + 'views/product_views.xml', + # 'views/res_config_settings_views.xml', + 'report/account_asset_report_views.xml', + 'data/account_asset_data.xml', + ], + 'qweb': [ + "static/src/xml/account_asset_template.xml", + ], +} diff --git a/data/account_asset_data.xml b/data/account_asset_data.xml new file mode 100644 index 0000000..28f3491 --- /dev/null +++ b/data/account_asset_data.xml @@ -0,0 +1,15 @@ + + + + + Account Asset: Generate asset entries + + code + model._cron_generate_entries() + 1 + months + -1 + + + + \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..93b31dc --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account +from . import account_asset +from . import account_invoice +from . import product diff --git a/models/account.py b/models/account.py new file mode 100644 index 0000000..e65e9d6 --- /dev/null +++ b/models/account.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + asset_depreciation_ids = fields.One2many('account.asset.depreciation.line', 'move_id', + string='Assets Depreciation Lines') + + def button_cancel(self): + for move in self: + for line in move.asset_depreciation_ids: + line.move_posted_check = False + return super(AccountMove, self).button_cancel() + + def post(self): + for move in self: + for depreciation_line in move.asset_depreciation_ids: + depreciation_line.post_lines_and_close_asset() + return super(AccountMove, self).post() diff --git a/models/account_asset.py b/models/account_asset.py new file mode 100644 index 0000000..9424508 --- /dev/null +++ b/models/account_asset.py @@ -0,0 +1,653 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import calendar +from datetime import date, datetime +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare, float_is_zero + + +class AccountAssetCategory(models.Model): + _name = 'account.asset.category' + _description = 'Asset category' + + active = fields.Boolean(default=True) + name = fields.Char(required=True, index=True, string="Asset Type") + account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tag') + account_asset_id = fields.Many2one('account.account', string='Asset Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)], help="Account used to record the purchase of the asset at its original price.") + account_depreciation_id = fields.Many2one('account.account', string='Depreciation Entries: Asset Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)], help="Account used in the depreciation entries, to decrease the asset value.") + account_depreciation_expense_id = fields.Many2one('account.account', string='Depreciation Entries: Expense Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)], help="Account used in the periodical entries, to record a part of the asset as expense.") + journal_id = fields.Many2one('account.journal', string='Journal', required=True) + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']._company_default_get('account.asset.category')) + method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, default='linear', + help="Choose the method to use to compute the amount of depreciation lines.\n" + " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" + " * Degressive: Calculated on basis of: Residual Value * Degressive Factor") + method_number = fields.Integer(string='Number of Depreciations', default=5, help="The number of depreciations needed to depreciate your asset") + method_period = fields.Integer(string='Period Length', default=1, help="State here the time between 2 depreciations, in months", required=True) + method_progress_factor = fields.Float('Degressive Factor', default=0.3) + method_time = fields.Selection([('number', 'Number of Entries'), ('end', 'Ending Date')], string='Time Method', required=True, default='number', + help="Choose the method to use to compute the dates and number of entries.\n" + " * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n" + " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") + method_end = fields.Date('Ending date') + prorata = fields.Boolean(string='Prorata Temporis', help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first of January') + open_asset = fields.Boolean(string='Auto-Confirm Assets', help="Check this if you want to automatically confirm the assets of this category when created by invoices.") + group_entries = fields.Boolean(string='Group Journal Entries', help="Check this if you want to group the generated entries by categories.") + type = fields.Selection([('sale', 'Sale: Revenue Recognition'), ('purchase', 'Purchase: Asset')], required=True, index=True, default='purchase') + date_first_depreciation = fields.Selection([ + ('last_day_period', 'Based on Last Day of Purchase Period'), + ('manual', 'Manual (Defaulted on Purchase Date)')], + string='Depreciation Dates', default='manual', required=True, + help='The way to compute the date of the first depreciation.\n' + ' * Based on last day of purchase period: The depreciation dates will be based on the last day of the purchase month or the purchase year (depending on the periodicity of the depreciations).\n' + ' * Based on purchase date: The depreciation dates will be based on the purchase date.') + + @api.onchange('account_asset_id') + def onchange_account_asset(self): + if self.type == "purchase": + self.account_depreciation_id = self.account_asset_id + elif self.type == "sale": + self.account_depreciation_expense_id = self.account_asset_id + + @api.onchange('type') + def onchange_type(self): + if self.type == 'sale': + self.prorata = True + self.method_period = 1 + else: + self.method_period = 12 + + @api.onchange('method_time') + def _onchange_method_time(self): + if self.method_time != 'number': + self.prorata = False + + +class AccountAssetAsset(models.Model): + _name = 'account.asset.asset' + _description = 'Asset/Revenue Recognition' + _inherit = ['mail.thread'] + + entry_count = fields.Integer(compute='_entry_count', string='# Asset Entries') + name = fields.Char(string='Asset Name', required=True, readonly=True, states={'draft': [('readonly', False)]}) + code = fields.Char(string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]}) + value = fields.Float(string='Gross Value', required=True, readonly=True, digits=0, states={'draft': [('readonly', False)]}) + currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, + default=lambda self: self.env.user.company_id.currency_id.id) + company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, + default=lambda self: self.env['res.company']._company_default_get('account.asset.asset')) + note = fields.Text() + category_id = fields.Many2one('account.asset.category', string='Category', required=True, change_default=True, readonly=True, states={'draft': [('readonly', False)]}) + date = fields.Date(string='Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today) + state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], 'Status', required=True, copy=False, default='draft', + help="When an asset is created, the status is 'Draft'.\n" + "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" + "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status.") + active = fields.Boolean(default=True) + partner_id = fields.Many2one('res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) + method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='linear', + help="Choose the method to use to compute the amount of depreciation lines.\n * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" + " * Degressive: Calculated on basis of: Residual Value * Degressive Factor") + method_number = fields.Integer(string='Number of Depreciations', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of depreciations needed to depreciate your asset") + method_period = fields.Integer(string='Number of Months in a Period', required=True, readonly=True, default=12, states={'draft': [('readonly', False)]}, + help="The amount of time between two depreciations, in months") + method_end = fields.Date(string='Ending Date', readonly=True, states={'draft': [('readonly', False)]}) + method_progress_factor = fields.Float(string='Degressive Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)]}) + value_residual = fields.Float(compute='_amount_residual', digits=0, string='Residual Value') + method_time = fields.Selection([('number', 'Number of Entries'), ('end', 'Ending Date')], string='Time Method', required=True, readonly=True, default='number', states={'draft': [('readonly', False)]}, + help="Choose the method to use to compute the dates and number of entries.\n" + " * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n" + " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") + prorata = fields.Boolean(string='Prorata Temporis', readonly=True, states={'draft': [('readonly', False)]}, + help='Indicates that the first depreciation entry for this asset have to be done from the asset date (purchase date) instead of the first January / Start date of fiscal year') + depreciation_line_ids = fields.One2many('account.asset.depreciation.line', 'asset_id', string='Depreciation Lines', readonly=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)]}) + salvage_value = fields.Float(string='Salvage Value', digits=0, readonly=True, states={'draft': [('readonly', False)]}, + help="It is the amount you plan to have that you cannot depreciate.") + invoice_id = fields.Many2one('account.move', string='Invoice', states={'draft': [('readonly', False)]}, copy=False) + type = fields.Selection(related="category_id.type", string='Type', required=True) + account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tag') + date_first_depreciation = fields.Selection([ + ('last_day_period', 'Based on Last Day of Purchase Period'), + ('manual', 'Manual')], + string='Depreciation Dates', default='manual', + readonly=True, states={'draft': [('readonly', False)]}, required=True, + help='The way to compute the date of the first depreciation.\n' + ' * Based on last day of purchase period: The depreciation dates will be based on the last day of the purchase month or the purchase year (depending on the periodicity of the depreciations).\n' + ' * Based on purchase date: The depreciation dates will be based on the purchase date.\n') + first_depreciation_manual_date = fields.Date( + string='First Depreciation Date', + readonly=True, states={'draft': [('readonly', False)]}, + help='Note that this date does not alter the computation of the first journal entry in case of prorata temporis assets. It simply changes its accounting date' + ) + + + def unlink(self): + for asset in self: + if asset.state in ['open', 'close']: + raise UserError(_('You cannot delete a document that is in %s state.') % (asset.state,)) + for depreciation_line in asset.depreciation_line_ids: + if depreciation_line.move_id: + raise UserError(_('You cannot delete a document that contains posted entries.')) + return super(AccountAssetAsset, self).unlink() + + @api.model + def _cron_generate_entries(self): + self.compute_generated_entries(datetime.today()) + + @api.model + def compute_generated_entries(self, date, asset_type=None): + # Entries generated : one by grouped category and one by asset from ungrouped category + created_move_ids = [] + type_domain = [] + if asset_type: + type_domain = [('type', '=', asset_type)] + + ungrouped_assets = self.env['account.asset.asset'].search(type_domain + [('state', '=', 'open'), ('category_id.group_entries', '=', False)]) + created_move_ids += ungrouped_assets._compute_entries(date, group_entries=False) + + for grouped_category in self.env['account.asset.category'].search(type_domain + [('group_entries', '=', True)]): + assets = self.env['account.asset.asset'].search([('state', '=', 'open'), ('category_id', '=', grouped_category.id)]) + created_move_ids += assets._compute_entries(date, group_entries=True) + return created_move_ids + + def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date): + amount = 0 + if sequence == undone_dotation_number: + amount = residual_amount + else: + if self.method == 'linear': + amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids)) + if self.prorata: + amount = amount_to_depr / self.method_number + if sequence == 1: + date = self.date + if self.method_period % 12 != 0: + month_days = calendar.monthrange(date.year, date.month)[1] + days = month_days - date.day + 1 + amount = (amount_to_depr / self.method_number) / month_days * days + else: + days = (self.company_id.compute_fiscalyear_dates(date)['date_to'] - date).days + 1 + amount = (amount_to_depr / self.method_number) / total_days * days + elif self.method == 'degressive': + amount = residual_amount * self.method_progress_factor + if self.prorata: + if sequence == 1: + date = self.date + if self.method_period % 12 != 0: + month_days = calendar.monthrange(date.year, date.month)[1] + days = month_days - date.day + 1 + amount = (residual_amount * self.method_progress_factor) / month_days * days + else: + days = (self.company_id.compute_fiscalyear_dates(date)['date_to'] - date).days + 1 + amount = (residual_amount * self.method_progress_factor) / total_days * days + return amount + + def _compute_board_undone_dotation_nb(self, depreciation_date, total_days): + undone_dotation_number = self.method_number + if self.method_time == 'end': + end_date = self.method_end + undone_dotation_number = 0 + while depreciation_date <= end_date: + depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period) + undone_dotation_number += 1 + if self.prorata: + undone_dotation_number += 1 + return undone_dotation_number + + + def compute_depreciation_board(self): + self.ensure_one() + + posted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: x.move_check).sorted(key=lambda l: l.depreciation_date) + unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: not x.move_check) + + # Remove old unposted depreciation lines. We cannot use unlink() with One2many field + commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] + + if self.value_residual != 0.0: + amount_to_depr = residual_amount = self.value_residual + + # if we already have some previous validated entries, starting date is last entry + method period + if posted_depreciation_line_ids and posted_depreciation_line_ids[-1].depreciation_date: + last_depreciation_date = fields.Date.from_string(posted_depreciation_line_ids[-1].depreciation_date) + depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) + else: + # depreciation_date computed from the purchase date + depreciation_date = self.date + if self.date_first_depreciation == 'last_day_period': + # depreciation_date = the last day of the month + depreciation_date = depreciation_date + relativedelta(day=31) + # ... or fiscalyear depending the number of period + if self.method_period == 12: + depreciation_date = depreciation_date + relativedelta(month=self.company_id.fiscalyear_last_month) + depreciation_date = depreciation_date + relativedelta(day=self.company_id.fiscalyear_last_day) + if depreciation_date < self.date: + depreciation_date = depreciation_date + relativedelta(years=1) + elif self.first_depreciation_manual_date and self.first_depreciation_manual_date != self.date: + # depreciation_date set manually from the 'first_depreciation_manual_date' field + depreciation_date = self.first_depreciation_manual_date + + total_days = (depreciation_date.year % 4) and 365 or 366 + month_day = depreciation_date.day + undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days) + + for x in range(len(posted_depreciation_line_ids), undone_dotation_number): + sequence = x + 1 + amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date) + amount = self.currency_id.round(amount) + if float_is_zero(amount, precision_rounding=self.currency_id.rounding): + continue + residual_amount -= amount + vals = { + 'amount': amount, + 'asset_id': self.id, + 'sequence': sequence, + 'name': (self.code or '') + '/' + str(sequence), + 'remaining_value': residual_amount, + 'depreciated_value': self.value - (self.salvage_value + residual_amount), + 'depreciation_date': depreciation_date, + } + commands.append((0, False, vals)) + + depreciation_date = depreciation_date + relativedelta(months=+self.method_period) + + if month_day > 28 and self.date_first_depreciation == 'manual': + max_day_in_month = calendar.monthrange(depreciation_date.year, depreciation_date.month)[1] + depreciation_date = depreciation_date.replace(day=min(max_day_in_month, month_day)) + + # datetime doesn't take into account that the number of days is not the same for each month + if not self.prorata and self.method_period % 12 != 0 and self.date_first_depreciation == 'last_day_period': + max_day_in_month = calendar.monthrange(depreciation_date.year, depreciation_date.month)[1] + depreciation_date = depreciation_date.replace(day=max_day_in_month) + + self.write({'depreciation_line_ids': commands}) + + return True + + + def validate(self): + self.write({'state': 'open'}) + fields = [ + 'method', + 'method_number', + 'method_period', + 'method_end', + 'method_progress_factor', + 'method_time', + 'salvage_value', + 'invoice_id', + ] + ref_tracked_fields = self.env['account.asset.asset'].fields_get(fields) + for asset in self: + tracked_fields = ref_tracked_fields.copy() + if asset.method == 'linear': + del(tracked_fields['method_progress_factor']) + if asset.method_time != 'end': + del(tracked_fields['method_end']) + else: + del(tracked_fields['method_number']) + dummy, tracking_value_ids = asset._message_track(tracked_fields, dict.fromkeys(fields)) + asset.message_post(subject=_('Asset created'), tracking_value_ids=tracking_value_ids) + + def _return_disposal_view(self, move_ids): + name = _('Disposal Move') + view_mode = 'form' + if len(move_ids) > 1: + name = _('Disposal Moves') + view_mode = 'tree,form' + return { + 'name': name, + 'view_type': 'form', + 'view_mode': view_mode, + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'target': 'current', + 'res_id': move_ids[0], + } + + def _get_disposal_moves(self): + move_ids = [] + for asset in self: + unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered(lambda x: not x.move_check) + if unposted_depreciation_line_ids: + old_values = { + 'method_end': asset.method_end, + 'method_number': asset.method_number, + } + + # Remove all unposted depr. lines + commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] + + # Create a new depr. line with the residual amount and post it + sequence = len(asset.depreciation_line_ids) - len(unposted_depreciation_line_ids) + 1 + today = fields.Datetime.today() + vals = { + 'amount': asset.value_residual, + 'asset_id': asset.id, + 'sequence': sequence, + 'name': (asset.code or '') + '/' + str(sequence), + 'remaining_value': 0, + 'depreciated_value': asset.value - asset.salvage_value, # the asset is completely depreciated + 'depreciation_date': today, + } + commands.append((0, False, vals)) + asset.write({'depreciation_line_ids': commands, 'method_end': today, 'method_number': sequence}) + tracked_fields = self.env['account.asset.asset'].fields_get(['method_number', 'method_end']) + changes, tracking_value_ids = asset._message_track(tracked_fields, old_values) + if changes: + asset.message_post(subject=_('Asset sold or disposed. Accounting entry awaiting for validation.'), tracking_value_ids=tracking_value_ids) + move_ids += asset.depreciation_line_ids[-1].create_move(post_move=False) + + return move_ids + + + def set_to_close(self): + move_ids = self._get_disposal_moves() + if move_ids: + return self._return_disposal_view(move_ids) + # Fallback, as if we just clicked on the smartbutton + return self.open_entries() + + def set_to_draft(self): + self.write({'state': 'draft'}) + + @api.depends('value', 'salvage_value', 'depreciation_line_ids.move_check', 'depreciation_line_ids.amount') + def _amount_residual(self): + for rec in self: + total_amount = 0.0 + for line in rec.depreciation_line_ids: + if line.move_check: + total_amount += line.amount + rec.value_residual = rec.value - total_amount - rec.salvage_value + + @api.onchange('company_id') + def onchange_company_id(self): + self.currency_id = self.company_id.currency_id.id + + + @api.onchange('date_first_depreciation') + def onchange_date_first_depreciation(self): + for record in self: + if record.date_first_depreciation == 'manual': + record.first_depreciation_manual_date = record.date + + + @api.depends('depreciation_line_ids.move_id') + def _entry_count(self): + for asset in self: + res = self.env['account.asset.depreciation.line'].search_count([('asset_id', '=', asset.id), ('move_id', '!=', False)]) + asset.entry_count = res or 0 + + + @api.constrains('prorata', 'method_time') + def _check_prorata(self): + if self.prorata and self.method_time != 'number': + raise ValidationError(_('Prorata temporis can be applied only for the "number of depreciations" time method.')) + + @api.onchange('category_id') + def onchange_category_id(self): + vals = self.onchange_category_id_values(self.category_id.id) + # We cannot use 'write' on an object that doesn't exist yet + if vals: + for k, v in vals['value'].items(): + setattr(self, k, v) + + def onchange_category_id_values(self, category_id): + if category_id: + category = self.env['account.asset.category'].browse(category_id) + return { + 'value': { + 'method': category.method, + 'method_number': category.method_number, + 'method_time': category.method_time, + 'method_period': category.method_period, + 'method_progress_factor': category.method_progress_factor, + 'method_end': category.method_end, + 'prorata': category.prorata, + 'date_first_depreciation': category.date_first_depreciation, + 'account_analytic_id': category.account_analytic_id.id, + 'analytic_tag_ids': [(6, 0, category.analytic_tag_ids.ids)], + } + } + + @api.onchange('method_time') + def onchange_method_time(self): + if self.method_time != 'number': + self.prorata = False + + + def copy_data(self, default=None): + if default is None: + default = {} + default['name'] = self.name + _(' (copy)') + return super(AccountAssetAsset, self).copy_data(default) + + + def _compute_entries(self, date, group_entries=False): + depreciation_ids = self.env['account.asset.depreciation.line'].search([ + ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date), + ('move_check', '=', False)]) + if group_entries: + return depreciation_ids.create_grouped_move() + return depreciation_ids.create_move() + + @api.model + def create(self, vals): + asset = super(AccountAssetAsset, self.with_context(mail_create_nolog=True)).create(vals) + asset.sudo().compute_depreciation_board() + return asset + + + def write(self, vals): + res = super(AccountAssetAsset, self).write(vals) + if 'depreciation_line_ids' not in vals and 'state' not in vals: + for rec in self: + rec.compute_depreciation_board() + return res + + + def open_entries(self): + move_ids = [] + for asset in self: + for depreciation_line in asset.depreciation_line_ids: + if depreciation_line.move_id: + move_ids.append(depreciation_line.move_id.id) + return { + 'name': _('Journal Entries'), + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'account.move', + 'view_id': False, + 'type': 'ir.actions.act_window', + 'domain': [('id', 'in', move_ids)], + } + + +class AccountAssetDepreciationLine(models.Model): + _name = 'account.asset.depreciation.line' + _description = 'Asset depreciation line' + + name = fields.Char(string='Depreciation Name', required=True, index=True) + sequence = fields.Integer(required=True) + asset_id = fields.Many2one('account.asset.asset', string='Asset', required=True, ondelete='cascade') + parent_state = fields.Selection(related='asset_id.state', string='State of Asset') + amount = fields.Float(string='Current Depreciation', digits=0, required=True) + remaining_value = fields.Float(string='Next Period Depreciation', digits=0, required=True) + depreciated_value = fields.Float(string='Cumulative Depreciation', required=True) + depreciation_date = fields.Date('Depreciation Date', index=True) + move_id = fields.Many2one('account.move', string='Depreciation Entry') + move_check = fields.Boolean(compute='_get_move_check', string='Linked', store=True) + move_posted_check = fields.Boolean(compute='_get_move_posted_check', string='Posted', store=True) + + + @api.depends('move_id') + def _get_move_check(self): + for line in self: + line.move_check = bool(line.move_id) + + + @api.depends('move_id.state') + def _get_move_posted_check(self): + for line in self: + line.move_posted_check = True if line.move_id and line.move_id.state == 'posted' else False + + + def create_move(self, post_move=True): + created_moves = self.env['account.move'] + for line in self: + if line.move_id: + raise UserError(_('This depreciation is already linked to a journal entry. Please post or delete it.')) + move_vals = self._prepare_move(line) + move = self.env['account.move'].create(move_vals) + line.write({'move_id': move.id, 'move_check': True}) + created_moves |= move + + if post_move and created_moves: + created_moves.filtered(lambda m: any(m.asset_depreciation_ids.mapped('asset_id.category_id.open_asset'))).post() + return [x.id for x in created_moves] + + def _prepare_move(self, line): + category_id = line.asset_id.category_id + account_analytic_id = line.asset_id.account_analytic_id + analytic_tag_ids = line.asset_id.analytic_tag_ids + depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) + company_currency = line.asset_id.company_id.currency_id + current_currency = line.asset_id.currency_id + prec = company_currency.decimal_places + amount = current_currency._convert( + line.amount, company_currency, line.asset_id.company_id, depreciation_date) + asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids)) + move_line_1 = { + 'name': asset_name, + 'account_id': category_id.account_depreciation_id.id, + 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, + 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, + 'partner_id': line.asset_id.partner_id.id, + 'analytic_account_id': account_analytic_id.id if category_id.type == 'sale' else False, + 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'sale' else False, + 'currency_id': company_currency != current_currency and current_currency.id or False, + 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0, + } + move_line_2 = { + 'name': asset_name, + 'account_id': category_id.account_depreciation_expense_id.id, + 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, + 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, + 'partner_id': line.asset_id.partner_id.id, + 'analytic_account_id': account_analytic_id.id if category_id.type == 'purchase' else False, + 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'purchase' else False, + 'currency_id': company_currency != current_currency and current_currency.id or False, + 'amount_currency': company_currency != current_currency and line.amount or 0.0, + } + move_vals = { + 'ref': line.asset_id.code, + 'date': depreciation_date or False, + 'journal_id': category_id.journal_id.id, + 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], + } + return move_vals + + def _prepare_move_grouped(self): + asset_id = self[0].asset_id + category_id = asset_id.category_id # we can suppose that all lines have the same category + account_analytic_id = asset_id.account_analytic_id + analytic_tag_ids = asset_id.analytic_tag_ids + depreciation_date = self.env.context.get('depreciation_date') or fields.Date.context_today(self) + amount = 0.0 + for line in self: + # Sum amount of all depreciation lines + company_currency = line.asset_id.company_id.currency_id + current_currency = line.asset_id.currency_id + company = line.asset_id.company_id + amount += current_currency._convert(line.amount, company_currency, company, fields.Date.today()) + + name = category_id.name + _(' (grouped)') + move_line_1 = { + 'name': name, + 'account_id': category_id.account_depreciation_id.id, + 'debit': 0.0, + 'credit': amount, + 'journal_id': category_id.journal_id.id, + 'analytic_account_id': account_analytic_id.id if category_id.type == 'sale' else False, + 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'sale' else False, + } + move_line_2 = { + 'name': name, + 'account_id': category_id.account_depreciation_expense_id.id, + 'credit': 0.0, + 'debit': amount, + 'journal_id': category_id.journal_id.id, + 'analytic_account_id': account_analytic_id.id if category_id.type == 'purchase' else False, + 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'purchase' else False, + } + move_vals = { + 'ref': category_id.name, + 'date': depreciation_date or False, + 'journal_id': category_id.journal_id.id, + 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], + } + + return move_vals + + + def create_grouped_move(self, post_move=True): + if not self.exists(): + return [] + + created_moves = self.env['account.move'] + move = self.env['account.move'].create(self._prepare_move_grouped()) + self.write({'move_id': move.id, 'move_check': True}) + created_moves |= move + + if post_move and created_moves: + self.post_lines_and_close_asset() + created_moves.post() + return [x.id for x in created_moves] + + + def post_lines_and_close_asset(self): + # we re-evaluate the assets to determine whether we can close them + for line in self: + line.log_message_when_posted() + asset = line.asset_id + if asset.currency_id.is_zero(asset.value_residual): + asset.message_post(body=_("Document closed.")) + asset.write({'state': 'close'}) + + + def log_message_when_posted(self): + def _format_message(message_description, tracked_values): + message = '' + if message_description: + message = '%s' % message_description + for name, values in tracked_values.items(): + message += '
    • %s: ' % name + message += '%s
' % values + return message + + for line in self: + if line.move_id and line.move_id.state == 'draft': + partner_name = line.asset_id.partner_id.name + currency_name = line.asset_id.currency_id.name + msg_values = {_('Currency'): currency_name, _('Amount'): line.amount} + if partner_name: + msg_values[_('Partner')] = partner_name + msg = _format_message(_('Depreciation line posted.'), msg_values) + line.asset_id.message_post(body=msg) + + + def unlink(self): + for record in self: + if record.move_check: + if record.asset_id.category_id.type == 'purchase': + msg = _("You cannot delete posted depreciation lines.") + else: + msg = _("You cannot delete posted installment lines.") + raise UserError(msg) + return super(AccountAssetDepreciationLine, self).unlink() diff --git a/models/account_invoice.py b/models/account_invoice.py new file mode 100644 index 0000000..07d1d1e --- /dev/null +++ b/models/account_invoice.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class AccountInvoice(models.Model): + _inherit = 'account.move' + + @api.model + def _refund_cleanup_lines(self, lines): + result = super(AccountInvoice, self)._refund_cleanup_lines(lines) + for i, line in enumerate(lines): + for name, field in line._fields.items(): + if name == 'asset_category_id': + result[i][2][name] = False + break + return result + + def action_cancel(self): + res = super(AccountInvoice, self).action_cancel() + self.env['account.asset.asset'].sudo().search([('invoice_id', 'in', self.ids)]).write({'active': False}) + return res + + def action_post(self): + result = super(AccountInvoice, self).action_post() + for inv in self: + context = dict(self.env.context) + context.pop('default_type', None) + for mv_line in inv.invoice_line_ids: + mv_line.with_context(context).asset_create() + return result + + +class AccountInvoiceLine(models.Model): + _inherit = 'account.move.line' + + asset_category_id = fields.Many2one('account.asset.category', string='Asset Category') + asset_start_date = fields.Date(string='Asset Start Date', compute='_get_asset_date', readonly=True, store=True) + asset_end_date = fields.Date(string='Asset End Date', compute='_get_asset_date', readonly=True, store=True) + asset_mrr = fields.Float(string='Monthly Recurring Revenue', compute='_get_asset_date', readonly=True, + digits="Account", store=True) + + @api.depends('asset_category_id', 'move_id.invoice_date') + def _get_asset_date(self): + for rec in self: + rec.asset_mrr = 0 + rec.asset_start_date = False + rec.asset_end_date = False + cat = rec.asset_category_id + if cat: + if cat.method_number == 0 or cat.method_period == 0: + raise UserError(_('The number of depreciations or the period length of ' + 'your asset category cannot be 0.')) + months = cat.method_number * cat.method_period + if rec.move_id.move_type in ['out_invoice', 'out_refund']: + rec.asset_mrr = rec.price_subtotal / months + if rec.move_id.invoice_date: + start_date = rec.move_id.invoice_date.replace(day=1) + end_date = (start_date + relativedelta(months=months, days=-1)) + rec.asset_start_date = start_date + rec.asset_end_date = end_date + + def asset_create(self): + if self.asset_category_id: + vals = { + 'name': self.name, + 'code': self.name or False, + 'category_id': self.asset_category_id.id, + 'value': self.price_subtotal, + 'partner_id': self.move_id.partner_id.id, + 'company_id': self.move_id.company_id.id, + 'currency_id': self.move_id.company_currency_id.id, + 'date': self.move_id.invoice_date, + 'invoice_id': self.move_id.id, + } + changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id']) + vals.update(changed_vals['value']) + asset = self.env['account.asset.asset'].create(vals) + if self.asset_category_id.open_asset: + asset.validate() + return True + + @api.onchange('asset_category_id') + def onchange_asset_category_id(self): + if self.move_id.move_type == 'out_invoice' and self.asset_category_id: + self.account_id = self.asset_category_id.account_asset_id.id + elif self.move_id.move_type == 'in_invoice' and self.asset_category_id: + self.account_id = self.asset_category_id.account_asset_id.id + + @api.onchange('uom_id') + def _onchange_uom_id(self): + result = super(AccountInvoiceLine, self)._onchange_uom_id() + self.onchange_asset_category_id() + return result + + @api.onchange('product_id') + def _onchange_product_id(self): + vals = super(AccountInvoiceLine, self)._onchange_product_id() + if self.product_id: + if self.move_id.move_type == 'out_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id + elif self.move_id.move_type == 'in_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id + return vals + + def _set_additional_fields(self, invoice): + if not self.asset_category_id: + if invoice.move_type == 'out_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id + elif invoice.move_type == 'in_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id + self.onchange_asset_category_id() + super(AccountInvoiceLine, self)._set_additional_fields(invoice) + + def get_invoice_line_account(self, type, product, fpos, company): + return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company) diff --git a/models/product.py b/models/product.py new file mode 100644 index 0000000..866871d --- /dev/null +++ b/models/product.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + asset_category_id = fields.Many2one('account.asset.category', string='Asset Type', company_dependent=True, ondelete="restrict") + deferred_revenue_category_id = fields.Many2one('account.asset.category', string='Deferred Revenue Type', company_dependent=True, ondelete="restrict") + + + def _get_asset_accounts(self): + res = super(ProductTemplate, self)._get_asset_accounts() + if self.asset_category_id: + res['stock_input'] = self.property_account_expense_id + if self.deferred_revenue_category_id: + res['stock_output'] = self.property_account_income_id + return res diff --git a/report/__init__.py b/report/__init__.py new file mode 100644 index 0000000..092b527 --- /dev/null +++ b/report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_asset_report diff --git a/report/account_asset_report.py b/report/account_asset_report.py new file mode 100644 index 0000000..7f5d9d3 --- /dev/null +++ b/report/account_asset_report.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, tools + + +class AssetAssetReport(models.Model): + _name = "asset.asset.report" + _description = "Assets Analysis" + _auto = False + + name = fields.Char(string='Year', required=False, readonly=True) + date = fields.Date(readonly=True) + depreciation_date = fields.Date(string='Depreciation Date', readonly=True) + asset_id = fields.Many2one('account.asset.asset', string='Asset', readonly=True) + asset_category_id = fields.Many2one('account.asset.category', string='Asset category', readonly=True) + partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) + state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], string='Status', readonly=True) + depreciation_value = fields.Float(string='Amount of Depreciation Lines', readonly=True) + installment_value = fields.Float(string='Amount of Installment Lines', readonly=True) + move_check = fields.Boolean(string='Posted', readonly=True) + installment_nbr = fields.Integer(string='Installment Count', readonly=True) + depreciation_nbr = fields.Integer(string='Depreciation Count', readonly=True) + gross_value = fields.Float(string='Gross Amount', readonly=True) + posted_value = fields.Float(string='Posted Amount', readonly=True) + unposted_value = fields.Float(string='Unposted Amount', readonly=True) + company_id = fields.Many2one('res.company', string='Company', readonly=True) + + def init(self): + tools.drop_view_if_exists(self._cr, 'asset_asset_report') + self._cr.execute(""" + create or replace view asset_asset_report as ( + select + min(dl.id) as id, + dl.name as name, + dl.depreciation_date as depreciation_date, + a.date as date, + (CASE WHEN dlmin.id = min(dl.id) + THEN a.value + ELSE 0 + END) as gross_value, + dl.amount as depreciation_value, + dl.amount as installment_value, + (CASE WHEN dl.move_check + THEN dl.amount + ELSE 0 + END) as posted_value, + (CASE WHEN NOT dl.move_check + THEN dl.amount + ELSE 0 + END) as unposted_value, + dl.asset_id as asset_id, + dl.move_check as move_check, + a.category_id as asset_category_id, + a.partner_id as partner_id, + a.state as state, + count(dl.*) as installment_nbr, + count(dl.*) as depreciation_nbr, + a.company_id as company_id + from account_asset_depreciation_line dl + left join account_asset_asset a on (dl.asset_id=a.id) + left join (select min(d.id) as id,ac.id as ac_id from account_asset_depreciation_line as d inner join account_asset_asset as ac ON (ac.id=d.asset_id) group by ac_id) as dlmin on dlmin.ac_id=a.id + where a.active is true + group by + dl.amount,dl.asset_id,dl.depreciation_date,dl.name, + a.date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id, + a.value, a.id, a.salvage_value, dlmin.id + )""") diff --git a/report/account_asset_report_views.xml b/report/account_asset_report_views.xml new file mode 100644 index 0000000..61b65c1 --- /dev/null +++ b/report/account_asset_report_views.xml @@ -0,0 +1,81 @@ + + + + + asset.asset.report.pivot + asset.asset.report + + + + + + + + + + + asset.asset.report.graph + asset.asset.report + + + + + + + + + + + asset.asset.report.search + asset.asset.report + + + + + + + + + + + + + + + + + + + + + + + + + + + + Assets Analysis + asset.asset.report + graph,pivot + + [('asset_category_id.type', '=', 'purchase')] + {} + +

+ No content +

+ From this report, you can have an overview on all depreciations. The + search bar can also be used to personalize your assets depreciation reporting. +

+
+
+ + +
diff --git a/security/account_asset_security.xml b/security/account_asset_security.xml new file mode 100644 index 0000000..2b02528 --- /dev/null +++ b/security/account_asset_security.xml @@ -0,0 +1,27 @@ + + + + + + Account Asset Category multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Account Asset multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Accountant + + + + + + + diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..f6ceaf8 --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,14 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_asset_category,account.asset.category,model_account_asset_category,account.group_account_user,1,0,0,0 +access_asset_depreciation_confirmation_wizard,access_asset_depreciation_confirmation_wizard,model_asset_depreciation_confirmation_wizard,,1,1,1,0 +access_asset_modify,access_asset_modify,model_asset_modify,,1,1,1,0 +access_account_asset_asset,account.asset.asset,model_account_asset_asset,account.group_account_user,1,0,0,0 +access_account_asset_category_manager,account.asset.category,model_account_asset_category,account.group_account_manager,1,1,1,1 +access_account_asset_asset_manager,account.asset.asset,model_account_asset_asset,account.group_account_manager,1,1,1,1 +access_account_asset_depreciation_line,account.asset.depreciation.line,model_account_asset_depreciation_line,account.group_account_user,1,0,0,0 +access_account_asset_depreciation_line_manager,account.asset.depreciation.line,model_account_asset_depreciation_line,account.group_account_manager,1,1,1,1 +access_asset_asset_report,asset.asset.report,model_asset_asset_report,account.group_account_user,1,0,0,0 +access_asset_asset_report_manager,asset.asset.report,model_asset_asset_report,account.group_account_manager,1,1,1,1 +access_account_asset_category_invoicing_payment,account.asset.category,model_account_asset_category,account.group_account_invoice,1,0,0,0 +access_account_asset_asset_invoicing_payment,account.asset.asset,model_account_asset_asset,account.group_account_invoice,1,0,1,0 +access_account_asset_depreciation_line_invoicing_payment,account.asset.depreciation.line,model_account_asset_depreciation_line,account.group_account_invoice,1,0,1,0 diff --git a/static/description/asset_types.png b/static/description/asset_types.png new file mode 100644 index 0000000..cb491b3 Binary files /dev/null and b/static/description/asset_types.png differ diff --git a/static/description/assets.gif b/static/description/assets.gif new file mode 100644 index 0000000..abfb1ae Binary files /dev/null and b/static/description/assets.gif differ diff --git a/static/description/assets.png b/static/description/assets.png new file mode 100644 index 0000000..da7605a Binary files /dev/null and b/static/description/assets.png differ diff --git a/static/description/icon.png b/static/description/icon.png new file mode 100644 index 0000000..899c853 Binary files /dev/null and b/static/description/icon.png differ diff --git a/static/description/icon.svg b/static/description/icon.svg new file mode 100644 index 0000000..7d215c3 --- /dev/null +++ b/static/description/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/description/index.html b/static/description/index.html new file mode 100644 index 0000000..c1e094a --- /dev/null +++ b/static/description/index.html @@ -0,0 +1,84 @@ +
+
+

Odoo 14 Asset Management

+
+
+ +
+
+
+

+ Manage assets owned by a company or a person. +


+

+ Keeps track of depreciation's, and creates corresponding journal entries +


+ +
+
+
+
+ +
+ +
+ +
+
+

Asset Types

+
+ +
+
+
+ +
+
+

Assets

+
+ +
+
+
+ + + +
+
+
+
+
+ +
+

If you need any help or want more features, just contact us:


+

Email: odoomates@gmail.com

+
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/static/description/odoo_mates.png b/static/description/odoo_mates.png new file mode 100644 index 0000000..8408fa3 Binary files /dev/null and b/static/description/odoo_mates.png differ diff --git a/static/src/js/account_asset.js b/static/src/js/account_asset.js new file mode 100644 index 0000000..99de318 --- /dev/null +++ b/static/src/js/account_asset.js @@ -0,0 +1,87 @@ +odoo.define('om_account_asset.widget', function(require) { +"use strict"; + +/** + * The purpose of this widget is to shows a toggle button on depreciation and + * installment lines for posted/unposted line. When clicked, it calls the method + * create_move on the object account.asset.depreciation.line. + * Note that this widget can only work on the account.asset.depreciation.line + * model as some of its fields are harcoded. + */ + +var AbstractField = require('web.AbstractField'); +var core = require('web.core'); +var registry = require('web.field_registry'); + +var _t = core._t; + +var AccountAssetWidget = AbstractField.extend({ + events: _.extend({}, AbstractField.prototype.events, { + 'click': '_onClick', + }), + description: "", + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + isSet: function () { + return true; // it should always be displayed, whatever its value + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _render: function () { + var className = ''; + var disabled = true; + var title; + if (this.recordData.move_posted_check) { + className = 'o_is_posted'; + title = _t('Posted'); + } else if (this.recordData.move_check) { + className = 'o_unposted'; + title = _t('Accounting entries waiting for manual verification'); + } else { + disabled = false; + title = _t('Unposted'); + } + var $button = $(' + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + account.asset.asset.kanban + account.asset.asset + + + + + + + + +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+
+ + + account.asset.asset.purchase.tree + account.asset.asset + + + + + + + + + + + + + + + + + account.asset.asset.search + account.asset.asset + + + + + + + + + + + + + + + + + + + + Assets + account.asset.asset + tree,kanban,form + + [('category_id.type', '=', 'purchase')] + + + + + + + + + + + Asset Types + account.asset.category + [('type', '=', 'purchase')] + tree,kanban,form + {'default_type': 'purchase'} + + + + + diff --git a/views/account_invoice_views.xml b/views/account_invoice_views.xml new file mode 100644 index 0000000..07e7b36 --- /dev/null +++ b/views/account_invoice_views.xml @@ -0,0 +1,20 @@ + + + + + account.move.supplier.form + account.move + + + + + + + + + + + + diff --git a/views/product_views.xml b/views/product_views.xml new file mode 100644 index 0000000..93c29fa --- /dev/null +++ b/views/product_views.xml @@ -0,0 +1,19 @@ + + + + + + Product Template (form) + product.template + + + + + + + + + \ No newline at end of file diff --git a/views/res_config_settings_views.xml b/views/res_config_settings_views.xml new file mode 100644 index 0000000..10e77fc --- /dev/null +++ b/views/res_config_settings_views.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..0cd5349 --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import asset_depreciation_confirmation_wizard +from . import asset_modify diff --git a/wizard/asset_depreciation_confirmation_wizard.py b/wizard/asset_depreciation_confirmation_wizard.py new file mode 100644 index 0000000..1815c83 --- /dev/null +++ b/wizard/asset_depreciation_confirmation_wizard.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + + +class AssetDepreciationConfirmationWizard(models.TransientModel): + _name = "asset.depreciation.confirmation.wizard" + _description = "asset.depreciation.confirmation.wizard" + + date = fields.Date('Account Date', required=True, help="Choose the period for which you want to automatically post the depreciation lines of running assets", default=fields.Date.context_today) + + + def asset_compute(self): + self.ensure_one() + context = self._context + created_move_ids = self.env['account.asset.asset'].compute_generated_entries(self.date, asset_type=context.get('asset_type')) + + return { + 'name': _('Created Asset Moves') if context.get('asset_type') == 'purchase' else _('Created Revenue Moves'), + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'account.move', + 'view_id': False, + 'domain': "[('id','in',[" + ','.join(str(id) for id in created_move_ids) + "])]", + 'type': 'ir.actions.act_window', + } diff --git a/wizard/asset_depreciation_confirmation_wizard_views.xml b/wizard/asset_depreciation_confirmation_wizard_views.xml new file mode 100644 index 0000000..45bd63f --- /dev/null +++ b/wizard/asset_depreciation_confirmation_wizard_views.xml @@ -0,0 +1,42 @@ + + + + + asset.depreciation.confirmation.wizard + asset.depreciation.confirmation.wizard + +
+
+

+ This wizard will post installment/depreciation lines for the selected month.
+ This will generate journal entries for all related installment lines on this period of asset/revenue recognition as well. +

+
+ + + +
+
+
+
+
+ + + Post Depreciation Lines + asset.depreciation.confirmation.wizard + tree,form + + new + {'asset_type': 'purchase'} + + + + +
diff --git a/wizard/asset_modify.py b/wizard/asset_modify.py new file mode 100644 index 0000000..0b64df4 --- /dev/null +++ b/wizard/asset_modify.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from lxml import etree + +from odoo import api, fields, models, _ +# from odoo.osv.orm import setup_modifiers + + +class AssetModify(models.TransientModel): + _name = 'asset.modify' + _description = 'Modify Asset' + + name = fields.Text(string='Reason', required=True) + method_number = fields.Integer(string='Number of Depreciations', required=True) + method_period = fields.Integer(string='Period Length') + method_end = fields.Date(string='Ending date') + asset_method_time = fields.Char(compute='_get_asset_method_time', string='Asset Method Time', readonly=True) + + + def _get_asset_method_time(self): + if self.env.context.get('active_id'): + asset = self.env['account.asset.asset'].browse(self.env.context.get('active_id')) + self.asset_method_time = asset.method_time + + # @api.model + # def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + # result = super(AssetModify, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=submenu) + # asset_id = self.env.context.get('active_id') + # active_model = self.env.context.get('active_model') + # if active_model == 'account.asset.asset' and asset_id: + # asset = self.env['account.asset.asset'].browse(asset_id) + # doc = etree.XML(result['arch']) + # if asset.method_time == 'number' and doc.xpath("//field[@name='method_end']"): + # node = doc.xpath("//field[@name='method_end']")[0] + # node.set('invisible', '1') + # setup_modifiers(node, result['fields']['method_end']) + # elif asset.method_time == 'end' and doc.xpath("//field[@name='method_number']"): + # node = doc.xpath("//field[@name='method_number']")[0] + # node.set('invisible', '1') + # setup_modifiers(node, result['fields']['method_number']) + # result['arch'] = etree.tostring(doc, encoding='unicode') + # return result + + @api.model + def default_get(self, fields): + res = super(AssetModify, self).default_get(fields) + asset_id = self.env.context.get('active_id') + asset = self.env['account.asset.asset'].browse(asset_id) + if 'name' in fields: + res.update({'name': asset.name}) + if 'method_number' in fields and asset.method_time == 'number': + res.update({'method_number': asset.method_number}) + if 'method_period' in fields: + res.update({'method_period': asset.method_period}) + if 'method_end' in fields and asset.method_time == 'end': + res.update({'method_end': asset.method_end}) + if self.env.context.get('active_id'): + active_asset = self.env['account.asset.asset'].browse(self.env.context.get('active_id')) + res['asset_method_time'] = active_asset.method_time + return res + + + def modify(self): + """ Modifies the duration of asset for calculating depreciation + and maintains the history of old values, in the chatter. + """ + asset_id = self.env.context.get('active_id', False) + asset = self.env['account.asset.asset'].browse(asset_id) + old_values = { + 'method_number': asset.method_number, + 'method_period': asset.method_period, + 'method_end': asset.method_end, + } + asset_vals = { + 'method_number': self.method_number, + 'method_period': self.method_period, + 'method_end': self.method_end, + } + asset.write(asset_vals) + asset.compute_depreciation_board() + tracked_fields = self.env['account.asset.asset'].fields_get(['method_number', 'method_period', 'method_end']) + changes, tracking_value_ids = asset._message_track(tracked_fields, old_values) + if changes: + asset.message_post(subject=_('Depreciation board modified'), body=self.name, tracking_value_ids=tracking_value_ids) + return {'type': 'ir.actions.act_window_close'} diff --git a/wizard/asset_modify_views.xml b/wizard/asset_modify_views.xml new file mode 100644 index 0000000..e99258b --- /dev/null +++ b/wizard/asset_modify_views.xml @@ -0,0 +1,40 @@ + + + + + wizard.asset.modify.form + asset.modify + +
+ + + + + + + + + + +
+
+ +
+
+ + + Modify Asset + asset.modify + ir.actions.act_window + tree,form + + new + + +