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

# -*- 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