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