diff --git a/account_asset_management/__manifest__.py b/account_asset_management/__manifest__.py index 84c4e1bf4..61b841197 100644 --- a/account_asset_management/__manifest__.py +++ b/account_asset_management/__manifest__.py @@ -1,4 +1,4 @@ -# Copyright 2009-2018 Noviat +# Copyright 2009-2019 Noviat # Copyright 2019 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -8,6 +8,7 @@ 'license': 'AGPL-3', 'depends': [ 'account', + 'report_xlsx_helper', ], 'excludes': ['account_asset'], 'author': "Noviat,Odoo Community Association (OCA)", @@ -29,5 +30,6 @@ 'views/account_move.xml', 'views/account_move_line.xml', 'views/menuitem.xml', + 'wizard/wiz_account_asset_report.xml', ], } diff --git a/account_asset_management/models/account_asset.py b/account_asset_management/models/account_asset.py index a8acfc4ba..a052141f8 100644 --- a/account_asset_management/models/account_asset.py +++ b/account_asset_management/models/account_asset.py @@ -1064,3 +1064,68 @@ class AccountAsset(models.Model): triggers.sudo().write(recompute_vals) return (result, error_log) + + @api.model + def _xls_acquisition_fields(self): + """ + Update list in custom module to add/drop columns or change order + """ + return [ + 'account', 'name', 'code', 'date_start', 'depreciation_base', + 'salvage_value', + ] + + @api.model + def _xls_active_fields(self): + """ + Update list in custom module to add/drop columns or change order + """ + return [ + 'account', 'name', 'code', 'date_start', + 'depreciation_base', 'salvage_value', + 'period_start_value', 'period_depr', 'period_end_value', + 'period_end_depr', + 'method', 'method_number', 'prorata', 'state', + ] + + @api.model + def _xls_removal_fields(self): + """ + Update list in custom module to add/drop columns or change order + """ + return [ + 'account', 'name', 'code', 'date_remove', 'depreciation_base', + 'salvage_value', + ] + + @api.model + def _xls_asset_template(self): + """ + Template updates + + """ + return {} + + @api.model + def _xls_acquisition_template(self): + """ + Template updates + + """ + return {} + + @api.model + def _xls_active_template(self): + """ + Template updates + + """ + return {} + + @api.model + def _xls_removal_template(self): + """ + Template updates + + """ + return {} diff --git a/account_asset_management/models/account_asset_group.py b/account_asset_management/models/account_asset_group.py index d05870ce1..84d49ddb3 100644 --- a/account_asset_management/models/account_asset_group.py +++ b/account_asset_management/models/account_asset_group.py @@ -1,14 +1,15 @@ -# Copyright 2009-2018 Noviat +# Copyright 2009-2020 Noviat # Copyright 2019 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api, fields, models +from odoo.osv import expression class AccountAssetGroup(models.Model): _name = 'account.asset.group' _description = 'Asset Group' - _order = 'name' + _order = 'code, name' _parent_store = True name = fields.Char(string='Name', size=64, required=True, index=True) @@ -23,7 +24,52 @@ class AccountAssetGroup(models.Model): comodel_name='account.asset.group', string='Parent Asset Group', ondelete='restrict') + child_ids = fields.One2many( + comodel_name='account.asset.group', + inverse_name='parent_id', + string='Child Asset Groups') @api.model def _default_company_id(self): return self.env['res.company']._company_default_get('account.asset') + + @api.multi + def name_get(self): + result = [] + params = self.env.context.get('params') + list_view = params and params.get('view_type') == 'list' + short_name_len = 16 + for rec in self: + if rec.code: + full_name = rec.code + ' ' + rec.name + short_name = rec.code + else: + full_name = rec.name + if len(full_name) > short_name_len: + short_name = full_name[:16] + '...' + else: + short_name = full_name + if list_view: + name = short_name + else: + name = full_name + result.append((rec.id, name)) + return result + + @api.model + def _name_search(self, name, args=None, operator='ilike', limit=100, + name_get_uid=None): + args = args or [] + domain = [] + if name: + domain = [ + '|', + ('code', '=ilike', name.split(' ')[0] + '%'), + ('name', operator, name) + ] + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = ['&', '!'] + domain[1:] + rec_ids = self._search( + expression.AND([domain, args]), limit=limit, + access_rights_uid=name_get_uid) + return self.browse(rec_ids).name_get() diff --git a/account_asset_management/readme/DESCRIPTION.rst b/account_asset_management/readme/DESCRIPTION.rst index 403e99d48..1e7426e14 100644 --- a/account_asset_management/readme/DESCRIPTION.rst +++ b/account_asset_management/readme/DESCRIPTION.rst @@ -7,7 +7,5 @@ The full asset life-cycle is managed (from asset creation to asset removal). Assets can be created manually as well as automatically (via the creation of an accounting entry on the asset account). -Excel based reporting is available via the 'account_asset_management_xls' module. - The module contains a large number of functional enhancements compared to the standard account_asset module from Odoo. diff --git a/account_asset_management/report/__init__.py b/account_asset_management/report/__init__.py index d73a2f553..e5f30c662 100644 --- a/account_asset_management/report/__init__.py +++ b/account_asset_management/report/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import account_asset_report +from . import account_asset_report_xls diff --git a/account_asset_management/report/account_asset_report_xls.py b/account_asset_management/report/account_asset_report_xls.py new file mode 100644 index 000000000..a83037984 --- /dev/null +++ b/account_asset_management/report/account_asset_report_xls.py @@ -0,0 +1,673 @@ +# Copyright 2009-2019 Noviat +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import models, _ +from odoo.exceptions import UserError +from odoo.tools.translate import translate + +_logger = logging.getLogger(__name__) + + +IR_TRANSLATION_NAME = 'account.asset.report' + + +class AssetReportXlsx(models.AbstractModel): + _name = 'report.account_asset_management.asset_report_xls' + _inherit = 'report.report_xlsx.abstract' + + def _(self, src): + lang = self.env.context.get('lang', 'en_US') + val = translate( + self.env.cr, IR_TRANSLATION_NAME, 'report', lang, src) or src + return val + + def _get_ws_params(self, wb, data, wiz): + self._grouped_assets = self._get_assets(wiz) + s1 = self._get_acquisition_ws_params(wb, data, wiz) + s2 = self._get_active_ws_params(wb, data, wiz) + s3 = self._get_removal_ws_params(wb, data, wiz) + return [s1, s2, s3] + + def _get_asset_template(self): + + asset_template = { + 'account': { + 'header': { + 'type': 'string', + 'value': self._('Account'), + }, + 'asset': { + 'type': 'string', + 'value': self._render( + "asset.profile_id.account_asset_id.code"), + }, + 'totals': { + 'type': 'string', + 'value': self._('Totals'), + }, + 'width': 20, + }, + 'name': { + 'header': { + 'type': 'string', + 'value': self._('Name'), + }, + 'asset_group': { + 'type': 'string', + 'value': self._render("group.name or ''"), + }, + 'asset': { + 'type': 'string', + 'value': self._render("asset.name"), + }, + 'width': 40, + }, + 'code': { + 'header': { + 'type': 'string', + 'value': self._('Reference'), + }, + 'asset_group': { + 'type': 'string', + 'value': self._render("group.code or ''"), + }, + 'asset': { + 'type': 'string', + 'value': self._render("asset.code or ''"), + }, + 'width': 20, + }, + 'date_start': { + 'header': { + 'type': 'string', + 'value': self._('Asset Start Date'), + }, + 'asset': { + 'value': self._render("asset.date_start"), + 'format': self.format_tcell_date_left, + }, + 'width': 20, + }, + 'date_remove': { + 'header': { + 'type': 'string', + 'value': self._('Asset Removal Date'), + }, + 'asset': { + 'value': self._render("asset.date_remove"), + 'format': self.format_tcell_date_left, + }, + 'width': 20, + }, + 'depreciation_base': { + 'header': { + 'type': 'string', + 'value': self._('Depreciation Base'), + 'format': self.format_theader_yellow_right, + }, + 'asset_group': { + 'type': 'number', + 'value': self._render("group._depreciation_base"), + 'format': self.format_theader_blue_amount_right, + }, + 'asset': { + 'type': 'number', + 'value': self._render("asset.depreciation_base"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render('asset_total_formula'), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'salvage_value': { + 'header': { + 'type': 'string', + 'value': self._('Salvage Value'), + 'format': self.format_theader_yellow_right, + }, + 'asset_group': { + 'type': 'number', + 'value': self._render("group._salvage_value"), + 'format': self.format_theader_blue_amount_right, + }, + 'asset': { + 'type': 'number', + 'value': self._render("asset.salvage_value"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render('salvage_total_formula'), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'period_start_value': { + 'header': { + 'type': 'string', + 'value': self._('Period Start Value'), + 'format': self.format_theader_yellow_right, + }, + 'asset_group': { + 'type': 'number', + 'value': self._render("group._period_start_value"), + 'format': self.format_theader_blue_amount_right, + }, + 'asset': { + 'type': 'number', + 'value': self._render("asset._period_start_value"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render('period_start_total_formula'), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'period_depr': { + 'header': { + 'type': 'string', + 'value': self._('Period Depreciation'), + 'format': self.format_theader_yellow_right, + }, + 'asset_group': { + 'type': 'formula', + 'value': self._render("period_diff_formula"), + 'format': self.format_theader_blue_amount_right, + }, + 'asset': { + 'type': 'formula', + 'value': self._render("period_diff_formula"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render('period_diff_formula'), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'period_end_value': { + 'header': { + 'type': 'string', + 'value': self._('Period End Value'), + 'format': self.format_theader_yellow_right, + }, + 'asset_group': { + 'type': 'number', + 'value': self._render("group._period_end_value"), + 'format': self.format_theader_blue_amount_right, + }, + 'asset': { + 'type': 'number', + 'value': self._render("asset._period_end_value"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render('period_end_total_formula'), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'period_end_depr': { + 'header': { + 'type': 'string', + 'value': self._('Tot. Depreciation'), + 'format': self.format_theader_yellow_right, + }, + 'asset_group': { + 'type': 'formula', + 'value': self._render("total_depr_formula"), + 'format': self.format_theader_blue_amount_right, + }, + 'asset': { + 'type': 'formula', + 'value': self._render("total_depr_formula"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render('total_depr_formula'), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'method': { + 'header': { + 'type': 'string', + 'value': self._('Comput. Method'), + 'format': self.format_theader_yellow_center, + }, + 'asset': { + 'type': 'string', + 'value': self._render("asset.method or ''"), + 'format': self.format_tcell_center, + }, + 'width': 20, + }, + 'method_number': { + 'header': { + 'type': 'string', + 'value': self._('Number of Years'), + 'format': self.format_theader_yellow_center, + }, + 'asset': { + 'type': 'number', + 'value': self._render("asset.method_number"), + 'format': self.format_tcell_integer_center, + }, + 'width': 20, + }, + 'prorata': { + 'header': { + 'type': 'string', + 'value': self._('Prorata Temporis'), + 'format': self.format_theader_yellow_center, + }, + 'asset': { + 'type': 'boolean', + 'value': self._render("asset.prorata"), + 'format': self.format_tcell_center, + }, + 'width': 20, + }, + 'state': { + 'header': { + 'type': 'string', + 'value': self._('Status'), + 'format': self.format_theader_yellow_center, + }, + 'asset': { + 'type': 'string', + 'value': self._render("asset.state"), + 'format': self.format_tcell_center, + }, + 'width': 8, + }, + } + asset_template.update( + self.env['account.asset']._xls_asset_template()) + + return asset_template + + def _get_acquisition_ws_params(self, wb, data, wiz): + + acquisition_template = self._get_asset_template() + acquisition_template.update( + self.env['account.asset']._xls_acquisition_template()) + wl_acq = self.env['account.asset']._xls_acquisition_fields() + title = self._get_title(wiz, 'acquisition', format='normal') + title_short = self._get_title(wiz, 'acquisition', format='short') + sheet_name = title_short[:31].replace('/', '-') + + return { + 'ws_name': sheet_name, + 'generate_ws_method': '_asset_report', + 'title': title, + 'wanted_list': wl_acq, + 'col_specs': acquisition_template, + 'report_type': 'acquisition', + } + + def _get_active_ws_params(self, wb, data, wiz): + + active_template = self._get_asset_template() + active_template.update( + self.env['account.asset']._xls_active_template()) + wl_act = self.env['account.asset']._xls_active_fields() + title = self._get_title(wiz, 'active', format='normal') + title_short = self._get_title(wiz, 'active', format='short') + sheet_name = title_short[:31].replace('/', '-') + + return { + 'ws_name': sheet_name, + 'generate_ws_method': '_asset_report', + 'title': title, + 'wanted_list': wl_act, + 'col_specs': active_template, + 'report_type': 'active', + } + + def _get_removal_ws_params(self, wb, data, wiz): + + removal_template = self._get_asset_template() + removal_template.update( + self.env['account.asset']._xls_removal_template()) + wl_dsp = self.env['account.asset']._xls_removal_fields() + title = self._get_title(wiz, 'removal', format='normal') + title_short = self._get_title(wiz, 'removal', format='short') + sheet_name = title_short[:31].replace('/', '-') + + return { + 'ws_name': sheet_name, + 'generate_ws_method': '_asset_report', + 'title': title, + 'wanted_list': wl_dsp, + 'col_specs': removal_template, + 'report_type': 'removal', + } + + def _get_title(self, wiz, report, format='normal'): + + prefix = '{} - {}'.format(wiz.date_from, wiz.date_to) + if report == 'acquisition': + if format == 'normal': + title = prefix + ' : ' + _('New Acquisitions') + else: + title = 'ACQ' + elif report == 'active': + if format == 'normal': + title = prefix + ' : ' + _('Active Assets') + else: + title = 'ACT' + else: + if format == 'normal': + title = prefix + ' : ' + _('Removed Assets') + else: + title = 'DSP' + return title + + def _report_title(self, ws, row_pos, ws_params, data, wiz): + return self._write_ws_title(ws, row_pos, ws_params) + + def _empty_report(self, ws, row_pos, ws_params, data, wiz): + report = ws_params['report_type'] + if report == 'acquisition': + suffix = _('New Acquisitions') + elif report == 'active': + suffix = _('Active Assets') + else: + suffix = _('Removed Assets') + no_entries = _("No") + " " + suffix + ws.write_string(row_pos, 0, no_entries, self.format_left_bold) + + def _get_assets(self, wiz): + + dom = [('date_start', '<=', wiz.date_to), + '|', + ('date_remove', '=', False), + ('date_remove', '>=', wiz.date_from)] + + parent_group = wiz.asset_group_id + if parent_group: + + def _child_get(parent): + groups = [parent] + children = parent.child_ids + children = children.sorted(lambda r: r.code or r.name) + for child in children: + if child in groups: + raise UserError(_( + "Inconsistent reporting structure." + "\nPlease correct Asset Group '%s' (id %s)" + ) % (child.name, child.id)) + groups.extend(_child_get(child)) + return groups + + groups = _child_get(parent_group) + dom.append(('group_ids', 'in', [x.id for x in groups])) + + if not wiz.draft: + dom.append(('state', '!=', 'draft')) + self._assets = self.env['account.asset'].search(dom) + grouped_assets = {} + self._group_assets(self._assets, parent_group, grouped_assets) + return grouped_assets + + @staticmethod + def acquisition_filter(wiz, asset): + return asset.date_start >= wiz.date_from + + @staticmethod + def active_filter(wiz, asset): + return True + + @staticmethod + def removal_filter(wiz, asset): + return ( + asset.date_remove and asset.date_remove >= wiz.date_from + and asset.date_remove <= wiz.date_to) + + def _group_assets(self, assets, group, grouped_assets): + if group: + group_assets = assets.filtered(lambda r: group in r.group_ids) + else: + group_assets = assets + group_assets = group_assets.sorted( + lambda r: (r.date_start or '', r.code)) + grouped_assets[group] = {'assets': group_assets} + for child in group.child_ids: + self._group_assets(assets, child, grouped_assets[group]) + + def _create_report_entries(self, ws_params, wiz, entries, + group, group_val, error_dict): + report = ws_params['report_type'] + + def asset_filter(asset): + filter = getattr(self, '{}_filter'.format(report)) + return filter(wiz, asset) + + def _has_assets(group, group_val): + assets = group_val.get('assets') + assets = assets.filtered(asset_filter) + if assets: + return True + for child in group.child_ids: + if _has_assets(child, group_val[child]): + return True + return False + + assets = group_val.get('assets') + assets = assets.filtered(asset_filter) + + # remove empty entries + if not _has_assets(group, group_val): + return + + asset_entries = [] + group._depreciation_base = 0.0 + group._salvage_value = 0.0 + group._period_start_value = 0.0 + group._period_end_value = 0.0 + for asset in assets: + group._depreciation_base += asset.depreciation_base + group._salvage_value += asset.salvage_value + dls_all = asset.depreciation_line_ids.filtered( + lambda r: r.type == 'depreciate') + dls_all = dls_all.sorted(key=lambda r: r.line_date) + if not dls_all: + error_dict['no_table'] += asset + # period_start_value + dls = dls_all.filtered(lambda r: r.line_date <= wiz.date_from) + if dls: + value_depreciated = dls[-1].depreciated_value + dls[-1].amount + else: + value_depreciated = 0.0 + asset._period_start_value = \ + asset.depreciation_base - value_depreciated + group._period_start_value += asset._period_start_value + # period_end_value + dls = dls_all.filtered(lambda r: r.line_date <= wiz.date_to) + if dls: + value_depreciated = dls[-1].depreciated_value + dls[-1].amount + else: + value_depreciated = 0.0 + asset._period_end_value = \ + asset.depreciation_base - value_depreciated + group._period_end_value += asset._period_end_value + + asset_entries.append({'asset': asset}) + + todos = [] + for g in group.child_ids: + if _has_assets(g, group_val[g]): + todos.append(g) + + entries.append({'group': group}) + entries.extend(asset_entries) + for todo in todos: + self._create_report_entries(ws_params, wiz, entries, + todo, group_val[todo], error_dict) + + def _asset_report(self, workbook, ws, ws_params, data, wiz): + report = ws_params['report_type'] + + ws.set_portrait() + ws.fit_to_pages(1, 0) + ws.set_header(self.xls_headers['standard']) + ws.set_footer(self.xls_footers['standard']) + + wl = ws_params['wanted_list'] + if 'account' not in wl: + raise UserError(_( + "The 'account' field is a mandatory entry of the " + "'_xls_%s_fields' list !" + ) % report) + + self._set_column_width(ws, ws_params) + + row_pos = 0 + row_pos = self._report_title(ws, row_pos, ws_params, data, wiz) + + def asset_filter(asset): + filter = getattr(self, '{}_filter'.format(report)) + return filter(wiz, asset) + + assets = self._assets.filtered(asset_filter) + + if not assets: + return self._empty_report( + ws, row_pos, ws_params, data, wiz) + + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='header', + default_format=self.format_theader_yellow_left) + + ws.freeze_panes(row_pos, 0) + + row_pos_start = row_pos + depreciation_base_pos = 'depreciation_base' in wl and \ + wl.index('depreciation_base') + salvage_value_pos = 'salvage_value' in wl and \ + wl.index('salvage_value') + period_start_value_pos = 'period_start_value' in wl and \ + wl.index('period_start_value') + period_end_value_pos = 'period_end_value' in wl and \ + wl.index('period_end_value') + + entries = [] + root = wiz.asset_group_id + root_val = self._grouped_assets[root] + error_dict = { + 'no_table': self.env['account.asset'], + 'dups': self.env['account.asset'], + } + + self._create_report_entries(ws_params, wiz, entries, + root, root_val, error_dict) + + # traverse entries in reverse order to calc totals + for i, entry in enumerate(reversed(entries)): + group = entry.get('group') + if 'group' in entry: + parent = group.parent_id + for e in reversed(entries[:-i-1]): + g = e.get('group') + if g == parent: + g._depreciation_base += group._depreciation_base + g._salvage_value += group._salvage_value + g._period_start_value += group._period_start_value + g._period_end_value += group._period_end_value + continue + + processed = [] + for entry in entries: + + period_start_value_cell = period_start_value_pos \ + and self._rowcol_to_cell(row_pos, period_start_value_pos) + period_end_value_cell = period_end_value_pos \ + and self._rowcol_to_cell(row_pos, period_end_value_pos) + depreciation_base_cell = depreciation_base_pos \ + and self._rowcol_to_cell(row_pos, depreciation_base_pos) + period_diff_formula = period_end_value_cell and ( + period_start_value_cell + '-' + period_end_value_cell) + total_depr_formula = period_end_value_cell and ( + depreciation_base_cell + '-' + period_end_value_cell) + + if 'group' in entry: + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='asset_group', + render_space={ + 'group': entry['group'], + 'period_diff_formula': period_diff_formula, + 'total_depr_formula': total_depr_formula, + }, + default_format=self.format_theader_blue_left) + + else: + asset = entry['asset'] + if asset in processed: + error_dict['dups'] += asset + continue + else: + processed.append(asset) + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='asset', + render_space={ + 'asset': asset, + 'period_diff_formula': period_diff_formula, + 'total_depr_formula': total_depr_formula, + }, + default_format=self.format_tcell_left) + + asset_total_formula = depreciation_base_pos \ + and self._rowcol_to_cell(row_pos_start, depreciation_base_pos) + salvage_total_formula = salvage_value_pos \ + and self._rowcol_to_cell(row_pos_start, salvage_value_pos) + period_start_total_formula = period_start_value_pos \ + and self._rowcol_to_cell(row_pos_start, period_start_value_pos) + period_end_total_formula = period_end_value_pos \ + and self._rowcol_to_cell(row_pos_start, period_end_value_pos) + period_start_value_cell = period_start_value_pos \ + and self._rowcol_to_cell(row_pos, period_start_value_pos) + period_end_value_cell = period_end_value_pos \ + and self._rowcol_to_cell(row_pos, period_end_value_pos) + depreciation_base_cell = depreciation_base_pos \ + and self._rowcol_to_cell(row_pos, depreciation_base_pos) + period_diff_formula = period_end_value_cell and ( + period_start_value_cell + '-' + period_end_value_cell) + total_depr_formula = period_end_value_cell and ( + depreciation_base_cell + '-' + period_end_value_cell) + + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='totals', + render_space={ + 'asset_total_formula': asset_total_formula, + 'salvage_total_formula': salvage_total_formula, + 'period_start_total_formula': period_start_total_formula, + 'period_end_total_formula': period_end_total_formula, + 'period_diff_formula': period_diff_formula, + 'total_depr_formula': total_depr_formula, + }, + default_format=self.format_theader_yellow_left) + + for k in error_dict: + if error_dict[k]: + if k == 'no_table': + reason = _("Missing depreciation table") + elif k == 'dups': + reason = _("Duplicate reporting entries") + else: + reason = _("Undetermined error") + row_pos += 1 + err_msg = _("Assets to be corrected") + ': ' + err_msg += '%s' % [x[1] for x in error_dict[k].name_get()] + err_msg += ' - ' + _("Reason") + ': ' + reason + ws.write_string(row_pos, 0, err_msg, self.format_left_bold) diff --git a/account_asset_management/tests/__init__.py b/account_asset_management/tests/__init__.py index abadecb7f..8187a598e 100644 --- a/account_asset_management/tests/__init__.py +++ b/account_asset_management/tests/__init__.py @@ -1 +1,2 @@ from . import test_account_asset_management +from . import test_asset_management_xls diff --git a/account_asset_management/tests/account_asset_test_data.xml b/account_asset_management/tests/account_asset_test_data.xml index c6883f166..89e310ce8 100644 --- a/account_asset_management/tests/account_asset_test_data.xml +++ b/account_asset_management/tests/account_asset_test_data.xml @@ -16,6 +16,19 @@ + + + + Fixed Assets + FA + + + + Tangible Fixed Assets + TFA + + + @@ -52,6 +65,7 @@ PI00101 + diff --git a/account_asset_management/tests/test_asset_management_xls.py b/account_asset_management/tests/test_asset_management_xls.py new file mode 100644 index 000000000..ac4cb0f4b --- /dev/null +++ b/account_asset_management/tests/test_asset_management_xls.py @@ -0,0 +1,47 @@ +# Copyright 2009-2019 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import SavepointCase +from odoo import fields, tools +from odoo.modules.module import get_resource_path + + +class TestAssetManagementXls(SavepointCase): + + @classmethod + def _load(cls, module, *args): + tools.convert_file(cls.cr, module, + get_resource_path(module, *args), + {}, 'init', False, 'test', + cls.registry._assertion_report) + + @classmethod + def setUpClass(cls): + super(TestAssetManagementXls, cls).setUpClass() + + cls._load('account', 'test', 'account_minimal_test.xml') + cls._load('account_asset_management', 'tests', + 'account_asset_test_data.xml') + module = __name__.split('addons.')[1].split('.')[0] + cls.xls_report_name = '{}.asset_report_xls'.format(module) + cls.wiz_model = cls.env['wiz.account.asset.report'] + cls.company = cls.env.ref('base.main_company') + asset_group_id = cls.wiz_model._default_asset_group_id() + fy_dates = cls.company.compute_fiscalyear_dates( + fields.date.today()) + + wiz_vals = { + 'asset_group_id': asset_group_id, + 'date_from': fy_dates['date_from'], + 'date_to': fy_dates['date_to'], + } + cls.xls_report = cls.wiz_model.create(wiz_vals) + cls.report_action = cls.xls_report.xls_export() + + def test_01_action_xls(self): + """ Check report XLS action """ + self.assertDictContainsSubset( + {'type': 'ir.actions.report', + 'report_type': 'xlsx', + 'report_name': self.xls_report_name}, + self.report_action) diff --git a/account_asset_management/views/account_asset.xml b/account_asset_management/views/account_asset.xml index e9b7f3d4d..8bc1b8605 100644 --- a/account_asset_management/views/account_asset.xml +++ b/account_asset_management/views/account_asset.xml @@ -179,6 +179,7 @@ + @@ -198,6 +199,7 @@ + diff --git a/account_asset_management/wizard/__init__.py b/account_asset_management/wizard/__init__.py index 475dc4e72..a9ffac035 100644 --- a/account_asset_management/wizard/__init__.py +++ b/account_asset_management/wizard/__init__.py @@ -1,2 +1,3 @@ from . import account_asset_compute from . import account_asset_remove +from . import wiz_account_asset_report diff --git a/account_asset_management/wizard/wiz_account_asset_report.py b/account_asset_management/wizard/wiz_account_asset_report.py new file mode 100644 index 000000000..51df58b9c --- /dev/null +++ b/account_asset_management/wizard/wiz_account_asset_report.py @@ -0,0 +1,74 @@ +# Copyright 2009-2019 Noviat +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import unicodedata + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class WizAccountAssetReport(models.TransientModel): + _name = 'wiz.account.asset.report' + _description = 'Financial Assets report' + + asset_group_id = fields.Many2one( + comodel_name='account.asset.group', + string='Asset Group', + default=lambda self: self._default_asset_group_id()) + date_from = fields.Date( + string='Start Date', + required=True) + date_to = fields.Date( + string='End Date', + required=True) + draft = fields.Boolean( + string='Include draft assets') + company_id = fields.Many2one( + comodel_name='res.company', + string='Company', required=True, + default=lambda self: self._default_company_id()) + + @api.model + def _default_asset_group_id(self): + return self.env['account.asset.group'].search([ + ('parent_id', '=', False)], limit=1).id + + @api.model + def _default_company_id(self): + return self.env.user.company_id + + @api.onchange('company_id') + def _onchange_company_id(self): + fy_dates = self.company_id.compute_fiscalyear_dates( + fields.date.today()) + self.date_from = fy_dates['date_from'] + self.date_to = fy_dates['date_to'] + + @api.multi + @api.constrains('date_from', 'date_to') + def _check_dates(self): + for wiz in self: + if wiz.date_to <= wiz.date_from: + raise UserError(_( + "The Start Date must precede the Ending Date.")) + + @api.multi + def xls_export(self): + self.ensure_one() + report_name = 'account_asset_management.asset_report_xls' + if self.asset_group_id: + prefix = unicodedata.normalize( + 'NFKD', self.asset_group_id.name + ).encode('ascii', 'ignore').decode('ascii') + prefix = ''.join(x for x in prefix if x.isalnum()) + report_file = '{}_asset_report'.format(prefix) + else: + report_file = 'asset_report' + report = { + 'type': 'ir.actions.report', + 'report_type': 'xlsx', + 'report_name': report_name, + 'context': dict(self.env.context, report_file=report_file), + 'data': {'dynamic_report': True}, + } + return report diff --git a/account_asset_management/wizard/wiz_account_asset_report.xml b/account_asset_management/wizard/wiz_account_asset_report.xml new file mode 100644 index 000000000..180c450d1 --- /dev/null +++ b/account_asset_management/wizard/wiz_account_asset_report.xml @@ -0,0 +1,46 @@ + + + + + Financial Assets report + wiz.account.asset.report + +
+ + + + + + + +
+
+
+
+
+ + + Financial Assets report + ir.actions.act_window + wiz.account.asset.report + form + form + + new + + + + + + +
diff --git a/oca_dependencies.txt b/oca_dependencies.txt index 2751ff926..c3abe3c25 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -1,3 +1,4 @@ queue +reporting-engine server-ux server-tools