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