/
Todrow
/
fast_catalog
Обзор
Документация
Войти
/
Todrow
/
fast_catalog
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
transform.py
232 строки
9 KB
Nikolay Pinchuk
Много строк
01 июн 2026, 11:22
01 июн 2026, 11:22
1b11eb4
Код
Авторство
О чём код?
import io import re from openpyxl import load_workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.styles.differential import DifferentialStyle from openpyxl.formatting.rule import Rule # ---- Фирменная гамма ПТЗ: красно-тёмно-белая ---- RED = 'E2001A' # фирменный красный DARK = '1A1A1A' # тёмный (планка заголовка таблицы) BAND = 'F5F5F5' # светло-серая полоса чётных строк BORDER = 'DDDDDD' MUTED = '6B6B6B' WHITE = 'FFFFFF' TEXT = '1A1A1A' INPUT = 'FFFFFF' FONT_NAME = 'Calibri' DELETE_HEADERS = [ 'Склад', 'Дата проводки', 'Вид движения', 'Документ материала', 'Сумма во ВВ (WAERS)', 'Дата документа', ] TEXT_HEADER_PREFIX = 'Текст Материала' PARTIA_HEADER = 'Партия' ORDER_HEADER = 'Заказ' def _f(sz=11, b=False, color=TEXT, italic=False): return Font(name=FONT_NAME, size=sz, bold=b, color=color, italic=italic) def _fill(c): return PatternFill('solid', fgColor=c) def _sanitize(name): name = re.sub(r'[\\/:*?"<>|\r\n]', '-', name).strip() return name or 'catalog' def transform(input_bytes): """Принимает байты XLSX, возвращает (BytesIO, имя_файла).""" wb = load_workbook(io.BytesIO(input_bytes)) # 1) найти лист данных по заголовку "Текст Материала" data = None for ws in wb.worksheets: row1 = [(c.value or '') for c in ws[1]] if any(str(v).startswith(TEXT_HEADER_PREFIX) for v in row1): data = ws break if data is None: data = wb.worksheets[0] # карта заголовков -> номер столбца (1-based) headers = {} for idx, cell in enumerate(data[1], start=1): if cell.value is not None: headers[str(cell.value).strip()] = idx def col_by(key, prefix=False): for name, idx in headers.items(): if (name.startswith(key) if prefix else name == key): return idx return None text_col = col_by(TEXT_HEADER_PREFIX, prefix=True) partia_col = col_by(PARTIA_HEADER) order_col = col_by(ORDER_HEADER) # 2) имя документа из первой записи: {Текст материала}_{Партия} b_val = str(data.cell(2, text_col).value or '').strip() if text_col else '' j_val = str(data.cell(2, partia_col).value or '').strip() if partia_col else '' doc_name = _sanitize('{}_{}'.format(b_val, j_val)) order_val = data.cell(2, order_col).value if order_col else '' # 3) удалить указанные столбцы (по имени, в порядке убывания индекса) to_delete = sorted([headers[h] for h in DELETE_HEADERS if h in headers], reverse=True) for idx in to_delete: data.delete_cols(idx) # убрать автофильтр и заливку заголовков data.auto_filter.ref = None no_fill = PatternFill(fill_type=None) for c in range(1, data.max_column + 1): data.cell(1, c).fill = no_fill # после удаления: A=Заказ B=Текст C=Материал D=Кол-во E=ЕИ F=Партия MAXR = data.max_row dq = "'{}'".format(data.title) # имя листа данных для формул (в кавычках) # 4) служебные колонки H (флаг 1/0) и I (накопительный счётчик) for r in range(2, MAXR + 1): data.cell(r, 8).value = ( "=IF(AND($B" + str(r) + "<>\"\"," "OR('Поиск'!$C$6=\"\",ISNUMBER(SEARCH('Поиск'!$C$6,$B" + str(r) + ")))),1,0)" ) data.cell(r, 9).value = "=SUM($H$2:H" + str(r) + ")" data.sheet_state = 'hidden' # удалить все прочие листы, оставив только лист данных for ws in list(wb.worksheets): if ws is not data: del wb[ws.title] # 5) лист-приложение «Поиск» s = wb.create_sheet('Поиск', 0) s.sheet_view.showGridLines = False widths = {'A': 2.5, 'B': 16, 'C': 56, 'D': 24, 'E': 11, 'F': 7, 'G': 14, 'H': 3} for col, w in widths.items(): s.column_dimensions[col].width = w thin = Side(style='thin', color=BORDER) box = Border(left=thin, right=thin, top=thin, bottom=thin) # шапка-баннер (красная планка) s.merge_cells('B2:G3') t = s['B2'] t.value = 'Каталог материалов' t.font = _f(22, True, WHITE) t.alignment = Alignment(horizontal='left', vertical='center', indent=1) for r in (2, 3): for c in range(2, 8): s.cell(r, c).fill = _fill(RED) s.row_dimensions[2].height = 26 s.row_dimensions[3].height = 24 # подзаголовок s.merge_cells('B4:G4') sub = s['B4'] sub.value = 'Заказ {} · {}'.format(order_val, doc_name) sub.font = _f(11, False, MUTED) sub.alignment = Alignment(horizontal='left', vertical='center', indent=1) s.row_dimensions[4].height = 20 s.row_dimensions[5].height = 8 # блок поиска ql = s['B6'] ql.value = 'Поиск:' ql.font = _f(12, True, RED) ql.alignment = Alignment(horizontal='left', vertical='center') s.merge_cells('C6:F6') q = s['C6'] q.value = '' # сюда пользователь вводит текст материала q.font = _f(13, False, TEXT) q.alignment = Alignment(horizontal='left', vertical='center', indent=1) for c in range(3, 7): s.cell(6, c).fill = _fill(INPUT) s.cell(6, c).border = box s.row_dimensions[6].height = 30 # счётчик s.merge_cells('B7:G7') cnt = s['B7'] cnt.value = ( "=IF(SUM(" + dq + "!$H$2:$H$" + str(MAXR) + ")=0," "\"Совпадений не найдено\"," "\"Найдено: \"&SUM(" + dq + "!$H$2:$H$" + str(MAXR) + ")&" "\" — введите часть названия материала в поле «Поиск'\")" ) cnt.font = _f(10, False, MUTED, italic=True) cnt.alignment = Alignment(horizontal='left', vertical='center') s.row_dimensions[7].height = 18 s.row_dimensions[8].height = 6 # заголовок таблицы результатов (тёмная планка) HDR = 9 for i, h in enumerate(['Заказ', 'Текст материала', 'Материал', 'Кол-во', 'ЕИ', 'Партия']): c = s.cell(HDR, 2 + i) c.value = h c.font = _f(11, True, WHITE) c.fill = _fill(DARK) c.alignment = Alignment( horizontal=('left' if i in (1, 2) else 'center'), vertical='center', indent=(1 if i in (1, 2) else 0)) s.row_dimensions[HDR].height = 24 # строки результатов (300) + скрытая колонка I с позицией совпадения N = 3000 first = HDR + 1 s.column_dimensions['I'].hidden = True for k in range(1, N + 1): R = HDR + k s.cell(R, 9).value = ( "=IFERROR(MATCH(" + str(k) + "," + dq + "!$I$2:$I$" + str(MAXR) + ",0),\"\")" ) pos = "$I" + str(R) for i, col in enumerate(['A', 'B', 'C', 'D', 'E', 'F']): rng_col = dq + "!$" + col + "$2:$" + col + "$" + str(MAXR) cc = s.cell(R, 2 + i) cc.value = ( "=IF(" + pos + "=\"\",\"\"," "IF(INDEX(" + rng_col + "," + pos + ")=\"\",\"\"," "INDEX(" + rng_col + "," + pos + ")))" ) cc.alignment = Alignment( horizontal=('left' if i in (1, 2) else 'center'), vertical='center', indent=(1 if i in (1, 2) else 0)) cc.font = _f(10.5, False, TEXT) cc.fill = _fill(WHITE) s.row_dimensions[R].height = 18 # полоса и линия — только на заполненных строках (условное форматирование) rng = "B{}:G{}".format(first, HDR + N) band_dxf = DifferentialStyle( fill=PatternFill(start_color=BAND, end_color=BAND, fill_type='solid')) s.conditional_formatting.add(rng, Rule( type='expression', formula=["AND($C{}<>\"\",MOD(ROW(),2)=0)".format(first)], dxf=band_dxf)) line_dxf = DifferentialStyle( border=Border(bottom=Side(style='thin', color=BORDER))) s.conditional_formatting.add(rng, Rule( type='expression', formula=["$C{}<>\"\"".format(first)], dxf=line_dxf)) s.freeze_panes = 'B10' wb.active = 0 # пересчёт формул при открытии файла (Excel/LibreOffice/Google Sheets) try: wb.calculation.fullCalcOnLoad = True except Exception: pass out = io.BytesIO() wb.save(out) out.seek(0) return out, doc_name + '.xlsx'