Версия: 2.0.0
Дата: 2025-12-28
Проект: pirotehnika → PIM
PIM (Product Information Management) — каталог товаров с базовыми характеристиками.
Цены в PIM: ТОЛЬКО входящие цены (от производителей и поставщиков).
Расчётные цены: В отдельном модуле price_* (НЕ часть PIM).
┌──────────────────────────────────────────────────────────┐
│ PIM (каталог) │
│ │
│ pim_products: │
│ - article │
│ - name │
│ - brand │
│ - category │
│ - base_price ← Базовая цена бренда │
│ - fixed_price ← Фиксированная цена поставщика │
│ │
│ ВСЁ. Других цен в PIM НЕТ. │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ МОДУЛЬ ПРАЙС (price_*) │
│ │
│ price_cost_rules ← Скидки поставщиков │
│ price_sale_rules ← Правила наценки │
│ price_supplier_cost ← Реальная себестоимость │
│ price_history ← История изменений │
│ │
│ Функции calculate_*() ← Расчёты на лету │
│ VIEW v_retail_prices ← Представления │
└──────────────────────────────────────────────────────────┘
Что это:
- Базовая цена, установленная брендом/производителем
- Одинаковая для всех, кто продаёт этот бренд
- НЕ зависит от конкретного поставщика
Примеры:
JF-DM30 (JF Pyro): base_price = 1000 ₽
MC150 (Гордеев): base_price = 2000 ₽
CC2600 (КТС): base_price = 1800 ₽
Источник:
- Прайс-лист производителя
- Импорт из 1С
- Ручной ввод
Когда заполнять:
- Есть официальный прайс бренда
- Товар от известного производителя
Что это:
- Финальная цена от конкретного поставщика
- Может отличаться у разных поставщиков
- Скидка УЖЕ учтена (или её нет)
Примеры:
OLGINO-CC2600: fixed_price = 850 ₽ (от Ольгино)
PIROSNAB-VH100: fixed_price = 920 ₽ (от ПироСнаб)
Источник:
- Прайс-лист поставщика (Excel)
- API поставщика
- Счёт от поставщика
Когда заполнять:
- Поставщик НЕ даёт базовый прайс бренда
- Покупаем у посредника
- Разовая закупка
Если заполнены ОБА поля:
-- Приоритет у base_price
price = COALESCE(base_price, fixed_price)
Почему base_price приоритетнее:
- Базовая цена бренда — более стабильная
- Используется для расчётов в модуле прайс
- fixed_price — это частный случай (конкретный поставщик)
Можно ли оба NULL:
- ❌ НЕТ — валидация запрещает
- Хотя бы ОДНО поле должно быть заполнено
-- Обязательность
ALTER TABLE pim_products
ADD CONSTRAINT check_price_exists
CHECK (base_price IS NOT NULL OR fixed_price IS NOT NULL);
-- Положительные значения
ALTER TABLE pim_products
ADD CONSTRAINT check_base_price_positive
CHECK (base_price IS NULL OR base_price > 0);
ALTER TABLE pim_products
ADD CONSTRAINT check_fixed_price_positive
CHECK (fixed_price IS NULL OR fixed_price > 0);
# import_from_1c.py
def import_product(product_data):
article = product_data['Артикул']
brand_price = product_data['Цена'] # из прайса бренда
db.execute("""
INSERT INTO pim_products (article, name, brand, base_price)
VALUES (?, ?, ?, ?)
ON CONFLICT (article) DO UPDATE
SET base_price = EXCLUDED.base_price
""", article, name, brand, brand_price)
# import_price.py
def import_supplier_price(file_path, supplier):
for row in read_excel(file_path):
article = row['Артикул']
price = row['Цена']
db.execute("""
UPDATE pim_products
SET fixed_price = ?
WHERE article = ?
""", price, article)
Таблица: price_history (в модуле прайс)
CREATE TABLE price_history (
id SERIAL PRIMARY KEY,
article VARCHAR(100),
price_field VARCHAR(20), -- 'base_price' или 'fixed_price'
old_value NUMERIC(10,2),
new_value NUMERIC(10,2),
changed_at TIMESTAMP DEFAULT NOW(),
changed_by VARCHAR(100)
);
Триггер:
CREATE TRIGGER price_change_log
AFTER UPDATE ON pim_products
FOR EACH ROW
WHEN (OLD.base_price IS DISTINCT FROM NEW.base_price
OR OLD.fixed_price IS DISTINCT FROM NEW.fixed_price)
EXECUTE FUNCTION log_price_change();
PIM предоставляет:
- base_price — базовую цену бренда
- fixed_price — фиксированную цену поставщика
Модуль прайс использует:
- base_price ИЛИ fixed_price для расчёта себестоимости
- Применяет скидки из price_cost_rules
- Рассчитывает наценки из price_sale_rules
- Возвращает retail_price, wholesale_price и т.д.
Документация модуля прайс:
- PRICING_INBOUND.md — входящие цены
- PRICING_OUTBOUND.md — исходящие цены
GET /api/products/{article}
Ответ:
{
"article": "JF-DM30",
"name": "Цветной дым JF-DM30",
"brand": "JF Pyro",
"base_price": 1000.00,
"fixed_price": null
}
PATCH /api/products/{article}
Content-Type: application/json
{
"base_price": 1050.00
}
PATCH /api/products/{article}
Content-Type: application/json
{
"fixed_price": 920.00
}
# Товары с base_price
psql -c "SELECT article, name, base_price FROM pim_products WHERE base_price IS NOT NULL LIMIT 10;"
# Товары с fixed_price
psql -c "SELECT article, name, fixed_price FROM pim_products WHERE fixed_price IS NOT NULL LIMIT 10;"
# Товары с обеими ценами
psql -c "SELECT article, base_price, fixed_price FROM pim_products WHERE base_price IS NOT NULL AND fixed_price IS NOT NULL;"
# Товары БЕЗ цен (ошибка!)
psql -c "SELECT article, name FROM pim_products WHERE base_price IS NULL AND fixed_price IS NULL;"
INSERT INTO pim_products (article, name, brand, base_price)
VALUES ('JF-DM30', 'Цветной дым JF Pyro 30 сек', 'JF Pyro', 1000.00);
INSERT INTO pim_products (article, name, supplier, fixed_price)
VALUES ('OLGINO-CC2600', 'Фонтан Золотой дождь', 'Ольгино', 850.00);
INSERT INTO pim_products (article, name, brand, base_price, fixed_price)
VALUES ('MC150', 'Батарея MC150', 'Гордеев', 2000.00, 1600.00);
-- Для расчётов будет использоваться base_price = 2000
| Документ | Описание |
|---|---|
| PRICING_INBOUND.md | Как импортировать base_price и fixed_price |
| PRICING_OUTBOUND.md | Как модуль прайс рассчитывает продажные цены |
| ../ARCHITECTURE.md | Общая архитектура PIM |
Версия: 2.0.0