From cf30d5a77fa94a54d0d7a28220c91590e0dc4272 Mon Sep 17 00:00:00 2001 From: Kitti U Date: Mon, 24 Aug 2020 17:57:03 +0700 Subject: [PATCH] [IMP] : black, isort, prettier --- excel_import_export/__manifest__.py | 45 +-- excel_import_export/controllers/main.py | 53 +-- excel_import_export/models/common.py | 186 ++++----- excel_import_export/models/ir_report.py | 29 +- excel_import_export/models/styles.py | 49 ++- excel_import_export/models/xlsx_export.py | 147 +++---- excel_import_export/models/xlsx_import.py | 201 +++++----- excel_import_export/models/xlsx_report.py | 69 ++-- excel_import_export/models/xlsx_template.py | 371 ++++++++---------- .../src/js/report/action_manager_report.js | 65 +-- .../views/webclient_templates.xml | 7 +- excel_import_export/views/xlsx_report.xml | 44 ++- .../views/xlsx_template_view.xml | 314 ++++++++++----- .../wizard/export_xlsx_wizard.py | 86 ++-- .../wizard/export_xlsx_wizard.xml | 65 +-- .../wizard/import_xlsx_wizard.py | 165 ++++---- .../wizard/import_xlsx_wizard.xml | 55 ++- 17 files changed, 1037 insertions(+), 914 deletions(-) diff --git a/excel_import_export/__manifest__.py b/excel_import_export/__manifest__.py index 5a942f070..212408875 100644 --- a/excel_import_export/__manifest__.py +++ b/excel_import_export/__manifest__.py @@ -2,29 +2,24 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) { - 'name': 'Excel Import/Export/Report', - 'summary': 'Base module for developing Excel import/export/report', - 'version': '12.0.1.0.4', - 'author': 'Ecosoft,Odoo Community Association (OCA)', - 'license': 'AGPL-3', - 'website': 'https://github.com/OCA/server-tools/', - 'category': 'Tools', - 'depends': ['mail'], - 'external_dependencies': { - 'python': [ - 'xlrd', - 'xlwt', - 'openpyxl', - ], - }, - 'data': ['security/ir.model.access.csv', - 'wizard/export_xlsx_wizard.xml', - 'wizard/import_xlsx_wizard.xml', - 'views/xlsx_template_view.xml', - 'views/xlsx_report.xml', - 'views/webclient_templates.xml', - ], - 'installable': True, - 'development_status': 'beta', - 'maintainers': ['kittiu'], + "name": "Excel Import/Export/Report", + "summary": "Base module for developing Excel import/export/report", + "version": "12.0.1.0.4", + "author": "Ecosoft,Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/server-tools/", + "category": "Tools", + "depends": ["mail"], + "external_dependencies": {"python": ["xlrd", "xlwt", "openpyxl",],}, + "data": [ + "security/ir.model.access.csv", + "wizard/export_xlsx_wizard.xml", + "wizard/import_xlsx_wizard.xml", + "views/xlsx_template_view.xml", + "views/xlsx_report.xml", + "views/webclient_templates.xml", + ], + "installable": True, + "development_status": "beta", + "maintainers": ["kittiu"], } diff --git a/excel_import_export/controllers/main.py b/excel_import_export/controllers/main.py index ea9effefe..2382c071c 100644 --- a/excel_import_export/controllers/main.py +++ b/excel_import_export/controllers/main.py @@ -1,52 +1,53 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import json import base64 +import json import time -from odoo.addons.web.controllers import main as report -from odoo.http import content_disposition, route, request + +from odoo.http import content_disposition, request, route from odoo.tools.safe_eval import safe_eval +from odoo.addons.web.controllers import main as report + class ReportController(report.ReportController): - @route() def report_routes(self, reportname, docids=None, converter=None, **data): - if converter == 'excel': - report = request.env['ir.actions.report']._get_report_from_name( - reportname) + if converter == "excel": + report = request.env["ir.actions.report"]._get_report_from_name(reportname) context = dict(request.env.context) if docids: - docids = [int(i) for i in docids.split(',')] - if data.get('options'): - data.update(json.loads(data.pop('options'))) - if data.get('context'): + docids = [int(i) for i in docids.split(",")] + if data.get("options"): + data.update(json.loads(data.pop("options"))) + if data.get("context"): # Ignore 'lang' here, because the context in data is the one # from the webclient *but* if the user explicitely wants to # change the lang, this mechanism overwrites it. - data['context'] = json.loads(data['context']) - if data['context'].get('lang'): - del data['context']['lang'] - context.update(data['context']) + data["context"] = json.loads(data["context"]) + if data["context"].get("lang"): + del data["context"]["lang"] + context.update(data["context"]) excel, report_name = report.with_context(context).render_excel( docids, data=data ) excel = base64.decodestring(excel) if report.print_report_name and not len(docids) > 1: obj = request.env[report.model].browse(docids[0]) - file_ext = report_name.split('.')[-1:].pop() - report_name = safe_eval(report.print_report_name, - {'object': obj, 'time': time}) - report_name = '%s.%s' % (report_name, file_ext) - excelhttpheaders = [ - ('Content-Type', 'application/vnd.openxmlformats-' - 'officedocument.spreadsheetml.sheet'), - ('Content-Length', len(excel)), - ( - 'Content-Disposition', - content_disposition(report_name) + file_ext = report_name.split(".")[-1:].pop() + report_name = safe_eval( + report.print_report_name, {"object": obj, "time": time} ) + report_name = "{}.{}".format(report_name, file_ext) + excelhttpheaders = [ + ( + "Content-Type", + "application/vnd.openxmlformats-" + "officedocument.spreadsheetml.sheet", + ), + ("Content-Length", len(excel)), + ("Content-Disposition", content_disposition(report_name)), ] return request.make_response(excel, headers=excelhttpheaders) return super(ReportController, self).report_routes( diff --git a/excel_import_export/models/common.py b/excel_import_export/models/common.py index 5e70f0d95..e0314904f 100644 --- a/excel_import_export/models/common.py +++ b/excel_import_export/models/common.py @@ -1,20 +1,21 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import re -import uuid -import csv import base64 -import string +import csv import itertools import logging -from datetime import datetime as dt +import re +import string +import uuid from ast import literal_eval -from dateutil.parser import parse +from datetime import datetime as dt from io import StringIO -from odoo.exceptions import ValidationError -from odoo import _ +from dateutil.parser import parse + +from odoo import _ +from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) try: @@ -26,40 +27,40 @@ except ImportError: def adjust_cell_formula(value, k): """ Cell formula, i.e., if i=5, val=?(A11)+?(B12) -> val=A16+B17 """ if isinstance(value, str): - for i in range(value.count('?(')): - if value and '?(' in value and ')' in value: - i = value.index('?(') - j = value.index(')', i) - val = value[i + 2:j] + for i in range(value.count("?(")): + if value and "?(" in value and ")" in value: + i = value.index("?(") + j = value.index(")", i) + val = value[i + 2 : j] col, row = split_row_col(val) - new_val = '%s%s' % (col, row+k) - value = value.replace('?(%s)' % val, new_val) + new_val = "{}{}".format(col, row + k) + value = value.replace("?(%s)" % val, new_val) return value def get_field_aggregation(field): """ i..e, 'field@{sum}' """ - if field and '@{' in field and '}' in field: - i = field.index('@{') - j = field.index('}', i) - cond = field[i + 2:j] + if field and "@{" in field and "}" in field: + i = field.index("@{") + j = field.index("}", i) + cond = field[i + 2 : j] try: - if cond or cond == '': + if cond or cond == "": return (field[:i], cond) except Exception: - return (field.replace('@{%s}' % cond, ''), False) + return (field.replace("@{%s}" % cond, ""), False) return (field, False) def get_field_condition(field): """ i..e, 'field${value > 0 and value or False}' """ - if field and '${' in field and '}' in field: - i = field.index('${') - j = field.index('}', i) - cond = field[i + 2:j] + if field and "${" in field and "}" in field: + i = field.index("${") + j = field.index("}", i) + cond = field[i + 2 : j] try: - if cond or cond == '': - return (field.replace('${%s}' % cond, ''), cond) + if cond or cond == "": + return (field.replace("${%s}" % cond, ""), cond) except Exception: return (field, False) return (field, False) @@ -74,13 +75,13 @@ def get_field_style(field): - number = true, false i.e., 'field#{font=bold;fill=red;align=center;style=number}' """ - if field and '#{' in field and '}' in field: - i = field.index('#{') - j = field.index('}', i) - cond = field[i + 2:j] + if field and "#{" in field and "}" in field: + i = field.index("#{") + j = field.index("}", i) + cond = field[i + 2 : j] try: - if cond or cond == '': - return (field.replace('#{%s}' % cond, ''), cond) + if cond or cond == "": + return (field.replace("#{%s}" % cond, ""), cond) except Exception: return (field, False) return (field, False) @@ -88,39 +89,40 @@ def get_field_style(field): def get_field_style_cond(field): """ i..e, 'field#?object.partner_id and #{font=bold} or #{}?' """ - if field and '#?' in field and '?' in field: - i = field.index('#?') - j = field.index('?', i+2) - cond = field[i + 2:j] + if field and "#?" in field and "?" in field: + i = field.index("#?") + j = field.index("?", i + 2) + cond = field[i + 2 : j] try: - if cond or cond == '': - return (field.replace('#?%s?' % cond, ''), cond) + if cond or cond == "": + return (field.replace("#?%s?" % cond, ""), cond) except Exception: return (field, False) return (field, False) def fill_cell_style(field, field_style, styles): - field_styles = field_style.split(';') + field_styles = field_style.split(";") for f in field_styles: - (key, value) = f.split('=') + (key, value) = f.split("=") if key not in styles.keys(): - raise ValidationError(_('Invalid style type %s' % key)) + raise ValidationError(_("Invalid style type %s" % key)) if value.lower() not in styles[key].keys(): raise ValidationError( - _('Invalid value %s for style type %s' % (value, key))) + _("Invalid value {} for style type {}".format(value, key)) + ) cell_style = styles[key][value] - if key == 'font': + if key == "font": field.font = cell_style - if key == 'fill': + if key == "fill": field.fill = cell_style - if key == 'align': + if key == "align": field.alignment = cell_style - if key == 'style': - if value == 'text': + if key == "style": + if value == "text": try: # In case value can't be encoded as utf, we do normal str() - field.value = field.value.encode('utf-8') + field.value = field.value.encode("utf-8") except Exception: field.value = str(field.value) field.number_format = cell_style @@ -128,10 +130,10 @@ def fill_cell_style(field, field_style, styles): def get_line_max(line_field): """ i.e., line_field = line_ids[100], max = 100 else 0 """ - if line_field and '[' in line_field and ']' in line_field: - i = line_field.index('[') - j = line_field.index(']') - max_str = line_field[i + 1:j] + if line_field and "[" in line_field and "]" in line_field: + i = line_field.index("[") + j = line_field.index("]") + max_str = line_field[i + 1 : j] try: if len(max_str) > 0: return (line_field[:i], int(max_str)) @@ -144,10 +146,10 @@ def get_line_max(line_field): def get_groupby(line_field): """i.e., line_field = line_ids["a_id, b_id"], groupby = ["a_id", "b_id"]""" - if line_field and '[' in line_field and ']' in line_field: - i = line_field.index('[') - j = line_field.index(']') - groupby = literal_eval(line_field[i:j+1]) + if line_field and "[" in line_field and "]" in line_field: + i = line_field.index("[") + j = line_field.index("]") + groupby = literal_eval(line_field[i : j + 1]) return groupby return False @@ -155,7 +157,7 @@ def get_groupby(line_field): def split_row_col(pos): match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) if not match: - raise ValidationError(_('Position %s is not valid') % pos) + raise ValidationError(_("Position %s is not valid") % pos) col, row = match.groups() return col, int(row) @@ -199,9 +201,9 @@ def isinteger(input_val): def isdatetime(input_val): try: if len(input_val) == 10: - dt.strptime(input_val, '%Y-%m-%d') + dt.strptime(input_val, "%Y-%m-%d") elif len(input_val) == 19: - dt.strptime(input_val, '%Y-%m-%d %H:%M:%S') + dt.strptime(input_val, "%Y-%m-%d %H:%M:%S") else: return False return True @@ -211,14 +213,14 @@ def isdatetime(input_val): def str_to_number(input_val): if isinstance(input_val, str): - if ' ' not in input_val: + if " " not in input_val: if isdatetime(input_val): return parse(input_val) elif isinteger(input_val): - if not (len(input_val) > 1 and input_val[:1] == '0'): + if not (len(input_val) > 1 and input_val[:1] == "0"): return int(input_val) elif isfloat(input_val): - if not (input_val.find(".") > 2 and input_val[:1] == '0'): + if not (input_val.find(".") > 2 and input_val[:1] == "0"): return float(input_val) return input_val @@ -239,25 +241,28 @@ def csv_from_excel(excel_content, delimiter, quote): for x in sh.row_values(rownum): if quoting == csv.QUOTE_NONE and delimiter in x: raise ValidationError( - _('Template with CSV Quoting = False, data must not ' - 'contain the same char as delimiter -> "%s"') % - delimiter) + _( + "Template with CSV Quoting = False, data must not " + 'contain the same char as delimiter -> "%s"' + ) + % delimiter + ) row.append(x) wr.writerow(row) content.seek(0) # Set index to 0, and start reading - out_file = base64.b64encode(content.getvalue().encode('utf-8')) + out_file = base64.b64encode(content.getvalue().encode("utf-8")) return out_file def pos2idx(pos): match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) if not match: - raise ValidationError(_('Position %s is not valid') % (pos, )) + raise ValidationError(_("Position %s is not valid") % (pos,)) col, row = match.groups() col_num = 0 for c in col: if c in string.ascii_letters: - col_num = col_num * 26 + (ord(c.upper()) - ord('A')) + 1 + col_num = col_num * 26 + (ord(c.upper()) - ord("A")) + 1 return (int(row) - 1, col_num - 1) @@ -266,28 +271,31 @@ def _get_cell_value(cell, field_type=False): if not know, just get value as is """ value = False datemode = 0 # From book.datemode, but we fix it for simplicity - if field_type in ['date', 'datetime']: - ctype = xlrd.sheet.ctype_text.get(cell.ctype, 'unknown type') - if ctype in ('xldate', 'number'): + if field_type in ["date", "datetime"]: + ctype = xlrd.sheet.ctype_text.get(cell.ctype, "unknown type") + if ctype in ("xldate", "number"): is_datetime = cell.value % 1 != 0.0 time_tuple = xlrd.xldate_as_tuple(cell.value, datemode) date = dt(*time_tuple) - value = (date.strftime("%Y-%m-%d %H:%M:%S") - if is_datetime else date.strftime("%Y-%m-%d")) + value = ( + date.strftime("%Y-%m-%d %H:%M:%S") + if is_datetime + else date.strftime("%Y-%m-%d") + ) else: value = cell.value - elif field_type in ['integer', 'float']: - value_str = str(cell.value).strip().replace(',', '') + elif field_type in ["integer", "float"]: + value_str = str(cell.value).strip().replace(",", "") if len(value_str) == 0: - value = '' - elif value_str.replace('.', '', 1).isdigit(): # Is number - if field_type == 'integer': + value = "" + elif value_str.replace(".", "", 1).isdigit(): # Is number + if field_type == "integer": value = int(float(value_str)) - elif field_type == 'float': + elif field_type == "float": value = float(value_str) else: # Is string, no conversion value = value_str - elif field_type in ['many2one']: + elif field_type in ["many2one"]: # If number, change to string if isinstance(cell.value, (int, float, complex)): value = str(cell.value) @@ -297,38 +305,38 @@ def _get_cell_value(cell, field_type=False): value = cell.value # If string, cleanup if isinstance(value, str): - if value[-2:] == '.0': + if value[-2:] == ".0": value = value[:-2] # Except boolean, when no value, we should return as '' - if field_type not in ['boolean']: + if field_type not in ["boolean"]: if not value: - value = '' + value = "" return value def _add_column(column_name, column_value, file_txt): i = 0 txt_lines = [] - for line in file_txt.split('\n'): + for line in file_txt.split("\n"): if line and i == 0: line = '"' + str(column_name) + '",' + line elif line: line = '"' + str(column_value) + '",' + line txt_lines.append(line) i += 1 - file_txt = '\n'.join(txt_lines) + file_txt = "\n".join(txt_lines) return file_txt def _add_id_column(file_txt): i = 0 txt_lines = [] - for line in file_txt.split('\n'): + for line in file_txt.split("\n"): if line and i == 0: line = '"id",' + line elif line: - line = '%s.%s' % ('xls', uuid.uuid4()) + ',' + line + line = "{}.{}".format("xls", uuid.uuid4()) + "," + line txt_lines.append(line) i += 1 - file_txt = '\n'.join(txt_lines) + file_txt = "\n".join(txt_lines) return file_txt diff --git a/excel_import_export/models/ir_report.py b/excel_import_export/models/ir_report.py index 656096f9c..8b5bf26cd 100644 --- a/excel_import_export/models/ir_report.py +++ b/excel_import_export/models/ir_report.py @@ -1,7 +1,7 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import api, fields, models, _ +from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -13,15 +13,18 @@ class ReportAction(models.Model): @api.model def render_excel(self, docids, data): if len(docids) != 1: - raise UserError( - _('Only one id is allowed for excel_import_export')) - xlsx_template = self.env['xlsx.template'].search( - [('fname', '=', self.report_name), ('res_model', '=', self.model)]) + raise UserError(_("Only one id is allowed for excel_import_export")) + xlsx_template = self.env["xlsx.template"].search( + [("fname", "=", self.report_name), ("res_model", "=", self.model)] + ) if not xlsx_template or len(xlsx_template) != 1: raise UserError( - _("Template %s on model %s is not unique!" % - (self.report_name, self.model))) - Export = self.env['xlsx.export'] + _( + "Template %s on model %s is not unique!" + % (self.report_name, self.model) + ) + ) + Export = self.env["xlsx.export"] return Export.export_xlsx(xlsx_template, self.model, docids[0]) @api.model @@ -29,11 +32,11 @@ class ReportAction(models.Model): res = super(ReportAction, self)._get_report_from_name(report_name) if res: return res - report_obj = self.env['ir.actions.report'] - qwebtypes = ['excel'] + report_obj = self.env["ir.actions.report"] + qwebtypes = ["excel"] conditions = [ - ('report_type', 'in', qwebtypes), - ('report_name', '=', report_name), + ("report_type", "in", qwebtypes), + ("report_name", "=", report_name), ] - context = self.env['res.users'].context_get() + context = self.env["res.users"].context_get() return report_obj.with_context(context).search(conditions, limit=1) diff --git a/excel_import_export/models/styles.py b/excel_import_export/models/styles.py index 9738a3c8a..2c753f345 100644 --- a/excel_import_export/models/styles.py +++ b/excel_import_export/models/styles.py @@ -1,48 +1,47 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import models, api import logging +from odoo import api, models + _logger = logging.getLogger(__name__) try: from openpyxl.styles import colors, PatternFill, Alignment, Font except ImportError: - _logger.debug( - 'Cannot import "openpyxl". Please make sure it is installed.') + _logger.debug('Cannot import "openpyxl". Please make sure it is installed.') class XLSXStyles(models.AbstractModel): - _name = 'xlsx.styles' - _description = 'Available styles for excel' + _name = "xlsx.styles" + _description = "Available styles for excel" @api.model def get_openpyxl_styles(self): """ List all syles that can be used with styleing directive #{...} """ return { - 'font': { - 'bold': Font(name="Arial", size=10, bold=True), - 'bold_red': Font(name="Arial", size=10, - color=colors.RED, bold=True), + "font": { + "bold": Font(name="Arial", size=10, bold=True), + "bold_red": Font(name="Arial", size=10, color=colors.RED, bold=True), }, - 'fill': { - 'red': PatternFill("solid", fgColor="FF0000"), - 'grey': PatternFill("solid", fgColor="DDDDDD"), - 'yellow': PatternFill("solid", fgColor="FFFCB7"), - 'blue': PatternFill("solid", fgColor="9BF3FF"), - 'green': PatternFill("solid", fgColor="B0FF99"), + "fill": { + "red": PatternFill("solid", fgColor="FF0000"), + "grey": PatternFill("solid", fgColor="DDDDDD"), + "yellow": PatternFill("solid", fgColor="FFFCB7"), + "blue": PatternFill("solid", fgColor="9BF3FF"), + "green": PatternFill("solid", fgColor="B0FF99"), }, - 'align': { - 'left': Alignment(horizontal='left'), - 'center': Alignment(horizontal='center'), - 'right': Alignment(horizontal='right'), + "align": { + "left": Alignment(horizontal="left"), + "center": Alignment(horizontal="center"), + "right": Alignment(horizontal="right"), }, - 'style': { - 'number': '#,##0.00', - 'date': 'dd/mm/yyyy', - 'datestamp': 'yyyy-mm-dd', - 'text': '@', - 'percent': '0.00%', + "style": { + "number": "#,##0.00", + "date": "dd/mm/yyyy", + "datestamp": "yyyy-mm-dd", + "text": "@", + "percent": "0.00%", }, } diff --git a/excel_import_export/models/xlsx_export.py b/excel_import_export/models/xlsx_export.py index fcc2512a5..2aac49b90 100644 --- a/excel_import_export/models/xlsx_export.py +++ b/excel_import_export/models/xlsx_export.py @@ -1,16 +1,18 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import os -import logging import base64 -from io import BytesIO +import logging +import os import time from datetime import date, datetime as dt -from odoo.tools.float_utils import float_compare -from odoo import models, fields, api, _ -from odoo.tools.safe_eval import safe_eval +from io import BytesIO + +from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_compare +from odoo.tools.safe_eval import safe_eval + from . import common as co _logger = logging.getLogger(__name__) @@ -18,26 +20,26 @@ try: from openpyxl import load_workbook from openpyxl.utils.exceptions import IllegalCharacterError except ImportError: - _logger.debug( - 'Cannot import "openpyxl". Please make sure it is installed.') + _logger.debug('Cannot import "openpyxl". Please make sure it is installed.') class XLSXExport(models.AbstractModel): - _name = 'xlsx.export' - _description = 'Excel Export AbstractModel' + _name = "xlsx.export" + _description = "Excel Export AbstractModel" @api.model def get_eval_context(self, model, record, value): - eval_context = {'float_compare': float_compare, - 'time': time, - 'datetime': dt, - 'date': date, - 'value': value, - 'object': record, - 'model': self.env[model], - 'env': self.env, - 'context': self._context, - } + eval_context = { + "float_compare": float_compare, + "time": time, + "datetime": dt, + "date": date, + "value": value, + "object": record, + "model": self.env[model], + "env": self.env, + "context": self._context, + } return eval_context @api.model @@ -48,12 +50,11 @@ class XLSXExport(models.AbstractModel): - fields: fields in line_ids, i.e., partner_id.display_name """ line_field, max_row = co.get_line_max(line_field) - line_field = line_field.replace('_CONT_', '') # Remove _CONT_ if any + line_field = line_field.replace("_CONT_", "") # Remove _CONT_ if any lines = record[line_field] if max_row > 0 and len(lines) > max_row: - raise Exception( - _('Records in %s exceed max records allowed') % line_field) - vals = dict([(field, []) for field in fields]) # value and do_style + raise Exception(_("Records in %s exceed max records allowed") % line_field) + vals = {field: [] for field in fields} # value and do_style # Get field condition & aggre function field_cond_dict = {} aggre_func_dict = {} @@ -77,31 +78,32 @@ class XLSXExport(models.AbstractModel): for field in pair_fields: # (field, raw_field) value = self._get_field_data(field[1], line) eval_cond = field_cond_dict[field[0]] - eval_context = \ - self.get_eval_context(line._name, line, value) + eval_context = self.get_eval_context(line._name, line, value) if eval_cond: value = safe_eval(eval_cond, eval_context) # style w/Cond takes priority style_cond = style_cond_dict[field[0]] - style = self._eval_style_cond(line._name, line, - value, style_cond) + style = self._eval_style_cond(line._name, line, value, style_cond) if style is None: style = False # No style elif style is False: style = field_style_dict[field[0]] # Use default style vals[field[0]].append((value, style)) - return (vals, aggre_func_dict,) + return ( + vals, + aggre_func_dict, + ) @api.model def _eval_style_cond(self, model, record, value, style_cond): eval_context = self.get_eval_context(model, record, value) - field = style_cond = style_cond or '#??' + field = style_cond = style_cond or "#??" styles = {} - for i in range(style_cond.count('#{')): + for i in range(style_cond.count("#{")): i += 1 field, style = co.get_field_style(field) styles.update({i: style}) - style_cond = style_cond.replace('#{%s}' % style, str(i)) + style_cond = style_cond.replace("#{%s}" % style, str(i)) if not styles: return False res = safe_eval(style_cond, eval_context) @@ -122,23 +124,25 @@ class XLSXExport(models.AbstractModel): st = co.openpyxl_get_sheet_by_name(workbook, sheet_name) elif isinstance(sheet_name, int): if sheet_name > len(workbook.worksheets): - raise Exception(_('Not enough worksheets')) + raise Exception(_("Not enough worksheets")) st = workbook.worksheets[sheet_name - 1] if not st: - raise ValidationError( - _('Sheet %s not found') % sheet_name) + raise ValidationError(_("Sheet %s not found") % sheet_name) # Fill data, header and rows self._fill_head(ws, st, record) self._fill_lines(ws, st, record) except KeyError as e: - raise ValidationError(_('Key Error\n%s') % e) + raise ValidationError(_("Key Error\n%s") % e) except IllegalCharacterError as e: raise ValidationError( - _('IllegalCharacterError\n' - 'Some exporting data contain special character\n%s') % e) + _( + "IllegalCharacterError\n" + "Some exporting data contain special character\n%s" + ) + % e + ) except Exception as e: - raise ValidationError( - _('Error filling data into Excel sheets\n%s') % e) + raise ValidationError(_("Error filling data into Excel sheets\n%s") % e) @api.model def _get_field_data(self, _field, _line): @@ -146,43 +150,41 @@ class XLSXExport(models.AbstractModel): if not _field: return None line_copy = _line - for f in _field.split('.'): + for f in _field.split("."): line_copy = line_copy[f] if isinstance(line_copy, str): - line_copy = line_copy.encode('utf-8') + line_copy = line_copy.encode("utf-8") return line_copy @api.model def _fill_head(self, ws, st, record): - for rc, field in ws.get('_HEAD_', {}).items(): + for rc, field in ws.get("_HEAD_", {}).items(): tmp_field, eval_cond = co.get_field_condition(field) eval_cond = eval_cond or 'value or ""' tmp_field, field_style = co.get_field_style(tmp_field) tmp_field, style_cond = co.get_field_style_cond(tmp_field) value = tmp_field and self._get_field_data(tmp_field, record) # Eval - eval_context = self.get_eval_context(record._name, - record, value) + eval_context = self.get_eval_context(record._name, record, value) if eval_cond: value = safe_eval(eval_cond, eval_context) if value is not None: st[rc] = value - fc = not style_cond and True or \ - safe_eval(style_cond, eval_context) + fc = not style_cond and True or safe_eval(style_cond, eval_context) if field_style and fc: # has style and pass style_cond - styles = self.env['xlsx.styles'].get_openpyxl_styles() + styles = self.env["xlsx.styles"].get_openpyxl_styles() co.fill_cell_style(st[rc], field_style, styles) @api.model def _fill_lines(self, ws, st, record): line_fields = list(ws) - if '_HEAD_' in line_fields: - line_fields.remove('_HEAD_') + if "_HEAD_" in line_fields: + line_fields.remove("_HEAD_") cont_row = 0 # last data row to continue for line_field in line_fields: fields = ws.get(line_field, {}).values() vals, func = self._get_line_vals(record, line_field, fields) - is_cont = '_CONT_' in line_field and True or False # continue row + is_cont = "_CONT_" in line_field and True or False # continue row cont_set = 0 rows_inserted = False # flag to insert row for rc, field in ws.get(line_field, {}).items(): @@ -192,7 +194,7 @@ class XLSXExport(models.AbstractModel): cont_set = cont_row + 1 if is_cont: row = cont_set - rc = '%s%s' % (col, cont_set) + rc = "{}{}".format(col, cont_set) i = 0 new_row = 0 new_rc = False @@ -200,24 +202,24 @@ class XLSXExport(models.AbstractModel): # Insert rows to preserve total line if not rows_inserted: rows_inserted = True - st.insert_rows(row+1, amount=row_count-1) + st.insert_rows(row + 1, amount=row_count - 1) # -- for (row_val, style) in vals[field]: new_row = row + i - new_rc = '%s%s' % (col, new_row) + new_rc = "{}{}".format(col, new_row) row_val = co.adjust_cell_formula(row_val, i) - if row_val not in ('None', None): + if row_val not in ("None", None): st[new_rc] = co.str_to_number(row_val) if style: - styles = self.env['xlsx.styles'].get_openpyxl_styles() + styles = self.env["xlsx.styles"].get_openpyxl_styles() co.fill_cell_style(st[new_rc], style, styles) i += 1 # Add footer line if at least one field have sum f = func.get(field, False) if f and new_row > 0: new_row += 1 - f_rc = '%s%s' % (col, new_row) - st[f_rc] = '=%s(%s:%s)' % (f, rc, new_rc) + f_rc = "{}{}".format(col, new_row) + st[f_rc] = "={}({}:{})".format(f, rc, new_rc) co.fill_cell_style(st[f_rc], style, styles) cont_row = cont_row < new_row and new_row or cont_row return @@ -227,7 +229,7 @@ class XLSXExport(models.AbstractModel): if template.res_model != res_model: raise ValidationError(_("Template's model mismatch")) data_dict = co.literal_eval(template.instruction.strip()) - export_dict = data_dict.get('__EXPORT__', False) + export_dict = data_dict.get("__EXPORT__", False) out_name = template.name if not export_dict: # If there is not __EXPORT__ formula, just export out_name = template.fname @@ -235,11 +237,11 @@ class XLSXExport(models.AbstractModel): return (out_file, out_name) # Prepare temp file (from now, only xlsx file works for openpyxl) decoded_data = base64.decodestring(template.datas) - ConfParam = self.env['ir.config_parameter'].sudo() - ptemp = ConfParam.get_param('path_temp_file') or '/tmp' - stamp = dt.utcnow().strftime('%H%M%S%f')[:-3] - ftemp = '%s/temp%s.xlsx' % (ptemp, stamp) - f = open(ftemp, 'wb') + ConfParam = self.env["ir.config_parameter"].sudo() + ptemp = ConfParam.get_param("path_temp_file") or "/tmp" + stamp = dt.utcnow().strftime("%H%M%S%f")[:-3] + ftemp = "{}/temp{}.xlsx".format(ptemp, stamp) + f = open(ftemp, "wb") f.write(decoded_data) f.seek(0) f.close() @@ -254,19 +256,18 @@ class XLSXExport(models.AbstractModel): wb.save(content) content.seek(0) # Set index to 0, and start reading out_file = base64.encodestring(content.read()) - if record and 'name' in record and record.name: - out_name = record.name.replace(' ', '').replace('/', '') + if record and "name" in record and record.name: + out_name = record.name.replace(" ", "").replace("/", "") else: - fname = out_name.replace(' ', '').replace('/', '') + fname = out_name.replace(" ", "").replace("/", "") ts = fields.Datetime.context_timestamp(self, dt.now()) - out_name = '%s_%s' % (fname, ts.strftime('%Y%m%d_%H%M%S')) + out_name = "{}_{}".format(fname, ts.strftime("%Y%m%d_%H%M%S")) if not out_name or len(out_name) == 0: - out_name = 'noname' - out_ext = 'xlsx' + out_name = "noname" + out_ext = "xlsx" # CSV (convert only on 1st sheet) if template.to_csv: delimiter = template.csv_delimiter - out_file = co.csv_from_excel(out_file, delimiter, - template.csv_quote) + out_file = co.csv_from_excel(out_file, delimiter, template.csv_quote) out_ext = template.csv_extension - return (out_file, '%s.%s' % (out_name, out_ext)) + return (out_file, "{}.{}".format(out_name, out_ext)) diff --git a/excel_import_export/models/xlsx_import.py b/excel_import_export/models/xlsx_import.py index 389a0a544..9af7b4b4f 100644 --- a/excel_import_export/models/xlsx_import.py +++ b/excel_import_export/models/xlsx_import.py @@ -2,53 +2,61 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) import base64 -import uuid -import xlrd -import xlwt import time -from io import BytesIO -from . import common as co +import uuid from ast import literal_eval from datetime import date, datetime as dt -from odoo.tools.float_utils import float_compare -from odoo import models, api, _ +from io import BytesIO + +import xlrd +import xlwt + +from odoo import _, api, models from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_compare from odoo.tools.safe_eval import safe_eval +from . import common as co + class XLSXImport(models.AbstractModel): - _name = 'xlsx.import' - _description = 'Excel Import AbstractModel' + _name = "xlsx.import" + _description = "Excel Import AbstractModel" @api.model def get_eval_context(self, model=False, value=False): - eval_context = {'float_compare': float_compare, - 'time': time, - 'datetime': dt, - 'date': date, - 'env': self.env, - 'context': self._context, - 'value': False, - 'model': False, - } + eval_context = { + "float_compare": float_compare, + "time": time, + "datetime": dt, + "date": date, + "env": self.env, + "context": self._context, + "value": False, + "model": False, + } if model: - eval_context.update({'model': self.env[model]}) + eval_context.update({"model": self.env[model]}) if value: if isinstance(value, str): # Remove non Ord 128 character - value = ''.join([i if ord(i) < 128 else ' ' for i in value]) - eval_context.update({'value': value}) + value = "".join([i if ord(i) < 128 else " " for i in value]) + eval_context.update({"value": value}) return eval_context @api.model def get_external_id(self, record): """ Get external ID of the record, if not already exists create one """ - ModelData = self.env['ir.model.data'] + ModelData = self.env["ir.model.data"] xml_id = record.get_external_id() - if not xml_id or (record.id in xml_id and xml_id[record.id] == ''): - ModelData.create({'name': '%s_%s' % (record._table, record.id), - 'module': 'excel_import_export', - 'model': record._name, - 'res_id': record.id, }) + if not xml_id or (record.id in xml_id and xml_id[record.id] == ""): + ModelData.create( + { + "name": "{}_{}".format(record._table, record.id), + "module": "excel_import_export", + "model": record._name, + "res_id": record.id, + } + ) xml_id = record.get_external_id() return xml_id[record.id] @@ -56,15 +64,16 @@ class XLSXImport(models.AbstractModel): def _get_field_type(self, model, field): try: record = self.env[model].new() - for f in field.split('/'): + for f in field.split("/"): field_type = record._fields[f].type - if field_type in ('one2many', 'many2many'): + if field_type in ("one2many", "many2many"): record = record[f] else: return field_type except Exception: raise ValidationError( - _('Invalid declaration, %s has no valid field type') % field) + _("Invalid declaration, %s has no valid field type") % field + ) @api.model def _delete_record_data(self, record, data_dict): @@ -74,19 +83,19 @@ class XLSXImport(models.AbstractModel): try: for sheet_name in data_dict: worksheet = data_dict[sheet_name] - line_fields = filter(lambda x: x != '_HEAD_', worksheet) + line_fields = filter(lambda x: x != "_HEAD_", worksheet) for line_field in line_fields: - if '_NODEL_' not in line_field: + if "_NODEL_" not in line_field: if line_field in record and record[line_field]: record[line_field].unlink() # Remove _NODEL_ from dict for s, sv in data_dict.items(): for f, fv in data_dict[s].items(): - if '_NODEL_' in f: + if "_NODEL_" in f: new_fv = data_dict[s].pop(f) - data_dict[s][f.replace('_NODEL_', '')] = new_fv + data_dict[s][f.replace("_NODEL_", "")] = new_fv except Exception as e: - raise ValidationError(_('Error deleting data\n%s') % e) + raise ValidationError(_("Error deleting data\n%s") % e) @api.model def _get_line_vals(self, st, worksheet, model, line_field): @@ -99,20 +108,18 @@ class XLSXImport(models.AbstractModel): rc, key_eval_cond = co.get_field_condition(rc) x_field, val_eval_cond = co.get_field_condition(field) row, col = co.pos2idx(rc) - out_field = '%s/%s' % (line_field, x_field) + out_field = "{}/{}".format(line_field, x_field) field_type = self._get_field_type(model, out_field) vals.update({out_field: []}) for idx in range(row, st.nrows): - value = co._get_cell_value(st.cell(idx, col), - field_type=field_type) - eval_context = self.get_eval_context(model=model, - value=value) + value = co._get_cell_value(st.cell(idx, col), field_type=field_type) + eval_context = self.get_eval_context(model=model, value=value) if key_eval_cond: value = safe_eval(key_eval_cond, eval_context) if val_eval_cond: value = safe_eval(val_eval_cond, eval_context) vals[out_field].append(value) - if not filter(lambda x: x != '', vals[out_field]): + if not filter(lambda x: x != "", vals[out_field]): vals.pop(out_field) return vals @@ -128,11 +135,14 @@ class XLSXImport(models.AbstractModel): col_idx = 0 out_wb = xlwt.Workbook() out_st = out_wb.add_sheet("Sheet 1") - xml_id = record and self.get_external_id(record) or \ - '%s.%s' % ('xls', uuid.uuid4()) - out_st.write(0, 0, 'id') # id and xml_id on first column + xml_id = ( + record + and self.get_external_id(record) + or "{}.{}".format("xls", uuid.uuid4()) + ) + out_st.write(0, 0, "id") # id and xml_id on first column out_st.write(1, 0, xml_id) - header_fields.append('id') + header_fields.append("id") col_idx += 1 model = record._name for sheet_name in data_dict: # For each Sheet @@ -143,22 +153,21 @@ class XLSXImport(models.AbstractModel): elif isinstance(sheet_name, int): st = wb.sheet_by_index(sheet_name - 1) if not st: - raise ValidationError( - _('Sheet %s not found') % sheet_name) + raise ValidationError(_("Sheet %s not found") % sheet_name) # HEAD updates - for rc, field in worksheet.get('_HEAD_', {}).items(): + for rc, field in worksheet.get("_HEAD_", {}).items(): rc, key_eval_cond = co.get_field_condition(rc) field, val_eval_cond = co.get_field_condition(field) field_type = self._get_field_type(model, field) value = False try: row, col = co.pos2idx(rc) - value = co._get_cell_value(st.cell(row, col), - field_type=field_type) + value = co._get_cell_value( + st.cell(row, col), field_type=field_type + ) except Exception: pass - eval_context = self.get_eval_context(model=model, - value=value) + eval_context = self.get_eval_context(model=model, value=value) if key_eval_cond: value = str(safe_eval(key_eval_cond, eval_context)) if val_eval_cond: @@ -168,10 +177,9 @@ class XLSXImport(models.AbstractModel): header_fields.append(field) col_idx += 1 # Line Items - line_fields = filter(lambda x: x != '_HEAD_', worksheet) + line_fields = filter(lambda x: x != "_HEAD_", worksheet) for line_field in line_fields: - vals = self._get_line_vals(st, worksheet, - model, line_field) + vals = self._get_line_vals(st, worksheet, model, line_field) for field in vals: # Columns, i.e., line_ids/field_id out_st.write(0, col_idx, field) @@ -187,41 +195,47 @@ class XLSXImport(models.AbstractModel): content.seek(0) # Set index to 0, and start reading xls_file = content.read() # Do the import - Import = self.env['base_import.import'] - imp = Import.create({ - 'res_model': model, - 'file': xls_file, - 'file_type': 'application/vnd.ms-excel', - 'file_name': 'temp.xls', - }) + Import = self.env["base_import.import"] + imp = Import.create( + { + "res_model": model, + "file": xls_file, + "file_type": "application/vnd.ms-excel", + "file_name": "temp.xls", + } + ) errors = imp.do( header_fields, header_fields, - {'headers': True, - 'advanced': True, - 'keep_matches': False, - 'encoding': '', - 'separator': '', - 'quoting': '"', - 'date_format': '%Y-%m-%d', - 'datetime_format': '%Y-%m-%d %H:%M:%S', - 'float_thousand_separator': ',', - 'float_decimal_separator': '.', - 'fields': []}) - if errors.get('messages'): - message = _('Error importing data') - messages = errors['messages'] + { + "headers": True, + "advanced": True, + "keep_matches": False, + "encoding": "", + "separator": "", + "quoting": '"', + "date_format": "%Y-%m-%d", + "datetime_format": "%Y-%m-%d %H:%M:%S", + "float_thousand_separator": ",", + "float_decimal_separator": ".", + "fields": [], + }, + ) + if errors.get("messages"): + message = _("Error importing data") + messages = errors["messages"] if isinstance(messages, dict): - message = messages['message'] + message = messages["message"] if isinstance(messages, list): - message = ', '.join([x['message'] for x in messages]) - raise ValidationError(message.encode('utf-8')) + message = ", ".join([x["message"] for x in messages]) + raise ValidationError(message.encode("utf-8")) return self.env.ref(xml_id) except xlrd.XLRDError: raise ValidationError( - _('Invalid file style, only .xls or .xlsx file allowed')) + _("Invalid file style, only .xls or .xlsx file allowed") + ) except Exception as e: - raise ValidationError(_('Error importing data\n%s') % e) + raise ValidationError(_("Error importing data\n%s") % e) @api.model def _post_import_operation(self, record, operation): @@ -229,16 +243,15 @@ class XLSXImport(models.AbstractModel): if not record or not operation: return try: - if '${' in operation: - code = (operation.split('${'))[1].split('}')[0] - eval_context = {'object': record} + if "${" in operation: + code = (operation.split("${"))[1].split("}")[0] + eval_context = {"object": record} safe_eval(code, eval_context) except Exception as e: - raise ValidationError(_('Post import operation error\n%s') % e) + raise ValidationError(_("Post import operation error\n%s") % e) @api.model - def import_xlsx(self, import_file, template, - res_model=False, res_id=False): + def import_xlsx(self, import_file, template, res_model=False, res_id=False): """ - If res_id = False, we want to create new document first - Delete fields' data according to data_dict['__IMPORT__'] @@ -249,16 +262,16 @@ class XLSXImport(models.AbstractModel): raise ValidationError(_("Template's model mismatch")) record = self.env[template.res_model].browse(res_id) data_dict = literal_eval(template.instruction.strip()) - if not data_dict.get('__IMPORT__'): + if not data_dict.get("__IMPORT__"): raise ValidationError( - _("No data_dict['__IMPORT__'] in template %s") % template.name) + _("No data_dict['__IMPORT__'] in template %s") % template.name + ) if record: # Delete existing data first - self._delete_record_data(record, data_dict['__IMPORT__']) + self._delete_record_data(record, data_dict["__IMPORT__"]) # Fill up record with data from excel sheets - record = self._import_record_data(import_file, record, - data_dict['__IMPORT__']) + record = self._import_record_data(import_file, record, data_dict["__IMPORT__"]) # Post Import Operation, i.e., cleanup some data - if data_dict.get('__POST_IMPORT__', False): - self._post_import_operation(record, data_dict['__POST_IMPORT__']) + if data_dict.get("__POST_IMPORT__", False): + self._post_import_operation(record, data_dict["__POST_IMPORT__"]) return record diff --git a/excel_import_export/models/xlsx_report.py b/excel_import_export/models/xlsx_report.py index f123d2a65..fdc741458 100644 --- a/excel_import_export/models/xlsx_report.py +++ b/excel_import_export/models/xlsx_report.py @@ -1,69 +1,58 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import models, fields, api, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError class XLSXReport(models.AbstractModel): """ Common class for xlsx reporting wizard """ - _name = 'xlsx.report' - _description = 'Excel Report AbstractModel' - name = fields.Char( - string='File Name', - readonly=True, - size=500, - ) - data = fields.Binary( - string='File', - readonly=True, - ) + _name = "xlsx.report" + _description = "Excel Report AbstractModel" + + name = fields.Char(string="File Name", readonly=True, size=500,) + data = fields.Binary(string="File", readonly=True,) template_id = fields.Many2one( - 'xlsx.template', - string='Template', + "xlsx.template", + string="Template", required=True, - ondelete='cascade', - domain=lambda self: self._context.get('template_domain', []), - ) - choose_template = fields.Boolean( - string='Allow Choose Template', - default=False, + ondelete="cascade", + domain=lambda self: self._context.get("template_domain", []), ) + choose_template = fields.Boolean(string="Allow Choose Template", default=False,) state = fields.Selection( - [('choose', 'Choose'), - ('get', 'Get')], - default='choose', + [("choose", "Choose"), ("get", "Get")], + default="choose", help="* Choose: wizard show in user selection mode" - "\n* Get: wizard show results from user action", + "\n* Get: wizard show results from user action", ) @api.model def default_get(self, fields): - template_domain = self._context.get('template_domain', []) - templates = self.env['xlsx.template'].search(template_domain) + template_domain = self._context.get("template_domain", []) + templates = self.env["xlsx.template"].search(template_domain) if not templates: - raise ValidationError(_('No template found')) + raise ValidationError(_("No template found")) defaults = super(XLSXReport, self).default_get(fields) for template in templates: if not template.datas: - raise ValidationError(_('No file in %s') % (template.name,)) - defaults['template_id'] = len(templates) == 1 and templates.id or False + raise ValidationError(_("No file in %s") % (template.name,)) + defaults["template_id"] = len(templates) == 1 and templates.id or False return defaults @api.multi def report_xlsx(self): self.ensure_one() - Export = self.env['xlsx.export'] - out_file, out_name = \ - Export.export_xlsx(self.template_id, self._name, self.id) - self.write({'state': 'get', 'data': out_file, 'name': out_name}) + Export = self.env["xlsx.export"] + out_file, out_name = Export.export_xlsx(self.template_id, self._name, self.id) + self.write({"state": "get", "data": out_file, "name": out_name}) return { - 'type': 'ir.actions.act_window', - 'res_model': self._name, - 'view_mode': 'form', - 'view_type': 'form', - 'res_id': self.id, - 'views': [(False, 'form')], - 'target': 'new', + "type": "ir.actions.act_window", + "res_model": self._name, + "view_mode": "form", + "view_type": "form", + "res_id": self.id, + "views": [(False, "form")], + "target": "new", } diff --git a/excel_import_export/models/xlsx_template.py b/excel_import_export/models/xlsx_template.py index 1460473a8..e34528042 100644 --- a/excel_import_export/models/xlsx_template.py +++ b/excel_import_export/models/xlsx_template.py @@ -1,14 +1,16 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import os import base64 +import os from ast import literal_eval -from odoo import api, fields, models, _ -from odoo.modules.module import get_module_path from os.path import join as opj -from . import common as co + +from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.modules.module import get_module_path + +from . import common as co class XLSXTemplate(models.Model): @@ -17,105 +19,96 @@ class XLSXTemplate(models.Model): - Import/Export Meta Data (dict text) - Default values, etc. """ - _name = 'xlsx.template' - _description = 'Excel template file and instruction' - _order = 'name' - name = fields.Char( - string='Template Name', - required=True, - ) + _name = "xlsx.template" + _description = "Excel template file and instruction" + _order = "name" + + name = fields.Char(string="Template Name", required=True,) res_model = fields.Char( - string='Resource Model', + string="Resource Model", help="The database object this attachment will be attached to.", ) - fname = fields.Char( - string='File Name', - ) + fname = fields.Char(string="File Name",) gname = fields.Char( - string='Group Name', + string="Group Name", help="Multiple template of same model, can belong to same group,\n" "result in multiple template selection", ) - description = fields.Char( - string='Description', - ) + description = fields.Char(string="Description",) input_instruction = fields.Text( - string='Instruction (Input)', + string="Instruction (Input)", help="This is used to construct instruction in tab Import/Export", ) instruction = fields.Text( - string='Instruction', - compute='_compute_output_instruction', - help="Instruction on how to import/export, prepared by system." - ) - datas = fields.Binary( - string='File Content', - ) - to_csv = fields.Boolean( - string='Convert to CSV?', - default=False, + string="Instruction", + compute="_compute_output_instruction", + help="Instruction on how to import/export, prepared by system.", ) + datas = fields.Binary(string="File Content",) + to_csv = fields.Boolean(string="Convert to CSV?", default=False,) csv_delimiter = fields.Char( - string='CSV Delimiter', + string="CSV Delimiter", size=1, - default=',', + default=",", required=True, help="Optional for CSV, default is comma.", ) csv_extension = fields.Char( - string='CSV File Extension', + string="CSV File Extension", size=5, - default='csv', + default="csv", required=True, - help="Optional for CSV, default is .csv" + help="Optional for CSV, default is .csv", ) csv_quote = fields.Boolean( - string='CSV Quoting', + string="CSV Quoting", default=True, - help="Optional for CSV, default is full quoting." + help="Optional for CSV, default is full quoting.", ) export_ids = fields.One2many( - comodel_name='xlsx.template.export', - inverse_name='template_id', + comodel_name="xlsx.template.export", inverse_name="template_id", ) import_ids = fields.One2many( - comodel_name='xlsx.template.import', - inverse_name='template_id', + comodel_name="xlsx.template.import", inverse_name="template_id", ) post_import_hook = fields.Char( - string='Post Import Function Hook', + string="Post Import Function Hook", help="Call a function after successful import, i.e.,\n" "${object.post_import_do_something()}", ) show_instruction = fields.Boolean( - string='Show Output', + string="Show Output", default=False, help="This is the computed instruction based on tab Import/Export,\n" "to be used by xlsx import/export engine", ) redirect_action = fields.Many2one( - comodel_name='ir.actions.act_window', - string='Return Action', - domain=[('type', '=', 'ir.actions.act_window')], + comodel_name="ir.actions.act_window", + string="Return Action", + domain=[("type", "=", "ir.actions.act_window")], help="Optional action, redirection after finish import operation", ) @api.multi - @api.constrains('redirect_action', 'res_model') + @api.constrains("redirect_action", "res_model") def _check_action_model(self): for rec in self: - if rec.res_model and rec.redirect_action and \ - rec.res_model != rec.redirect_action.res_model: - raise ValidationError(_('The selected redirect action is ' - 'not for model %s') % rec.res_model) + if ( + rec.res_model + and rec.redirect_action + and rec.res_model != rec.redirect_action.res_model + ): + raise ValidationError( + _("The selected redirect action is " "not for model %s") + % rec.res_model + ) @api.model def load_xlsx_template(self, tempalte_ids, addon=False): for template in self.browse(tempalte_ids): if not addon: - addon = list(template.get_external_id(). - values())[0].split('.')[0] + addon = list(template.get_external_id().values())[0].split(".")[0] addon_path = get_module_path(addon) file_path = False for root, dirs, files in os.walk(addon_path): @@ -123,13 +116,13 @@ class XLSXTemplate(models.Model): if name == template.fname: file_path = os.path.abspath(opj(root, name)) if file_path: - template.datas = base64.b64encode(open(file_path, 'rb').read()) + template.datas = base64.b64encode(open(file_path, "rb").read()) return True @api.model def create(self, vals): rec = super().create(vals) - if vals.get('input_instruction'): + if vals.get("input_instruction"): rec._compute_input_export_instruction() rec._compute_input_import_instruction() rec._compute_input_post_import_hook() @@ -138,7 +131,7 @@ class XLSXTemplate(models.Model): @api.multi def write(self, vals): res = super().write(vals) - if vals.get('input_instruction'): + if vals.get("input_instruction"): for rec in self: rec._compute_input_export_instruction() rec._compute_input_import_instruction() @@ -152,7 +145,7 @@ class XLSXTemplate(models.Model): # Export Instruction input_dict = literal_eval(rec.input_instruction.strip()) rec.export_ids.unlink() - export_dict = input_dict.get('__EXPORT__') + export_dict = input_dict.get("__EXPORT__") if not export_dict: continue export_lines = [] @@ -160,34 +153,36 @@ class XLSXTemplate(models.Model): # Sheet for sheet, rows in export_dict.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'sheet', - 'sheet': str(sheet), - } + vals = { + "sequence": sequence, + "section_type": "sheet", + "sheet": str(sheet), + } export_lines.append((0, 0, vals)) # Rows for row_field, lines in rows.items(): sequence += 1 is_cont = False - if '_CONT_' in row_field: + if "_CONT_" in row_field: is_cont = True - row_field = row_field.replace('_CONT_', '') - vals = {'sequence': sequence, - 'section_type': (row_field == '_HEAD_' and - 'head' or 'row'), - 'row_field': row_field, - 'is_cont': is_cont, - } + row_field = row_field.replace("_CONT_", "") + vals = { + "sequence": sequence, + "section_type": (row_field == "_HEAD_" and "head" or "row"), + "row_field": row_field, + "is_cont": is_cont, + } export_lines.append((0, 0, vals)) for excel_cell, field_name in lines.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'data', - 'excel_cell': excel_cell, - 'field_name': field_name, - } + vals = { + "sequence": sequence, + "section_type": "data", + "excel_cell": excel_cell, + "field_name": field_name, + } export_lines.append((0, 0, vals)) - rec.write({'export_ids': export_lines}) + rec.write({"export_ids": export_lines}) @api.multi def _compute_input_import_instruction(self): @@ -196,7 +191,7 @@ class XLSXTemplate(models.Model): # Import Instruction input_dict = literal_eval(rec.input_instruction.strip()) rec.import_ids.unlink() - import_dict = input_dict.get('__IMPORT__') + import_dict = input_dict.get("__IMPORT__") if not import_dict: continue import_lines = [] @@ -204,34 +199,36 @@ class XLSXTemplate(models.Model): # Sheet for sheet, rows in import_dict.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'sheet', - 'sheet': str(sheet), - } + vals = { + "sequence": sequence, + "section_type": "sheet", + "sheet": str(sheet), + } import_lines.append((0, 0, vals)) # Rows for row_field, lines in rows.items(): sequence += 1 no_delete = False - if '_NODEL_' in row_field: + if "_NODEL_" in row_field: no_delete = True - row_field = row_field.replace('_NODEL_', '') - vals = {'sequence': sequence, - 'section_type': (row_field == '_HEAD_' and - 'head' or 'row'), - 'row_field': row_field, - 'no_delete': no_delete, - } + row_field = row_field.replace("_NODEL_", "") + vals = { + "sequence": sequence, + "section_type": (row_field == "_HEAD_" and "head" or "row"), + "row_field": row_field, + "no_delete": no_delete, + } import_lines.append((0, 0, vals)) for excel_cell, field_name in lines.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'data', - 'excel_cell': excel_cell, - 'field_name': field_name, - } + vals = { + "sequence": sequence, + "section_type": "data", + "excel_cell": excel_cell, + "field_name": field_name, + } import_lines.append((0, 0, vals)) - rec.write({'import_ids': import_lines}) + rec.write({"import_ids": import_lines}) @api.multi def _compute_input_post_import_hook(self): @@ -239,7 +236,7 @@ class XLSXTemplate(models.Model): for rec in self: # Import Instruction input_dict = literal_eval(rec.input_instruction.strip()) - rec.post_import_hook = input_dict.get('__POST_IMPORT__') + rec.post_import_hook = input_dict.get("__POST_IMPORT__") @api.multi def _compute_output_instruction(self): @@ -249,62 +246,60 @@ class XLSXTemplate(models.Model): prev_sheet = False prev_row = False # Export Instruction - itype = '__EXPORT__' + itype = "__EXPORT__" inst_dict[itype] = {} for line in rec.export_ids: - if line.section_type == 'sheet': - sheet = co.isinteger(line.sheet) and \ - int(line.sheet) or line.sheet + if line.section_type == "sheet": + sheet = co.isinteger(line.sheet) and int(line.sheet) or line.sheet sheet_dict = {sheet: {}} inst_dict[itype].update(sheet_dict) prev_sheet = sheet continue - if line.section_type in ('head', 'row'): + if line.section_type in ("head", "row"): row_field = line.row_field - if line.section_type == 'row' and line.is_cont: - row_field = '_CONT_%s' % row_field + if line.section_type == "row" and line.is_cont: + row_field = "_CONT_%s" % row_field row_dict = {row_field: {}} inst_dict[itype][prev_sheet].update(row_dict) prev_row = row_field continue - if line.section_type == 'data': + if line.section_type == "data": excel_cell = line.excel_cell - field_name = line.field_name or '' - field_name += line.field_cond or '' - field_name += line.style or '' - field_name += line.style_cond or '' + field_name = line.field_name or "" + field_name += line.field_cond or "" + field_name += line.style or "" + field_name += line.style_cond or "" if line.is_sum: - field_name += '@{sum}' + field_name += "@{sum}" cell_dict = {excel_cell: field_name} inst_dict[itype][prev_sheet][prev_row].update(cell_dict) continue # Import Instruction - itype = '__IMPORT__' + itype = "__IMPORT__" inst_dict[itype] = {} for line in rec.import_ids: - if line.section_type == 'sheet': - sheet = co.isinteger(line.sheet) and \ - int(line.sheet) or line.sheet + if line.section_type == "sheet": + sheet = co.isinteger(line.sheet) and int(line.sheet) or line.sheet sheet_dict = {sheet: {}} inst_dict[itype].update(sheet_dict) prev_sheet = sheet continue - if line.section_type in ('head', 'row'): + if line.section_type in ("head", "row"): row_field = line.row_field - if line.section_type == 'row' and line.no_delete: - row_field = '_NODEL_%s' % row_field + if line.section_type == "row" and line.no_delete: + row_field = "_NODEL_%s" % row_field row_dict = {row_field: {}} inst_dict[itype][prev_sheet].update(row_dict) prev_row = row_field continue - if line.section_type == 'data': + if line.section_type == "data": excel_cell = line.excel_cell - field_name = line.field_name or '' - field_name += line.field_cond or '' + field_name = line.field_name or "" + field_name += line.field_cond or "" cell_dict = {excel_cell: field_name} inst_dict[itype][prev_sheet][prev_row].update(cell_dict) continue - itype = '__POST_IMPORT__' + itype = "__POST_IMPORT__" inst_dict[itype] = False if rec.post_import_hook: inst_dict[itype] = rec.post_import_hook @@ -312,51 +307,36 @@ class XLSXTemplate(models.Model): class XLSXTemplateImport(models.Model): - _name = 'xlsx.template.import' - _description = 'Detailed of how excel data will be imported' - _order = 'sequence' + _name = "xlsx.template.import" + _description = "Detailed of how excel data will be imported" + _order = "sequence" template_id = fields.Many2one( - comodel_name='xlsx.template', - string='XLSX Template', + comodel_name="xlsx.template", + string="XLSX Template", index=True, - ondelete='cascade', + ondelete="cascade", readonly=True, ) - sequence = fields.Integer( - string='Sequence', - default=10, - ) - sheet = fields.Char( - string='Sheet', - ) + sequence = fields.Integer(string="Sequence", default=10,) + sheet = fields.Char(string="Sheet",) section_type = fields.Selection( - [('sheet', 'Sheet'), - ('head', 'Head'), - ('row', 'Row'), - ('data', 'Data')], - string='Section Type', + [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")], + string="Section Type", required=True, ) row_field = fields.Char( - string='Row Field', - help="If section type is row, this field is required", + string="Row Field", help="If section type is row, this field is required", ) no_delete = fields.Boolean( - string='No Delete', + string="No Delete", default=False, help="By default, all rows will be deleted before import.\n" - "Select No Delete, otherwise" - ) - excel_cell = fields.Char( - string='Cell', - ) - field_name = fields.Char( - string='Field', - ) - field_cond = fields.Char( - string='Field Cond.', + "Select No Delete, otherwise", ) + excel_cell = fields.Char(string="Cell",) + field_name = fields.Char(string="Field",) + field_cond = fields.Char(string="Field Cond.",) @api.model def create(self, vals): @@ -365,70 +345,46 @@ class XLSXTemplateImport(models.Model): @api.model def _extract_field_name(self, vals): - if self._context.get('compute_from_input') and vals.get('field_name'): - field_name, field_cond = co.get_field_condition(vals['field_name']) - field_cond = field_cond and '${%s}' % (field_cond or '') or False - vals.update({'field_name': field_name, - 'field_cond': field_cond, - }) + if self._context.get("compute_from_input") and vals.get("field_name"): + field_name, field_cond = co.get_field_condition(vals["field_name"]) + field_cond = field_cond and "${%s}" % (field_cond or "") or False + vals.update( + {"field_name": field_name, "field_cond": field_cond,} + ) return vals class XLSXTemplateExport(models.Model): - _name = 'xlsx.template.export' - _description = 'Detailed of how excel data will be exported' - _order = 'sequence' + _name = "xlsx.template.export" + _description = "Detailed of how excel data will be exported" + _order = "sequence" template_id = fields.Many2one( - comodel_name='xlsx.template', - string='XLSX Template', + comodel_name="xlsx.template", + string="XLSX Template", index=True, - ondelete='cascade', + ondelete="cascade", readonly=True, ) - sequence = fields.Integer( - string='Sequence', - default=10, - ) - sheet = fields.Char( - string='Sheet', - ) + sequence = fields.Integer(string="Sequence", default=10,) + sheet = fields.Char(string="Sheet",) section_type = fields.Selection( - [('sheet', 'Sheet'), - ('head', 'Head'), - ('row', 'Row'), - ('data', 'Data')], - string='Section Type', + [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")], + string="Section Type", required=True, ) row_field = fields.Char( - string='Row Field', - help="If section type is row, this field is required", + string="Row Field", help="If section type is row, this field is required", ) is_cont = fields.Boolean( - string='Continue', - default=False, - help="Continue data rows after last data row", - ) - excel_cell = fields.Char( - string='Cell', - ) - field_name = fields.Char( - string='Field', - ) - field_cond = fields.Char( - string='Field Cond.', - ) - is_sum = fields.Boolean( - string='Sum', - default=False, - ) - style = fields.Char( - string='Default Style', - ) - style_cond = fields.Char( - string='Style w/Cond.', + string="Continue", default=False, help="Continue data rows after last data row", ) + excel_cell = fields.Char(string="Cell",) + field_name = fields.Char(string="Field",) + field_cond = fields.Char(string="Field Cond.",) + is_sum = fields.Boolean(string="Sum", default=False,) + style = fields.Char(string="Default Style",) + style_cond = fields.Char(string="Style w/Cond.",) @api.model def create(self, vals): @@ -437,16 +393,19 @@ class XLSXTemplateExport(models.Model): @api.model def _extract_field_name(self, vals): - if self._context.get('compute_from_input') and vals.get('field_name'): - field_name, field_cond = co.get_field_condition(vals['field_name']) + if self._context.get("compute_from_input") and vals.get("field_name"): + field_name, field_cond = co.get_field_condition(vals["field_name"]) field_cond = field_cond or 'value or ""' field_name, style = co.get_field_style(field_name) field_name, style_cond = co.get_field_style_cond(field_name) field_name, func = co.get_field_aggregation(field_name) - vals.update({'field_name': field_name, - 'field_cond': '${%s}' % (field_cond or ''), - 'style': '#{%s}' % (style or ''), - 'style_cond': '#?%s?' % (style_cond or ''), - 'is_sum': func == 'sum' and True or False, - }) + vals.update( + { + "field_name": field_name, + "field_cond": "${%s}" % (field_cond or ""), + "style": "#{%s}" % (style or ""), + "style_cond": "#?%s?" % (style_cond or ""), + "is_sum": func == "sum" and True or False, + } + ) return vals diff --git a/excel_import_export/static/src/js/report/action_manager_report.js b/excel_import_export/static/src/js/report/action_manager_report.js index 303df50a0..6b1330705 100644 --- a/excel_import_export/static/src/js/report/action_manager_report.js +++ b/excel_import_export/static/src/js/report/action_manager_report.js @@ -1,6 +1,6 @@ // Copyright 2019 Ecosoft Co., Ltd. // License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). -odoo.define("excel_import_export.report", function (require) { +odoo.define("excel_import_export.report", function(require) { "use strict"; var core = require("web.core"); @@ -11,23 +11,27 @@ odoo.define("excel_import_export.report", function (require) { var _t = core._t; ActionManager.include({ - - _downloadReportExcel: function (url, actions) { + _downloadReportExcel: function(url, actions) { framework.blockUI(); var def = $.Deferred(); var type = "excel"; var cloned_action = _.clone(actions); - if (_.isUndefined(cloned_action.data) || + if ( + _.isUndefined(cloned_action.data) || _.isNull(cloned_action.data) || - (_.isObject(cloned_action.data) && _.isEmpty(cloned_action.data))) - { + (_.isObject(cloned_action.data) && _.isEmpty(cloned_action.data)) + ) { if (cloned_action.context.active_ids) { - url += "/" + cloned_action.context.active_ids.join(','); + url += "/" + cloned_action.context.active_ids.join(","); } } else { - url += "?options=" + encodeURIComponent(JSON.stringify(cloned_action.data)); - url += "&context=" + encodeURIComponent(JSON.stringify(cloned_action.context)); + url += + "?options=" + + encodeURIComponent(JSON.stringify(cloned_action.data)); + url += + "&context=" + + encodeURIComponent(JSON.stringify(cloned_action.context)); } var blocked = !session.get_file({ @@ -36,7 +40,7 @@ odoo.define("excel_import_export.report", function (require) { data: JSON.stringify([url, type]), }, success: def.resolve.bind(def), - error: function () { + error: function() { crash_manager.rpc_error.apply(crash_manager, arguments); def.reject(); }, @@ -46,43 +50,48 @@ odoo.define("excel_import_export.report", function (require) { // AAB: this check should be done in get_file service directly, // should not be the concern of the caller (and that way, get_file // could return a deferred) - var message = _t('A popup window with your report was blocked. You ' + - 'may need to change your browser settings to allow ' + - 'popup windows for this page.'); - this.do_warn(_t('Warning'), message, true); + var message = _t( + "A popup window with your report was blocked. You " + + "may need to change your browser settings to allow " + + "popup windows for this page." + ); + this.do_warn(_t("Warning"), message, true); } return def; }, - _triggerDownload: function (action, options, type) { + _triggerDownload: function(action, options, type) { var self = this; var reportUrls = this._makeReportUrls(action); if (type === "excel") { - return this._downloadReportExcel(reportUrls[type], action).then(function () { - if (action.close_on_report_download) { - var closeAction = {type: 'ir.actions.act_window_close'}; - return self.doAction(closeAction, _.pick(options, 'on_close')); - } else { + return this._downloadReportExcel(reportUrls[type], action).then( + function() { + if (action.close_on_report_download) { + var closeAction = {type: "ir.actions.act_window_close"}; + return self.doAction( + closeAction, + _.pick(options, "on_close") + ); + } return options.on_close(); } - }); + ); } return this._super.apply(this, arguments); }, - _makeReportUrls: function (action) { + _makeReportUrls: function(action) { var reportUrls = this._super.apply(this, arguments); - reportUrls.excel = '/report/excel/' + action.report_name; + reportUrls.excel = "/report/excel/" + action.report_name; return reportUrls; }, - _executeReportAction: function (action, options) { + _executeReportAction: function(action, options) { var self = this; - if (action.report_type === 'excel') { - return self._triggerDownload(action, options, 'excel'); + if (action.report_type === "excel") { + return self._triggerDownload(action, options, "excel"); } return this._super.apply(this, arguments); - } + }, }); - }); diff --git a/excel_import_export/views/webclient_templates.xml b/excel_import_export/views/webclient_templates.xml index 96cdbdb22..db11d018c 100644 --- a/excel_import_export/views/webclient_templates.xml +++ b/excel_import_export/views/webclient_templates.xml @@ -1,11 +1,14 @@