Skip to content

Раздел 5: База данных

Проект: Финансовый учёт и управленческая аналитика
Модуль: CFO
Версия: 2.0
Дата: Февраль 2026


5.1 Назначение

Раздел описывает схему базы данных модуля CFO: таблицы, VIEW, индексы, связи и политики хранения.

Таблицы и VIEW модуля

ОбъектТипОписание
cfo_transactionsТаблицаФинансовые транзакции с маркетплейсов
cfo_pnl_dailyТаблицаЕжедневный P&L по SKU
cfo_pnl_aggregatedТаблицаАгрегированный P&L по периодам
cfo_abc_resultsТаблицаРезультаты ABC-анализа
cfo_abc_snapshotsТаблицаСнимки ABC-анализа
cfo_anomaliesТаблицаОбнаруженные аномалии
cfo_alertsТаблицаИстория алертов
cfo_reportsТаблицаСохранённые отчёты
cfo_settingsТаблицаНастройки модуля
cfo_v_cost_pricesVIEWСебестоимость по SKU (поверх 1C_account_turns_90)
cfo_v_revenue_1cVIEWВыручка по данным 1С (для сверки)
vw_cfo_pnl_summaryVIEWСводка P&L по дням
vw_cfo_loss_makersVIEWУбыточные SKU
vw_cfo_unmapped_costsVIEWSKU без себестоимости

5.2 ER-диаграмма


5.3 Связь CFO с 1C_* таблицами

5.3.1 Архитектура доступа

CFO использует подход VIEW поверх 1C_* — без дублирования данных, с real-time доступом.

5.3.2 Таблицы 1C_*, используемые CFO

Таблица 1C_*ЗапросДанныеVIEW CFO
1C_account_turns_90Q-01Выручка и себестоимость по номенклатуреcfo_v_cost_prices, cfo_v_revenue_1c
1C_account_turns_90_expensesQ-02Коммерческие и управленческие расходы— (прямые запросы)
1C_nomenclatureQ-10Справочник номенклатуры— (прямые запросы)
1C_nomenclature_pricesQ-11Цены номенклатуры— (прямые запросы)

5.3.3 VIEW: cfo_v_cost_prices

Заменяет таблицу cfo_cost_prices (удалена в v2.0). Себестоимость рассчитывается из оборотов по счёту 90.02.1.

sql
CREATE OR REPLACE VIEW cfo_v_cost_prices AS
SELECT
    t.article                           AS sku,
    t.nomenclature                      AS product_name,
    t.organization                      AS organization,
    CASE 
        WHEN SUM(t.quantity_dt) > 0 
        THEN ROUND(SUM(t.amount_dt) / SUM(t.quantity_dt), 2)
        ELSE NULL
    END                                 AS cost_price,
    SUM(t.amount_dt)                    AS total_cost,
    SUM(t.quantity_dt)                  AS total_quantity,
    t.period                            AS period,
    MAX(t.loaded_at)                    AS data_freshness
FROM 1C_account_turns_90 t
WHERE t.account_dt = '90.02.1'
  AND t.article IS NOT NULL
  AND t.article != ''
GROUP BY t.article, t.nomenclature, t.organization, t.period;

COMMENT ON VIEW cfo_v_cost_prices IS 
    'Себестоимость по SKU из 1C_account_turns_90 (Q-01, счёт 90.02.1). Заменяет таблицу cfo_cost_prices.';

5.3.4 VIEW: cfo_v_revenue_1c

Выручка по данным бухучёта — для сверки с данными маркетплейсов.

sql
CREATE OR REPLACE VIEW cfo_v_revenue_1c AS
SELECT
    t.article                           AS sku,
    t.nomenclature                      AS product_name,
    t.counterparty                      AS counterparty,
    t.organization                      AS organization,
    SUM(t.amount_ct)                    AS revenue,
    SUM(t.quantity_dt)                  AS quantity,
    t.period                            AS period,
    MAX(t.loaded_at)                    AS data_freshness
FROM 1C_account_turns_90 t
WHERE t.account_ct = '90.01.1'
  AND t.article IS NOT NULL
  AND t.article != ''
GROUP BY t.article, t.nomenclature, t.counterparty, t.organization, t.period;

COMMENT ON VIEW cfo_v_revenue_1c IS 
    'Выручка по SKU из 1C_account_turns_90 (Q-01, счёт 90.01.1). Для сверки с данными МП.';

