437 lines
22 KiB
Python
437 lines
22 KiB
Python
#!/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()
|