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