5.4 Таблица cfo_transactions

5.4.1 Описание

Хранение всех финансовых транзакций с маркетплейсов. Основной источник данных для расчёта P&L.

5.4.2 DDL

sql
CREATE TABLE cfo_transactions (
    id BIGSERIAL PRIMARY KEY,
    
    -- Идентификация
    external_id VARCHAR(255) NOT NULL,
    marketplace VARCHAR(20) NOT NULL,
    source VARCHAR(20) NOT NULL DEFAULT 'api',
    
    -- Товар
    sku VARCHAR(100) NOT NULL,
    barcode VARCHAR(50),              -- Опционально, из API/Excel МП
    nm_id BIGINT,
    product_name VARCHAR(500),
    category VARCHAR(200),
    brand_name VARCHAR(200),
    size VARCHAR(50),
    
    -- Финансы
    revenue DECIMAL(15,2) NOT NULL DEFAULT 0,
    commission DECIMAL(15,2) NOT NULL DEFAULT 0,
    logistics DECIMAL(15,2) NOT NULL DEFAULT 0,
    return_logistics DECIMAL(15,2) NOT NULL DEFAULT 0,
    storage DECIMAL(15,2) NOT NULL DEFAULT 0,
    advertising DECIMAL(15,2) NOT NULL DEFAULT 0,
    payout DECIMAL(15,2) NOT NULL DEFAULT 0,
    
    -- Мета
    sale_date DATE NOT NULL,
    operation_type VARCHAR(100),
    quantity INTEGER NOT NULL DEFAULT 1,
    source_file VARCHAR(500),
    
    -- Служебные
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    -- Constraints
    CONSTRAINT uq_cfo_transactions_external UNIQUE (external_id, marketplace),
    CONSTRAINT chk_cfo_transactions_marketplace CHECK (marketplace IN ('wb', 'ozon', 'ym')),
    CONSTRAINT chk_cfo_transactions_source CHECK (source IN ('api', 'excel'))
);

-- Индексы
CREATE INDEX idx_cfo_transactions_sku ON cfo_transactions(sku);
CREATE INDEX idx_cfo_transactions_sale_date ON cfo_transactions(sale_date);
CREATE INDEX idx_cfo_transactions_marketplace ON cfo_transactions(marketplace);
CREATE INDEX idx_cfo_transactions_barcode ON cfo_transactions(barcode) WHERE barcode IS NOT NULL;
CREATE INDEX idx_cfo_transactions_category ON cfo_transactions(category) WHERE category IS NOT NULL;
CREATE INDEX idx_cfo_transactions_date_mp ON cfo_transactions(sale_date, marketplace);

-- Комментарии
COMMENT ON TABLE cfo_transactions IS 'Финансовые транзакции с маркетплейсов';
COMMENT ON COLUMN cfo_transactions.external_id IS 'Уникальный ID из источника (srid для WB, operation_id для Ozon)';
COMMENT ON COLUMN cfo_transactions.marketplace IS 'Маркетплейс: wb, ozon, ym';
COMMENT ON COLUMN cfo_transactions.source IS 'Источник данных: api или excel';
COMMENT ON COLUMN cfo_transactions.sku IS 'Артикул продавца — основной идентификатор для связи с 1C_*';
COMMENT ON COLUMN cfo_transactions.barcode IS 'Штрихкод из API/Excel МП (опционально, не из 1С)';
COMMENT ON COLUMN cfo_transactions.revenue IS 'Выручка (цена продажи с учётом скидок)';
COMMENT ON COLUMN cfo_transactions.payout IS 'Сумма к выплате продавцу';

5.4.3 Примеры данных

idexternal_idmarketplaceskurevenuecommissionsale_date
1abc123wbOM-123452975.00461.132026-01-15
2def456ozonOM-123452500.00375.002026-01-15
3ghi789wbOK-543211200.00180.002026-01-15

5.5 Таблица cfo_pnl_daily

5.5.1 Описание

Ежедневный P&L по каждому SKU. Рассчитывается автоматически из транзакций. Себестоимость подтягивается из VIEW cfo_v_cost_prices.

5.5.2 DDL

