Дата: 2025-12-28
Цель: Оставить ТОЛЬКО base_price и fixed_price, убрать дубликаты
Из TABLE_STRUCTURES.md, строки 335-377:
Поля цен:
| `cost_price` | numeric | YES | | ← УДАЛИТЬ (расчётное поле)
| `base_price` | numeric | YES | | ← ОСТАВИТЬ
| `fixed_cost_price` | numeric | YES | | ← УДАЛИТЬ (дубликат fixed_price)
| `fixed_price` | numeric | YES | | ← ОСТАВИТЬ
| `target_cost` | numeric | YES | | ← УДАЛИТЬ (переместить в price_cost_rules)
Поля премиум:
| `is_premium` | boolean | YES | | ← УДАЛИТЬ (теперь в tier)
| `premium_reason` | varchar | YES | | ← УДАЛИТЬ (теперь в tier)
| `tier` | varchar | YES | 'standard' ← ОСТАВИТЬ (standard/premium/vip)
CREATE TABLE pim_products (
article VARCHAR PRIMARY KEY,
name VARCHAR NOT NULL,
-- ДВА поля для входящих цен:
base_price NUMERIC(10,2), -- Прайс производителя (с возможной скидкой)
fixed_price NUMERIC(10,2), -- Цена поставщика (финальная)
-- БЕЗ cost_price! Вычисляется функцией.
...
);
Функция: calculate_cost_price(article TEXT) RETURNS NUMERIC
CREATE OR REPLACE FUNCTION calculate_cost_price(p_article TEXT)
RETURNS NUMERIC AS $$
DECLARE
v_base_price NUMERIC;
v_fixed_price NUMERIC;
v_brand VARCHAR;
v_brand_discount NUMERIC := 0;
v_result NUMERIC;
BEGIN
-- 1. Получить цены и бренд
SELECT base_price, fixed_price, brand
INTO v_base_price, v_fixed_price, v_brand
FROM pim_products
WHERE article = p_article;
-- 2. Если есть base_price → применить скидку бренда
IF v_base_price IS NOT NULL THEN
-- Получить скидку бренда
SELECT discount_percent INTO v_brand_discount
FROM price_cost_rules
WHERE brand = v_brand
LIMIT 1;
-- Расчёт с учётом скидки
v_result := v_base_price * (1 - COALESCE(v_brand_discount, 0) / 100);
RETURN v_result;
END IF;
-- 3. Иначе → вернуть fixed_price (без скидки)
IF v_fixed_price IS NOT NULL THEN
RETURN v_fixed_price;
END IF;
-- 4. Если ничего нет → NULL
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE VIEW v_products_with_cost AS
SELECT
p.article,
p.name,
p.brand,
p.base_price,
p.fixed_price,
-- Вычисляемая себестоимость
calculate_cost_price(p.article) as cost_price,
-- Источник цены (для отладки)
CASE
WHEN p.base_price IS NOT NULL THEN 'base_price'
WHEN p.fixed_price IS NOT NULL THEN 'fixed_price'
ELSE 'none'
END as price_source
FROM pim_products p;
Файл: migrations/029_simplify_price_fields.sql
Скрипт применения: apply_price_migration.sh
Что делает миграция:
fixed_price (если нет)fixed_cost_price → fixed_priceis_premium → tiercost_price, fixed_cost_price, target_cost, is_premium, premium_reasoncalculate_cost_price(article)calculate_retail_price(article)v_products_with_costv_retail_pricesПрименение:
cd /opt/claude-workspace/projects/org/pirotehnika/app/pim
./apply_price_migration.sh
SQL миграции: См. файл migrations/029_simplify_price_fields.sql
Ключевые изменения:
-- Удаление дубликатов цен
ALTER TABLE pim_products
DROP COLUMN cost_price, -- → вычисляется функцией
DROP COLUMN fixed_cost_price, -- → дубликат fixed_price
DROP COLUMN target_cost; -- → в price_cost_rules
-- Удаление полей премиум (теперь в tier)
ALTER TABLE pim_products
DROP COLUMN is_premium, -- → tier='premium'
DROP COLUMN premium_reason; -- → не нужно
-- Функция расчёта себестоимости
CREATE FUNCTION calculate_cost_price(p_article TEXT) RETURNS NUMERIC
-- Логика:
-- IF base_price EXISTS: cost = base_price × (1 - brand_discount)
-- ELSE IF fixed_price EXISTS: cost = fixed_price
-- ELSE: NULL
-- VIEW с вычисляемой себестоимостью
CREATE VIEW v_products_with_cost AS
SELECT
p.*,
calculate_cost_price(p.article) as cost_price
FROM pim_products p;
class PimProduct(Base):
__tablename__ = "pim_products"
__table_args__ = {"schema": settings.DATABASE_SCHEMA}
article = Column(String, primary_key=True)
name = Column(String, nullable=False)
brand = Column(String)
category = Column(String)
# ТОЛЬКО ДВА поля цен
base_price = Column(Numeric(10, 2), comment="Прайс производителя")
fixed_price = Column(Numeric(10, 2), comment="Цена поставщика")
# Вычисляемое свойство
@property
def cost_price(self):
"""Себестоимость - вычисляется на лету"""
from sqlalchemy import text
result = db.session.execute(
text("SELECT calculate_cost_price(:article)"),
{"article": self.article}
).scalar()
return float(result) if result else None
@property
def retail_price(self):
"""Розничная цена - вычисляется на лету"""
from sqlalchemy import text
result = db.session.execute(
text("SELECT calculate_retail_price(:article)"),
{"article": self.article}
).scalar()
return float(result) if result else None
# api/products.py
@router.get("/products/{article}")
def get_product(article: str):
product = db.query(PimProduct).filter_by(article=article).first()
return {
"article": product.article,
"name": product.name,
"base_price": float(product.base_price) if product.base_price else None,
"fixed_price": float(product.fixed_price) if product.fixed_price else None,
# Вычисляемые поля
"cost_price": product.cost_price, # через @property
"retail_price": product.retail_price, # через @property
}
Было:
- cost_price (хранимое)
- fixed_cost_price (дубликат)
- target_cost (непонятное назначение)
Стало:
- base_price (входящая цена от производителя)
- fixed_price (входящая цена от поставщика)
- cost_price → вычисляется функцией
Логика расчёта — ТОЛЬКО в функции calculate_cost_price():
IF base_price EXISTS:
cost = base_price × (1 - brand_discount)
ELSE IF fixed_price EXISTS:
cost = fixed_price
ELSE:
cost = NULL
При изменении:
- base_price → cost_price пересчитается автоматически
- Скидки в price_cost_rules → cost_price обновится для всех товаров бренда
pim_products = ТОЛЬКО входящие цены (ввод)
Функции = ТОЛЬКО расчёты (логика)
VIEW = ТОЛЬКО представление (вывод)
Если что-то пойдёт не так:
-- Восстановить cost_price из бэкапа
ALTER TABLE pim_products ADD COLUMN cost_price NUMERIC(10,2);
UPDATE pim_products
SET cost_price = calculate_cost_price(article);
-- Или из резервной копии таблицы
INSERT INTO pim_products (article, cost_price, ...)
SELECT article, cost_price, ...
FROM pim_products_backup_20251228;
-- 1. Проверить что поля удалены
\d pim_products
-- Должны быть ТОЛЬКО: base_price, fixed_price
-- 2. Проверить функцию
SELECT
article,
base_price,
fixed_price,
calculate_cost_price(article) as cost_price
FROM pim_products
LIMIT 10;
-- 3. Проверить VIEW
SELECT * FROM v_products_with_cost LIMIT 10;
-- 4. Проверить розничные цены
SELECT * FROM v_retail_prices LIMIT 10;
-- 5. Сравнить со старыми значениями (если есть бэкап)
SELECT
p.article,
b.cost_price as old_cost,
calculate_cost_price(p.article) as new_cost,
ABS(b.cost_price - calculate_cost_price(p.article)) as diff
FROM pim_products p
JOIN pim_products_backup b ON p.article = b.article
WHERE ABS(b.cost_price - calculate_cost_price(p.article)) > 0.01
ORDER BY diff DESC
LIMIT 20;
Версия: 1.0.0