Приклади коду

Робочі приклади скриптів та обробок. Код повністю читається нижче — можна перевірити логіку без завантаження файлів.

python · pandas

Автоматизація прайс-листів

Масове оновлення цін і залишків у xlsx/csv: конвертація валюти, націнка, статус наявності. Аргументи через CLI.

excel_price_updater.py
"""
Excel/CSV Price & Stock Updater
Масове оновлення цін і залишків товарів у прайс-листі (xlsx/csv) за курсом
та правилами націнки.

Використання:
    python excel_price_updater.py input.xlsx output.xlsx --markup 25 --usd-rate 41.5
"""
import argparse
import sys
from pathlib import Path

import pandas as pd

REQUIRED_COLUMNS = {"name", "price_usd", "qty"}


def load_prices(path: Path) -> pd.DataFrame:
    if path.suffix.lower() == ".csv":
        df = pd.read_csv(path)
    else:
        df = pd.read_excel(path)

    missing = REQUIRED_COLUMNS - set(df.columns.str.lower())
    if missing:
        raise ValueError(f"У файлі відсутні колонки: {', '.join(sorted(missing))}")

    df.columns = [c.lower().strip() for c in df.columns]
    return df


def apply_markup(df: pd.DataFrame, markup_percent: float, usd_rate: float) -> pd.DataFrame:
    df = df.copy()
    df["price_uah"] = (df["price_usd"] * usd_rate * (1 + markup_percent / 100)).round(2)
    df["in_stock"] = df["qty"] > 0
    df["status"] = df["qty"].apply(
        lambda q: "немає в наявності" if q <= 0 else ("мало" if q < 5 else "в наявності")
    )
    return df


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input_file", type=Path)
    parser.add_argument("output_file", type=Path)
    parser.add_argument("--markup", type=float, default=20.0)
    parser.add_argument("--usd-rate", type=float, required=True)
    args = parser.parse_args()

    if not args.input_file.exists():
        print(f"Файл не знайдено: {args.input_file}", file=sys.stderr)
        return 1

    df = load_prices(args.input_file)
    result = apply_markup(df, args.markup, args.usd_rate)

    if args.output_file.suffix.lower() == ".csv":
        result.to_csv(args.output_file, index=False)
    else:
        result.to_excel(args.output_file, index=False)

    print(f"Готово: {len(result)} товарів оброблено -> {args.output_file}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
python · telegram-bot

Telegram-бот для прийому заявок

Діалог з клієнтом (ім'я, телефон, коментар), збереження в SQLite та пересилання адміну в чат.

telegram_orders_bot.py
"""
Telegram-бот прийому заявок
Приймає замовлення від клієнтів (ім'я, телефон, коментар), зберігає в SQLite
та пересилає адміністратору в чат.

Запуск:
    pip install python-telegram-bot==21.*
    TG_BOT_TOKEN=... ADMIN_CHAT_ID=... python telegram_orders_bot.py
"""
import os
import sqlite3
from datetime import datetime

from telegram import Update, ReplyKeyboardRemove
from telegram.ext import (
    Application, CommandHandler, ContextTypes,
    ConversationHandler, MessageHandler, filters,
)

NAME, PHONE, COMMENT = range(3)
DB_PATH = "orders.db"


def init_db() -> None:
    with sqlite3.connect(DB_PATH) as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS orders (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER, name TEXT, phone TEXT,
                comment TEXT, created_at TEXT
            )
        """)


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    await update.message.reply_text("Як вас звати?", reply_markup=ReplyKeyboardRemove())
    return NAME


async def get_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    context.user_data["name"] = update.message.text
    await update.message.reply_text("Ваш номер телефону?")
    return PHONE


async def get_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    context.user_data["phone"] = update.message.text
    await update.message.reply_text("Коментар до замовлення (або '-'):")
    return COMMENT


async def get_comment(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    data = context.user_data
    data["comment"] = update.message.text

    with sqlite3.connect(DB_PATH) as conn:
        conn.execute(
            "INSERT INTO orders (user_id, name, phone, comment, created_at) VALUES (?, ?, ?, ?, ?)",
            (update.effective_user.id, data["name"], data["phone"],
             data["comment"], datetime.utcnow().isoformat()),
        )

    admin_chat_id = os.environ.get("ADMIN_CHAT_ID")
    if admin_chat_id:
        await context.bot.send_message(
            chat_id=admin_chat_id,
            text=f"Нова заявка:\n{data['name']} | {data['phone']}\n{data['comment']}",
        )

    await update.message.reply_text("Дякуємо! Заявку прийнято.")
    return ConversationHandler.END


def main() -> None:
    init_db()
    app = Application.builder().token(os.environ["TG_BOT_TOKEN"]).build()
    conv = ConversationHandler(
        entry_points=[CommandHandler("start", start)],
        states={
            NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)],
            PHONE: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_phone)],
            COMMENT: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_comment)],
        },
        fallbacks=[],
    )
    app.add_handler(conv)
    app.run_polling()


if __name__ == "__main__":
    main()
python · requests + bs4

Парсер товарів з сайту в CSV

Обхід сторінок каталогу, збір назви/ціни/наявності, збереження в CSV з паузами між запитами.

web_scraper_to_csv.py
"""
Парсер товарів з сайту в CSV
Обходить сторінки каталогу, збирає назву/ціну/наявність, зберігає в CSV.

Запуск:
    pip install requests beautifulsoup4
    python web_scraper_to_csv.py https://example.com/catalog out.csv --pages 5
"""
import argparse
import csv
import time
from dataclasses import dataclass, asdict

import requests
from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; PriceBot/1.0)"}


@dataclass
class Product:
    name: str
    price: str
    in_stock: bool
    url: str


def parse_page(url: str) -> list[Product]:
    resp = requests.get(url, headers=HEADERS, timeout=15)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")

    products = []
    for card in soup.select(".product-card"):
        name_el = card.select_one(".product-card__title")
        price_el = card.select_one(".product-card__price")
        link_el = card.select_one("a")
        stock_el = card.select_one(".product-card__stock")

        if not (name_el and price_el and link_el):
            continue

        products.append(Product(
            name=name_el.get_text(strip=True),
            price=price_el.get_text(strip=True),
            in_stock=bool(stock_el and "немає" not in stock_el.get_text(strip=True).lower()),
            url=requests.compat.urljoin(url, link_el.get("href", "")),
        ))
    return products


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("base_url")
    parser.add_argument("output_csv")
    parser.add_argument("--pages", type=int, default=1)
    parser.add_argument("--delay", type=float, default=1.0)
    args = parser.parse_args()

    all_products: list[Product] = []
    for page in range(1, args.pages + 1):
        page_url = f"{args.base_url}?page={page}"
        print(f"Обробка: {page_url}")
        all_products.extend(parse_page(page_url))
        time.sleep(args.delay)

    with open(args.output_csv, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["name", "price", "in_stock", "url"])
        writer.writeheader()
        for product in all_products:
            writer.writerow(asdict(product))

    print(f"Готово: {len(all_products)} товарів -> {args.output_csv}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
python · devops

Автобекап бази даних з ротацією

Дамп PostgreSQL/MySQL, стискання в gzip, видалення застарілих копій. Запускається по cron.

db_backup_rotate.py
"""
Автоматичний бекап бази даних (PostgreSQL/MySQL) з ротацією
Робить дамп БД, стискає в gzip, видаляє застарілі копії (старші N днів).

Запуск:
    python db_backup_rotate.py --db-name shop --engine postgres --keep-days 14
"""
import argparse
import gzip
import shutil
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path

ENGINES = {
    "postgres": ["pg_dump", "-Fp"],
    "mysql": ["mysqldump"],
}


def make_dump(engine: str, db_name: str, out_dir: Path) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    dump_path = out_dir / f"{db_name}_{timestamp}.sql"

    cmd = ENGINES[engine] + [db_name]
    with open(dump_path, "wb") as f:
        result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE)
    if result.returncode != 0:
        raise RuntimeError(f"Помилка дампу: {result.stderr.decode(errors='ignore')}")

    gz_path = dump_path.with_suffix(".sql.gz")
    with open(dump_path, "rb") as f_in, gzip.open(gz_path, "wb") as f_out:
        shutil.copyfileobj(f_in, f_out)
    dump_path.unlink()

    return gz_path


def rotate_old_backups(out_dir: Path, keep_days: int) -> int:
    cutoff = datetime.now() - timedelta(days=keep_days)
    removed = 0
    for file in out_dir.glob("*.sql.gz"):
        if datetime.fromtimestamp(file.stat().st_mtime) < cutoff:
            file.unlink()
            removed += 1
    return removed


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--db-name", required=True)
    parser.add_argument("--engine", choices=ENGINES.keys(), required=True)
    parser.add_argument("--out-dir", default="./backups")
    parser.add_argument("--keep-days", type=int, default=14)
    args = parser.parse_args()

    out_dir = Path(args.out_dir)
    try:
        gz_path = make_dump(args.engine, args.db_name, out_dir)
        print(f"Бекап створено: {gz_path}")
    except RuntimeError as exc:
        print(str(exc), file=sys.stderr)
        return 1

    removed = rotate_old_backups(out_dir, args.keep_days)
    print(f"Видалено застарілих копій: {removed}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
1С:Підприємство 8.3 · BSL

Вивантаження залишків в Excel

Обробка збирає залишки товарів по складах з регістру накопичення та вивантажує у файл Excel.

vygruzka_zalyshkiv.bsl
// Обробка "Вивантаження залишків товарів в Excel"
// Платформа: 1С:Підприємство 8.3 (керована форма)

&НаКлиенте
Процедура ВыполнитьВыгрузку(Команда)
    РезультатФайл = ВыполнитьНаСервере();
    Если РезультатФайл <> Неопределено Тогда
        ПоказатьОповещениеПользователя(
            "Вивантаження завершено", , "Файл збережено: " + РезультатФайл);
    КонецЕсли;
КонецПроцедуры

&НаСервере
Функция ВыполнитьНаСервере()

    Запрос = Новый Запрос;
    Запрос.Текст =
    "ВЫБРАТЬ
    |    ОстаткиТоваров.Номенклатура КАК Номенклатура,
    |    ОстаткиТоваров.Склад КАК Склад,
    |    ОстаткиТоваров.КоличествоОстаток КАК Количество
    |ИЗ
    |    РегистрНакопления.ТоварыНаСкладах.Остатки(
    |        &ДатаОстатка, Склад = &Склад) КАК ОстаткиТоваров
    |ГДЕ
    |    ОстаткиТоваров.КоличествоОстаток > 0
    |УПОРЯДОЧИТЬ ПО
    |    Номенклатура.Наименование";

    Запрос.УстановитьПараметр("ДатаОстатка", ТекущаяДата());
    Запрос.УстановитьПараметр("Склад", СкладОтбора);

    Выборка = Запрос.Выполнить().Выбрать();

    ТабДок = Новый ТабличныйДокумент;
    Макет = Новый ТабличныйДокумент;

    ОбластьШапка = Макет.ПолучитьОбласть("R1C1:R1C3");
    ОбластьШапка.Параметры.Заполнить("Номенклатура,Склад,Кількість");
    ТабДок.Вывести(ОбластьШапка);

    Пока Выборка.Следующий() Цикл
        ОбластьСтрока = Макет.ПолучитьОбласть("R2C1:R2C3");
        ОбластьСтрока.Параметры.Номенклатура = Выборка.Номенклатура;
        ОбластьСтрока.Параметры.Склад = Выборка.Склад;
        ОбластьСтрока.Параметры.Количество = Выборка.Количество;
        ТабДок.Вывести(ОбластьСтрока);
    КонецЦикла;

    ИмяФайла = ПолучитьИмяВременногоФайла("xlsx");
    ТабДок.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.XLSX);

    Возврат ИмяФайла;

КонецФункции
1С:Підприємство 8.3 · BSL

Масовий друк документів

Вибір декількох документів у списку та формування єдиного PDF для пакетного друку.

massovaya_pechat_dokumentov.bsl
// Обробка "Масовий друк документів"
// Платформа: 1С:Підприємство 8.3 (керована форма)

&НаКлиенте
Процедура СформироватьПечать(Команда)

    Список = Элементы.СписокДокументов.ВыделенныеСтроки;
    Если Список.Количество() = 0 Тогда
        ПоказатьПредупреждение(, "Оберіть хоча б один документ для друку");
        Возврат;
    КонецЕсли;

    РезультатФайл = СформироватьНаСервере(Список);
    ПоказатьОповещениеПользователя("Друк сформовано", , "Файл: " + РезультатФайл);

КонецПроцедуры

&НаСервере
Функция СформироватьНаСервере(МассивСсылок)

    ОбщийДокумент = Новый ТабличныйДокумент;
    ОбщийДокумент.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;

    Для Каждого Ссылка Из МассивСсылок Цикл

        Если ТипЗнч(Ссылка) = Тип("ДокументСсылка.РеализацияТоваровУслуг") Тогда
            ПечатнаяФорма = Документы.РеализацияТоваровУслуг.ПечатьРасходнойНакладной(Ссылка);
        ИначеЕсли ТипЗнч(Ссылка) = Тип("ДокументСсылка.СчетНаОплатуПокупателю") Тогда
            ПечатнаяФорма = Документы.СчетНаОплатуПокупателю.ПечатьСчета(Ссылка);
        Иначе
            Продолжить;
        КонецЕсли;

        ОбщийДокумент.Вывести(ПечатнаяФорма);
        ОбщийДокумент.ПоместитьГоризонтальныйРазделительСтраниц();

    КонецЦикла;

    ИмяФайла = ПолучитьИмяВременногоФайла("pdf");
    ОбщийДокумент.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.PDF);

    Возврат ИмяФайла;

КонецФункции
1С:Підприємство 8.3 · HTTP-сервіс

HTTP-обмін даними 1С з сайтом

Приймає замовлення з сайту (JSON по POST), створює документ і віддає актуальні залишки по GET.

http_obmen_s_saitom.bsl
// HTTP-сервіс обміну даними 1С <-> сайт
// Платформа: 1С:Підприємство 8.3

Функция ОбработатьHTTPЗапрос(Запрос)

    Если Запрос.HTTPМетод = "POST" И Запрос.ОтносительныйURL = "/orders" Тогда
        Возврат СоздатьЗаказИзJSON(Запрос.ПолучитьТелоКакСтроку());
    ИначеЕсли Запрос.HTTPМетод = "GET" И Запрос.ОтносительныйURL = "/stock" Тогда
        Возврат ПолучитьОстаткиJSON();
    КонецЕсли;

    Ответ = Новый HTTPСервисОтвет(404);
    Возврат Ответ;

КонецФункции

Функция СоздатьЗаказИзJSON(ТелоЗапроса)

    ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(ТелоЗапроса);
    Данные = ПрочитатьJSON(ЧтениеJSON);
    ЧтениеJSON.Закрыть();

    НовыйЗаказ = Документы.ЗаказПокупателя.СоздатьДокумент();
    НовыйЗаказ.Контрагент = НайтиИлиСоздатьКонтрагента(Данные.Телефон, Данные.ИмяКлиента);
    НовыйЗаказ.Дата = ТекущаяДата();

    Для Каждого Позиция Из Данные.Товары Цикл
        НоваяСтрока = НовыйЗаказ.Товары.Добавить();
        НоваяСтрока.Номенклатура = Справочники.Номенклатура.НайтиПоКоду(Позиция.Код);
        НоваяСтрока.Количество = Позиция.Количество;
        НоваяСтрока.Цена = Позиция.Цена;
    КонецЦикла;

    НовыйЗаказ.Записать(РежимЗаписиДокумента.Проведение);

    Результат = Новый Структура("Статус, НомерЗаказа", "ok", НовыйЗаказ.Номер);

    ЗаписьJSON = Новый ЗаписьJSON;
    ПотокПамяти = Новый ПотокВПамяти;
    ЗаписьJSON.ОткрытьПоток(ПотокПамяти);
    ЗаписатьJSON(ЗаписьJSON, Результат);
    ЗаписьJSON.ЗакрытьПоток();

    Ответ = Новый HTTPСервисОтвет(200);
    Ответ.УстановитьТелоИзБайтов(ПотокПамяти.ЗакрытьИПолучитьДвоичныеДанные());
    Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");

    Возврат Ответ;

КонецФункции

Функция НайтиИлиСоздатьКонтрагента(Телефон, Имя)

    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 Ссылка ИЗ Справочник.Контрагенты ГДЕ Телефон = &Телефон";
    Запрос.УстановитьПараметр("Телефон", Телефон);
    Выборка = Запрос.Выполнить().Выбрать();

    Если Выборка.Следующий() Тогда
        Возврат Выборка.Ссылка;
    КонецЕсли;

    НовыйКонтрагент = Справочники.Контрагенты.СоздатьЭлемент();
    НовыйКонтрагент.Наименование = Имя;
    НовыйКонтрагент.Телефон = Телефон;
    НовыйКонтрагент.Записать();

    Возврат НовыйКонтрагент.Ссылка;

КонецФункции