sql
CREATE TABLE cfo_pnl_daily (
    id BIGSERIAL PRIMARY KEY,
    
    -- Идентификация
    sku VARCHAR(100) NOT NULL,
    marketplace VARCHAR(20) NOT NULL,
    brand_id VARCHAR(50),
    category VARCHAR(200),
    sale_date DATE NOT NULL,
    
    -- Количество
    quantity_sold INTEGER NOT NULL DEFAULT 0,
    quantity_returned INTEGER NOT NULL DEFAULT 0,
    net_quantity INTEGER GENERATED ALWAYS AS (quantity_sold - quantity_returned) STORED,
    
    -- Выручка
    gross_revenue DECIMAL(15,2) NOT NULL DEFAULT 0,
    discounts DECIMAL(15,2) NOT NULL DEFAULT 0,
    returns_amount DECIMAL(15,2) NOT NULL DEFAULT 0,
    net_revenue DECIMAL(15,2) NOT NULL DEFAULT 0,
    
    -- Себестоимость
    cogs DECIMAL(15,2) NOT NULL DEFAULT 0,
    cogs_mapped BOOLEAN NOT NULL DEFAULT FALSE,
    
    -- Расходы маркетплейса
    commission DECIMAL(15,2) NOT NULL DEFAULT 0,
    logistics DECIMAL(15,2) NOT NULL DEFAULT 0,
    return_logistics DECIMAL(15,2) NOT NULL DEFAULT 0,
    storage DECIMAL(15,2) NOT NULL DEFAULT 0,
    advertising DECIMAL(15,2) NOT NULL DEFAULT 0,
    
    -- Итоги
    total_expenses DECIMAL(15,2) GENERATED ALWAYS AS (
        cogs + commission + logistics + return_logistics + storage + advertising
    ) STORED,
    
    gross_profit DECIMAL(15,2) GENERATED ALWAYS AS (net_revenue - cogs) STORED,
    gross_margin_pct DECIMAL(8,2),
    
    net_profit DECIMAL(15,2) GENERATED ALWAYS AS (
        net_revenue - cogs - commission - logistics - return_logistics - storage - advertising
    ) STORED,
    net_margin_pct DECIMAL(8,2),
    
    -- Служебные
    calculated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    -- Constraints
    CONSTRAINT uq_cfo_pnl_daily UNIQUE (sku, marketplace, sale_date),
    CONSTRAINT chk_cfo_pnl_daily_marketplace CHECK (marketplace IN ('wb', 'ozon', 'ym'))
);

-- Индексы
CREATE INDEX idx_cfo_pnl_daily_date ON cfo_pnl_daily(sale_date);
CREATE INDEX idx_cfo_pnl_daily_sku ON cfo_pnl_daily(sku);
CREATE INDEX idx_cfo_pnl_daily_marketplace ON cfo_pnl_daily(marketplace);
CREATE INDEX idx_cfo_pnl_daily_brand ON cfo_pnl_daily(brand_id) WHERE brand_id IS NOT NULL;
CREATE INDEX idx_cfo_pnl_daily_category ON cfo_pnl_daily(category) WHERE category IS NOT NULL;
CREATE INDEX idx_cfo_pnl_daily_unmapped ON cfo_pnl_daily(sku) WHERE cogs_mapped = FALSE;

-- Комментарии
COMMENT ON TABLE cfo_pnl_daily IS 'Ежедневный P&L по SKU';
COMMENT ON COLUMN cfo_pnl_daily.cogs IS 'Себестоимость из cfo_v_cost_prices (1C_account_turns_90)';
COMMENT ON COLUMN cfo_pnl_daily.cogs_mapped IS 'Флаг наличия себестоимости в 1C_*';
COMMENT ON COLUMN cfo_pnl_daily.net_margin_pct IS 'Чистая маржа в процентах';

5.6 — 5.12 Остальные таблицы

Структура таблиц cfo_pnl_aggregated, cfo_abc_snapshots, cfo_abc_results, cfo_anomalies, cfo_alerts, cfo_reports, cfo_settings не изменилась в v2.0.


5.13 Аналитические VIEW

5.13.1 View: vw_cfo_pnl_summary

sql
CREATE OR REPLACE VIEW vw_cfo_pnl_summary AS
SELECT 
    sale_date,
    marketplace,
    brand_id,
    COUNT(DISTINCT sku) as sku_count,
    SUM(quantity_sold) as total_quantity,
    SUM(net_revenue) as total_revenue,
    SUM(cogs) as total_cogs,
    SUM(commission + logistics + return_logistics + storage + advertising) as total_mp_expenses,
    SUM(net_profit) as total_profit,
    ROUND(AVG(net_margin_pct), 2) as avg_margin_pct
