[IMP] : black, isort, prettier

pull/2505/head
Kitti U 2020-08-24 17:57:03 +07:00 committed by Aungkokolin1997
parent f8166fafd2
commit cf30d5a77f
17 changed files with 1037 additions and 914 deletions

View File

@ -2,29 +2,24 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
{ {
'name': 'Excel Import/Export/Report', "name": "Excel Import/Export/Report",
'summary': 'Base module for developing Excel import/export/report', "summary": "Base module for developing Excel import/export/report",
'version': '12.0.1.0.4', "version": "12.0.1.0.4",
'author': 'Ecosoft,Odoo Community Association (OCA)', "author": "Ecosoft,Odoo Community Association (OCA)",
'license': 'AGPL-3', "license": "AGPL-3",
'website': 'https://github.com/OCA/server-tools/', "website": "https://github.com/OCA/server-tools/",
'category': 'Tools', "category": "Tools",
'depends': ['mail'], "depends": ["mail"],
'external_dependencies': { "external_dependencies": {"python": ["xlrd", "xlwt", "openpyxl",],},
'python': [ "data": [
'xlrd', "security/ir.model.access.csv",
'xlwt', "wizard/export_xlsx_wizard.xml",
'openpyxl', "wizard/import_xlsx_wizard.xml",
], "views/xlsx_template_view.xml",
}, "views/xlsx_report.xml",
'data': ['security/ir.model.access.csv', "views/webclient_templates.xml",
'wizard/export_xlsx_wizard.xml', ],
'wizard/import_xlsx_wizard.xml', "installable": True,
'views/xlsx_template_view.xml', "development_status": "beta",
'views/xlsx_report.xml', "maintainers": ["kittiu"],
'views/webclient_templates.xml',
],
'installable': True,
'development_status': 'beta',
'maintainers': ['kittiu'],
} }

View File

