# Part of Odoo. See LICENSE file for full copyright and licensing details. import copy import io import zipfile from odoo import api, models, _ from odoo.exceptions import UserError from odoo.tools.misc import xlsxwriter class IntrastatReportCustomHandler(models.AbstractModel): _inherit = 'account.intrastat.report.handler' def _custom_options_initializer(self, report, options, previous_options): super()._custom_options_initializer(report, options, previous_options) if self.env.company.partner_id.country_id.code != 'DK': return # Override the generic xlsx export to instead export official xlsx documents xlsx_button_option = next(button_opt for button_opt in options['buttons'] if button_opt.get('action_param') == 'export_to_xlsx') xlsx_button_option['action_param'] = 'dk_export_to_xlsx' xlsx_button_option['name'] = _('XLSX (IDEP.web)') def dk_export_to_xlsx(self, options): """ Exports a xlsx document containing the required intrastat data, compliant with the official format. If filter intrastat_type is set to either 'Arrival' or 'Dispatch', then exports only the xlsx file. If both options are activated, then exports a zip file containing both arrivals and dispatches xlsx documents. """ report = self.env['account.report'].browse(options['report_id']) include_arrivals, include_dispatches = super()._determine_inclusion(options) xlsx_files = [] if include_arrivals: options_arrivals = self._dk_prepare_options(options, self._dk_get_col_order('arrivals'), 'arrivals') xlsx_files.append(( _("arrivals_%s", report.get_default_report_filename(options, 'xlsx')), self._dk_generate_xlsx_report(report, options_arrivals), )) if include_dispatches: options_dispatches = self._dk_prepare_options(options, self._dk_get_col_order('dispatches'), 'dispatches') xlsx_files.append(( _("dispatches_%s", report.get_default_report_filename(options, 'xlsx')), self._dk_generate_xlsx_report(report, options_dispatches), )) # Should never happen, just to make sure returned values below are valid if not xlsx_files: raise UserError(_("Something went wrong when generating xlsx file, please make sure to include arrivals, or dispatches, or both")) elif len(xlsx_files) == 1: return { 'file_name': xlsx_files[0][0], 'file_content': xlsx_files[0][1], 'file_type': 'xlsx', } return self._dk_build_zip_response(report, options, xlsx_files) @api.model def _dk_prepare_options(self, options, col_order, intrastat_type): """ Returns a modified copy of the options that will be necessary to generate the documents in order for them to follow the proper format """ assert intrastat_type in {'arrivals', 'dispatches'} options_copy = copy.deepcopy(options) # Necessary in case we reorder/filter the columns for both arrivals and dispatches # Replace the 'country_name' column by the 'country_code' column because the xlsx files only need the code option_col_country = next(option_col for option_col in options_copy['columns'] if option_col['expression_label'] == 'country_name') option_col_country['expression_label'] = 'country_code' options_copy['columns'] = self._dk_format_columns_options(col_order, options_copy) options_copy['intrastat_grouped'] = False # deactivate the grouping in case it's activated Move = self.env['account.move'] move_types = Move.get_outbound_types(False) if intrastat_type == 'arrivals' else Move.get_inbound_types(False) options_copy.setdefault('forced_domain', []).append(('move_id.move_type', 'in', move_types)) return options_copy @api.model def _dk_get_col_order(self, intrastat_type): """ Returns a list of columns that should be present in the official xlsx file. The provided order must be respected. Each value in the list corresponds to the expression_label of the column. The 'Reference' column is missing in these lists, and is added later because this data is the name of the lines and not a column in the options """ assert intrastat_type in {'arrivals', 'dispatches'} return { 'arrivals': ['commodity_code', 'transaction_code', 'country_code', 'weight', 'supplementary_units', 'value'], 'dispatches': ['commodity_code', 'transaction_code', 'country_code', 'weight', 'supplementary_units', 'value', 'partner_vat', 'intrastat_product_origin_country_code'] }[intrastat_type] @api.model def _dk_generate_xlsx_report(self, report, options): """ Returns a xlsx file that follows the official format, which can be found at this address https://www.dst.dk/en/Indberet/hjaelp-til-indberetning/om-idep-web/intrastat in the file examples 'Intrastat eksport/import Excel Line' """ with io.BytesIO() as output: with xlsxwriter.Workbook(output, { 'in_memory': True, 'strings_to_formulas': False, }) as workbook: self._dk_inject_report_into_xlsx_sheet(report, options, workbook, workbook.add_worksheet()) return output.getvalue() @api.model def _dk_format_columns_options(self, col_order, options): """ Reorder and filter the columns in the options based on the specified order provided in col_order. Also improves the name of the column titles such that it matches the official document. """ columns_names = { 'commodity_code': _("CN8 goods code"), 'transaction_code': _("Nature of transaction"), 'country_code': _("Partner country (Country of Destination/Country of Consignment)"), 'weight': _("Net Mass"), 'supplementary_units': _("Supplementary Units"), 'value': _("Invoice Value"), 'partner_vat': _("Partner VAT No."), 'intrastat_product_origin_country_code': _("Country of Origin"), } return [ {**col, 'name': columns_names.get(col['expression_label'], col['name'])} for col in sorted( (col for col in options['columns'] if col['expression_label'] in col_order), key=lambda col: col_order.index(col['expression_label']) ) ] @api.model def _dk_intrastat_xlsx_get_data(self, report, options): """ Returns a list of lists, each containing one row used in the xlsx. The first row is the column titles """ lines = report._get_lines(options) results = [] column_titles = [] # Add column row for column in options['columns']: column_titles.append(column.get('name', '')) # In both the arrivals and dispatches cases, the 'Reference' column is on the right of the 'value' column if column['expression_label'] == 'value': column_titles.append(_('Declarant Ref. No. (optional)')) results.append(column_titles) # Add line rows for line in lines: res_line = [] for column in line['columns']: res_line.append(str(column['no_format'] or '')) if column['expression_label'] == 'value': res_line.append(line['name']) results.append(res_line) return results @api.model def _dk_inject_report_into_xlsx_sheet(self, report, options, workbook, sheet): title_style = workbook.add_format({'font_name': 'Arial', 'bold': True, 'bottom': 2}) line_style = workbook.add_format({'font_name': 'Arial', 'font_size': 11, 'font_color': '#666666'}) sheet.set_column(0, len(options['columns']), 25) # set a length of 25 for each column results = self._dk_intrastat_xlsx_get_data(report, options) for line_offset, line in enumerate(results): for col_offset, value in enumerate(line): sheet.write(line_offset, col_offset, value, title_style if line_offset == 0 else line_style) @api.model def _dk_build_zip_response(self, report, options, xlsx_files): """ Build a ZIP response containing both arrivals and dispatches XLSX files """ with io.BytesIO() as buffer: with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED) as zipfile_obj: for filename, file_content in xlsx_files: zipfile_obj.writestr(filename, file_content) return { 'file_name': report.get_default_report_filename(options, 'zip'), 'file_content': buffer.getvalue(), 'file_type': 'zip', }