FROM cfo_pnl_daily
GROUP BY sale_date, marketplace, brand_id;

COMMENT ON VIEW vw_cfo_pnl_summary IS 'Сводка P&L по дням';

5.13.2 View: vw_cfo_loss_makers

sql
CREATE OR REPLACE VIEW vw_cfo_loss_makers AS
SELECT 
    sku,
    marketplace,
    brand_id,
    category,
    sale_date,
    net_revenue,
    total_expenses,
    net_profit,
    net_margin_pct,
    CASE 
        WHEN logistics / NULLIF(net_revenue, 0) > 0.15 THEN 'Высокая логистика'
        WHEN net_margin_pct > -10 THEN 'Небольшой убыток'
        ELSE 'Значительный убыток'
    END as issue_type
FROM cfo_pnl_daily
WHERE net_profit < 0
ORDER BY net_profit ASC;

COMMENT ON VIEW vw_cfo_loss_makers IS 'Убыточные SKU';

5.13.3 View: vw_cfo_unmapped_costs

sql
CREATE OR REPLACE VIEW vw_cfo_unmapped_costs AS
SELECT 
    sku,
    marketplace,
    COUNT(*) as transaction_count,
    SUM(net_revenue) as total_revenue,
    MIN(sale_date) as first_sale,
    MAX(sale_date) as last_sale
FROM cfo_pnl_daily
WHERE cogs_mapped = FALSE
GROUP BY sku, marketplace
ORDER BY total_revenue DESC;

COMMENT ON VIEW vw_cfo_unmapped_costs IS 'SKU без себестоимости в 1C_*';

5.14 Миграция v1.1 → v2.0

5.14.1 Скрипт миграции

sql
-- Migration: 003_cfo_1c_views.sql
-- Version: 2.0
-- Date: 2026-02
-- Description: Переход на 1C_* VIEW, удаление cfo_cost_prices

BEGIN;

-- 1. Создание VIEW для себестоимости поверх 1C_*
CREATE OR REPLACE VIEW cfo_v_cost_prices AS
SELECT
    t.article                           AS sku,
    t.nomenclature                      AS product_name,
    t.organization                      AS organization,
    CASE 
        WHEN SUM(t.quantity_dt) > 0 
        THEN ROUND(SUM(t.amount_dt) / SUM(t.quantity_dt), 2)
        ELSE NULL
    END                                 AS cost_price,
    SUM(t.amount_dt)                    AS total_cost,
    SUM(t.quantity_dt)                  AS total_quantity,
    t.period                            AS period,
    MAX(t.loaded_at)                    AS data_freshness
FROM 1C_account_turns_90 t
WHERE t.account_dt = '90.02.1'
  AND t.article IS NOT NULL
  AND t.article != ''
GROUP BY t.article, t.nomenclature, t.organization, t.period;

COMMENT ON VIEW cfo_v_cost_prices IS 
    'Себестоимость по SKU из 1C_account_turns_90 (Q-01, счёт 90.02.1)';

-- 2. Создание VIEW для выручки (сверка)
CREATE OR REPLACE VIEW cfo_v_revenue_1c AS
SELECT
    t.article                           AS sku,
    t.nomenclature                      AS product_name,
    t.counterparty                      AS counterparty,
    t.organization                      AS organization,
    SUM(t.amount_ct)                    AS revenue,
    SUM(t.quantity_dt)                  AS quantity,
    t.period                            AS period,
    MAX(t.loaded_at)                    AS data_freshness
FROM 1C_account_turns_90 t
WHERE t.account_ct = '90.01.1'
  AND t.article IS NOT NULL
  AND t.article != ''
GROUP BY t.article, t.nomenclature, t.counterparty, t.organization, t.period;

COMMENT ON VIEW cfo_v_revenue_1c IS 
    'Выручка по SKU из 1C_account_turns_90 (Q-01, счёт 90.01.1)';

-- 3. Удаление устаревшей таблицы cfo_cost_prices
-- Сначала бэкап на случай отката
CREATE TABLE IF NOT EXISTS cfo_cost_prices_backup_v11 AS 
    SELECT * FROM cfo_cost_prices;

DROP TABLE IF EXISTS cfo_cost_prices CASCADE;

-- 4. Обновление комментариев зависимых таблиц
COMMENT ON COLUMN cfo_pnl_daily.cogs IS 
    'Себестоимость из cfo_v_cost_prices (1C_account_turns_90)';