@ -1,52 +1,53 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import json
import base64 import base64
import json
import time 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.tools.safe_eval import safe_eval
from odoo.addons.web.controllers import main as report
class ReportController(report.ReportController): class ReportController(report.ReportController):
@route() @route()
def report_routes(self, reportname, docids=None, converter=None, **data): def report_routes(self, reportname, docids=None, converter=None, **data):
if converter == 'excel': if converter == "excel":
report = request.env['ir.actions.report']._get_report_from_name( report = request.env["ir.actions.report"]._get_report_from_name(reportname)
reportname)
context = dict(request.env.context) context = dict(request.env.context)
if docids: if docids:
docids = [int(i) for i in docids.split(',')] docids = [int(i) for i in docids.split(",")]
if data.get('options'): if data.get("options"):
data.update(json.loads(data.pop('options'))) data.update(json.loads(data.pop("options")))
if data.get('context'): if data.get("context"):
# Ignore 'lang' here, because the context in data is the one # Ignore 'lang' here, because the context in data is the one
# from the webclient *but* if the user explicitely wants to # from the webclient *but* if the user explicitely wants to
# change the lang, this mechanism overwrites it. # change the lang, this mechanism overwrites it.
data['context'] = json.loads(data['context']) data["context"] = json.loads(data["context"])
if data['context'].get('lang'): if data["context"].get("lang"):
del data['context']['lang'] del data["context"]["lang"]
context.update(data['context']) context.update(data["context"])
excel, report_name = report.with_context(context).render_excel( excel, report_name = report.with_context(context).render_excel(
docids, data=data docids, data=data
) )
excel = base64.decodestring(excel) excel = base64.decodestring(excel)
if report.print_report_name and not len(docids) > 1: if report.print_report_name and not len(docids) > 1:
obj = request.env[report.model].browse(docids[0]) obj = request.env[report.model].browse(docids[0])
file_ext = report_name.split('.')[-1:].pop() file_ext = report_name.split(".")[-1:].pop()
report_name = safe_eval(report.print_report_name, report_name = safe_eval(
{'object': obj, 'time': time}) 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)
) )
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 request.make_response(excel, headers=excelhttpheaders)
return super(ReportController, self).report_routes( return super(ReportController, self).report_routes(

View File

@ -1,20 +1,21 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import re
import uuid
import csv
import base64 import base64
import string import csv
import itertools import itertools
import logging import logging
from datetime import datetime as dt import re
import string
import uuid
from ast import literal_eval from ast import literal_eval
from dateutil.parser import parse from datetime import datetime as dt
from io import StringIO 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__) _logger = logging.getLogger(__name__)
try: try:
@ -26,40 +27,40 @@ except ImportError:
def adjust_cell_formula(value, k): def adjust_cell_formula(value, k):
""" Cell formula, i.e., if i=5, val=?(A11)+?(B12) -> val=A16+B17 """ """ Cell formula, i.e., if i=5, val=?(A11)+?(B12) -> val=A16+B17 """
if isinstance(value, str): if isinstance(value, str):
for i in range(value.count('?(')): for i in range(value.count("?(")):
if value and '?(' in value and ')' in value: if value and "?(" in value and ")" in value:
i = value.index('?(') i = value.index("?(")
j = value.index(')', i) j = value.index(")", i)
val = value[i + 2:j] val = value[i + 2 : j]
col, row = split_row_col(val) col, row = split_row_col(val)
new_val = '%s%s' % (col, row+k) new_val = "{}{}".format(col, row + k)
value = value.replace('?(%s)' % val, new_val) value = value.replace("?(%s)" % val, new_val)
return value return value
def get_field_aggregation(field): def get_field_aggregation(field):
""" i..e, 'field@{sum}' """ """ i..e, 'field@{sum}' """
if field and '@{' in field and '}' in field: if field and "@{" in field and "}" in field:
i = field.index('@{') i = field.index("@{")
j = field.index('}', i) j = field.index("}", i)
cond = field[i + 2:j] cond = field[i + 2 : j]
try: try:
if cond or cond == '': if cond or cond == "":
return (field[:i], cond) return (field[:i], cond)
except Exception: except Exception:
return (field.replace('@{%s}' % cond, ''), False) return (field.replace("@{%s}" % cond, ""), False)
return (field, False) return (field, False)
def get_field_condition(field): def get_field_condition(field):
""" i..e, 'field${value > 0 and value or False}' """ """ i..e, 'field${value > 0 and value or False}' """
if field and '${' in field and '}' in field: if field and "${" in field and "}" in field:
i = field.index('${') i = field.index("${")
j = field.index('}', i) j = field.index("}", i)
cond = field[i + 2:j] cond = field[i + 2 : j]
try: try:
if cond or cond == '': if cond or cond == "":
return (field.replace('${%s}' % cond, ''), cond) return (field.replace("${%s}" % cond, ""), cond)
except Exception: except Exception:
return (field, False) return (field, False)
return (field, False) return (field, False)
@ -74,13 +75,13 @@ def get_field_style(field):
- number = true, false - number = true, false
i.e., 'field#{font=bold;fill=red;align=center;style=number}' i.e., 'field#{font=bold;fill=red;align=center;style=number}'
""" """
if field and '#{' in field and '}' in field: if field and "#{" in field and "}" in field:
i = field.index('#{') i = field.index("#{")
j = field.index('}', i) j = field.index("}", i)
cond = field[i + 2:j] cond = field[i + 2 : j]
try: try:
if cond or cond == '': if cond or cond == "":
return (field.replace('#{%s}' % cond, ''), cond) return (field.replace("#{%s}" % cond, ""), cond)
except Exception: except Exception:
return (field, False) return (field, False)
return (field, False) return (field, False)
@ -88,39 +89,40 @@ def get_field_style(field):
def get_field_style_cond(field): def get_field_style_cond(field):
""" i..e, 'field#?object.partner_id and #{font=bold} or #{}?' """ """ i..e, 'field#?object.partner_id and #{font=bold} or #{}?' """
if field and '#?' in field and '?' in field: if field and "#?" in field and "?" in field:
i = field.index('#?') i = field.index("#?")
j = field.index('?', i+2) j = field.index("?", i + 2)
cond = field[i + 2:j] cond = field[i + 2 : j]
try: try:
if cond or cond == '': if cond or cond == "":
return (field.replace('#?%s?' % cond, ''), cond) return (field.replace("#?%s?" % cond, ""), cond)
except Exception: except Exception:
return (field, False) return (field, False)
return (field, False) return (field, False)
def fill_cell_style(field, field_style, styles): def fill_cell_style(field, field_style, styles):
field_styles = field_style.split(';') field_styles = field_style.split(";")
for f in field_styles: for f in field_styles:
(key, value) = f.split('=') (key, value) = f.split("=")
if key not in styles.keys(): 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(): if value.lower() not in styles[key].keys():
raise ValidationError( raise ValidationError(
_('Invalid value %s for style type %s' % (value, key))) _("Invalid value {} for style type {}".format(value, key))
)
cell_style = styles[key][value] cell_style = styles[key][value]
if key == 'font': if key == "font":
field.font = cell_style field.font = cell_style
if key == 'fill': if key == "fill":
field.fill = cell_style field.fill = cell_style
if key == 'align': if key == "align":
field.alignment = cell_style field.alignment = cell_style
if key == 'style': if key == "style":
if value == 'text': if value == "text":
try: try:
# In case value can't be encoded as utf, we do normal str() # 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: except Exception:
field.value = str(field.value) field.value = str(field.value)
field.number_format = cell_style field.number_format = cell_style
@ -128,10 +130,10 @@ def fill_cell_style(field, field_style, styles):
def get_line_max(line_field): def get_line_max(line_field):
""" i.e., line_field = line_ids[100], max = 100 else 0 """ """ i.e., line_field = line_ids[100], max = 100 else 0 """
if line_field and '[' in line_field and ']' in line_field: if line_field and "[" in line_field and "]" in line_field:
i = line_field.index('[') i = line_field.index("[")
j = line_field.index(']') j = line_field.index("]")
max_str = line_field[i + 1:j] max_str = line_field[i + 1 : j]
try: try:
if len(max_str) > 0: if len(max_str) > 0:
return (line_field[:i], int(max_str)) return (line_field[:i], int(max_str))
@ -144,10 +146,10 @@ def get_line_max(line_field):
def get_groupby(line_field): def get_groupby(line_field):
"""i.e., line_field = line_ids["a_id, b_id"], groupby = ["a_id", "b_id"]""" """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: if line_field and "[" in line_field and "]" in line_field:
i = line_field.index('[') i = line_field.index("[")
j = line_field.index(']') j = line_field.index("]")
groupby = literal_eval(line_field[i:j+1]) groupby = literal_eval(line_field[i : j + 1])
return groupby return groupby
return False return False
@ -155,7 +157,7 @@ def get_groupby(line_field):
def split_row_col(pos): def split_row_col(pos):
match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) match = re.match(r"([a-z]+)([0-9]+)", pos, re.I)
if not match: if not match:
raise ValidationError(_('Position %s is not valid') % pos) raise ValidationError(_("Position %s is not valid") % pos)
col, row = match.groups() col, row = match.groups()
return col, int(row) return col, int(row)
@ -199,9 +201,9 @@ def isinteger(input_val):
def isdatetime(input_val): def isdatetime(input_val):
try: try:
if len(input_val) == 10: if len(input_val) == 10:
dt.strptime(input_val, '%Y-%m-%d') dt.strptime(input_val, "%Y-%m-%d")
elif len(input_val) == 19: 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: else:
return False return False
return True return True
@ -211,14 +213,14 @@ def isdatetime(input_val):
def str_to_number(input_val): def str_to_number(input_val):
if isinstance(input_val, str): if isinstance(input_val, str):
if ' ' not in input_val: if " " not in input_val:
if isdatetime(input_val): if isdatetime(input_val):
return parse(input_val) return parse(input_val)
elif isinteger(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) return int(input_val)
elif isfloat(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 float(input_val)
return input_val return input_val
@ -239,25 +241,28 @@ def csv_from_excel(excel_content, delimiter, quote):
for x in sh.row_values(rownum): for x in sh.row_values(rownum):
if quoting == csv.QUOTE_NONE and delimiter in x: if quoting == csv.QUOTE_NONE and delimiter in x:
raise ValidationError( raise ValidationError(
_('Template with CSV Quoting = False, data must not ' _(
'contain the same char as delimiter -> "%s"') % "Template with CSV Quoting = False, data must not "
delimiter) 'contain the same char as delimiter -> "%s"'
)
% delimiter
)
row.append(x) row.append(x)
wr.writerow(row) wr.writerow(row)
content.seek(0) # Set index to 0, and start reading 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 return out_file
def pos2idx(pos): def pos2idx(pos):
match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) match = re.match(r"([a-z]+)([0-9]+)", pos, re.I)
if not match: if not match:
raise ValidationError(_('Position %s is not valid') % (pos, )) raise ValidationError(_("Position %s is not valid") % (pos,))
col, row = match.groups() col, row = match.groups()
col_num = 0 col_num = 0
for c in col: for c in col:
if c in string.ascii_letters: 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) 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 """ if not know, just get value as is """
value = False value = False
datemode = 0 # From book.datemode, but we fix it for simplicity datemode = 0 # From book.datemode, but we fix it for simplicity
if field_type in ['date', 'datetime']: if field_type in ["date", "datetime"]:
ctype = xlrd.sheet.ctype_text.get(cell.ctype, 'unknown type') ctype = xlrd.sheet.ctype_text.get(cell.ctype, "unknown type")
if ctype in ('xldate', 'number'): if ctype in ("xldate", "number"):
is_datetime = cell.value % 1 != 0.0 is_datetime = cell.value % 1 != 0.0
time_tuple = xlrd.xldate_as_tuple(cell.value, datemode) time_tuple = xlrd.xldate_as_tuple(cell.value, datemode)
date = dt(*time_tuple) date = dt(*time_tuple)
value = (date.strftime("%Y-%m-%d %H:%M:%S") value = (
if is_datetime else date.strftime("%Y-%m-%d")) date.strftime("%Y-%m-%d %H:%M:%S")
if is_datetime
else date.strftime("%Y-%m-%d")
)
else: else:
value = cell.value value = cell.value
elif field_type in ['integer', 'float']: elif field_type in ["integer", "float"]:
value_str = str(cell.value).strip().replace(',', '') value_str = str(cell.value).strip().replace(",", "")
if len(value_str) == 0: if len(value_str) == 0:
value = '' value = ""
elif value_str.replace('.', '', 1).isdigit(): # Is number elif value_str.replace(".", "", 1).isdigit(): # Is number
if field_type == 'integer': if field_type == "integer":
value = int(float(value_str)) value = int(float(value_str))
elif field_type == 'float': elif field_type == "float":
value = float(value_str) value = float(value_str)
else: # Is string, no conversion else: # Is string, no conversion
value = value_str value = value_str
elif field_type in ['many2one']: elif field_type in ["many2one"]:
# If number, change to string # If number, change to string
if isinstance(cell.value, (int, float, complex)): if isinstance(cell.value, (int, float, complex)):
value = str(cell.value) value = str(cell.value)
@ -297,38 +305,38 @@ def _get_cell_value(cell, field_type=False):
value = cell.value value = cell.value
# If string, cleanup # If string, cleanup
if isinstance(value, str): if isinstance(value, str):
if value[-2:] == '.0': if value[-2:] == ".0":
value = value[:-2] value = value[:-2]
# Except boolean, when no value, we should return as '' # Except boolean, when no value, we should return as ''
if field_type not in ['boolean']: if field_type not in ["boolean"]:
if not value: if not value:
value = '' value = ""
return value return value
def _add_column(column_name, column_value, file_txt): def _add_column(column_name, column_value, file_txt):
i = 0 i = 0
txt_lines = [] txt_lines = []
for line in file_txt.split('\n'): for line in file_txt.split("\n"):
if line and i == 0: if line and i == 0:
line = '"' + str(column_name) + '",' + line line = '"' + str(column_name) + '",' + line
elif line: elif line:
line = '"' + str(column_value) + '",' + line line = '"' + str(column_value) + '",' + line
txt_lines.append(line) txt_lines.append(line)
i += 1 i += 1
file_txt = '\n'.join(txt_lines) file_txt = "\n".join(txt_lines)
return file_txt return file_txt
def _add_id_column(file_txt): def _add_id_column(file_txt):
i = 0 i = 0
txt_lines = [] txt_lines = []
for line in file_txt.split('\n'): for line in file_txt.split("\n"):
if line and i == 0: if line and i == 0:
line = '"id",' + line line = '"id",' + line
elif line: elif line:
line = '%s.%s' % ('xls', uuid.uuid4()) + ',' + line line = "{}.{}".format("xls", uuid.uuid4()) + "," + line
txt_lines.append(line) txt_lines.append(line)
i += 1 i += 1
file_txt = '\n'.join(txt_lines) file_txt = "\n".join(txt_lines)
return file_txt return file_txt

View File

@ -1,7 +1,7 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # 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 from odoo.exceptions import UserError
@ -13,15 +13,18 @@ class ReportAction(models.Model):
@api.model @api.model
def render_excel(self, docids, data): def render_excel(self, docids, data):
if len(docids) != 1: if len(docids) != 1:
raise UserError( raise UserError(_("Only one id is allowed for excel_import_export"))
_('Only one id is allowed for excel_import_export')) xlsx_template = self.env["xlsx.template"].search(
xlsx_template = self.env['xlsx.template'].search( [("fname", "=", self.report_name), ("res_model", "=", self.model)]
[('fname', '=', self.report_name), ('res_model', '=', self.model)]) )
if not xlsx_template or len(xlsx_template) != 1: if not xlsx_template or len(xlsx_template) != 1:
raise UserError( raise UserError(
_("Template %s on model %s is not unique!" % _(
(self.report_name, self.model))) "Template %s on model %s is not unique!"
Export = self.env['xlsx.export'] % (self.report_name, self.model)
)
)
Export = self.env["xlsx.export"]
return Export.export_xlsx(xlsx_template, self.model, docids[0]) return Export.export_xlsx(xlsx_template, self.model, docids[0])
@api.model @api.model
@ -29,11 +32,11 @@ class ReportAction(models.Model):
res = super(ReportAction, self)._get_report_from_name(report_name) res = super(ReportAction, self)._get_report_from_name(report_name)
if res: if res:
return res return res
report_obj = self.env['ir.actions.report'] report_obj = self.env["ir.actions.report"]
qwebtypes = ['excel'] qwebtypes = ["excel"]
conditions = [ conditions = [
('report_type', 'in', qwebtypes), ("report_type", "in", qwebtypes),
('report_name', '=', report_name), ("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) return report_obj.with_context(context).search(conditions, limit=1)

View File

@ -1,48 +1,47 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import models, api
import logging import logging
from odoo import api, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
try: try:
from openpyxl.styles import colors, PatternFill, Alignment, Font from openpyxl.styles import colors, PatternFill, Alignment, Font
except ImportError: except ImportError:
_logger.debug( _logger.debug('Cannot import "openpyxl". Please make sure it is installed.')
'Cannot import "openpyxl". Please make sure it is installed.')
class XLSXStyles(models.AbstractModel): class XLSXStyles(models.AbstractModel):
_name = 'xlsx.styles' _name = "xlsx.styles"
_description = 'Available styles for excel' _description = "Available styles for excel"
@api.model @api.model
def get_openpyxl_styles(self): def get_openpyxl_styles(self):
""" List all syles that can be used with styleing directive #{...} """ """ List all syles that can be used with styleing directive #{...} """
return { return {
'font': { "font": {
'bold': Font(name="Arial", size=10, bold=True), "bold": Font(name="Arial", size=10, bold=True),
'bold_red': Font(name="Arial", size=10, "bold_red": Font(name="Arial", size=10, color=colors.RED, bold=True),
color=colors.RED, bold=True),
}, },
'fill': { "fill": {
'red': PatternFill("solid", fgColor="FF0000"), "red": PatternFill("solid", fgColor="FF0000"),
'grey': PatternFill("solid", fgColor="DDDDDD"), "grey": PatternFill("solid", fgColor="DDDDDD"),
'yellow': PatternFill("solid", fgColor="FFFCB7"), "yellow": PatternFill("solid", fgColor="FFFCB7"),
'blue': PatternFill("solid", fgColor="9BF3FF"), "blue": PatternFill("solid", fgColor="9BF3FF"),
'green': PatternFill("solid", fgColor="B0FF99"), "green": PatternFill("solid", fgColor="B0FF99"),
}, },
'align': { "align": {
'left': Alignment(horizontal='left'), "left": Alignment(horizontal="left"),
'center': Alignment(horizontal='center'), "center": Alignment(horizontal="center"),
'right': Alignment(horizontal='right'), "right": Alignment(horizontal="right"),
}, },
'style': { "style": {
'number': '#,##0.00', "number": "#,##0.00",
'date': 'dd/mm/yyyy', "date": "dd/mm/yyyy",
'datestamp': 'yyyy-mm-dd', "datestamp": "yyyy-mm-dd",
'text': '@', "text": "@",
'percent': '0.00%', "percent": "0.00%",
}, },
} }

View File

@ -1,16 +1,18 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import os
import logging
import base64 import base64
from io import BytesIO import logging
import os
import time import time
from datetime import date, datetime as dt from datetime import date, datetime as dt
from odoo.tools.float_utils import float_compare from io import BytesIO
from odoo import models, fields, api, _
from odoo.tools.safe_eval import safe_eval from odoo import _, api, fields, models
from odoo.exceptions import ValidationError 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 from . import common as co
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -18,26 +20,26 @@ try:
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.utils.exceptions import IllegalCharacterError from openpyxl.utils.exceptions import IllegalCharacterError
except ImportError: except ImportError:
_logger.debug( _logger.debug('Cannot import "openpyxl". Please make sure it is installed.')
'Cannot import "openpyxl". Please make sure it is installed.')
class XLSXExport(models.AbstractModel): class XLSXExport(models.AbstractModel):
_name = 'xlsx.export' _name = "xlsx.export"
_description = 'Excel Export AbstractModel' _description = "Excel Export AbstractModel"
@api.model @api.model
def get_eval_context(self, model, record, value): def get_eval_context(self, model, record, value):
eval_context = {'float_compare': float_compare, eval_context = {
'time': time, "float_compare": float_compare,
'datetime': dt, "time": time,
'date': date, "datetime": dt,
'value': value, "date": date,
'object': record, "value": value,
'model': self.env[model], "object": record,
'env': self.env, "model": self.env[model],
'context': self._context, "env": self.env,
} "context": self._context,
}
return eval_context return eval_context
@api.model @api.model
@ -48,12 +50,11 @@ class XLSXExport(models.AbstractModel):
- fields: fields in line_ids, i.e., partner_id.display_name - fields: fields in line_ids, i.e., partner_id.display_name
""" """
line_field, max_row = co.get_line_max(line_field) 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] lines = record[line_field]
if max_row > 0 and len(lines) > max_row: if max_row > 0 and len(lines) > max_row:
raise Exception( raise Exception(_("Records in %s exceed max records allowed") % line_field)
_('Records in %s exceed max records allowed') % line_field) vals = {field: [] for field in fields} # value and do_style
vals = dict([(field, []) for field in fields]) # value and do_style
# Get field condition & aggre function # Get field condition & aggre function
field_cond_dict = {} field_cond_dict = {}
aggre_func_dict = {} aggre_func_dict = {}
@ -77,31 +78,32 @@ class XLSXExport(models.AbstractModel):
for field in pair_fields: # (field, raw_field) for field in pair_fields: # (field, raw_field)
value = self._get_field_data(field[1], line) value = self._get_field_data(field[1], line)
eval_cond = field_cond_dict[field[0]] eval_cond = field_cond_dict[field[0]]
eval_context = \ eval_context = self.get_eval_context(line._name, line, value)
self.get_eval_context(line._name, line, value)
if eval_cond: if eval_cond:
value = safe_eval(eval_cond, eval_context) value = safe_eval(eval_cond, eval_context)
# style w/Cond takes priority # style w/Cond takes priority
style_cond = style_cond_dict[field[0]] style_cond = style_cond_dict[field[0]]
style = self._eval_style_cond(line._name, line, style = self._eval_style_cond(line._name, line, value, style_cond)
value, style_cond)
if style is None: if style is None:
style = False # No style style = False # No style
elif style is False: elif style is False:
style = field_style_dict[field[0]] # Use default style style = field_style_dict[field[0]] # Use default style
vals[field[0]].append((value, style)) vals[field[0]].append((value, style))
return (vals, aggre_func_dict,) return (
vals,
aggre_func_dict,
)
@api.model @api.model
def _eval_style_cond(self, model, record, value, style_cond): def _eval_style_cond(self, model, record, value, style_cond):
eval_context = self.get_eval_context(model, record, value) eval_context = self.get_eval_context(model, record, value)
field = style_cond = style_cond or '#??' field = style_cond = style_cond or "#??"
styles = {} styles = {}
for i in range(style_cond.count('#{')): for i in range(style_cond.count("#{")):
i += 1 i += 1
field, style = co.get_field_style(field) field, style = co.get_field_style(field)
styles.update({i: style}) 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: if not styles:
return False return False
res = safe_eval(style_cond, eval_context) 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) st = co.openpyxl_get_sheet_by_name(workbook, sheet_name)
elif isinstance(sheet_name, int): elif isinstance(sheet_name, int):
if sheet_name > len(workbook.worksheets): if sheet_name > len(workbook.worksheets):
raise Exception(_('Not enough worksheets')) raise Exception(_("Not enough worksheets"))
st = workbook.worksheets[sheet_name - 1] st = workbook.worksheets[sheet_name - 1]
if not st: if not st:
raise ValidationError( raise ValidationError(_("Sheet %s not found") % sheet_name)
_('Sheet %s not found') % sheet_name)
# Fill data, header and rows # Fill data, header and rows
self._fill_head(ws, st, record) self._fill_head(ws, st, record)
self._fill_lines(ws, st, record) self._fill_lines(ws, st, record)
except KeyError as e: except KeyError as e:
raise ValidationError(_('Key Error\n%s') % e) raise ValidationError(_("Key Error\n%s") % e)
except IllegalCharacterError as e: except IllegalCharacterError as e:
raise ValidationError( 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: except Exception as e:
raise ValidationError( raise ValidationError(_("Error filling data into Excel sheets\n%s") % e)
_('Error filling data into Excel sheets\n%s') % e)
@api.model @api.model
def _get_field_data(self, _field, _line): def _get_field_data(self, _field, _line):
@ -146,43 +150,41 @@ class XLSXExport(models.AbstractModel):
if not _field: if not _field:
return None return None
line_copy = _line line_copy = _line
for f in _field.split('.'): for f in _field.split("."):
line_copy = line_copy[f] line_copy = line_copy[f]
if isinstance(line_copy, str): if isinstance(line_copy, str):
line_copy = line_copy.encode('utf-8') line_copy = line_copy.encode("utf-8")
return line_copy return line_copy
@api.model @api.model
def _fill_head(self, ws, st, record): 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) tmp_field, eval_cond = co.get_field_condition(field)
eval_cond = eval_cond or 'value or ""' eval_cond = eval_cond or 'value or ""'
tmp_field, field_style = co.get_field_style(tmp_field) tmp_field, field_style = co.get_field_style(tmp_field)
tmp_field, style_cond = co.get_field_style_cond(tmp_field) tmp_field, style_cond = co.get_field_style_cond(tmp_field)
value = tmp_field and self._get_field_data(tmp_field, record) value = tmp_field and self._get_field_data(tmp_field, record)
# Eval # Eval
eval_context = self.get_eval_context(record._name, eval_context = self.get_eval_context(record._name, record, value)
record, value)
if eval_cond: if eval_cond:
value = safe_eval(eval_cond, eval_context) value = safe_eval(eval_cond, eval_context)
if value is not None: if value is not None:
st[rc] = value st[rc] = value
fc = not style_cond and True or \ fc = not style_cond and True or safe_eval(style_cond, eval_context)
safe_eval(style_cond, eval_context)
if field_style and fc: # has style and pass style_cond 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) co.fill_cell_style(st[rc], field_style, styles)
@api.model @api.model
def _fill_lines(self, ws, st, record): def _fill_lines(self, ws, st, record):
line_fields = list(ws) line_fields = list(ws)
if '_HEAD_' in line_fields: if "_HEAD_" in line_fields:
line_fields.remove('_HEAD_') line_fields.remove("_HEAD_")
cont_row = 0 # last data row to continue cont_row = 0 # last data row to continue
for line_field in line_fields: for line_field in line_fields:
fields = ws.get(line_field, {}).values() fields = ws.get(line_field, {}).values()
vals, func = self._get_line_vals(record, line_field, fields) 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 cont_set = 0
rows_inserted = False # flag to insert row rows_inserted = False # flag to insert row
for rc, field in ws.get(line_field, {}).items(): for rc, field in ws.get(line_field, {}).items():
@ -192,7 +194,7 @@ class XLSXExport(models.AbstractModel):
cont_set = cont_row + 1 cont_set = cont_row + 1
if is_cont: if is_cont:
row = cont_set row = cont_set
rc = '%s%s' % (col, cont_set) rc = "{}{}".format(col, cont_set)
i = 0 i = 0
new_row = 0 new_row = 0
new_rc = False new_rc = False
@ -200,24 +202,24 @@ class XLSXExport(models.AbstractModel):
# Insert rows to preserve total line # Insert rows to preserve total line
if not rows_inserted: if not rows_inserted:
rows_inserted = True 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]: for (row_val, style) in vals[field]:
new_row = row + i 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) 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) st[new_rc] = co.str_to_number(row_val)
if style: 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) co.fill_cell_style(st[new_rc], style, styles)
i += 1 i += 1
# Add footer line if at least one field have sum # Add footer line if at least one field have sum
f = func.get(field, False) f = func.get(field, False)
if f and new_row > 0: if f and new_row > 0:
new_row += 1 new_row += 1
f_rc = '%s%s' % (col, new_row) f_rc = "{}{}".format(col, new_row)
st[f_rc] = '=%s(%s:%s)' % (f, rc, new_rc) st[f_rc] = "={}({}:{})".format(f, rc, new_rc)
co.fill_cell_style(st[f_rc], style, styles) co.fill_cell_style(st[f_rc], style, styles)
cont_row = cont_row < new_row and new_row or cont_row cont_row = cont_row < new_row and new_row or cont_row
return return
@ -227,7 +229,7 @@ class XLSXExport(models.AbstractModel):
if template.res_model != res_model: if template.res_model != res_model:
raise ValidationError(_("Template's model mismatch")) raise ValidationError(_("Template's model mismatch"))
data_dict = co.literal_eval(template.instruction.strip()) 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 out_name = template.name
if not export_dict: # If there is not __EXPORT__ formula, just export if not export_dict: # If there is not __EXPORT__ formula, just export
out_name = template.fname out_name = template.fname
@ -235,11 +237,11 @@ class XLSXExport(models.AbstractModel):
return (out_file, out_name) return (out_file, out_name)
# Prepare temp file (from now, only xlsx file works for openpyxl) # Prepare temp file (from now, only xlsx file works for openpyxl)
decoded_data = base64.decodestring(template.datas) decoded_data = base64.decodestring(template.datas)
ConfParam = self.env['ir.config_parameter'].sudo() ConfParam = self.env["ir.config_parameter"].sudo()
ptemp = ConfParam.get_param('path_temp_file') or '/tmp' ptemp = ConfParam.get_param("path_temp_file") or "/tmp"
stamp = dt.utcnow().strftime('%H%M%S%f')[:-3] stamp = dt.utcnow().strftime("%H%M%S%f")[:-3]
ftemp = '%s/temp%s.xlsx' % (ptemp, stamp) ftemp = "{}/temp{}.xlsx".format(ptemp, stamp)
f = open(ftemp, 'wb') f = open(ftemp, "wb")
f.write(decoded_data) f.write(decoded_data)
f.seek(0) f.seek(0)
f.close() f.close()
@ -254,19 +256,18 @@ class XLSXExport(models.AbstractModel):
wb.save(content) wb.save(content)
content.seek(0) # Set index to 0, and start reading content.seek(0) # Set index to 0, and start reading
out_file = base64.encodestring(content.read()) out_file = base64.encodestring(content.read())
if record and 'name' in record and record.name: if record and "name" in record and record.name:
out_name = record.name.replace(' ', '').replace('/', '') out_name = record.name.replace(" ", "").replace("/", "")
else: else:
fname = out_name.replace(' ', '').replace('/', '') fname = out_name.replace(" ", "").replace("/", "")
ts = fields.Datetime.context_timestamp(self, dt.now()) 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: if not out_name or len(out_name) == 0:
out_name = 'noname' out_name = "noname"
out_ext = 'xlsx' out_ext = "xlsx"
# CSV (convert only on 1st sheet) # CSV (convert only on 1st sheet)
if template.to_csv: if template.to_csv:
delimiter = template.csv_delimiter delimiter = template.csv_delimiter
out_file = co.csv_from_excel(out_file, delimiter, out_file = co.csv_from_excel(out_file, delimiter, template.csv_quote)
template.csv_quote)
out_ext = template.csv_extension out_ext = template.csv_extension
return (out_file, '%s.%s' % (out_name, out_ext)) return (out_file, "{}.{}".format(out_name, out_ext))

