import io import xlsxwriter from odoo import http, _ from odoo.http import request class AssetTemplateController(http.Controller): @http.route('/web/binary/download_asset_template/', type='http', auth='user') def download_template(self, company_id): """ Handles the HTTP request and serves the generated Excel file for download. """ content = self._generate_asset_import_template(company_id) return request.make_response( content, headers=[ ('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'), ('Content-Disposition', 'attachment; filename="asset_import_template.xlsx"'), ] ) def _get_technical_headers(self): """ Defines and returns the common functional and technical headers. """ return [item[0] for item in self._get_instructions_data()] def _get_localized_example_data(self, current_company_id, env): """ Dynamically fetches localized example values for accounts and journal based on the current company and generates multiple example rows. """ current_company = env['res.company'].browse(current_company_id) def get_account_name_by_type(account_types): for acc_type in account_types: if account := env['account.account'].with_company(current_company).search([ *env['account.account']._check_company_domain(current_company), ('account_type', '=', acc_type), ], limit=1): return account.display_name if account.display_name.startswith(account.code) else f"{account.code} {account.display_name}" return '' def get_journal_name_by_type(journal_type): journal = env['account.journal'].with_company(current_company).search([ *env['account.journal']._check_company_domain(current_company), ('type', '=', journal_type), ], limit=1) return journal.name or '' asset_account_example = get_account_name_by_type(['asset_fixed', 'asset_non_current']) depreciation_account_example = get_account_name_by_type(['asset_fixed', 'asset_non_current']) expense_account_example = get_account_name_by_type(['expense_depreciation', 'expense']) journal_example = get_journal_name_by_type('general') all_example_rows = [ [ 'Computer 1', '800.00', '01-01-2025', '', 'Straight Line', '4', 'Years', '', 'No Prorata', '01-01-2025', '200.00', '0.00', current_company.name, asset_account_example, depreciation_account_example, expense_account_example, journal_example, ], [ 'Computer 2', '25000.00', '03-15-2025', '', 'Declining', '5', 'Years', '2', 'Based on days per period', '03-15-2025', '0.00', '2000.00', current_company.name, asset_account_example, depreciation_account_example, expense_account_example, journal_example, ], [ 'Machine A', '15000.00', '09-01-2024', '', 'Straight Line', '10', 'Years', '', 'Constant Periods', '09-01-2024', '1500.00', '500.00', current_company.name, asset_account_example, depreciation_account_example, expense_account_example, journal_example, ], [ 'Machine B', '100000.00', '06-20-2025', '', 'Straight Line', '60', 'Months', '', 'No Prorata', '06-20-2025', '10000.00', '10000.00', current_company.name, asset_account_example, depreciation_account_example, expense_account_example, journal_example, ] ] return all_example_rows def _get_instructions_data(self): """Defines and returns the data for the 'Instructions' sheet.""" return [ ('name', _('Asset Name'), True, _('Mandatory column. This is the name of the asset.')), ('original_value', _('Original Value'), True, _('The amount will be considered in company currency.')), ('acquisition_date', _('Acquisition Date'), True, _('e.g. 01-27-2025 (format: MM-DD-YYYY)')), ('asset_group_id', _('Asset Group'), False, _('Optional. The name or external ID of the asset group. Must exist in Odoo (e.g., Office Equipment, Vehicles, Machinery).')), ('method', _('Method'), True, _('e.g. Straight Line, Declining Balance. Determines how depreciation is calculated.')), ('method_number', _('Duration'), True, _('e.g. 3 for 3 years, or 12 for 12 months. It must be an integer representing the total number of periods.')), ('method_period', _('Months/Years'), True, _('Either "Months" or "Years" (case-insensitive). Specifies the unit of duration.')), ('method_progress_factor', _('Declining Factor'), False, _('Only applicable for Declining Balance method (e.g., 2 for double declining). Ignored for Straight Line.')), ('prorata_computation_type', _('Computation'), True, _('e.g. No Prorata, Constant Periods, Based on days per period. This determines how the first depreciation entry is calculated.')), ('prorata_date', _('Prorata Date'), True, _('Start date of the depreciation period. Should be in MM-DD-YYYY format. If left blank, it defaults to the acquisition date.')), ('already_depreciated_amount_import', _('Depreciated Amount'), False, _('The total amount of depreciation already recorded for the asset before import. This amount will be considered in company currency.')), ('salvage_value', _('Not Depreciable Value'), False, _('The estimated residual value of the asset at the end of its useful life. This amount will not be depreciated. Considered in company currency.')), ('company_id', _('Company'), True, _("Must match the selected company during import. Use the company's display name.")), ('account_asset_id', _('Fixed Asset Account'), True, _('The balance sheet account for the asset itself (e.g., "151000 Fixed Asset"). Must exist in Odoo and be of "Fixed Asset" or "Non-current Assets" type.')), ('account_depreciation_id', _('Depreciation Account'), True, _('The accumulated depreciation account (contra-asset account). Must exist in Odoo and be of "Fixed Asset" or "Non-current Assets" type.')), ('account_depreciation_expense_id', _('Expense Account'), True, _('The expense account for posting periodic depreciation entries (e.g., "630000 Depreciation Expenses"). Must exist in Odoo and be of "Depreciation" or "Expense" type.')), ('journal_id', _('Journal'), True, _('The code or name of the associated journal in Odoo for depreciation entries (e.g., MISC - Miscellaneous Operations, INV - Customer Invoices, BILL - Vendor Bills).')), ] def _write_asset_sheet(self, workbook, headers, all_example_rows): """ Creates and populates the 'Assets' sheet with headers, multiple example data rows, and comments. """ worksheet_assets = workbook.add_worksheet('Assets') header_format = workbook.add_format({'bold': True}) for col, header in enumerate(headers): worksheet_assets.write(0, col, header, header_format) for row_idx, example_row in enumerate(all_example_rows, start=1): for col, example_value in enumerate(example_row): worksheet_assets.write(row_idx, col, example_value) for col, header in enumerate(headers): max_len = len(str(header)) for example_row in all_example_rows: if col < len(example_row): max_len = max(max_len, len(str(example_row[col]))) worksheet_assets.set_column(col, col, max_len + 4) def _write_instructions_sheet(self, workbook, instructions_data): """ Creates and populates the 'Instructions' sheet with column details and notes. """ worksheet_instructions = workbook.add_worksheet('Instructions') instruction_headers = ['Column Name', 'Column Functional Name', 'Comments / Notes'] instruction_header_format = workbook.add_format({'bold': True}) for col_idx, header_text in enumerate(instruction_headers): worksheet_instructions.write(0, col_idx, header_text, instruction_header_format) for row_idx, (field, label, mandatory, help_text) in enumerate(instructions_data): display_label = f"{label}*" if mandatory else label worksheet_instructions.write(row_idx + 1, 0, field) worksheet_instructions.write(row_idx + 1, 1, display_label) worksheet_instructions.write(row_idx + 1, 2, help_text) for col_idx, header_text in enumerate(instruction_headers): max_len = len(header_text) for row_data in instructions_data: current_len = len(str(row_data[col_idx])) if current_len > max_len: max_len = current_len worksheet_instructions.set_column(col_idx, col_idx, max_len + 4) def _generate_asset_import_template(self, company_id): """ Generates an Excel file with: - Sheet 1: Asset import template (headers, example, comments) - Sheet 2: Instructions (column details, notes) """ technical_headers = self._get_technical_headers() all_example_rows = self._get_localized_example_data(company_id, request.env) instructions_data = self._get_instructions_data() output = io.BytesIO() workbook = xlsxwriter.Workbook(output, {'in_memory': True}) self._write_asset_sheet(workbook, technical_headers, all_example_rows) self._write_instructions_sheet(workbook, instructions_data) workbook.close() return output.getvalue()