COMMENT ON COLUMN cfo_pnl_daily.cogs_mapped IS 
    'Флаг наличия себестоимости в 1C_*';

-- 5. Удаление настройки cost_identifier (больше не нужна)
DELETE FROM cfo_settings WHERE key = 'cost_identifier';

-- 6. Добавление настройки 1c_freshness_threshold
INSERT INTO cfo_settings (key, value, value_type, description)
VALUES (
    '1c_freshness_threshold_hours', 
    '240', 
    'integer', 
    'Порог устаревания 1C_* данных в часах (10 дней)'
)
ON CONFLICT (key) DO NOTHING;

COMMIT;

5.14.2 Откат миграции

sql
-- Rollback: 003_cfo_1c_views.sql

BEGIN;

-- 1. Восстановление таблицы из бэкапа
CREATE TABLE cfo_cost_prices AS 
    SELECT * FROM cfo_cost_prices_backup_v11;

-- 2. Восстановление индексов и constraints
ALTER TABLE cfo_cost_prices ADD PRIMARY KEY (id);
CREATE UNIQUE INDEX idx_cfo_cost_prices_sku_active 
    ON cfo_cost_prices(sku) WHERE is_active = TRUE;

-- 3. Удаление VIEW
DROP VIEW IF EXISTS cfo_v_cost_prices;
DROP VIEW IF EXISTS cfo_v_revenue_1c;

-- 4. Восстановление настройки
INSERT INTO cfo_settings (key, value, value_type, description)
VALUES ('cost_identifier', '"sku"', 'string', 'Идентификатор для связи с себестоимостью')
ON CONFLICT (key) DO NOTHING;

DELETE FROM cfo_settings WHERE key = '1c_freshness_threshold_hours';

-- 5. Удаление бэкапа
DROP TABLE IF EXISTS cfo_cost_prices_backup_v11;

COMMIT;

5.15 Предыдущие миграции

Миграция v1.0 → v1.1

sql
-- Migration: 002_cfo_remove_barcode_from_costs.sql
-- Version: 1.1
-- Date: 2026-01
-- Description: Удаление barcode из cfo_cost_prices, переход на идентификацию по SKU

BEGIN;

DROP INDEX IF EXISTS idx_cfo_cost_prices_barcode_active;
ALTER TABLE cfo_cost_prices DROP COLUMN IF EXISTS barcode;

ALTER TABLE cfo_cost_prices DROP CONSTRAINT IF EXISTS chk_cfo_cost_prices_identifier;
ALTER TABLE cfo_cost_prices ADD CONSTRAINT chk_cfo_cost_prices_sku_required 
    CHECK (sku IS NOT NULL AND sku != '');

ALTER TABLE cfo_cost_prices 
    ADD COLUMN IF NOT EXISTS brand_id VARCHAR(100);

INSERT INTO cfo_settings (key, value, value_type, description)
VALUES ('cost_identifier', '"sku"', 'string', 'Идентификатор для связи с себестоимостью')
ON CONFLICT (key) DO NOTHING;

UPDATE cfo_settings 
SET value = '{"time": "06:00", "timezone": "Asia/Omsk"}'
WHERE key = 'import_schedule';

COMMIT;

5.16 История изменений

v2.0 (Февраль 2026)

ОбъектИзменениеОписание
cfo_cost_pricesУдаленаЗаменена VIEW cfo_v_cost_prices поверх 1C_account_turns_90
cfo_v_cost_pricesСоздан VIEWСебестоимость из бухучёта (счёт 90.02.1)
cfo_v_revenue_1cСоздан VIEWВыручка из бухучёта (счёт 90.01.1), для сверки
ER-диаграммаОбновленаДобавлен блок 1C_* как источник данных
Раздел 5.3НовыйОписание связи CFO ↔ 1C_*
cfo_settingsДобавлена настройка1c_freshness_threshold_hours = 240

v1.1 (Январь 2026)

ОбъектИзменениеОписание
cfo_cost_pricesУдалена колонка barcodeИдентификация только по SKU
cfo_cost_pricesДобавлена колонка brand_idВид номенклатуры (бренд)
cfo_settingsИзменён timezoneAsia/Omsk вместо Europe/Moscow

Документ подготовлен: Февраль 2026
Версия: 2.0
Статус: Черновик

Документация ADOLF Platform