View File

@ -2,53 +2,61 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import base64 import base64
import uuid
import xlrd
import xlwt
import time import time
from io import BytesIO import uuid
from . import common as co
from ast import literal_eval from ast import literal_eval
from datetime import date, datetime as dt from datetime import date, datetime as dt
from odoo.tools.float_utils import float_compare from io import BytesIO
from odoo import models, api, _
import xlrd
import xlwt
from odoo import _, api, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tools.float_utils import float_compare
from odoo.tools.safe_eval import safe_eval from odoo.tools.safe_eval import safe_eval
from . import common as co
class XLSXImport(models.AbstractModel): class XLSXImport(models.AbstractModel):
_name = 'xlsx.import' _name = "xlsx.import"
_description = 'Excel Import AbstractModel' _description = "Excel Import AbstractModel"
@api.model @api.model
def get_eval_context(self, model=False, value=False): def get_eval_context(self, model=False, value=False):
eval_context = {'float_compare': float_compare, eval_context = {
'time': time, "float_compare": float_compare,
'datetime': dt, "time": time,
'date': date, "datetime": dt,
'env': self.env, "date": date,
'context': self._context, "env": self.env,
'value': False, "context": self._context,
'model': False, "value": False,
} "model": False,
}
if model: if model:
eval_context.update({'model': self.env[model]}) eval_context.update({"model": self.env[model]})
if value: if value:
if isinstance(value, str): # Remove non Ord 128 character if isinstance(value, str): # Remove non Ord 128 character
value = ''.join([i if ord(i) < 128 else ' ' for i in value]) value = "".join([i if ord(i) < 128 else " " for i in value])
eval_context.update({'value': value}) eval_context.update({"value": value})
return eval_context return eval_context
@api.model @api.model
def get_external_id(self, record): def get_external_id(self, record):
""" Get external ID of the record, if not already exists create one """ """ 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() xml_id = record.get_external_id()
if not xml_id or (record.id in xml_id and xml_id[record.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), ModelData.create(
'module': 'excel_import_export', {
'model': record._name, "name": "{}_{}".format(record._table, record.id),
'res_id': record.id, }) "module": "excel_import_export",
"model": record._name,
"res_id": record.id,
}
)
xml_id = record.get_external_id() xml_id = record.get_external_id()
return xml_id[record.id] return xml_id[record.id]
@ -56,15 +64,16 @@ class XLSXImport(models.AbstractModel):
def _get_field_type(self, model, field): def _get_field_type(self, model, field):
try: try:
record = self.env[model].new() record = self.env[model].new()
for f in field.split('/'): for f in field.split("/"):
field_type = record._fields[f].type field_type = record._fields[f].type
if field_type in ('one2many', 'many2many'): if field_type in ("one2many", "many2many"):
record = record[f] record = record[f]
else: else:
return field_type return field_type
except Exception: except Exception:
raise ValidationError( raise ValidationError(
_('Invalid declaration, %s has no valid field type') % field) _("Invalid declaration, %s has no valid field type") % field
)
@api.model @api.model
def _delete_record_data(self, record, data_dict): def _delete_record_data(self, record, data_dict):
@ -74,19 +83,19 @@ class XLSXImport(models.AbstractModel):
try: try:
for sheet_name in data_dict: for sheet_name in data_dict:
worksheet = data_dict[sheet_name] 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: 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]: if line_field in record and record[line_field]:
record[line_field].unlink() record[line_field].unlink()
# Remove _NODEL_ from dict # Remove _NODEL_ from dict
for s, sv in data_dict.items(): for s, sv in data_dict.items():
for f, fv in data_dict[s].items(): for f, fv in data_dict[s].items():
if '_NODEL_' in f: if "_NODEL_" in f:
new_fv = data_dict[s].pop(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: except Exception as e:
raise ValidationError(_('Error deleting data\n%s') % e) raise ValidationError(_("Error deleting data\n%s") % e)
@api.model @api.model
def _get_line_vals(self, st, worksheet, model, line_field): 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) rc, key_eval_cond = co.get_field_condition(rc)
x_field, val_eval_cond = co.get_field_condition(field) x_field, val_eval_cond = co.get_field_condition(field)
row, col = co.pos2idx(rc) 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) field_type = self._get_field_type(model, out_field)
vals.update({out_field: []}) vals.update({out_field: []})
for idx in range(row, st.nrows): for idx in range(row, st.nrows):
value = co._get_cell_value(st.cell(idx, col), value = co._get_cell_value(st.cell(idx, col), field_type=field_type)
field_type=field_type) eval_context = self.get_eval_context(model=model, value=value)
eval_context = self.get_eval_context(model=model,
value=value)
if key_eval_cond: if key_eval_cond:
value = safe_eval(key_eval_cond, eval_context) value = safe_eval(key_eval_cond, eval_context)
if val_eval_cond: if val_eval_cond:
value = safe_eval(val_eval_cond, eval_context) value = safe_eval(val_eval_cond, eval_context)
vals[out_field].append(value) 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) vals.pop(out_field)
return vals return vals
@ -128,11 +135,14 @@ class XLSXImport(models.AbstractModel):
col_idx = 0 col_idx = 0
out_wb = xlwt.Workbook() out_wb = xlwt.Workbook()
out_st = out_wb.add_sheet("Sheet 1") out_st = out_wb.add_sheet("Sheet 1")
xml_id = record and self.get_external_id(record) or \ xml_id = (
'%s.%s' % ('xls', uuid.uuid4()) record
out_st.write(0, 0, 'id') # id and xml_id on first column 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) out_st.write(1, 0, xml_id)
header_fields.append('id') header_fields.append("id")
col_idx += 1 col_idx += 1
model = record._name model = record._name
for sheet_name in data_dict: # For each Sheet for sheet_name in data_dict: # For each Sheet
@ -143,22 +153,21 @@ class XLSXImport(models.AbstractModel):
elif isinstance(sheet_name, int): elif isinstance(sheet_name, int):
st = wb.sheet_by_index(sheet_name - 1) st = wb.sheet_by_index(sheet_name - 1)
if not st: if not st:
raise ValidationError( raise ValidationError(_("Sheet %s not found") % sheet_name)
_('Sheet %s not found') % sheet_name)
# HEAD updates # 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) rc, key_eval_cond = co.get_field_condition(rc)
field, val_eval_cond = co.get_field_condition(field) field, val_eval_cond = co.get_field_condition(field)
field_type = self._get_field_type(model, field) field_type = self._get_field_type(model, field)
value = False value = False
try: try:
row, col = co.pos2idx(rc) row, col = co.pos2idx(rc)
value = co._get_cell_value(st.cell(row, col), value = co._get_cell_value(
field_type=field_type) st.cell(row, col), field_type=field_type
)
except Exception: except Exception:
pass pass
eval_context = self.get_eval_context(model=model, eval_context = self.get_eval_context(model=model, value=value)
value=value)
if key_eval_cond: if key_eval_cond:
value = str(safe_eval(key_eval_cond, eval_context)) value = str(safe_eval(key_eval_cond, eval_context))
if val_eval_cond: if val_eval_cond:
@ -168,10 +177,9 @@ class XLSXImport(models.AbstractModel):
header_fields.append(field) header_fields.append(field)
col_idx += 1 col_idx += 1
# Line Items # Line Items
line_fields = filter(lambda x: x != '_HEAD_', worksheet) line_fields = filter(lambda x: x != "_HEAD_", worksheet)
for line_field in line_fields: for line_field in line_fields:
vals = self._get_line_vals(st, worksheet, vals = self._get_line_vals(st, worksheet, model, line_field)
model, line_field)
for field in vals: for field in vals:
# Columns, i.e., line_ids/field_id # Columns, i.e., line_ids/field_id
out_st.write(0, col_idx, field) 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 content.seek(0) # Set index to 0, and start reading
xls_file = content.read() xls_file = content.read()
# Do the import # Do the import
Import = self.env['base_import.import'] Import = self.env["base_import.import"]
imp = Import.create({ imp = Import.create(
'res_model': model, {
'file': xls_file, "res_model": model,
'file_type': 'application/vnd.ms-excel', "file": xls_file,
'file_name': 'temp.xls', "file_type": "application/vnd.ms-excel",
}) "file_name": "temp.xls",
}
)
errors = imp.do( errors = imp.do(
header_fields, header_fields,
header_fields, header_fields,
{'headers': True, {
'advanced': True, "headers": True,
'keep_matches': False, "advanced": True,
'encoding': '', "keep_matches": False,
'separator': '', "encoding": "",
'quoting': '"', "separator": "",
'date_format': '%Y-%m-%d', "quoting": '"',
'datetime_format': '%Y-%m-%d %H:%M:%S', "date_format": "%Y-%m-%d",
'float_thousand_separator': ',', "datetime_format": "%Y-%m-%d %H:%M:%S",
'float_decimal_separator': '.', "float_thousand_separator": ",",
'fields': []}) "float_decimal_separator": ".",
if errors.get('messages'): "fields": [],
message = _('Error importing data') },
messages = errors['messages'] )
if errors.get("messages"):
message = _("Error importing data")
messages = errors["messages"]
if isinstance(messages, dict): if isinstance(messages, dict):
message = messages['message'] message = messages["message"]
if isinstance(messages, list): if isinstance(messages, list):
message = ', '.join([x['message'] for x in messages]) message = ", ".join([x["message"] for x in messages])
raise ValidationError(message.encode('utf-8')) raise ValidationError(message.encode("utf-8"))
return self.env.ref(xml_id) return self.env.ref(xml_id)
except xlrd.XLRDError: except xlrd.XLRDError:
raise ValidationError( raise ValidationError(
_('Invalid file style, only .xls or .xlsx file allowed')) _("Invalid file style, only .xls or .xlsx file allowed")
)
except Exception as e: except Exception as e:
raise ValidationError(_('Error importing data\n%s') % e) raise ValidationError(_("Error importing data\n%s") % e)
@api.model @api.model
def _post_import_operation(self, record, operation): def _post_import_operation(self, record, operation):
@ -229,16 +243,15 @@ class XLSXImport(models.AbstractModel):
if not record or not operation: if not record or not operation:
return return
try: try:
if '${' in operation: if "${" in operation:
code = (operation.split('${'))[1].split('}')[0] code = (operation.split("${"))[1].split("}")[0]
eval_context = {'object': record} eval_context = {"object": record}
safe_eval(code, eval_context) safe_eval(code, eval_context)
except Exception as e: except Exception as e:
raise ValidationError(_('Post import operation error\n%s') % e) raise ValidationError(_("Post import operation error\n%s") % e)
@api.model @api.model
def import_xlsx(self, import_file, template, def import_xlsx(self, import_file, template, res_model=False, res_id=False):
res_model=False, res_id=False):
""" """
- If res_id = False, we want to create new document first - If res_id = False, we want to create new document first
- Delete fields' data according to data_dict['__IMPORT__'] - Delete fields' data according to data_dict['__IMPORT__']
@ -249,16 +262,16 @@ class XLSXImport(models.AbstractModel):
raise ValidationError(_("Template's model mismatch")) raise ValidationError(_("Template's model mismatch"))
record = self.env[template.res_model].browse(res_id) record = self.env[template.res_model].browse(res_id)
data_dict = literal_eval(template.instruction.strip()) data_dict = literal_eval(template.instruction.strip())
if not data_dict.get('__IMPORT__'): if not data_dict.get("__IMPORT__"):
raise ValidationError( raise ValidationError(
_("No data_dict['__IMPORT__'] in template %s") % template.name) _("No data_dict['__IMPORT__'] in template %s") % template.name
)
if record: if record:
# Delete existing data first # 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 # Fill up record with data from excel sheets
record = self._import_record_data(import_file, record, record = self._import_record_data(import_file, record, data_dict["__IMPORT__"])
data_dict['__IMPORT__'])
# Post Import Operation, i.e., cleanup some data # Post Import Operation, i.e., cleanup some data
if data_dict.get('__POST_IMPORT__', False): if data_dict.get("__POST_IMPORT__", False):
self._post_import_operation(record, data_dict['__POST_IMPORT__']) self._post_import_operation(record, data_dict["__POST_IMPORT__"])
return record return record

View File

@ -1,69 +1,58 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # 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 from odoo.exceptions import ValidationError
class XLSXReport(models.AbstractModel): class XLSXReport(models.AbstractModel):
""" Common class for xlsx reporting wizard """ """ Common class for xlsx reporting wizard """
_name = 'xlsx.report'
_description = 'Excel Report AbstractModel'
name = fields.Char( _name = "xlsx.report"
string='File Name', _description = "Excel Report AbstractModel"
readonly=True,
size=500, name = fields.Char(string="File Name", readonly=True, size=500,)
) data = fields.Binary(string="File", readonly=True,)
data = fields.Binary(
string='File',
readonly=True,
)
template_id = fields.Many2one( template_id = fields.Many2one(
'xlsx.template', "xlsx.template",
string='Template', string="Template",
required=True, required=True,
ondelete='cascade', ondelete="cascade",
domain=lambda self: self._context.get('template_domain', []), domain=lambda self: self._context.get("template_domain", []),
)
choose_template = fields.Boolean(
string='Allow Choose Template',
default=False,
) )
choose_template = fields.Boolean(string="Allow Choose Template", default=False,)
state = fields.Selection( state = fields.Selection(
[('choose', 'Choose'), [("choose", "Choose"), ("get", "Get")],
('get', 'Get')], default="choose",
default='choose',
help="* Choose: wizard show in user selection mode" 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 @api.model
def default_get(self, fields): def default_get(self, fields):
template_domain = self._context.get('template_domain', []) template_domain = self._context.get("template_domain", [])
templates = self.env['xlsx.template'].search(template_domain) templates = self.env["xlsx.template"].search(template_domain)
if not templates: if not templates:
raise ValidationError(_('No template found')) raise ValidationError(_("No template found"))
defaults = super(XLSXReport, self).default_get(fields) defaults = super(XLSXReport, self).default_get(fields)
for template in templates: for template in templates:
if not template.datas: if not template.datas:
raise ValidationError(_('No file in %s') % (template.name,)) raise ValidationError(_("No file in %s") % (template.name,))
defaults['template_id'] = len(templates) == 1 and templates.id or False defaults["template_id"] = len(templates) == 1 and templates.id or False
return defaults return defaults
@api.multi @api.multi
def report_xlsx(self): def report_xlsx(self):
self.ensure_one() self.ensure_one()
Export = self.env['xlsx.export'] Export = self.env["xlsx.export"]
out_file, out_name = \ out_file, out_name = Export.export_xlsx(self.template_id, self._name, self.id)
Export.export_xlsx(self.template_id, self._name, self.id) self.write({"state": "get", "data": out_file, "name": out_name})
self.write({'state': 'get', 'data': out_file, 'name': out_name})
return { return {
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'res_model': self._name, "res_model": self._name,
'view_mode': 'form', "view_mode": "form",
'view_type': 'form', "view_type": "form",
'res_id': self.id, "res_id": self.id,
'views': [(False, 'form')], "views": [(False, "form")],
'target': 'new', "target": "new",
} }

View File

@ -1,14 +1,16 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import os
import base64 import base64
import os
from ast import literal_eval 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 os.path import join as opj
from . import common as co
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.modules.module import get_module_path
from . import common as co
class XLSXTemplate(models.Model): class XLSXTemplate(models.Model):
@ -17,105 +19,96 @@ class XLSXTemplate(models.Model):
- Import/Export Meta Data (dict text) - Import/Export Meta Data (dict text)
- Default values, etc. - Default values, etc.
""" """
_name = 'xlsx.template'
_description = 'Excel template file and instruction'
_order = 'name'
name = fields.Char( _name = "xlsx.template"
string='Template Name', _description = "Excel template file and instruction"
required=True, _order = "name"
)
name = fields.Char(string="Template Name", required=True,)
res_model = fields.Char( res_model = fields.Char(
string='Resource Model', string="Resource Model",
help="The database object this attachment will be attached to.", help="The database object this attachment will be attached to.",
) )
fname = fields.Char( fname = fields.Char(string="File Name",)
string='File Name',
)
gname = fields.Char( gname = fields.Char(
string='Group Name', string="Group Name",
help="Multiple template of same model, can belong to same group,\n" help="Multiple template of same model, can belong to same group,\n"
"result in multiple template selection", "result in multiple template selection",
) )
description = fields.Char( description = fields.Char(string="Description",)
string='Description',
)
input_instruction = fields.Text( input_instruction = fields.Text(
string='Instruction (Input)', string="Instruction (Input)",
help="This is used to construct instruction in tab Import/Export", help="This is used to construct instruction in tab Import/Export",
) )
instruction = fields.Text( instruction = fields.Text(
string='Instruction', string="Instruction",
compute='_compute_output_instruction', compute="_compute_output_instruction",
help="Instruction on how to import/export, prepared by system." 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,
) )
datas = fields.Binary(string="File Content",)
to_csv = fields.Boolean(string="Convert to CSV?", default=False,)
csv_delimiter = fields.Char( csv_delimiter = fields.Char(
string='CSV Delimiter', string="CSV Delimiter",
size=1, size=1,
default=',', default=",",
required=True, required=True,
help="Optional for CSV, default is comma.", help="Optional for CSV, default is comma.",
) )
csv_extension = fields.Char( csv_extension = fields.Char(
string='CSV File Extension', string="CSV File Extension",
size=5, size=5,
default='csv', default="csv",
required=True, required=True,
help="Optional for CSV, default is .csv" help="Optional for CSV, default is .csv",
) )
csv_quote = fields.Boolean( csv_quote = fields.Boolean(
string='CSV Quoting', string="CSV Quoting",
default=True, default=True,
help="Optional for CSV, default is full quoting." help="Optional for CSV, default is full quoting.",
) )
export_ids = fields.One2many( export_ids = fields.One2many(
comodel_name='xlsx.template.export', comodel_name="xlsx.template.export", inverse_name="template_id",
inverse_name='template_id',
) )
import_ids = fields.One2many( import_ids = fields.One2many(
comodel_name='xlsx.template.import', comodel_name="xlsx.template.import", inverse_name="template_id",
inverse_name='template_id',
) )
post_import_hook = fields.Char( 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" help="Call a function after successful import, i.e.,\n"
"${object.post_import_do_something()}", "${object.post_import_do_something()}",
) )
show_instruction = fields.Boolean( show_instruction = fields.Boolean(
string='Show Output', string="Show Output",
default=False, default=False,
help="This is the computed instruction based on tab Import/Export,\n" help="This is the computed instruction based on tab Import/Export,\n"
"to be used by xlsx import/export engine", "to be used by xlsx import/export engine",
) )
redirect_action = fields.Many2one( redirect_action = fields.Many2one(
comodel_name='ir.actions.act_window', comodel_name="ir.actions.act_window",
string='Return Action', string="Return Action",
domain=[('type', '=', 'ir.actions.act_window')], domain=[("type", "=", "ir.actions.act_window")],
help="Optional action, redirection after finish import operation", help="Optional action, redirection after finish import operation",
) )
@api.multi @api.multi
@api.constrains('redirect_action', 'res_model') @api.constrains("redirect_action", "res_model")
def _check_action_model(self): def _check_action_model(self):
for rec in self: for rec in self:
if rec.res_model and rec.redirect_action and \ if (
rec.res_model != rec.redirect_action.res_model: rec.res_model
raise ValidationError(_('The selected redirect action is ' and rec.redirect_action
'not for model %s') % rec.res_model) 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 @api.model
def load_xlsx_template(self, tempalte_ids, addon=False): def load_xlsx_template(self, tempalte_ids, addon=False):
for template in self.browse(tempalte_ids): for template in self.browse(tempalte_ids):
if not addon: if not addon:
addon = list(template.get_external_id(). addon = list(template.get_external_id().values())[0].split(".")[0]
values())[0].split('.')[0]
addon_path = get_module_path(addon) addon_path = get_module_path(addon)
file_path = False file_path = False
for root, dirs, files in os.walk(addon_path): for root, dirs, files in os.walk(addon_path):
@ -123,13 +116,13 @@ class XLSXTemplate(models.Model):
if name == template.fname: if name == template.fname:
file_path = os.path.abspath(opj(root, name)) file_path = os.path.abspath(opj(root, name))
if file_path: if file_path:
template.datas = base64.b64encode(open(file_path, 'rb').read()) template.datas = base64.b64encode(open(file_path, "rb").read())
return True return True
@api.model @api.model
def create(self, vals): def create(self, vals):
rec = super().create(vals) rec = super().create(vals)
if vals.get('input_instruction'): if vals.get("input_instruction"):
rec._compute_input_export_instruction() rec._compute_input_export_instruction()
rec._compute_input_import_instruction() rec._compute_input_import_instruction()
rec._compute_input_post_import_hook() rec._compute_input_post_import_hook()
@ -138,7 +131,7 @@ class XLSXTemplate(models.Model):
@api.multi @api.multi
def write(self, vals): def write(self, vals):
res = super().write(vals) res = super().write(vals)
if vals.get('input_instruction'): if vals.get("input_instruction"):
for rec in self: for rec in self:
rec._compute_input_export_instruction() rec._compute_input_export_instruction()
rec._compute_input_import_instruction() rec._compute_input_import_instruction()
@ -152,7 +145,7 @@ class XLSXTemplate(models.Model):
# Export Instruction # Export Instruction
input_dict = literal_eval(rec.input_instruction.strip()) input_dict = literal_eval(rec.input_instruction.strip())
rec.export_ids.unlink() rec.export_ids.unlink()
export_dict = input_dict.get('__EXPORT__') export_dict = input_dict.get("__EXPORT__")
if not export_dict: if not export_dict:
continue continue
export_lines = [] export_lines = []
@ -160,34 +153,36 @@ class XLSXTemplate(models.Model):
# Sheet # Sheet
for sheet, rows in export_dict.items(): for sheet, rows in export_dict.items():
sequence += 1 sequence += 1
vals = {'sequence': sequence, vals = {
'section_type': 'sheet', "sequence": sequence,
'sheet': str(sheet), "section_type": "sheet",
} "sheet": str(sheet),
}
export_lines.append((0, 0, vals)) export_lines.append((0, 0, vals))
# Rows # Rows
for row_field, lines in rows.items(): for row_field, lines in rows.items():
sequence += 1 sequence += 1
is_cont = False is_cont = False
if '_CONT_' in row_field: if "_CONT_" in row_field:
is_cont = True is_cont = True
row_field = row_field.replace('_CONT_', '') row_field = row_field.replace("_CONT_", "")
vals = {'sequence': sequence, vals = {
'section_type': (row_field == '_HEAD_' and "sequence": sequence,
'head' or 'row'), "section_type": (row_field == "_HEAD_" and "head" or "row"),
'row_field': row_field, "row_field": row_field,
'is_cont': is_cont, "is_cont": is_cont,
} }
export_lines.append((0, 0, vals)) export_lines.append((0, 0, vals))
for excel_cell, field_name in lines.items(): for excel_cell, field_name in lines.items():
sequence += 1 sequence += 1
vals = {'sequence': sequence, vals = {
'section_type': 'data', "sequence": sequence,
'excel_cell': excel_cell, "section_type": "data",
'field_name': field_name, "excel_cell": excel_cell,
} "field_name": field_name,
}
export_lines.append((0, 0, vals)) export_lines.append((0, 0, vals))
rec.write({'export_ids': export_lines}) rec.write({"export_ids": export_lines})
@api.multi @api.multi
def _compute_input_import_instruction(self): def _compute_input_import_instruction(self):
@ -196,7 +191,7 @@ class XLSXTemplate(models.Model):
# Import Instruction # Import Instruction
input_dict = literal_eval(rec.input_instruction.strip()) input_dict = literal_eval(rec.input_instruction.strip())
rec.import_ids.unlink() rec.import_ids.unlink()
import_dict = input_dict.get('__IMPORT__') import_dict = input_dict.get("__IMPORT__")
if not import_dict: if not import_dict:
continue continue
import_lines = [] import_lines = []
@ -204,34 +199,36 @@ class XLSXTemplate(models.Model):
# Sheet # Sheet
for sheet, rows in import_dict.items(): for sheet, rows in import_dict.items():
sequence += 1 sequence += 1
vals = {'sequence': sequence, vals = {
'section_type': 'sheet', "sequence": sequence,
'sheet': str(sheet), "section_type": "sheet",
} "sheet": str(sheet),
}
import_lines.append((0, 0, vals)) import_lines.append((0, 0, vals))
# Rows # Rows
for row_field, lines in rows.items(): for row_field, lines in rows.items():
sequence += 1 sequence += 1
no_delete = False no_delete = False
if '_NODEL_' in row_field: if "_NODEL_" in row_field:
no_delete = True no_delete = True
row_field = row_field.replace('_NODEL_', '') row_field = row_field.replace("_NODEL_", "")
vals = {'sequence': sequence, vals = {
'section_type': (row_field == '_HEAD_' and "sequence": sequence,
'head' or 'row'), "section_type": (row_field == "_HEAD_" and "head" or "row"),
'row_field': row_field, "row_field": row_field,
'no_delete': no_delete, "no_delete": no_delete,
} }
import_lines.append((0, 0, vals)) import_lines.append((0, 0, vals))
for excel_cell, field_name in lines.items(): for excel_cell, field_name in lines.items():
sequence += 1 sequence += 1
vals = {'sequence': sequence, vals = {
'section_type': 'data', "sequence": sequence,
'excel_cell': excel_cell, "section_type": "data",
'field_name': field_name, "excel_cell": excel_cell,
} "field_name": field_name,
}
import_lines.append((0, 0, vals)) import_lines.append((0, 0, vals))
rec.write({'import_ids': import_lines}) rec.write({"import_ids": import_lines})
@api.multi @api.multi
def _compute_input_post_import_hook(self): def _compute_input_post_import_hook(self):
@ -239,7 +236,7 @@ class XLSXTemplate(models.Model):
for rec in self: for rec in self:
# Import Instruction # Import Instruction
input_dict = literal_eval(rec.input_instruction.strip()) 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 @api.multi
def _compute_output_instruction(self): def _compute_output_instruction(self):
@ -249,62 +246,60 @@ class XLSXTemplate(models.Model):
prev_sheet = False prev_sheet = False
prev_row = False prev_row = False
# Export Instruction # Export Instruction
itype = '__EXPORT__' itype = "__EXPORT__"
inst_dict[itype] = {} inst_dict[itype] = {}
for line in rec.export_ids: for line in rec.export_ids:
if line.section_type == 'sheet': if line.section_type == "sheet":
sheet = co.isinteger(line.sheet) and \ sheet = co.isinteger(line.sheet) and int(line.sheet) or line.sheet
int(line.sheet) or line.sheet
sheet_dict = {sheet: {}} sheet_dict = {sheet: {}}
inst_dict[itype].update(sheet_dict) inst_dict[itype].update(sheet_dict)
prev_sheet = sheet prev_sheet = sheet
continue continue
if line.section_type in ('head', 'row'): if line.section_type in ("head", "row"):
row_field = line.row_field row_field = line.row_field
if line.section_type == 'row' and line.is_cont: if line.section_type == "row" and line.is_cont:
row_field = '_CONT_%s' % row_field row_field = "_CONT_%s" % row_field
row_dict = {row_field: {}} row_dict = {row_field: {}}
inst_dict[itype][prev_sheet].update(row_dict) inst_dict[itype][prev_sheet].update(row_dict)
prev_row = row_field prev_row = row_field
continue continue
if line.section_type == 'data': if line.section_type == "data":
excel_cell = line.excel_cell excel_cell = line.excel_cell
field_name = line.field_name or '' field_name = line.field_name or ""
field_name += line.field_cond or '' field_name += line.field_cond or ""
field_name += line.style or '' field_name += line.style or ""
field_name += line.style_cond or '' field_name += line.style_cond or ""
if line.is_sum: if line.is_sum:
field_name += '@{sum}' field_name += "@{sum}"
cell_dict = {excel_cell: field_name} cell_dict = {excel_cell: field_name}
inst_dict[itype][prev_sheet][prev_row].update(cell_dict) inst_dict[itype][prev_sheet][prev_row].update(cell_dict)
continue continue
# Import Instruction # Import Instruction
itype = '__IMPORT__' itype = "__IMPORT__"
inst_dict[itype] = {} inst_dict[itype] = {}
for line in rec.import_ids: for line in rec.import_ids:
if line.section_type == 'sheet': if line.section_type == "sheet":
sheet = co.isinteger(line.sheet) and \ sheet = co.isinteger(line.sheet) and int(line.sheet) or line.sheet
int(line.sheet) or line.sheet
sheet_dict = {sheet: {}} sheet_dict = {sheet: {}}
inst_dict[itype].update(sheet_dict) inst_dict[itype].update(sheet_dict)
prev_sheet = sheet prev_sheet = sheet
continue continue
if line.section_type in ('head', 'row'): if line.section_type in ("head", "row"):
row_field = line.row_field row_field = line.row_field
if line.section_type == 'row' and line.no_delete: if line.section_type == "row" and line.no_delete:
row_field = '_NODEL_%s' % row_field row_field = "_NODEL_%s" % row_field
row_dict = {row_field: {}} row_dict = {row_field: {}}
inst_dict[itype][prev_sheet].update(row_dict) inst_dict[itype][prev_sheet].update(row_dict)
prev_row = row_field prev_row = row_field
continue continue
if line.section_type == 'data': if line.section_type == "data":
excel_cell = line.excel_cell excel_cell = line.excel_cell
field_name = line.field_name or '' field_name = line.field_name or ""
field_name += line.field_cond or '' field_name += line.field_cond or ""
cell_dict = {excel_cell: field_name} cell_dict = {excel_cell: field_name}
inst_dict[itype][prev_sheet][prev_row].update(cell_dict) inst_dict[itype][prev_sheet][prev_row].update(cell_dict)
continue continue
itype = '__POST_IMPORT__' itype = "__POST_IMPORT__"
inst_dict[itype] = False inst_dict[itype] = False
if rec.post_import_hook: if rec.post_import_hook:
inst_dict[itype] = rec.post_import_hook inst_dict[itype] = rec.post_import_hook
@ -312,51 +307,36 @@ class XLSXTemplate(models.Model):
class XLSXTemplateImport(models.Model): class XLSXTemplateImport(models.Model):
_name = 'xlsx.template.import' _name = "xlsx.template.import"
_description = 'Detailed of how excel data will be imported' _description = "Detailed of how excel data will be imported"
_order = 'sequence' _order = "sequence"
template_id = fields.Many2one( template_id = fields.Many2one(
comodel_name='xlsx.template', comodel_name="xlsx.template",
string='XLSX Template', string="XLSX Template",
index=True, index=True,
ondelete='cascade', ondelete="cascade",
readonly=True, readonly=True,
) )
sequence = fields.Integer( sequence = fields.Integer(string="Sequence", default=10,)
string='Sequence', sheet = fields.Char(string="Sheet",)
default=10,
)
sheet = fields.Char(
string='Sheet',
)
section_type = fields.Selection( section_type = fields.Selection(
[('sheet', 'Sheet'), [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")],
('head', 'Head'), string="Section Type",
('row', 'Row'),
('data', 'Data')],
string='Section Type',
required=True, required=True,
) )
row_field = fields.Char( row_field = fields.Char(
string='Row Field', string="Row Field", help="If section type is row, this field is required",
help="If section type is row, this field is required",
) )
no_delete = fields.Boolean( no_delete = fields.Boolean(
string='No Delete', string="No Delete",
default=False, default=False,
help="By default, all rows will be deleted before import.\n" help="By default, all rows will be deleted before import.\n"
"Select No Delete, otherwise" "Select No Delete, otherwise",
)
excel_cell = fields.Char(
string='Cell',
)
field_name = fields.Char(
string='Field',
)
field_cond = fields.Char(
string='Field Cond.',
) )
excel_cell = fields.Char(string="Cell",)
field_name = fields.Char(string="Field",)
field_cond = fields.Char(string="Field Cond.",)
@api.model @api.model
def create(self, vals): def create(self, vals):
@ -365,70 +345,46 @@ class XLSXTemplateImport(models.Model):
@api.model @api.model
def _extract_field_name(self, vals): def _extract_field_name(self, vals):
if self._context.get('compute_from_input') and vals.get('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_name, field_cond = co.get_field_condition(vals["field_name"])
field_cond = field_cond and '${%s}' % (field_cond or '') or False field_cond = field_cond and "${%s}" % (field_cond or "") or False
vals.update({'field_name': field_name, vals.update(
'field_cond': field_cond, {"field_name": field_name, "field_cond": field_cond,}
}) )
return vals return vals
class XLSXTemplateExport(models.Model): class XLSXTemplateExport(models.Model):
_name = 'xlsx.template.export' _name = "xlsx.template.export"
_description = 'Detailed of how excel data will be exported' _description = "Detailed of how excel data will be exported"
_order = 'sequence' _order = "sequence"
template_id = fields.Many2one( template_id = fields.Many2one(
comodel_name='xlsx.template', comodel_name="xlsx.template",
string='XLSX Template', string="XLSX Template",
index=True, index=True,
ondelete='cascade', ondelete="cascade",
readonly=True, readonly=True,
) )
sequence = fields.Integer( sequence = fields.Integer(string="Sequence", default=10,)
string='Sequence', sheet = fields.Char(string="Sheet",)
default=10,
)
sheet = fields.Char(
string='Sheet',
)
section_type = fields.Selection( section_type = fields.Selection(
[('sheet', 'Sheet'), [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")],
('head', 'Head'), string="Section Type",
('row', 'Row'),
('data', 'Data')],
string='Section Type',
required=True, required=True,
) )
row_field = fields.Char( row_field = fields.Char(
string='Row Field', string="Row Field", help="If section type is row, this field is required",
help="If section type is row, this field is required",
) )
is_cont = fields.Boolean( is_cont = fields.Boolean(
string='Continue', string="Continue", default=False, help="Continue data rows after last data row",
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.',
) )
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 @api.model
def create(self, vals): def create(self, vals):
@ -437,16 +393,19 @@ class XLSXTemplateExport(models.Model):
@api.model @api.model
def _extract_field_name(self, vals): def _extract_field_name(self, vals):
if self._context.get('compute_from_input') and vals.get('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_name, field_cond = co.get_field_condition(vals["field_name"])
field_cond = field_cond or 'value or ""' field_cond = field_cond or 'value or ""'
field_name, style = co.get_field_style(field_name) field_name, style = co.get_field_style(field_name)
field_name, style_cond = co.get_field_style_cond(field_name) field_name, style_cond = co.get_field_style_cond(field_name)
field_name, func = co.get_field_aggregation(field_name) field_name, func = co.get_field_aggregation(field_name)
vals.update({'field_name': field_name, vals.update(
'field_cond': '${%s}' % (field_cond or ''), {
'style': '#{%s}' % (style or ''), "field_name": field_name,
'style_cond': '#?%s?' % (style_cond or ''), "field_cond": "${%s}" % (field_cond or ""),
'is_sum': func == 'sum' and True or False, "style": "#{%s}" % (style or ""),
}) "style_cond": "#?%s?" % (style_cond or ""),
"is_sum": func == "sum" and True or False,
}
)
return vals return vals

View File

@ -1,6 +1,6 @@
// Copyright 2019 Ecosoft Co., Ltd. // Copyright 2019 Ecosoft Co., Ltd.
// License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). // 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"; "use strict";
var core = require("web.core"); var core = require("web.core");
@ -11,23 +11,27 @@ odoo.define("excel_import_export.report", function (require) {
var _t = core._t; var _t = core._t;
ActionManager.include({ ActionManager.include({
_downloadReportExcel: function(url, actions) {
_downloadReportExcel: function (url, actions) {
framework.blockUI(); framework.blockUI();
var def = $.Deferred(); var def = $.Deferred();
var type = "excel"; var type = "excel";
var cloned_action = _.clone(actions); var cloned_action = _.clone(actions);
if (_.isUndefined(cloned_action.data) || if (
_.isUndefined(cloned_action.data) ||
_.isNull(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) { if (cloned_action.context.active_ids) {
url += "/" + cloned_action.context.active_ids.join(','); url += "/" + cloned_action.context.active_ids.join(",");
} }
} else { } else {
url += "?options=" + encodeURIComponent(JSON.stringify(cloned_action.data)); url +=
url += "&context=" + encodeURIComponent(JSON.stringify(cloned_action.context)); "?options=" +
encodeURIComponent(JSON.stringify(cloned_action.data));
url +=
"&context=" +
encodeURIComponent(JSON.stringify(cloned_action.context));
} }
var blocked = !session.get_file({ var blocked = !session.get_file({
@ -36,7 +40,7 @@ odoo.define("excel_import_export.report", function (require) {
data: JSON.stringify([url, type]), data: JSON.stringify([url, type]),
}, },
success: def.resolve.bind(def), success: def.resolve.bind(def),
error: function () { error: function() {
crash_manager.rpc_error.apply(crash_manager, arguments); crash_manager.rpc_error.apply(crash_manager, arguments);
def.reject(); 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, // AAB: this check should be done in get_file service directly,
// should not be the concern of the caller (and that way, get_file // should not be the concern of the caller (and that way, get_file
// could return a deferred) // could return a deferred)
var message = _t('A popup window with your report was blocked. You ' + var message = _t(
'may need to change your browser settings to allow ' + "A popup window with your report was blocked. You " +
'popup windows for this page.'); "may need to change your browser settings to allow " +
this.do_warn(_t('Warning'), message, true); "popup windows for this page."
);
this.do_warn(_t("Warning"), message, true);
} }
return def; return def;
}, },
_triggerDownload: function (action, options, type) { _triggerDownload: function(action, options, type) {
var self = this; var self = this;
var reportUrls = this._makeReportUrls(action); var reportUrls = this._makeReportUrls(action);
if (type === "excel") { if (type === "excel") {
return this._downloadReportExcel(reportUrls[type], action).then(function () { return this._downloadReportExcel(reportUrls[type], action).then(
if (action.close_on_report_download) { function() {
var closeAction = {type: 'ir.actions.act_window_close'}; if (action.close_on_report_download) {
return self.doAction(closeAction, _.pick(options, 'on_close')); var closeAction = {type: "ir.actions.act_window_close"};
} else { return self.doAction(
closeAction,
_.pick(options, "on_close")
);
}
return options.on_close(); return options.on_close();
} }
}); );
} }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
_makeReportUrls: function (action) { _makeReportUrls: function(action) {
var reportUrls = this._super.apply(this, arguments); var reportUrls = this._super.apply(this, arguments);
reportUrls.excel = '/report/excel/' + action.report_name; reportUrls.excel = "/report/excel/" + action.report_name;
return reportUrls; return reportUrls;
}, },
_executeReportAction: function (action, options) { _executeReportAction: function(action, options) {
var self = this; var self = this;
if (action.report_type === 'excel') { if (action.report_type === "excel") {
return self._triggerDownload(action, options, 'excel'); return self._triggerDownload(action, options, "excel");
} }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
} },
}); });
}); });

View File

@ -1,11 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<!-- <!--
Copyright 2019 Ecosoft Co., Ltd. Copyright 2019 Ecosoft Co., Ltd.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<template id="assets_backend" inherit_id="web.assets_backend"> <template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside"> <xpath expr="." position="inside">
<script type="text/javascript" src="/excel_import_export/static/src/js/report/action_manager_report.js"/> <script
type="text/javascript"
src="/excel_import_export/static/src/js/report/action_manager_report.js"
/>
</xpath> </xpath>
</template> </template>
</odoo> </odoo>

View File

@ -1,29 +1,31 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<!-- <!--
Copyright 2019 Ecosoft Co., Ltd. Copyright 2019 Ecosoft Co., Ltd.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<record id="xlsx_report_view" model="ir.ui.view"> <record id="xlsx_report_view" model="ir.ui.view">
<field name="name">xlsx.report.view</field> <field name="name">xlsx.report.view</field>
<field name="model">xlsx.report</field> <field name="model">xlsx.report</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Excel Report"> <form string="Excel Report">
<!-- search criteria --> <!-- search criteria -->
<group name="criteria" states="choose"> <group name="criteria" states="choose">
</group> </group>
<!-- xlsx.report common field --> <!-- xlsx.report common field -->
<div name="xlsx.report"> <div name="xlsx.report">
<field name="state" invisible="1"/> <field name="state" invisible="1" />
<field name="name" invisible="1"/> <field name="name" invisible="1" />
<field name="choose_template" invisible="1"/> <field name="choose_template" invisible="1" />
<div states="choose"> <div states="choose">
<label string="Choose Template: " for="template_id" <label
attrs="{'invisible': [('choose_template', '=', False)]}"/> string="Choose Template: "
<field name="template_id" for="template_id"
attrs="{'invisible': [('choose_template', '=', False)]}"/> attrs="{'invisible': [('choose_template', '=', False)]}"
/>
<field
name="template_id"
attrs="{'invisible': [('choose_template', '=', False)]}"
/>
</div> </div>
<div states="get"> <div states="get">
<h2> <h2>
@ -31,21 +33,29 @@
</h2> </h2>
<p colspan="4"> <p colspan="4">
Here is the report file: Here is the report file:
<field name="data" filename="name" class="oe_inline"/> <field name="data" filename="name" class="oe_inline" />
</p> </p>
</div> </div>
<footer states="choose"> <footer states="choose">
<button name="report_xlsx" string="Execute Report" type="object" class="oe_highlight"/> <button
name="report_xlsx"
string="Execute Report"
type="object"
class="oe_highlight"
/>
or or
<button special="cancel" string="Cancel" type="object" class="oe_link"/> <button
special="cancel"
string="Cancel"
type="object"
class="oe_link"
/>
</footer> </footer>
<footer states="get"> <footer states="get">
<button special="cancel" string="Close" type="object"/> <button special="cancel" string="Close" type="object" />
</footer> </footer>
</div> </div>
</form> </form>
</field> </field>
</record> </record>
</odoo> </odoo>

View File

@ -1,40 +1,47 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<!-- <!--
Copyright 2019 Ecosoft Co., Ltd. Copyright 2019 Ecosoft Co., Ltd.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<record id="view_xlsx_template_tree" model="ir.ui.view"> <record id="view_xlsx_template_tree" model="ir.ui.view">
<field name="model">xlsx.template</field> <field name="model">xlsx.template</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="XLSX Template"> <tree string="XLSX Template">
<field name="name"/> <field name="name" />
</tree> </tree>
</field> </field>
</record> </record>
<record id="view_xlsx_template_form" model="ir.ui.view"> <record id="view_xlsx_template_form" model="ir.ui.view">
<field name="model">xlsx.template</field> <field name="model">xlsx.template</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="XLSX Template"> <form string="XLSX Template">
<sheet> <sheet>
<h1> <h1>
<field name="name" colspan="3"/> <field name="name" colspan="3" />
</h1> </h1>
<group> <group>
<group> <group>
<field name="description"/> <field name="description" />
<field name="to_csv"/> <field name="to_csv" />
<field name="csv_delimiter" attrs="{'invisible': [('to_csv', '=', False)]}"/> <field
<field name="csv_extension" attrs="{'invisible': [('to_csv', '=', False)]}"/> name="csv_delimiter"
<field name="csv_quote" attrs="{'invisible': [('to_csv', '=', False)]}"/> attrs="{'invisible': [('to_csv', '=', False)]}"
/>
<field
name="csv_extension"
attrs="{'invisible': [('to_csv', '=', False)]}"
/>
<field
name="csv_quote"
attrs="{'invisible': [('to_csv', '=', False)]}"
/>
</group> </group>
<group> <group>
<field name="fname" invisible="1"/> <field name="fname" invisible="1" />
<field name="datas" filename="fname"/> <field name="datas" filename="fname" />
<field name="gname"/> <field name="gname" />
<field name="res_model"/> <field name="res_model" />
<field name="redirect_action"/> <field name="redirect_action" />
</group> </group>
</group> </group>
<notebook> <notebook>
@ -42,26 +49,65 @@
<field name="export_ids"> <field name="export_ids">
<tree name="export_instruction" editable="bottom"> <tree name="export_instruction" editable="bottom">
<control> <control>
<create string="Add sheet section" context="{'default_section_type': 'sheet'}"/> <create
<create string="Add header section" context="{'default_section_type': 'head', 'default_row_field': '_HEAD_'}"/> string="Add sheet section"
<create string="Add row section" context="{'default_section_type': 'row'}"/> context="{'default_section_type': 'sheet'}"
<create string="Add data column" context="{'default_section_type': 'data'}"/> />
<create
string="Add header section"
context="{'default_section_type': 'head', 'default_row_field': '_HEAD_'}"
/>
<create
string="Add row section"
context="{'default_section_type': 'row'}"
/>
<create
string="Add data column"
context="{'default_section_type': 'data'}"
/>
</control> </control>
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle" />
<field name="section_type" invisible="1"/> <field name="section_type" invisible="1" />
<field name="sheet" attrs="{'required': [('section_type', '=', 'sheet')], <field
'invisible': [('section_type', '!=', 'sheet')]}"/> name="sheet"
<field name="row_field" attrs="{'required': [('section_type', 'in', ('head', 'row'))], attrs="{'required': [('section_type', '=', 'sheet')],
'invisible': [('section_type', 'not in', ('head', 'row'))]}"/> 'invisible': [('section_type', '!=', 'sheet')]}"
<field name="is_cont" attrs="{'required': [('section_type', 'in', ('head', 'row'))], />
'invisible': [('section_type', 'not in', ('head', 'row'))]}"/> <field
<field name="excel_cell" attrs="{'required': [('section_type', '=', 'data')], name="row_field"
'invisible': [('section_type', '!=', 'data')]}"/> attrs="{'required': [('section_type', 'in', ('head', 'row'))],
<field name="field_name" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> 'invisible': [('section_type', 'not in', ('head', 'row'))]}"
<field name="field_cond" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> />
<field name="is_sum" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> <field
<field name="style" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> name="is_cont"
<field name="style_cond" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> attrs="{'required': [('section_type', 'in', ('head', 'row'))],
'invisible': [('section_type', 'not in', ('head', 'row'))]}"
/>
<field
name="excel_cell"
attrs="{'required': [('section_type', '=', 'data')],
'invisible': [('section_type', '!=', 'data')]}"
/>
<field
name="field_name"
attrs="{'invisible': [('section_type', '!=', 'data')]}"
/>
<field
name="field_cond"
attrs="{'invisible': [('section_type', '!=', 'data')]}"
/>
<field
name="is_sum"
attrs="{'invisible': [('section_type', '!=', 'data')]}"
/>
<field
name="style"
attrs="{'invisible': [('section_type', '!=', 'data')]}"
/>
<field
name="style_cond"
attrs="{'invisible': [('section_type', '!=', 'data')]}"
/>
</tree> </tree>
</field> </field>
<div style="margin-top: 4px;"> <div style="margin-top: 4px;">
@ -73,28 +119,52 @@
You can look at following instruction as Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row sections (one2many fields). You can look at following instruction as Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row sections (one2many fields).
</p> </p>
<ul> <ul>
<li>In header section part, map data fields (e.g., number, partner_id.name) into cells (e.g., B1, B2).</li> <li
<li>In row section, data list will be rolled out from one2many row field (e.g., order_line), and map data field (i.e., product_id.name, uom_id.name, qty) into the first row cells to start rolling (e.g., A6, B6, C6).</li> >In header section part, map data fields (e.g., number, partner_id.name) into cells (e.g., B1, B2).</li>
<li
>In row section, data list will be rolled out from one2many row field (e.g., order_line), and map data field (i.e., product_id.name, uom_id.name, qty) into the first row cells to start rolling (e.g., A6, B6, C6).</li>
</ul> </ul>
<p>Following are more explaination on each column:</p> <p>Following are more explaination on each column:</p>
<ul> <ul>
<li><b>Sheet</b>: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to export data to</li> <li><b
<li><b>Row Field</b>: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data</li> >Sheet</b>: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to export data to</li>
<li><b>Continue</b>: If not selected, start rolling with specified first row cells. If selected, continue from previous one2many field</li> <li><b
<li><b>Cell</b>: Location of data in excel sheet (e.g., A1, B1, ...)</li> >Row Field</b>: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data</li>
<li><b>Field</b>: Field of the record, e.g., product_id.uom_id.name. They are orm compliant.</li> <li><b
<li><b>Field Cond.</b>: Python code in <code>${...}</code> to manipulate field value, e.g., if field = product_id, <code>value</code> will represent product object, e.g., <code>${value and value.uom_id.name or ""}</code></li> >Continue</b>: If not selected, start rolling with specified first row cells. If selected, continue from previous one2many field</li>
<li><b>Sum</b>: Add sum value on last row, <code>@{sum}</code></li> <li><b
<li><b>Style</b>: Default style in <code>#{...}</code> that apply to each cell, e.g., <code>#{align=left;style=text}</code>. See module's <b>style.py</b> for available styles.</li> >Cell</b>: Location of data in excel sheet (e.g., A1, B1, ...)</li>
<li><b>Style w/Cond.</b>: Conditional style by python code in <code>#?...?</code>, e.g., apply style for specific product, <code>#?value.name == "ABC" and #{font=bold;fill=red} or None?</code></li> <li><b
>Field</b>: Field of the record, e.g., product_id.uom_id.name. They are orm compliant.</li>
<li><b>Field Cond.</b>: Python code in <code
>${...}</code> to manipulate field value, e.g., if field = product_id, <code
>value</code> will represent product object, e.g., <code
>${value and value.uom_id.name or ""}</code></li>
<li><b>Sum</b>: Add sum value on last row, <code
>@{sum}</code></li>
<li><b>Style</b>: Default style in <code
>#{...}</code> that apply to each cell, e.g., <code
>#{align=left;style=text}</code>. See module's <b
>style.py</b> for available styles.</li>
<li><b
>Style w/Cond.</b>: Conditional style by python code in <code
>#?...?</code>, e.g., apply style for specific product, <code
>#?value.name == "ABC" and #{font=bold;fill=red} or None?</code></li>
</ul> </ul>
<p><b>Note:</b></p> <p>
For code block <code>${...}</code> and <code>#?...?</code>, following object are available, <b>Note:</b>
</p>
For code block <code>${...}</code> and <code
>#?...?</code>, following object are available,
<ul> <ul>
<li><code>value</code>: value from <b>Field</b></li> <li><code>value</code>: value from <b>Field</b></li>
<li><code>object</code>: record object or line object depends on <b>Row Field</b></li> <li><code
<li><code>model</code>: active model, e.g., self.env['my.model']</li> >object</code>: record object or line object depends on <b
<li><code>date, datetime, time</code>: some useful python classes</li> >Row Field</b></li>
<li><code
>model</code>: active model, e.g., self.env['my.model']</li>
<li><code
>date, datetime, time</code>: some useful python classes</li>
</ul> </ul>
</div> </div>
</page> </page>
@ -102,28 +172,61 @@
<field name="import_ids"> <field name="import_ids">
<tree name="import_instruction" editable="bottom"> <tree name="import_instruction" editable="bottom">
<control> <control>
<create string="Add sheet section" context="{'default_section_type': 'sheet'}"/> <create
<create string="Add header section" context="{'default_section_type': 'head', 'default_row_field': '_HEAD_'}"/> string="Add sheet section"
<create string="Add row section" context="{'default_section_type': 'row'}"/> context="{'default_section_type': 'sheet'}"
<create string="Add data column" context="{'default_section_type': 'data'}"/> />
<create
string="Add header section"
context="{'default_section_type': 'head', 'default_row_field': '_HEAD_'}"
/>
<create
string="Add row section"
context="{'default_section_type': 'row'}"
/>
<create
string="Add data column"
context="{'default_section_type': 'data'}"
/>
</control> </control>
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle" />
<field name="section_type" invisible="1"/> <field name="section_type" invisible="1" />
<field name="sheet" attrs="{'required': [('section_type', '=', 'sheet')], <field
'invisible': [('section_type', '!=', 'sheet')]}"/> name="sheet"
<field name="row_field" attrs="{'required': [('section_type', 'in', ('head', 'row'))], attrs="{'required': [('section_type', '=', 'sheet')],
'invisible': [('section_type', 'not in', ('head', 'row'))]}"/> 'invisible': [('section_type', '!=', 'sheet')]}"
<field name="no_delete" attrs="{'invisible': [('section_type', '!=', 'row')]}"/> />
<field name="excel_cell" attrs="{'required': [('section_type', '=', 'data')], <field
'invisible': [('section_type', '!=', 'data')]}"/> name="row_field"
<field name="field_name" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> attrs="{'required': [('section_type', 'in', ('head', 'row'))],
<field name="field_cond" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> 'invisible': [('section_type', 'not in', ('head', 'row'))]}"
/>
<field
name="no_delete"
attrs="{'invisible': [('section_type', '!=', 'row')]}"
/>
<field
name="excel_cell"
attrs="{'required': [('section_type', '=', 'data')],
'invisible': [('section_type', '!=', 'data')]}"
/>
<field
name="field_name"
attrs="{'invisible': [('section_type', '!=', 'data')]}"
/>
<field
name="field_cond"
attrs="{'invisible': [('section_type', '!=', 'data')]}"
/>
</tree> </tree>
</field> </field>
<group string="Post Import Hook"> <group string="Post Import Hook">
<field name="post_import_hook" placeholder="${object.post_import_do_something()}"/> <field
name="post_import_hook"
placeholder="${object.post_import_do_something()}"
/>
</group> </group>
<hr/> <hr />
<div style="margin-top: 4px;"> <div style="margin-top: 4px;">
<h3>Help with Import Instruction</h3> <h3>Help with Import Instruction</h3>
<p> <p>
@ -133,32 +236,51 @@
Cells can be mapped to record in header section (_HEAD_) and data table can be mapped to row section (one2many field, begins from specifed cells. Cells can be mapped to record in header section (_HEAD_) and data table can be mapped to row section (one2many field, begins from specifed cells.
</p> </p>
<ul> <ul>
<li>In header section, map cells (e.g., B1, B2) into data fields (e.g., number, partner_id).</li> <li
<li>In row section, data table from excel can be imported to one2many row field (e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) to fields (e.g., product_id, uom_id, qty) </li> >In header section, map cells (e.g., B1, B2) into data fields (e.g., number, partner_id).</li>
<li
>In row section, data table from excel can be imported to one2many row field (e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) to fields (e.g., product_id, uom_id, qty) </li>
</ul> </ul>
<p>Following are more explaination on each column:</p> <p>Following are more explaination on each column:</p>
<ul> <ul>
<li><b>Sheet</b>: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet</li> <li><b
<li><b>Row Field</b>: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data</li> >Sheet</b>: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet</li>
<li><b>No Delete</b>: By default, all one2many lines will be deleted before import. Select this, to avoid deletion</li> <li><b
<li><b>Cell</b>: Location of data in excel sheet (e.g., A1, B1, ...)</li> >Row Field</b>: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data</li>
<li><b>Field</b>: Field of the record to be imported to, e.g., product_id</li> <li><b
<li><b>Field Cond.</b>: Python code in <code>${...}</code> value will represent data from excel cell, e.g., if A1 = 'ABC', <code>value</code> will represent 'ABC', e.g., <code>${value == "ABC" and "X" or "Y"}</code> thus can change from cell value to other value for import.</li> >No Delete</b>: By default, all one2many lines will be deleted before import. Select this, to avoid deletion</li>
<li><b
>Cell</b>: Location of data in excel sheet (e.g., A1, B1, ...)</li>
<li><b
>Field</b>: Field of the record to be imported to, e.g., product_id</li>
<li><b>Field Cond.</b>: Python code in <code
>${...}</code> value will represent data from excel cell, e.g., if A1 = 'ABC', <code
>value</code> will represent 'ABC', e.g., <code
>${value == "ABC" and "X" or "Y"}</code> thus can change from cell value to other value for import.</li>
</ul> </ul>
<p><b>Note:</b></p> <p>
For code block <code>${...}</code>, following object are available, <b>Note:</b>
</p>
For code block <code
>${...}</code>, following object are available,
<ul> <ul>
<li><code>value</code>: value from <b>Cell</b></li> <li><code>value</code>: value from <b>Cell</b></li>
<li><code>model</code>: active model, e.g., self.env['my.model']</li> <li><code
<li><code>date, datetime, time</code>: some useful python classes</li> >model</code>: active model, e.g., self.env['my.model']</li>
<li><code
>date, datetime, time</code>: some useful python classes</li>
</ul> </ul>
</div> </div>
</page> </page>
<page string="Input Instruction (Dict.)"> <page string="Input Instruction (Dict.)">
<field name="input_instruction"/> <field name="input_instruction" />
<field name="show_instruction"/><label for="show_instruction"/> <field name="show_instruction" />
<field name="instruction" attrs="{'invisible': [('show_instruction', '=', False)]}"/> <label for="show_instruction" />
<hr/> <field
name="instruction"
attrs="{'invisible': [('show_instruction', '=', False)]}"
/>
<hr />
<div style="margin-top: 4px;"> <div style="margin-top: 4px;">
<h3>Sample Input Instruction as Dictionary</h3> <h3>Sample Input Instruction as Dictionary</h3>
<p> <p>
@ -166,7 +288,7 @@
Normally, this will be within templates.xml file within addons. Normally, this will be within templates.xml file within addons.
</p> </p>
<pre> <pre>
<code class="oe_grey"> <code class="oe_grey">
{ {
'__EXPORT__': { '__EXPORT__': {
'sale_order': { # sheet can be name (string) or index (integer) 'sale_order': { # sheet can be name (string) or index (integer)
@ -203,7 +325,6 @@
</form> </form>
</field> </field>
</record> </record>
<record id="action_xlsx_template" model="ir.actions.act_window"> <record id="action_xlsx_template" model="ir.actions.act_window">
<field name="name">XLSX Templates</field> <field name="name">XLSX Templates</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
@ -216,15 +337,16 @@
</p> </p>
</field> </field>
</record> </record>
<menuitem
<menuitem id="menu_excel_import_export" id="menu_excel_import_export"
name="Excel Import/Export" name="Excel Import/Export"
parent="base.menu_custom" parent="base.menu_custom"
sequence="130"/> sequence="130"
/>
<menuitem id="menu_xlsx_template" <menuitem
parent="menu_excel_import_export" id="menu_xlsx_template"
action="action_xlsx_template" parent="menu_excel_import_export"
sequence="10"/> action="action_xlsx_template"
sequence="10"
/>
</odoo> </odoo>

View File

@ -1,82 +1,68 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # 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 from odoo.exceptions import ValidationError
class ExportXLSXWizard(models.TransientModel): class ExportXLSXWizard(models.TransientModel):
""" This wizard is used with the template (xlsx.template) to export """ This wizard is used with the template (xlsx.template) to export
xlsx template filled with data form the active record """ xlsx template filled with data form the active record """
_name = 'export.xlsx.wizard'
_description = 'Wizard for exporting excel'
name = fields.Char( _name = "export.xlsx.wizard"
string='File Name', _description = "Wizard for exporting excel"
readonly=True,
size=500, name = fields.Char(string="File Name", readonly=True, size=500,)
) data = fields.Binary(string="File", readonly=True,)
data = fields.Binary(
string='File',
readonly=True,
)
template_id = fields.Many2one( template_id = fields.Many2one(
'xlsx.template', "xlsx.template",
string='Template', string="Template",
required=True,
ondelete='cascade',
domain=lambda self: self._context.get('template_domain', []),
)
res_id = fields.Integer(
string='Resource ID',
readonly=True,
required=True, required=True,
ondelete="cascade",
domain=lambda self: self._context.get("template_domain", []),
) )
res_id = fields.Integer(string="Resource ID", readonly=True, required=True,)
res_model = fields.Char( res_model = fields.Char(
string='Resource Model', string="Resource Model", readonly=True, required=True, size=500,
readonly=True,
required=True,
size=500,
) )
state = fields.Selection( state = fields.Selection(
[('choose', 'Choose'), [("choose", "Choose"), ("get", "Get")],
('get', 'Get')], default="choose",
default='choose',
help="* Choose: wizard show in user selection mode" 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 @api.model
def default_get(self, fields): def default_get(self, fields):
res_model = self._context.get('active_model', False) res_model = self._context.get("active_model", False)
res_id = self._context.get('active_id', False) res_id = self._context.get("active_id", False)
template_domain = self._context.get('template_domain', []) template_domain = self._context.get("template_domain", [])
templates = self.env['xlsx.template'].search(template_domain) templates = self.env["xlsx.template"].search(template_domain)
if not templates: if not templates:
raise ValidationError(_('No template found')) raise ValidationError(_("No template found"))
defaults = super(ExportXLSXWizard, self).default_get(fields) defaults = super(ExportXLSXWizard, self).default_get(fields)
for template in templates: for template in templates:
if not template.datas: if not template.datas:
raise ValidationError(_('No file in %s') % (template.name,)) raise ValidationError(_("No file in %s") % (template.name,))
defaults['template_id'] = len(templates) == 1 and templates.id or False defaults["template_id"] = len(templates) == 1 and templates.id or False
defaults['res_id'] = res_id defaults["res_id"] = res_id
defaults['res_model'] = res_model defaults["res_model"] = res_model
return defaults return defaults
@api.multi @api.multi
def action_export(self): def action_export(self):
self.ensure_one() self.ensure_one()
Export = self.env['xlsx.export'] Export = self.env["xlsx.export"]
out_file, out_name = Export.export_xlsx(self.template_id, out_file, out_name = Export.export_xlsx(
self.res_model, self.template_id, self.res_model, self.res_id
self.res_id) )
self.write({'state': 'get', 'data': out_file, 'name': out_name}) self.write({"state": "get", "data": out_file, "name": out_name})
return { return {
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'res_model': 'export.xlsx.wizard', "res_model": "export.xlsx.wizard",
'view_mode': 'form', "view_mode": "form",
'view_type': 'form', "view_type": "form",
'res_id': self.id, "res_id": self.id,
'views': [(False, 'form')], "views": [(False, "form")],
'target': 'new', "target": "new",
} }

View File

@ -1,39 +1,50 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<!-- <!--
Copyright 2019 Ecosoft Co., Ltd. Copyright 2019 Ecosoft Co., Ltd.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<record id="export_xlsx_wizard" model="ir.ui.view"> <record id="export_xlsx_wizard" model="ir.ui.view">
<field name="name">export.xlsx.wizard</field> <field name="name">export.xlsx.wizard</field>
<field name="model">export.xlsx.wizard</field> <field name="model">export.xlsx.wizard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Get Import Template"> <form string="Get Import Template">
<field invisible="1" name="state"/> <field invisible="1" name="state" />
<field name="name" invisible="1"/> <field name="name" invisible="1" />
<group states="choose"> <group states="choose">
<group> <group>
<field name="template_id" widget="selection"/> <field name="template_id" widget="selection" />
</group> </group>
<group> <group>
<field name="res_model" invisible="1"/> <field name="res_model" invisible="1" />
<field name="res_id" invisible="1"/> <field name="res_id" invisible="1" />
</group> </group>
</group> </group>
<div states="get"> <div states="get">
<h2>Complete Prepare File (.xlsx)</h2> <h2>Complete Prepare File (.xlsx)</h2>
<p>Here is the exported file: <field name="data" readonly="1" filename="name"/></p> <p>Here is the exported file: <field
</div> name="data"
<footer states="choose"> readonly="1"
<button name="action_export" string="Export" type="object" class="oe_highlight"/> or filename="name"
<button special="cancel" string="Cancel" type="object" class="oe_link"/> /></p>
</div>
<footer states="choose">
<button
name="action_export"
string="Export"
type="object"
class="oe_highlight"
/> or
<button
special="cancel"
string="Cancel"
type="object"
class="oe_link"
/>
</footer> </footer>
<footer states="get"> <footer states="get">
<button special="cancel" string="Close" type="object"/> <button special="cancel" string="Close" type="object" />
</footer> </footer>
</form> </form>
</field> </field>
</record> </record>
</odoo> </odoo>

View File

@ -1,161 +1,156 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) # 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, RedirectWarning from odoo.exceptions import RedirectWarning, ValidationError
class ImportXLSXWizard(models.TransientModel): class ImportXLSXWizard(models.TransientModel):
""" This wizard is used with the template (xlsx.template) to import """ This wizard is used with the template (xlsx.template) to import
xlsx template back to active record """ xlsx template back to active record """
_name = 'import.xlsx.wizard'
_description = 'Wizard for importing excel'
import_file = fields.Binary( _name = "import.xlsx.wizard"
string='Import File (*.xlsx)', _description = "Wizard for importing excel"
)
import_file = fields.Binary(string="Import File (*.xlsx)",)
template_id = fields.Many2one( template_id = fields.Many2one(
'xlsx.template', "xlsx.template",
string='Template', string="Template",
required=True, required=True,
ondelete='set null', ondelete="set null",
domain=lambda self: self._context.get('template_domain', []), domain=lambda self: self._context.get("template_domain", []),
)
res_id = fields.Integer(
string='Resource ID',
readonly=True,
)
res_model = fields.Char(
string='Resource Model',
readonly=True,
size=500,
)
datas = fields.Binary(
string='Sample',
related='template_id.datas',
readonly=True,
) )
res_id = fields.Integer(string="Resource ID", readonly=True,)
res_model = fields.Char(string="Resource Model", readonly=True, size=500,)
datas = fields.Binary(string="Sample", related="template_id.datas", readonly=True,)
fname = fields.Char( fname = fields.Char(
string='Template Name', string="Template Name", related="template_id.fname", readonly=True,
related='template_id.fname',
readonly=True,
) )
attachment_ids = fields.Many2many( attachment_ids = fields.Many2many(
'ir.attachment', "ir.attachment",
string='Import File(s) (*.xlsx)', string="Import File(s) (*.xlsx)",
required=True, required=True,
help="You can select multiple files to import.", help="You can select multiple files to import.",
) )
state = fields.Selection( state = fields.Selection(
[('choose', 'Choose'), [("choose", "Choose"), ("get", "Get")],
('get', 'Get')], default="choose",
default='choose',
help="* Choose: wizard show in user selection mode" 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 @api.model
def view_init(self, fields_list): def view_init(self, fields_list):
""" This template only works on some context of active record """ """ This template only works on some context of active record """
res = super(ImportXLSXWizard, self).view_init(fields_list) res = super(ImportXLSXWizard, self).view_init(fields_list)
res_model = self._context.get('active_model', False) res_model = self._context.get("active_model", False)
res_id = self._context.get('active_id', False) res_id = self._context.get("active_id", False)
if not res_model or not res_id: if not res_model or not res_id:
return res return res
record = self.env[res_model].browse(res_id) record = self.env[res_model].browse(res_id)
messages = [] messages = []
valid = True valid = True
# For all import, only allow import in draft state (for documents) # For all import, only allow import in draft state (for documents)
import_states = self._context.get('template_import_states', []) import_states = self._context.get("template_import_states", [])
if import_states: # states specified in context, test this if import_states: # states specified in context, test this
if 'state' in record and \ if "state" in record and record["state"] not in import_states:
record['state'] not in import_states: messages.append(_("Document must be in %s states") % import_states)
messages.append(
_('Document must be in %s states') % import_states)
valid = False valid = False
else: # no specific state specified, test with draft else: # no specific state specified, test with draft
if 'state' in record and 'draft' not in record['state']: # not in if "state" in record and "draft" not in record["state"]: # not in
messages.append(_('Document must be in draft state')) messages.append(_("Document must be in draft state"))
valid = False valid = False
# Context testing # Context testing
if self._context.get('template_context', False): if self._context.get("template_context", False):
template_context = self._context['template_context'] template_context = self._context["template_context"]
for key, value in template_context.items(): for key, value in template_context.items():
if key not in record or \ if (
(record._fields[key].type == 'many2one' and key not in record
record[key].id or record[key]) != value: or (
record._fields[key].type == "many2one"
and record[key].id
or record[key]
)
!= value
):
valid = False valid = False
messages.append( messages.append(
_('This import action is not usable ' _(
'in this document context')) "This import action is not usable "
"in this document context"
)
)
break break
if not valid: if not valid:
raise ValidationError('\n'.join(messages)) raise ValidationError("\n".join(messages))
return res return res
@api.model @api.model
def default_get(self, fields): def default_get(self, fields):
res_model = self._context.get('active_model', False) res_model = self._context.get("active_model", False)
res_id = self._context.get('active_id', False) res_id = self._context.get("active_id", False)
template_domain = self._context.get('template_domain', []) template_domain = self._context.get("template_domain", [])
templates = self.env['xlsx.template'].search(template_domain) templates = self.env["xlsx.template"].search(template_domain)
if not templates: if not templates:
raise ValidationError(_('No template found')) raise ValidationError(_("No template found"))
defaults = super(ImportXLSXWizard, self).default_get(fields) defaults = super(ImportXLSXWizard, self).default_get(fields)
for template in templates: for template in templates:
if not template.datas: if not template.datas:
act = self.env.ref('excel_import_export.action_xlsx_template') act = self.env.ref("excel_import_export.action_xlsx_template")
raise RedirectWarning( raise RedirectWarning(
_('File "%s" not found in template, %s.') % _('File "%s" not found in template, %s.')
(template.fname, template.name), % (template.fname, template.name),
act.id, _('Set Templates')) act.id,
defaults['template_id'] = len(templates) == 1 and template.id or False _("Set Templates"),
defaults['res_id'] = res_id )
defaults['res_model'] = res_model defaults["template_id"] = len(templates) == 1 and template.id or False
defaults["res_id"] = res_id
defaults["res_model"] = res_model
return defaults return defaults
@api.multi @api.multi
def get_import_sample(self): def get_import_sample(self):
self.ensure_one() self.ensure_one()
return { return {
'name': _('Import Excel'), "name": _("Import Excel"),
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'res_model': 'import.xlsx.wizard', "res_model": "import.xlsx.wizard",
'view_mode': 'form', "view_mode": "form",
'view_type': 'form', "view_type": "form",
'res_id': self.id, "res_id": self.id,
'views': [(False, 'form')], "views": [(False, "form")],
'target': 'new', "target": "new",
'context': self._context.copy() "context": self._context.copy(),
} }
@api.multi @api.multi
def action_import(self): def action_import(self):
self.ensure_one() self.ensure_one()
Import = self.env['xlsx.import'] Import = self.env["xlsx.import"]
res_ids = [] res_ids = []
if self.import_file: if self.import_file:
record = Import.import_xlsx(self.import_file, self.template_id, record = Import.import_xlsx(
self.res_model, self.res_id) self.import_file, self.template_id, self.res_model, self.res_id
)
res_ids = [record.id] res_ids = [record.id]
elif self.attachment_ids: elif self.attachment_ids:
for attach in self.attachment_ids: for attach in self.attachment_ids:
record = Import.import_xlsx(attach.datas, self.template_id) record = Import.import_xlsx(attach.datas, self.template_id)
res_ids.append(record.id) res_ids.append(record.id)
else: else:
raise ValidationError(_('Please select Excel file to import')) raise ValidationError(_("Please select Excel file to import"))
# If redirect_action is specified, do redirection # If redirect_action is specified, do redirection
if self.template_id.redirect_action: if self.template_id.redirect_action:
vals = self.template_id.redirect_action.read()[0] vals = self.template_id.redirect_action.read()[0]
vals['domain'] = [('id', 'in', res_ids)] vals["domain"] = [("id", "in", res_ids)]
return vals return vals
self.write({'state': 'get'}) self.write({"state": "get"})
return { return {
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'res_model': self._name, "res_model": self._name,
'view_mode': 'form', "view_mode": "form",
'view_type': 'form', "view_type": "form",
'res_id': self.id, "res_id": self.id,
'views': [(False, 'form')], "views": [(False, "form")],
'target': 'new', "target": "new",
} }

View File

@ -1,32 +1,47 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<!-- <!--
Copyright 2019 Ecosoft Co., Ltd. Copyright 2019 Ecosoft Co., Ltd.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).-->
<odoo> <odoo>
<record id="import_xlsx_wizard" model="ir.ui.view"> <record id="import_xlsx_wizard" model="ir.ui.view">
<field name="name">import.xlsx.wizard</field> <field name="name">import.xlsx.wizard</field>
<field name="model">import.xlsx.wizard</field> <field name="model">import.xlsx.wizard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Import File Template"> <form string="Import File Template">
<field name="id" invisible="1"/> <field name="id" invisible="1" />
<field name="state" invisible="1"/> <field name="state" invisible="1" />
<field name="fname" invisible="1"/> <field name="fname" invisible="1" />
<field name="res_model" invisible="1"/> <field name="res_model" invisible="1" />
<field name="res_id" invisible="1"/> <field name="res_id" invisible="1" />
<group states="choose"> <group states="choose">
<group> <group>
<field name="import_file" attrs="{'invisible': [('res_id', '=', False)]}"/> <field
<field name="attachment_ids" widget="many2many_binary" nolabel="1" name="import_file"
attrs="{'invisible': [('res_id', '!=', False)]}"/> attrs="{'invisible': [('res_id', '=', False)]}"
/>
<field
name="attachment_ids"
widget="many2many_binary"
nolabel="1"
attrs="{'invisible': [('res_id', '!=', False)]}"
/>
</group> </group>
<group> <group>
<field name="template_id" widget="selection"/> <field name="template_id" widget="selection" />
<div colspan="2"> <div colspan="2">
<button name="get_import_sample" string="⇒ Get Sample Import Template" <button
type="object" class="oe_link" attrs="{'invisible': [('id', '!=', False)]}"/> name="get_import_sample"
string="⇒ Get Sample Import Template"
type="object"
class="oe_link"
attrs="{'invisible': [('id', '!=', False)]}"
/>
</div> </div>
<field name="datas" filename="fname" attrs="{'invisible': [('id', '=', False)]}"/> <field
name="datas"
filename="fname"
attrs="{'invisible': [('id', '=', False)]}"
/>
</group> </group>
</group> </group>
<group states="get"> <group states="get">
@ -35,15 +50,19 @@
</p> </p>
</group> </group>
<footer states="choose"> <footer states="choose">
<button name="action_import" string="Import" type="object" class="oe_highlight"/> <button
name="action_import"
string="Import"
type="object"
class="oe_highlight"
/>
or or
<button string="Cancel" class="oe_link" special="cancel"/> <button string="Cancel" class="oe_link" special="cancel" />
</footer> </footer>
<footer states="get"> <footer states="get">
<button string="Close" class="oe_link" special="cancel"/> <button string="Close" class="oe_link" special="cancel" />
</footer> </footer>
</form> </form>
</field> </field>
</record> </record>
</odoo> </odoo>