Автоматизація прайс-листів
Масове оновлення цін і залишків у xlsx/csv: конвертація валюти, націнка, статус наявності. Аргументи через CLI.
"""
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())
Telegram-бот для прийому заявок
Діалог з клієнтом (ім'я, телефон, коментар), збереження в SQLite та пересилання адміну в чат.
"""
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()
Парсер товарів з сайту в CSV
Обхід сторінок каталогу, збір назви/ціни/наявності, збереження в CSV з паузами між запитами.
"""
Парсер товарів з сайту в 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())
Автобекап бази даних з ротацією
Дамп PostgreSQL/MySQL, стискання в gzip, видалення застарілих копій. Запускається по cron.
"""
Автоматичний бекап бази даних (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())
Вивантаження залишків в Excel
Обробка збирає залишки товарів по складах з регістру накопичення та вивантажує у файл Excel.
// Обробка "Вивантаження залишків товарів в Excel"
// Платформа: 1С:Підприємство 8.3 (керована форма)
&НаКлиенте
Процедура ВыполнитьВыгрузку(Команда)
РезультатФайл = ВыполнитьНаСервере();
Если РезультатФайл <> Неопределено Тогда
ПоказатьОповещениеПользователя(
"Вивантаження завершено", , "Файл збережено: " + РезультатФайл);
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция ВыполнитьНаСервере()
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиТоваров.Номенклатура КАК Номенклатура,
| ОстаткиТоваров.Склад КАК Склад,
| ОстаткиТоваров.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &ДатаОстатка, Склад = &Склад) КАК ОстаткиТоваров
|ГДЕ
| ОстаткиТоваров.КоличествоОстаток > 0
|УПОРЯДОЧИТЬ ПО
| Номенклатура.Наименование";
Запрос.УстановитьПараметр("ДатаОстатка", ТекущаяДата());
Запрос.УстановитьПараметр("Склад", СкладОтбора);
Выборка = Запрос.Выполнить().Выбрать();
ТабДок = Новый ТабличныйДокумент;
Макет = Новый ТабличныйДокумент;
ОбластьШапка = Макет.ПолучитьОбласть("R1C1:R1C3");
ОбластьШапка.Параметры.Заполнить("Номенклатура,Склад,Кількість");
ТабДок.Вывести(ОбластьШапка);
Пока Выборка.Следующий() Цикл
ОбластьСтрока = Макет.ПолучитьОбласть("R2C1:R2C3");
ОбластьСтрока.Параметры.Номенклатура = Выборка.Номенклатура;
ОбластьСтрока.Параметры.Склад = Выборка.Склад;
ОбластьСтрока.Параметры.Количество = Выборка.Количество;
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
ИмяФайла = ПолучитьИмяВременногоФайла("xlsx");
ТабДок.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.XLSX);
Возврат ИмяФайла;
КонецФункции
Масовий друк документів
Вибір декількох документів у списку та формування єдиного PDF для пакетного друку.
// Обробка "Масовий друк документів"
// Платформа: 1С:Підприємство 8.3 (керована форма)
&НаКлиенте
Процедура СформироватьПечать(Команда)
Список = Элементы.СписокДокументов.ВыделенныеСтроки;
Если Список.Количество() = 0 Тогда
ПоказатьПредупреждение(, "Оберіть хоча б один документ для друку");
Возврат;
КонецЕсли;
РезультатФайл = СформироватьНаСервере(Список);
ПоказатьОповещениеПользователя("Друк сформовано", , "Файл: " + РезультатФайл);
КонецПроцедуры
&НаСервере
Функция СформироватьНаСервере(МассивСсылок)
ОбщийДокумент = Новый ТабличныйДокумент;
ОбщийДокумент.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;
Для Каждого Ссылка Из МассивСсылок Цикл
Если ТипЗнч(Ссылка) = Тип("ДокументСсылка.РеализацияТоваровУслуг") Тогда
ПечатнаяФорма = Документы.РеализацияТоваровУслуг.ПечатьРасходнойНакладной(Ссылка);
ИначеЕсли ТипЗнч(Ссылка) = Тип("ДокументСсылка.СчетНаОплатуПокупателю") Тогда
ПечатнаяФорма = Документы.СчетНаОплатуПокупателю.ПечатьСчета(Ссылка);
Иначе
Продолжить;
КонецЕсли;
ОбщийДокумент.Вывести(ПечатнаяФорма);
ОбщийДокумент.ПоместитьГоризонтальныйРазделительСтраниц();
КонецЦикла;
ИмяФайла = ПолучитьИмяВременногоФайла("pdf");
ОбщийДокумент.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.PDF);
Возврат ИмяФайла;
КонецФункции
HTTP-обмін даними 1С з сайтом
Приймає замовлення з сайту (JSON по POST), створює документ і віддає актуальні залишки по GET.
// 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 Ссылка ИЗ Справочник.Контрагенты ГДЕ Телефон = &Телефон";
Запрос.УстановитьПараметр("Телефон", Телефон);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.Ссылка;
КонецЕсли;
НовыйКонтрагент = Справочники.Контрагенты.СоздатьЭлемент();
НовыйКонтрагент.Наименование = Имя;
НовыйКонтрагент.Телефон = Телефон;
НовыйКонтрагент.Записать();
Возврат НовыйКонтрагент.Ссылка;
КонецФункции