#!/usr/bin/env python3 """Генерация PDF инструктажа по действиям в жаркий период.""" from fpdf import FPDF from fpdf.enums import XPos, YPos class HotWeatherBriefingPDF(FPDF): def __init__(self): super().__init__(orientation="P", unit="mm", format="A4") self.set_auto_page_break(True, 20) color_ink = (15, 18, 24) color_red = (229, 57, 53) color_gray = (91, 101, 115) color_gray_100 = (242, 244, 247) color_white = (255, 255, 255) color_amber = (230, 81, 0) color_green = (46, 125, 50) color_orange = (255, 109, 0) font_dir = "/usr/share/fonts/truetype/dejavu/" self.add_font("DejaVu", "", font_dir + "DejaVuSans.ttf") self.add_font("DejaVu", "B", font_dir + "DejaVuSans-Bold.ttf") self.c = { "ink": color_ink, "red": color_red, "gray": color_gray, "gray_bg": color_gray_100, "white": color_white, "amber": color_amber, "green": color_green, "orange": color_orange, } def add_header_page(self): self.add_page() m = 15 w = self.w - 2 * m # Оранжевый градиентный заголовок self.set_fill_color(*self.c["orange"]) self.rect(0, 0, self.w, 72, "F") # Красная полоса self.set_fill_color(*self.c["red"]) self.rect(0, 72, self.w, 3, "F") # Плашка self.set_y(16) self.set_font("DejaVu", "B", 9) self.set_text_color(*self.c["white"]) self.set_fill_color(*self.c["red"]) badge_w = self.get_string_width(" ИНСТРУКТАЖ / ЖАРКИЙ ПЕРИОД ") + 6 self.set_x(m) self.cell(badge_w, 6, " ИНСТРУКТАЖ / ЖАРКИЙ ПЕРИОД ", fill=True, new_x=XPos.LMARGIN, new_y=YPos.NEXT) # Заголовок self.set_y(28) self.set_x(m) self.set_font("DejaVu", "B", 16) self.set_text_color(*self.c["white"]) self.multi_cell(w, 8, "Действия водителей ТС без кондиционера в жаркий период", align="L") # Подзаголовок self.set_y(52) self.set_x(m) self.set_font("DejaVu", "", 9) self.set_text_color(255, 240, 220) self.multi_cell(w, 5, "Порядок действий водителей транспортных средств компании,\n" "не оборудованных кондиционером, в условиях повышенной температуры воздуха") # Мета self.set_y(90) self.set_x(m) self.set_text_color(*self.c["ink"]) self.set_font("DejaVu", "", 9) self.cell(90, 6, "Дата инструктажа: _______________") self.cell(0, 6, "Провёл: _______________", new_x=XPos.LMARGIN, new_y=YPos.NEXT) def section_title(self, title): self.ln(4) self.set_font("DejaVu", "B", 13) self.set_text_color(*self.c["ink"]) self.cell(0, 8, title, new_x="LMARGIN", new_y="NEXT") y = self.get_y() self.set_draw_color(*self.c["gray_bg"]) self.set_line_width(0.4) self.line(self.l_margin, y + 2, self.w - self.r_margin, y + 2) self.ln(5) def body_text(self, text): self.set_font("DejaVu", "", 10) self.set_text_color(*self.c["ink"]) self.multi_cell(0, 5.5, text, align="L") self.ln(1) def sub_title(self, title): self.ln(2) self.set_font("DejaVu", "B", 10.5) self.set_text_color(*self.c["ink"]) self.cell(0, 6, title, new_x="LMARGIN", new_y="NEXT") self.ln(2) def bullet(self, text, color=None): self.set_font("DejaVu", "", 10) self.set_text_color(*self.c["ink"]) bx = self.l_margin + 4 marker_color = color or self.c["ink"] self.set_text_color(*marker_color) self.set_x(self.l_margin + 1) self.cell(5, 5.5, "\u25a0") self.set_text_color(*self.c["ink"]) self.set_x(bx + 6) self.multi_cell(self.w - self.r_margin - bx - 6, 5.5, text) self.ln(1) def callout_box(self, text, border_color, bg_color): self.set_fill_color(*bg_color) self.set_draw_color(*border_color) self.set_line_width(0.6) m = self.l_margin w = self.w - m - self.r_margin y0 = self.get_y() lines = text.split("\n") n = len(lines) h = 10 + n * 5.5 + 4 self.rect(m, y0, w, h, "DF") self.set_xy(m + 5, y0 + 3) self.set_font("DejaVu", "", 9.5) self.set_text_color(*self.c["ink"]) for line in lines: self.set_x(m + 5) self.cell(w - 10, 5.5, line, new_x="LMARGIN", new_y="NEXT") self.set_y(y0 + h + 3) def _measure_lines(self, w, line_h, text): return len(self.multi_cell(w, line_h, text, dry_run=True, output="LINES")) def draw_card(self, emoji, title, text, x, y, w): """Draw a single card with emoji, title and body text.""" pad = 8 # Calculate card height self.set_font("DejaVu", "B", 10) title_h = 6 self.set_font("DejaVu", "", 9) n_lines = self._measure_lines(w - 2 * pad, 4.5, text) body_h = n_lines * 4.5 + 4 card_h = pad + 12 + title_h + body_h + pad # emoji + title + body + padding # Card background self.set_fill_color(*self.c["white"]) self.set_draw_color(*self.c["gray_bg"]) self.set_line_width(0.3) self.rect(x, y, w, card_h, "DF") # Emoji self.set_xy(x + pad, y + pad) self.set_font("DejaVu", "", 20) self.cell(20, 12, emoji) # Title self.set_xy(x + pad, y + pad + 12) self.set_font("DejaVu", "B", 10) self.set_text_color(*self.c["ink"]) self.cell(w - 2 * pad, title_h, title) # Body self.set_xy(x + pad, y + pad + 12 + title_h + 2) self.set_font("DejaVu", "", 9) self.set_text_color(*self.c["gray"]) self.multi_cell(w - 2 * pad, 4.5, text, align="L") return card_h def draw_cards_row(self, cards, y, card_w=None): """Draw 3 cards in a row. cards = list of (emoji, title, text).""" n = len(cards) gap = 12 avail_w = self.w - self.l_margin - self.r_margin if card_w is None: card_w = (avail_w - gap * (n - 1)) / n for i, (emoji, title, text) in enumerate(cards): x = self.l_margin + i * (card_w + gap) self.draw_card(emoji, title, text, x, y, card_w) # Return max height max_h = 0 for emoji, title, text in cards: pad = 8 self.set_font("DejaVu", "", 9) n_lines = self._measure_lines(card_w - 2 * pad, 4.5, text) body_h = n_lines * 4.5 + 4 card_h = pad + 12 + 6 + body_h + pad if card_h > max_h: max_h = card_h return max_h def draw_cards_2x2(self, cards, y): """Draw 4 cards in a 2x2 grid. cards = list of (emoji, title, text).""" gap = 12 avail_w = self.w - self.l_margin - self.r_margin card_w = (avail_w - gap) / 2 max_h = 0 for row in range(2): for col in range(2): idx = row * 2 + col if idx < len(cards): emoji, title, text = cards[idx] x = self.l_margin + col * (card_w + gap) cy = y + row * (max_h + gap if max_h > 0 else 0) h = self.draw_card(emoji, title, text, x, cy, card_w) if row == 0 and col == 0: max_h = h elif h > max_h: max_h = h return max_h * 2 + gap def build(): pdf = HotWeatherBriefingPDF() # ── Титул ── pdf.add_header_page() # ══ 1. ОСНОВАНИЕ И ЦЕЛЬ ══ pdf.section_title("1. Основание и цель инструктажа") pdf.body_text( "Настоящий инструктаж проводится в связи с наступлением жаркого периода года " "(температура воздуха выше +30\u00b0C) и необходимостью обеспечения безопасности " "водителей, управляющих транспортными средствами без системы кондиционирования." ) pdf.callout_box( "Цель: предотвращение тепловых ударов, обезвоживания и снижения\n" "работоспособности водителей, исключение ДТП вследствие потери\n" "управления ТС по причине перегрева организма.", pdf.c["orange"], (255, 243, 224), ) pdf.body_text( "Инструктаж обязателен для всех водителей (штатных и совместителей), " "эксплуатирующих ТС без кондиционера, а также для лиц, допускающих ТС на линию." ) # ══ 2. НОРМАТИВНАЯ БАЗА ══ pdf.section_title("2. Нормативная база") for item in [ "Закон Республики Казахстан от 17 апреля 2014 г. № 194-V ЗРК «О дорожном движении»", "Приказ Министра внутренних дел РК от 30 июня 2023 г. № 534 «ПДД, Основные положения по допуску ТС к эксплуатации»", "Приказ и.о. Министра транспорта и коммуникаций РК от 4 марта 2005 г. № 114-I «Правила безопасности и охраны труда на автомобильном транспорте»", "Трудовой кодекс РК от 23 ноября 2015 г. № 414-V ЗРК (ст. 184–186)", "Санитарные правила и нормы (СанПиН) Республики Казахстан", "Локальные акты организации по охране труда и безопасности на транспорте", ]: pdf.bullet(item, pdf.c["green"]) # ══ 3. ЗОНА РИСКА ══ pdf.section_title("3. Зона риска и последствия перегрева") pdf.body_text( "Управление ТС без кондиционера в жаркий период представляет особую опасность, " "поскольку водитель вынужден длительное время находиться в замкнутом, нагретом " "солнцем пространстве кабины/салона." ) pdf.callout_box( "Опасная зона: температура воздуха на улице от +30\u00b0C и выше.\n" "В закрытой кабине ТС температура может превышать уличную на 15–25\u00b0C.", pdf.c["red"], (255, 240, 240), ) # Таблица рисков risk_data = [ ("+30–34\u00b0C", "+45–55\u00b0C", "Повышенная утомляемость, снижение внимания"), ("+35–39\u00b0C", "+55–65\u00b0C", "Тепловое недомогание, риск обезвоживания"), ("+40\u00b0C и выше", "+65\u00b0C и выше", "Тепловой удар, потеря сознания"), ] col1_w = 35 col2_w = 35 col3_w = pdf.w - pdf.l_margin - pdf.r_margin - col1_w - col2_w # header pdf.set_fill_color(*pdf.c["ink"]) pdf.set_text_color(*pdf.c["white"]) pdf.set_font("DejaVu", "B", 9) pdf.cell(col1_w, 7, " На улице", border=1, fill=True) pdf.cell(col2_w, 7, " В кабине", border=1, fill=True) pdf.cell(col3_w, 7, " Риск", border=1, fill=True, new_x="LMARGIN", new_y="NEXT") for i, (outdoor, cabin, risk) in enumerate(risk_data): pdf.set_font("DejaVu", "", 9) pdf.set_text_color(*pdf.c["ink"]) pdf.set_fill_color(pdf.c["gray_bg"] if i % 2 == 0 else pdf.c["white"]) pdf.cell(col1_w, 7, " " + outdoor, border=1, fill=True) pdf.cell(col2_w, 7, " " + cabin, border=1, fill=True) pdf.cell(col3_w, 7, " " + risk, border=1, fill=True, new_x="LMARGIN", new_y="NEXT") pdf.ln(6) # Карточки рисков cards_risk = [ ("T", "Тепловой удар", "Потеря сознания, спутанность сознания, судороги. Риск ДТП \u2014 100%."), ("\u2022", "Обезвоживание", "Снижение концентрации внимания, замедление реакции, головокружение."), ("Z", "Гиперсомния", "Повышенная сонливость, \u00abмикрозасыпания\u00bb за рулём \u2014 основная причина лобовых столкновений."), ] y_cards = pdf.get_y() cards_h = pdf.draw_cards_row(cards_risk, y_cards) pdf.set_y(y_cards + cards_h + 4) # ══ 4. ДО ВЫЕЗДА НА ЛИНИЮ ══ pdf.section_title("4. Действия до выезда на линию") items = [ "Оценить состояние здоровья. При головной боли, слабости, температуре выше +37\u00b0C — отказаться от выезда, сообщить диспетчеру.", "Взять запас воды: минимум 2 литра питьевой воды на смену (в термосумке). Запрещается заменять воду сладкими напитками и газировкой.", "Взять головной убор (панама, кепка с козырьком) для защиты от солнца при выходе из ТС.", "Взять влажные салфетки / полотенце для обтирания лица и шеи.", "Проветрить кабину перед выездом (открыть двери на 3–5 минут).", "Одежда — лёгкая, светлая, воздухопроницаемая (хлопок). Запрещается тёмная синтетика.", "Проверить, что окна открываются, вентиляция работает.", ] for item in items: pdf.bullet(item) # ══ 5. ВО ВРЕМЯ ДВИЖЕНИЯ ══ pdf.section_title("5. Действия во время движения") critical = [ "Пить воду каждые 20–30 минут, даже если нет жажды. Жажда — уже признак обезвоживания.", "Останавливаться каждые 1,5–2 часа на 10–15 минут в тенистом месте. Не реже одного привала каждые 100 км.", "При остановке — немедленно выйти из ТС, уйти в тень. Не оставаться в закрытой нагретой кабине.", "Обтирать лицо и шею влажным полотенцем при каждой остановке.", "Не закрывать окна наглухо — обеспечить сквозное проветривание кабины.", "Не оставлять ТС под прямым солнцем — ставать в тень деревьев, зданий, навесов.", ] for item in critical: pdf.bullet(item, pdf.c["red"]) pdf.sub_title("Признаки теплового недомогания — немедленная остановка:") symptoms = [ "Головная боль, головокружение, «мушки» перед глазами", "Тошнота, рвота", "Резкая слабость, сонливость,Difficulty concentrating", "Учащённое сердцебиение, одышка без физической нагрузки", "Покраснение лица, обильное потоотделение или его прекращение", "Любые отклонения от нормального самочувствия", ] for item in symptoms: pdf.bullet(item, pdf.c["amber"]) pdf.callout_box( "ПРИ ЛЮБЫХ ПРИЗНАКАХ ТЕПЛОВОГО НЕДОМОГАНИЯ:\n" "1. Немедленно остановить ТС, выключить двигатель\n" "2. Выйти из ТС, уйти в тень\n" "3. Выпить 200–300 мл воды мелкими глотками\n" "4. Обтереть лицо, шею, запястья влажным полотенцем\n" "5. При ухудшении — вызвать скорую помощь\n" "6. Сообщить диспетчеру о невозможности продолжения рейса", pdf.c["red"], (255, 240, 240), ) # ══ 6. ОСОБЫЕ ТРЕБОВАНИЯ ══ pdf.section_title("6. Особые требования") cards_special = [ ("\u25b6", "Режим работы в жару", "Выезд на линию \u2014 не ранее 6:00. Возвращение \u2014 не позднее 18:00. В период с 12:00 до 15:00 \u2014 обязательный перерыв в тенистом месте (не менее 30 минут)."), ("\u2716", "Запреты", "Запрещается: оставлять детей в ТС, оставлять ТС с работающим двигателем без присмотра, перевозить легковоспламеняющиеся жидкости без охлаждения."), ("\u25cb", "Охлаждение", "Допускается использование portative вентиляторов от прикуривателя, охлаждающих бандажей, бутылок с замороженной водой."), ("\u25a1", "Взаимный контроль", "Водители обязаны проверять состояние здоровья коллег при встрече. При признаках недомогания у коллеги \u2014 оказать помощь и сообщить диспетчеру."), ] y_cards6 = pdf.get_y() cards_h6 = pdf.draw_cards_2x2(cards_special, y_cards6) pdf.set_y(y_cards6 + cards_h6 + 4) # ══ 7. ЧЕК-ЛИСТ ══ pdf.section_title("7. Чек-лист перед выездом") checklist = [ "Состояние здоровья удовлетворительное", "Взял запас воды (мин. 2 литра) в термосумке", "Взял головной убор", "Взял влажные салфетки / полотенце", "Одежда лёгкая, светлая, из хлопка", "Кабина ТС проветрена перед выездом", "Окна открываются, вентиляция работает", "Проверил уровень охлаждающей жидкости", "Есть заряженный мобильный телефон", "Знание маршрута и мест для привалов в тени", ] for item in checklist: pdf.set_font("DejaVu", "", 10) pdf.set_text_color(*pdf.c["ink"]) pdf.set_x(pdf.l_margin + 1) pdf.cell(6, 6, "\u2610") pdf.set_x(pdf.l_margin + 8) pdf.cell(0, 6, item, new_x="LMARGIN", new_y="NEXT") pdf.ln(4) # ══ 8. ЛИСТ ОЗНАКОМЛЕНИЯ ══ pdf.section_title("8. Лист ознакомления с инструктажем") pdf.body_text( "Своей подписью подтверждаю, что с содержанием инструктажа ознакомлен(а), " "требования и меру ответственности осознаю, обязуюсь выполнять." ) pdf.ln(4) sig_header = ["Фамилия И.О.", "Должность / Категория", "Подпись и дата"] sig_w = [55, 75, (pdf.w - pdf.l_margin - pdf.r_margin - 130)] pdf.set_fill_color(*pdf.c["ink"]) pdf.set_text_color(*pdf.c["white"]) pdf.set_font("DejaVu", "B", 9) pdf.cell(sig_w[0], 7, " " + sig_header[0], border=1, fill=True) pdf.cell(sig_w[1], 7, " " + sig_header[1], border=1, fill=True) pdf.cell(sig_w[2], 7, " " + sig_header[2], border=1, fill=True, new_x="LMARGIN", new_y="NEXT") for i in range(12): if pdf.get_y() > pdf.h - 20: pdf.add_page() pdf.set_fill_color(pdf.c["gray_bg"] if i % 2 == 0 else pdf.c["white"]) pdf.set_text_color(*pdf.c["ink"]) pdf.set_font("DejaVu", "", 9) pdf.cell(sig_w[0], 9, "", border=1, fill=True) pdf.cell(sig_w[1], 9, "", border=1, fill=True) pdf.cell(sig_w[2], 9, "", border=1, fill=True, new_x="LMARGIN", new_y="NEXT") pdf.ln(6) pdf.body_text("Инструктаж провёл: _______________________________________ (должность, Ф.И.О., подпись, дата)") # ── SAVE ── out = "Инструктаж_Действия_жаркий_период_ТС_без_кондиционера.pdf" pdf.output(out) print(f"PDF сохранён: {out}") if __name__ == "__main__": build()