You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
526 lines
22 KiB
526 lines
22 KiB
# -*- coding: utf-8 -*-
|
|
import os
|
|
import re
|
|
import math
|
|
from StringIO import StringIO
|
|
|
|
import xlrd
|
|
import xlwt
|
|
|
|
from django.conf import settings
|
|
from django.template import (Template, RequestContext, BLOCK_TAG_START, BLOCK_TAG_END,
|
|
VARIABLE_TAG_START, VARIABLE_TAG_END)
|
|
|
|
from project.commons.xls import (get_xlwt_style_list, copy_cells, width_cols, horz_page_break, mm_to_twips,
|
|
sum_src_heights, sum_dst_heights)
|
|
|
|
|
|
TAG_RE = re.compile('(%s.*?%s|%s.*?%s)' % (
|
|
re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
|
|
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)
|
|
))
|
|
|
|
XLS_ROOT = getattr(settings, 'XLS_ROOT', '/')
|
|
|
|
DEBUG = getattr(settings, 'DEBUG', False)
|
|
|
|
del_rows = []
|
|
|
|
|
|
def render_xls_to_string(request, xls_template, dictionary=None):
|
|
"""Создает по шаблону новую книгу Excel.
|
|
Возвращает строку, в которой сожержится сгенерированный Excel.
|
|
"""
|
|
src_xls = os.path.join(XLS_ROOT, xls_template) # файл шаблона
|
|
src_book = None
|
|
try:
|
|
# откуда
|
|
src_book = xlrd.open_workbook(src_xls, encoding_override='cp1251',
|
|
on_demand=True, formatting_info=True)
|
|
src_sheet = src_book.sheet_by_index(0)
|
|
|
|
# достать список стилей
|
|
style_list = get_xlwt_style_list(src_book)
|
|
|
|
# куда
|
|
dst_book = xlwt.Workbook(encoding='cp1251', style_compression=2)
|
|
dst_sheet = dst_book.add_sheet(src_sheet.name, cell_overwrite_ok=True)
|
|
|
|
# настройки
|
|
xls_settings = get_settings(src_book)
|
|
apply_page_settings(dst_sheet, xls_settings)
|
|
|
|
# заполнить данными
|
|
fill_xls(request, dictionary, src_sheet, dst_sheet, style_list,
|
|
xls_settings)
|
|
|
|
# закрыть исходную книгу и сохранить созданную
|
|
src_book.release_resources()
|
|
f=StringIO()
|
|
dst_book.save(f)
|
|
xls_content = f.getvalue()
|
|
f.close()
|
|
return xls_content
|
|
except:
|
|
if src_book and not src_book._resources_released:
|
|
src_book.release_resources()
|
|
raise
|
|
|
|
|
|
def fill_xls(request, dictionary, src_sheet, dst_sheet, style_list, xls_settings):
|
|
"""Заполнить контентом новый лист Excel.
|
|
"""
|
|
obj_items = dictionary['obj_items']
|
|
|
|
context = RequestContext(request, dictionary)
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
def write(row, col, val, src_row=None, src_col=None, commands=None):
|
|
"""Записывает данные в ячейку с сохранением стилей."""
|
|
src_row = src_row or row
|
|
src_col = src_col or col
|
|
style = style_list[src_sheet.cell(src_row, src_col).xf_index]
|
|
if commands:
|
|
if commands.get('draw_thin_bottom_border'):
|
|
style.borders.bottom = xlwt.Borders.THIN
|
|
dst_sheet.write(row, col, val, style)
|
|
|
|
def process_template(template_string, **kwargs):
|
|
"""Передать шаблон на обработку django.template.Template."""
|
|
template = Template(template_string)
|
|
context.update(kwargs)
|
|
return template.render(context)
|
|
|
|
def parse_cells(row_from=0, row_to=None, col_from=0, col_to=None,
|
|
dst_row_shift=0, dst_col_shift=0, **kwargs):
|
|
"""Ищет шаблонные теги и переменные в ячейках заданного диапазона. Если находит, то передает содержимое ячейки
|
|
целиком на обработку в process_template. После чего записывает полученный результат обратно в ячейку.
|
|
Также ищет спец. токены и выполняет соответствующие действия.
|
|
"""
|
|
row_to = row_to or src_sheet.nrows-1
|
|
col_to = col_to or src_sheet.ncols-1
|
|
|
|
for row in xrange(row_from, row_to+1):
|
|
cmd_fix_height = []
|
|
|
|
for col in xrange(col_from, col_to+1):
|
|
cell = src_sheet.cell(row, col)
|
|
cell_value = new_value = cell.value
|
|
|
|
cmd_as_float = False
|
|
cmd_show_bmp = False
|
|
cmd_draw_thin_bottom_border = False
|
|
|
|
# если в ячейке не строка - пропускаем
|
|
if not isinstance(new_value, unicode):
|
|
continue
|
|
|
|
# поискать шаблонные теги и переменные в ячейке
|
|
if TAG_RE.search(new_value):
|
|
# передать на обработку
|
|
new_value = process_template(new_value, **kwargs)
|
|
|
|
# пофиксить переводы строки
|
|
#new_value = new_value.strip().replace('\r\n', '\n')
|
|
new_value = new_value.strip().replace('\r\n', ' ')
|
|
|
|
# команда 'конвертировать во float'
|
|
if u'@@AS_FLOAT@@' in new_value:
|
|
new_value = new_value.replace(u'@@AS_FLOAT@@', u'')
|
|
cmd_as_float = True
|
|
|
|
# команда 'вставить изображение из bmp-файла'
|
|
if u'@@SHOW_BMP@@' in new_value:
|
|
new_value = new_value.replace(u'@@SHOW_BMP@@', u'')
|
|
cmd_show_bmp = True
|
|
|
|
# команда 'нарисовать тонкую рамку внизу ячейки'
|
|
if u'@@DRAW_THIN_BOTTOM_BORDER@@' in new_value:
|
|
new_value = new_value.replace(
|
|
u'@@DRAW_THIN_BOTTOM_BORDER@@', u'')
|
|
cmd_draw_thin_bottom_border = True
|
|
|
|
# команда 'подобрать высоту строки в ячейке'
|
|
if u'@@FIX_HEIGHT@@' in new_value:
|
|
new_value = new_value.replace(u'@@FIX_HEIGHT@@', u'')
|
|
cmd_fix_height.append({
|
|
'col': col,
|
|
'value': unicode(new_value),
|
|
})
|
|
|
|
if new_value != cell_value:
|
|
# если надо, попробовать привести к float, чтоб нормально сохранилось в Excel
|
|
if cmd_as_float and not cmd_show_bmp:
|
|
try:
|
|
new_value = float(new_value.replace(',', '.', 1))
|
|
except:
|
|
pass
|
|
|
|
# записать новое значение, если оно отличается от прочитанного
|
|
if new_value != cell_value:
|
|
if cmd_show_bmp and new_value:
|
|
try:
|
|
dst_sheet.insert_bitmap(
|
|
new_value,
|
|
row = row + dst_row_shift,
|
|
col = col + dst_col_shift,
|
|
)
|
|
new_value = ''
|
|
except:
|
|
if DEBUG:
|
|
raise
|
|
else:
|
|
# print "Error inserting image from file '%s'" % new_value
|
|
raise
|
|
write(
|
|
row = row + dst_row_shift,
|
|
col = col + dst_col_shift,
|
|
val = new_value,
|
|
src_row = row,
|
|
src_col = col,
|
|
commands = {'draw_thin_bottom_border': cmd_draw_thin_bottom_border,
|
|
}
|
|
)
|
|
|
|
|
|
# --- конец цикла по ячейкам в строке
|
|
|
|
# подобрать высоту строки в ячейках
|
|
dst_row = row + dst_row_shift # строка назначения
|
|
row_height = dst_sheet.row(dst_row).height # текущая высота
|
|
max_height = 0
|
|
for fh in cmd_fix_height:
|
|
#print '---FIX_HEIGHT:', 'dst_row=', dst_row, 'col=', fh['col']
|
|
# взять ширину ячейки
|
|
width = 0
|
|
# учитываем только объединенные ячейки
|
|
for r1,r2,c1,c2 in src_sheet.merged_cells:
|
|
if r1 != row or c1 != fh['col']:
|
|
continue
|
|
for colx in xrange(c1, c2):
|
|
width += src_sheet.computed_column_width(colx)
|
|
else:
|
|
break
|
|
|
|
# хотя бы приблизительный расчет, т.к. точный невозможен.
|
|
# формулы тестировались для Times New Roman, 9pt.
|
|
# для других шрифтов/размеров могут и не подойти
|
|
width_in_pixels = width / 36.5
|
|
width_in_chars = width_in_pixels / 5.8
|
|
|
|
# может быть 0, если команда @@FIX_HEIGHT@@ задана в простой (не объединенной) ячейке
|
|
if width_in_chars == 0:
|
|
# print ('WARNING. xls generation, cmd @@FIX_HEIGHT@@. '
|
|
# 'variable `width_in_chars` = %s. skip this command.' % width_in_chars)
|
|
continue
|
|
|
|
# сколько строк под текст
|
|
value = fh['value']
|
|
min_rows = 1
|
|
need_rows = math.ceil(len(value) / width_in_chars)
|
|
need_rows = int(max(min_rows, need_rows))
|
|
#print 'need_rows=', need_rows
|
|
|
|
new_height = row_height * need_rows
|
|
# не фиксить высоту, если новая высота данной ячейки меньше либо равна текущей высоте
|
|
if new_height > max_height:
|
|
max_height = new_height
|
|
else:
|
|
#print 'SKIP,', new_height, '<=', max_height
|
|
continue
|
|
|
|
dst_sheet.row(dst_row).height = new_height
|
|
dst_sheet.row(dst_row).height_mismatch = True
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
# задать ширину колонок - достаточно один раз
|
|
width_cols(src_sheet, dst_sheet)
|
|
|
|
# у документа нет табличной части. заполнить целиком и сразу, после чего выйти.
|
|
if not obj_items or 'TBL_BODY_ROW' not in xls_settings:
|
|
copy_cells(src_sheet, dst_sheet, style_list)
|
|
parse_cells()
|
|
return
|
|
|
|
# ---------------------------------------- у документа есть табличная часть
|
|
|
|
# адъ констант!
|
|
p = get_all_these_boring_params(src_sheet, xls_settings)
|
|
if not p:
|
|
# print 'Please set ALL required settings!'
|
|
return
|
|
|
|
# --- !!! ------ вывести начало документа включительно по шапку табл. части
|
|
|
|
copy_cells(src_sheet, dst_sheet, style_list, row_from=0, row_to=p.TBL_BODY_ROW-1)
|
|
|
|
parse_cells(row_to=p.TBL_BODY_ROW-1)
|
|
|
|
# для отладки - выйти здесь
|
|
#return
|
|
|
|
# --- !!! ------------ вывести таблицу с учетом переходов на новую страницу
|
|
|
|
# суммарная высота уже занятых строк на первой странице нового листа
|
|
curr_height = sum_dst_heights(dst_sheet, 0, p.TBL_HEADER_TO)
|
|
|
|
# глобальное смещение на новом листе относительно листа исходного
|
|
add_offset = 0
|
|
|
|
just_wrote_page_footer = False
|
|
last_page_item_idx = 0
|
|
row = 0
|
|
row_shift = 0
|
|
|
|
#print '---table:'
|
|
|
|
def write_tbl_body_row():
|
|
"""Хелпер для отрисовки строки таблицы.
|
|
Зависит от внешних переменных row_shift, row и item!
|
|
"""
|
|
#print '---table body row, dst_row_shift =', row_shift
|
|
copy_cells(
|
|
src_sheet, dst_sheet, style_list,
|
|
row_from = p.TBL_BODY_ROW, row_to = p.TBL_BODY_ROW,
|
|
dst_row_shift = row_shift
|
|
)
|
|
parse_cells(
|
|
row_from = p.TBL_BODY_ROW,
|
|
row_to = p.TBL_BODY_ROW,
|
|
dst_row_shift = row_shift,
|
|
item = item,
|
|
item_npp = row+1
|
|
)
|
|
|
|
def write_tbl_page_footer():
|
|
"""Хелпер для отрисовки подитога таблицы.
|
|
Зависит от внешних переменных row_shift, last_page_item_idx и row!
|
|
"""
|
|
dst_row_shift = row_shift - (p.TBL_PAGE_FOOTER_FROM - p.TBL_BODY_ROW)
|
|
#print '---table page footer, dst_row_shift =', dst_row_shift, \
|
|
# 'items_start =', last_page_item_idx, 'items_stop =', row
|
|
copy_cells(
|
|
src_sheet, dst_sheet, style_list,
|
|
row_from = p.TBL_PAGE_FOOTER_FROM, row_to = p.TBL_PAGE_FOOTER_TO,
|
|
dst_row_shift = dst_row_shift
|
|
)
|
|
parse_cells(
|
|
row_from = p.TBL_PAGE_FOOTER_FROM,
|
|
row_to = p.TBL_PAGE_FOOTER_TO,
|
|
dst_row_shift = dst_row_shift,
|
|
items_start = last_page_item_idx,
|
|
items_stop = row
|
|
)
|
|
|
|
def write_tbl_header():
|
|
"""Хелпер для отрисовки шапки таблицы.
|
|
Зависит от внешних переменных row и add_offset!
|
|
"""
|
|
dst_row_shift = p.TBL_HEADER_ROWS + row + add_offset
|
|
#print '---table header, dst_row_shift =', dst_row_shift
|
|
copy_cells(
|
|
src_sheet, dst_sheet, style_list,
|
|
row_from = p.TBL_HEADER_FROM, row_to = p.TBL_HEADER_TO,
|
|
dst_row_shift = dst_row_shift
|
|
)
|
|
|
|
# цикл по табличной части документа
|
|
for row, item in enumerate(obj_items):
|
|
row_shift = row + add_offset
|
|
#print 'row = %s, add_offset = %s' % (row, add_offset)
|
|
|
|
write_tbl_body_row()
|
|
row_height = dst_sheet.row(p.TBL_BODY_ROW + row_shift).height
|
|
curr_height += row_height
|
|
|
|
just_wrote_page_footer = False
|
|
|
|
# строка, подитог и итог не помещаются на странице
|
|
if curr_height + p.TBL_PAGE_FOOTER_HEIGHT + p.TBL_FOOTER_HEIGHT > p.WORK_HEIGHT:
|
|
# если это первая строка, то:
|
|
if row == 0:
|
|
#print '---table new page, row =', row
|
|
# 1. добавить разрыв страницы перед первой шапкой
|
|
horz_page_break(dst_sheet, p.TBL_HEADER_FROM)
|
|
curr_height = p.TBL_HEADER_HEIGHT + row_height
|
|
# если это не последняя строка, то:
|
|
elif row < len(obj_items)-1:
|
|
#print '---table new page, row =', row
|
|
# 1. вместо строки вывести подитог
|
|
if p.TBL_PAGE_FOOTER_ROWS > 0:
|
|
write_tbl_page_footer()
|
|
last_page_item_idx = row
|
|
just_wrote_page_footer = True
|
|
add_offset += p.TBL_PAGE_FOOTER_ROWS
|
|
row_shift += add_offset
|
|
# 2. добавить разрыв страницы
|
|
horz_page_break(dst_sheet, (p.TBL_HEADER_FROM + p.TBL_HEADER_ROWS + row + add_offset))
|
|
# 3. вывести шапку
|
|
write_tbl_header()
|
|
add_offset += p.TBL_HEADER_ROWS
|
|
curr_height = p.TBL_HEADER_HEIGHT
|
|
# 4. перевывести под ними строку
|
|
row_shift = row + add_offset
|
|
write_tbl_body_row()
|
|
curr_height += row_height
|
|
|
|
else: # for ... else
|
|
# вывести подитог, если только что не выводили его в цикле
|
|
if p.TBL_PAGE_FOOTER_ROWS > 0 and not just_wrote_page_footer:
|
|
#print '---tbl last page, row =', row
|
|
row += 1 # чтоб захватить в подитог и последнюю запись тоже
|
|
row_shift = row + add_offset
|
|
write_tbl_page_footer()
|
|
curr_height += p.TBL_PAGE_FOOTER_HEIGHT
|
|
|
|
add_offset += row - p.TBL_PAGE_FOOTER_ROWS
|
|
|
|
#print '---end table'
|
|
|
|
# для отладки - выйти здесь
|
|
#return
|
|
|
|
# --- !!! --------------------------------------- вывести остаток документа
|
|
|
|
copy_cells(
|
|
src_sheet, dst_sheet, style_list,
|
|
row_from = p.TBL_FOOTER_FROM,
|
|
dst_row_shift = add_offset
|
|
)
|
|
|
|
# добавить разрыв страницы, если остаток документа не уместится целиком
|
|
# на текущей странице
|
|
rest_height = sum_src_heights(src_sheet, p.TBL_FOOTER_TO, src_sheet.nrows)
|
|
if curr_height + rest_height > p.WORK_HEIGHT:
|
|
horz_page_break(dst_sheet, p.TBL_FOOTER_TO + add_offset + 1)
|
|
|
|
parse_cells(
|
|
row_from = p.TBL_FOOTER_FROM,
|
|
dst_row_shift = add_offset
|
|
)
|
|
|
|
return
|
|
|
|
|
|
def get_settings(src_book, sheet_name=u'settings'):
|
|
"""Загрузить настройки с листа settings."""
|
|
settings = {}
|
|
|
|
try:
|
|
src_sheet = src_book.sheet_by_name(sheet_name)
|
|
except xlrd.XLRDError:
|
|
return settings
|
|
|
|
for row in xrange(src_sheet.nrows):
|
|
key_cell = src_sheet.cell(row, 0).value
|
|
if key_cell:
|
|
key_name = unicode(key_cell).strip()
|
|
if not key_name.startswith(u'#'):
|
|
val_cell = src_sheet.cell(row, 1).value
|
|
if (isinstance(val_cell, unicode) or
|
|
isinstance(val_cell, str)):
|
|
settings[key_name] = unicode(val_cell)
|
|
else:
|
|
settings[key_name] = val_cell
|
|
|
|
return settings
|
|
|
|
|
|
def apply_page_settings(dst_sheet, settings):
|
|
"""Применить параметры страницы."""
|
|
def setparam(attr, key):
|
|
if key in settings:
|
|
setattr(dst_sheet, attr, settings[key])
|
|
|
|
def setparam_as_inch(attr, key):
|
|
if key in settings:
|
|
setattr(dst_sheet, attr, settings[key]/25.4)
|
|
|
|
setparam('portrait', 'PAGE_PORTRAIT')
|
|
setparam('header_str', 'PAGE_HEADER_STR')
|
|
setparam('footer_str', 'PAGE_FOOTER_STR')
|
|
|
|
# отступы, задаются в дюймах: 1 дюйм = 25.4 мм = 2.54 см
|
|
setparam_as_inch('top_margin', 'PAGE_TOP_MARGIN')
|
|
setparam_as_inch('bottom_margin', 'PAGE_BOTTOM_MARGIN')
|
|
setparam_as_inch('left_margin', 'PAGE_LEFT_MARGIN')
|
|
setparam_as_inch('right_margin', 'PAGE_RIGHT_MARGIN')
|
|
setparam_as_inch('header_margin', 'PAGE_HEADER_MARGIN')
|
|
setparam_as_inch('footer_margin', 'PAGE_FOOTER_MARGIN')
|
|
|
|
|
|
def get_all_these_boring_params(src_sheet, xls_settings):
|
|
"""Достает нужные настройки из словаря и проверят, некоторые вычисляет -
|
|
и всё это складывает в класс, который потом и возвращает.
|
|
Если какие-то обязательные настройки не заданы, сообщает об этом в консоль и возвращает None.
|
|
"""
|
|
class Params(object):
|
|
pass
|
|
p = Params()
|
|
|
|
# строка контента таблицы - обязательно
|
|
p.TBL_BODY_ROW = xls_settings.get('TBL_BODY_ROW')
|
|
if p.TBL_BODY_ROW is not None:
|
|
# перевести в int
|
|
p.TBL_BODY_ROW = int(p.TBL_BODY_ROW)
|
|
# высота в twips
|
|
p.TBL_BODY_HEIGHT = sum_src_heights(src_sheet, p.TBL_BODY_ROW, p.TBL_BODY_ROW)
|
|
else:
|
|
# print u'Error! TBL_BODY_ROW not set!'
|
|
return None
|
|
|
|
# шапка таблицы - обязательно
|
|
p.TBL_HEADER_FROM = xls_settings.get('TBL_HEADER_FROM')
|
|
p.TBL_HEADER_TO = xls_settings.get('TBL_HEADER_TO')
|
|
if p.TBL_HEADER_FROM is not None and p.TBL_HEADER_TO is not None:
|
|
# перевести в int
|
|
p.TBL_HEADER_FROM = int(p.TBL_HEADER_FROM)
|
|
p.TBL_HEADER_TO = int(p.TBL_HEADER_TO)
|
|
# высота в строках
|
|
p.TBL_HEADER_ROWS = int(p.TBL_HEADER_TO - p.TBL_HEADER_FROM + 1)
|
|
# высота в twips
|
|
p.TBL_HEADER_HEIGHT = sum_src_heights(src_sheet, p.TBL_HEADER_FROM, p.TBL_HEADER_TO)
|
|
else:
|
|
# print u'Error! Either TBL_HEADER_FROM or TBL_HEADER_TO not set!'
|
|
return None
|
|
|
|
# итого по таблице - обязательно
|
|
p.TBL_FOOTER_FROM = xls_settings.get('TBL_FOOTER_FROM')
|
|
p.TBL_FOOTER_TO = xls_settings.get('TBL_FOOTER_TO')
|
|
if p.TBL_FOOTER_FROM is not None and p.TBL_FOOTER_TO is not None:
|
|
# перевести в int
|
|
p.TBL_FOOTER_FROM = int(p.TBL_FOOTER_FROM)
|
|
p.TBL_FOOTER_TO = int(p.TBL_FOOTER_TO)
|
|
# высота в строках
|
|
p.TBL_FOOTER_ROWS = int(p.TBL_FOOTER_TO - p.TBL_FOOTER_FROM + 1)
|
|
# высота в twips
|
|
p.TBL_FOOTER_HEIGHT = sum_src_heights(src_sheet, p.TBL_FOOTER_FROM, p.TBL_FOOTER_TO)
|
|
else:
|
|
# print u'Error! Either TBL_FOOTER_FROM or TBL_FOOTER_TO not set!'
|
|
return None
|
|
|
|
# подитог (итого по странице) - опционально
|
|
p.TBL_PAGE_FOOTER_FROM = xls_settings.get('TBL_PAGE_FOOTER_FROM')
|
|
p.TBL_PAGE_FOOTER_TO = xls_settings.get('TBL_PAGE_FOOTER_TO')
|
|
if p.TBL_PAGE_FOOTER_FROM is not None and p.TBL_PAGE_FOOTER_TO is not None:
|
|
# перевести в int
|
|
p.TBL_PAGE_FOOTER_FROM = int(p.TBL_PAGE_FOOTER_FROM)
|
|
p.TBL_PAGE_FOOTER_TO = int(p.TBL_PAGE_FOOTER_TO)
|
|
# высота в строках
|
|
p.TBL_PAGE_FOOTER_ROWS = int(p.TBL_PAGE_FOOTER_TO - p.TBL_PAGE_FOOTER_FROM + 1)
|
|
# высота в twips
|
|
p.TBL_PAGE_FOOTER_HEIGHT = sum_src_heights(src_sheet, p.TBL_PAGE_FOOTER_FROM, p.TBL_PAGE_FOOTER_TO)
|
|
else:
|
|
p.TBL_PAGE_FOOTER_ROWS = 0
|
|
p.TBL_PAGE_FOOTER_HEIGHT = 0
|
|
|
|
# высота рабочей области (высота страницы минус отступы сверху и снизу)
|
|
p.WORK_HEIGHT = mm_to_twips(
|
|
xls_settings.get('PAGE_HEIGHT', 210) -
|
|
xls_settings.get('PAGE_TOP_MARGIN', 0) -
|
|
xls_settings.get('PAGE_BOTTOM_MARGIN', 0)
|
|
)
|
|
|
|
return p
|
|
|