metallkart-erp/scripts/seed-staging.ts

513 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PrismaClient, Prisma } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { seedHelpArticles } from '../src/modules/help/help.seed.js';
// ═══════════════════════════════════════════════════════════════
// SEED-VPS-STAGING — Dataset réaliste pour VPS staging
// Target: erp_staging uniquement (safety check ci-dessous)
// ═══════════════════════════════════════════════════════════════
const PASSWORD = bcrypt.hashSync('MK_Staging_2026!', 10);
// ─── Safety check ───────────────────────────────────────────
function assertSafeDatabase(url: string) {
if (!url.includes('erp_staging') && !url.includes('erp_test')) {
throw new Error(
`[SAFETY] REFUSED — DATABASE_URL must target erp_staging or erp_test.\n` +
` Got: ${url.replace(/:[^@]+@/, ':***@')}\n` +
` This seed MUST NOT run on erp_local or any production DB.`
);
}
console.log(`[SAFETY] OK — DB target: ${url.replace(/:[^@]+@/, ':***@')}`);
}
// ─── Truncate ───────────────────────────────────────────────
async function truncateAll(prisma: PrismaClient) {
await prisma.$executeRawUnsafe(`
TRUNCATE TABLE
audit_logs,
cashflow_entries,
client_installments,
forecast_installments,
client_payments_received,
planned_delivery_lots,
delivery_lots,
po_installments,
po_payment_schedules,
payment_requests,
purchase_order_lines,
purchase_orders,
purchase_lot_lines,
purchase_lots,
tender_offer_lines,
tender_line_awards,
tender_offers,
tender_dispatches,
tenders,
purchase_requirements,
purchase_line_receptions,
supplier_delivery_notes,
smeta_lines,
smeta_revisions,
additional_costs,
smeta_budget_mappings,
smetas,
estimate_comments,
contract_payment_terms,
client_contracts,
client_invoices,
supplier_contract_payment_terms,
supplier_contracts,
documents,
document_files,
file_tracking_configs,
forecast_rebalance_logs,
order_status_history,
order_revisions,
order_category_notes,
order_photos,
financial_trackings,
budget_validations,
monthly_budgets,
recurring_payments,
notifications,
reminder_configs,
po_invoices,
bank_statements,
onec_sync_log,
onec_bank_accounts,
onec_cashflow_categories,
category_tag_mappings,
steel_references,
payment_templates,
payment_template_terms,
orders,
clients,
supplier_contacts,
supplier_tag_links,
supplier_tags,
suppliers,
user_notification_preferences,
users,
help_articles,
expense_codes,
public_holidays
RESTART IDENTITY CASCADE;
`);
console.log('[TRUNCATE] OK');
}
// ═══════════════════════════════════════════════════════════════
// DATA
// ═══════════════════════════════════════════════════════════════
async function seedStaging(prisma: PrismaClient) {
await truncateAll(prisma);
// ─── 1. USERS (10) ──────────────────────────────────────────
const users = await Promise.all([
prisma.user.create({ data: { email: 'louis@metallcart.ru', password: PASSWORD, name: 'Луи-Андре Милло', role: 'ADMIN' } }),
prisma.user.create({ data: { email: 'maksim@metallcart.ru', password: PASSWORD, name: 'Максим Кусайло', role: 'DIRECTION_FIN' } }),
prisma.user.create({ data: { email: 'nikita@metallcart.ru', password: PASSWORD, name: 'Никита Шипков', role: 'DIRECTION_OPS' } }),
prisma.user.create({ data: { email: 'evgeniy@metallcart.ru', password: PASSWORD, name: 'Евгений Пиняжин', role: 'ACHETEUR' } }),
prisma.user.create({ data: { email: 'tamara@metallcart.ru', password: PASSWORD, name: 'Тамара Бурова', role: 'COMPTABLE' } }),
prisma.user.create({ data: { email: 'anna@metallcart.ru', password: PASSWORD, name: 'Анна Федорова', role: 'COMMERCIAL' } }),
prisma.user.create({ data: { email: 'vasilieva@metallcart.ru', password: PASSWORD, name: 'Ирина Васильева', role: 'INGENIEUR_CHEF' } }),
prisma.user.create({ data: { email: 'martyanov@metallcart.ru', password: PASSWORD, name: 'Алексей Мартьянов', role: 'INGENIEUR_PTO' } }),
prisma.user.create({ data: { email: 'kozlov@metallcart.ru', password: PASSWORD, name: 'Дмитрий Козлов', role: 'ADM_PRODUCTION' } }),
prisma.user.create({ data: { email: 'petrov@metallcart.ru', password: PASSWORD, name: 'Сергей Петров', role: 'RESP_ENTREPOT' } }),
]);
const [louis, maksim, nikita, evgeniy, tamara, anna, vasilieva, martyanov, kozlov, petrov] = users;
console.log(`[USERS] ${users.length} users créés, password=MK_Staging_2026!`);
// ─── 2. CLIENTS (8) ────────────────────────────────────────
const clients = await Promise.all([
prisma.client.create({ data: { companyName: 'ООО «Ашан Ритейл Россия»', inn: '7703270067', abbreviation: 'АШАН', isActive: true } }),
prisma.client.create({ data: { companyName: 'АО «Леруа Мерлен Восток»', inn: '5029069967', abbreviation: 'ЛЕРУА', isActive: true } }),
prisma.client.create({ data: { companyName: 'ООО «Вайлдберриз»', inn: '7721546864', abbreviation: 'WB', isActive: true } }),
prisma.client.create({ data: { companyName: 'ООО «Озон Холдинг»', inn: '7704357909', abbreviation: 'ОЗОН', isActive: true } }),
prisma.client.create({ data: { companyName: 'АО «Тандер» (Магнит)', inn: '2310031475', abbreviation: 'МАГНИТ', isActive: true } }),
prisma.client.create({ data: { companyName: 'ООО «Комус»', inn: '7706202481', abbreviation: 'КОМУС', isActive: true } }),
prisma.client.create({ data: { companyName: 'ООО «РосМеталлСтрой»', inn: '6901234567', abbreviation: 'РМС', isActive: true } }),
prisma.client.create({ data: { companyName: 'АО «Тверской Вагоностроительный»', inn: '6900012345', abbreviation: 'ТВЗ', isActive: true } }),
]);
const [cAshan, cLerua, cWB, cOzon, cMagnit, cKomus, cRMS, cTVZ] = clients;
console.log(`[CLIENTS] ${clients.length} клиентов créés`);
// ─── 3. SUPPLIERS (7) ──────────────────────────────────────
const suppliers = await Promise.all([
prisma.supplier.create({ data: { companyName: 'ООО «Северсталь-Метиз»', inn: '3528000597', contactEmail: 'sales@severstal-metiz.ru', status: 'ACTIVE' } }),
prisma.supplier.create({ data: { companyName: 'ООО «ТЛТ» (Тверские Лазерные)', inn: '6950200001', contactEmail: 'info@tlt-tver.ru', status: 'ACTIVE', isInternal: true } }),
prisma.supplier.create({ data: { companyName: 'АО «НЛМК-Метиз»', inn: '4823006703', contactEmail: 'sales@nlmk-metiz.ru', status: 'ACTIVE' } }),
prisma.supplier.create({ data: { companyName: 'ООО «Крепёж-Тверь»', inn: '6950100002', contactEmail: 'orders@krepezh-tver.ru', status: 'ACTIVE' } }),
prisma.supplier.create({ data: { companyName: 'ООО «Полимер-Покрытие»', inn: '6950300003', contactEmail: 'paint@polymer-pokrytie.ru', status: 'ACTIVE' } }),
prisma.supplier.create({ data: { companyName: 'ООО «ТрансЛогистик»', inn: '7728800004', contactEmail: 'dispatch@translogistic.ru', status: 'ACTIVE' } }),
prisma.supplier.create({ data: { companyName: 'ООО «Электро-Тверь»', inn: '6950400005', contactEmail: 'info@electro-tver.ru', status: 'ACTIVE' } }),
]);
const [sSever, sTLT, sNLMK, sKrepezh, sPolymer, sTrans, sElectro] = suppliers;
console.log(`[SUPPLIERS] ${suppliers.length} поставщиков créés`);
// ─── 4. CLIENT CONTRACTS (5) ───────────────────────────────
const cc1 = await prisma.clientContract.create({
data: { clientId: cAshan.id, contractNumber: 'КД-2026-001', contractDate: new Date('2026-01-10'), startDate: new Date('2026-01-10'), status: 'ACTIVE' },
});
await prisma.contractPaymentTerm.createMany({ data: [
{ contractId: cc1.id, trigger: 'ORDER_DATE', percent: new Prisma.Decimal(30), label: 'Предоплата 30%', offsetDays: 0, sortOrder: 0 },
{ contractId: cc1.id, trigger: 'SHIPMENT_DATE', percent: new Prisma.Decimal(70), label: 'После отгрузки 70%', offsetDays: 14, sortOrder: 1 },
]});
const cc2 = await prisma.clientContract.create({
data: { clientId: cLerua.id, contractNumber: 'КД-2026-002', contractDate: new Date('2026-02-01'), startDate: new Date('2026-02-01'), status: 'ACTIVE' },
});
await prisma.contractPaymentTerm.createMany({ data: [
{ contractId: cc2.id, trigger: 'ORDER_DATE', percent: new Prisma.Decimal(50), label: 'Предоплата 50%', offsetDays: 0, sortOrder: 0 },
{ contractId: cc2.id, trigger: 'SHIPMENT_DATE', percent: new Prisma.Decimal(50), label: 'После отгрузки 50%', offsetDays: 30, sortOrder: 1 },
]});
const cc3 = await prisma.clientContract.create({
data: { clientId: cWB.id, contractNumber: 'КД-2026-003', contractDate: new Date('2026-03-01'), startDate: new Date('2026-03-01'), status: 'ACTIVE' },
});
await prisma.contractPaymentTerm.createMany({ data: [
{ contractId: cc3.id, trigger: 'ORDER_DATE', percent: new Prisma.Decimal(100), label: '100% предоплата', offsetDays: 0, sortOrder: 0 },
]});
const cc4 = await prisma.clientContract.create({
data: { clientId: cOzon.id, contractNumber: 'КД-2026-004', contractDate: new Date('2026-03-15'), startDate: new Date('2026-03-15'), status: 'ACTIVE' },
});
await prisma.contractPaymentTerm.createMany({ data: [
{ contractId: cc4.id, trigger: 'ORDER_DATE', percent: new Prisma.Decimal(20), label: 'Аванс 20%', offsetDays: 0, sortOrder: 0 },
{ contractId: cc4.id, trigger: 'PRODUCTION_READY', percent: new Prisma.Decimal(30), label: 'При готовности 30%', offsetDays: 0, sortOrder: 1 },
{ contractId: cc4.id, trigger: 'SHIPMENT_DATE', percent: new Prisma.Decimal(50), label: 'После отгрузки 50%', offsetDays: 21, sortOrder: 2 },
]});
const cc5 = await prisma.clientContract.create({
data: { clientId: cMagnit.id, contractNumber: 'КД-2026-005', contractDate: new Date('2026-04-01'), startDate: new Date('2026-04-01'), status: 'DRAFT' },
});
await prisma.contractPaymentTerm.createMany({ data: [
{ contractId: cc5.id, trigger: 'ORDER_DATE', percent: new Prisma.Decimal(40), label: 'Предоплата 40%', offsetDays: 0, sortOrder: 0 },
{ contractId: cc5.id, trigger: 'SHIPMENT_DATE', percent: new Prisma.Decimal(60), label: 'После отгрузки 60%', offsetDays: 14, sortOrder: 1 },
]});
console.log('[CLIENT_CONTRACTS] 5 contrats créés');
// ─── 5. SUPPLIER CONTRACTS (4) ─────────────────────────────
const sc1 = await prisma.supplierContract.create({
data: { supplierId: sSever.id, contractNumber: 'ПД-2026-001', contractDate: new Date('2026-01-15'), startDate: new Date('2026-01-15'), status: 'SIGNED', signedAt: new Date('2026-01-15') },
});
await prisma.supplierContractPaymentTerm.createMany({ data: [
{ contractId: sc1.id, trigger: 'PREPAYMENT', percent: new Prisma.Decimal(50), label: 'Предоплата 50%', offsetDays: 0, sortOrder: 0 },
{ contractId: sc1.id, trigger: 'ON_DELIVERY', percent: new Prisma.Decimal(50), label: 'По доставке 50%', offsetDays: 30, sortOrder: 1 },
]});
const sc2 = await prisma.supplierContract.create({
data: { supplierId: sTLT.id, contractNumber: 'ПД-2026-002', contractDate: new Date('2026-02-01'), startDate: new Date('2026-02-01'), status: 'SIGNED', signedAt: new Date('2026-02-01') },
});
await prisma.supplierContractPaymentTerm.createMany({ data: [
{ contractId: sc2.id, trigger: 'ON_DELIVERY', percent: new Prisma.Decimal(100), label: '100% по факту', offsetDays: 14, sortOrder: 0 },
]});
const sc3 = await prisma.supplierContract.create({
data: { supplierId: sNLMK.id, contractNumber: 'ПД-2026-003', contractDate: new Date('2026-02-15'), startDate: new Date('2026-02-15'), status: 'SIGNED', signedAt: new Date('2026-02-20') },
});
await prisma.supplierContractPaymentTerm.createMany({ data: [
{ contractId: sc3.id, trigger: 'PREPAYMENT', percent: new Prisma.Decimal(30), label: 'Аванс 30%', offsetDays: 0, sortOrder: 0 },
{ contractId: sc3.id, trigger: 'POST_DELIVERY', percent: new Prisma.Decimal(70), label: 'Постоплата 70%', offsetDays: 45, sortOrder: 1 },
]});
const sc4 = await prisma.supplierContract.create({
data: { supplierId: sKrepezh.id, contractNumber: 'ПД-2026-004', contractDate: new Date('2026-04-01'), startDate: new Date('2026-04-01'), status: 'DRAFT' },
});
await prisma.supplierContractPaymentTerm.createMany({ data: [
{ contractId: sc4.id, trigger: 'PREPAYMENT', percent: new Prisma.Decimal(100), label: '100% предоплата', offsetDays: 0, sortOrder: 0 },
]});
console.log('[SUPPLIER_CONTRACTS] 4 contrats créés');
// ─── 6. ORDERS (10) ────────────────────────────────────────
// 3 DRAFT, 3 IN_PRODUCTION, 2 SHIPPED (IN_DELIVERY equiv), 2 DELIVERED (CLOSED equiv)
const orderDefs: Array<{
orderCode: string; productName: string; quantity: number;
status: string; clientId: number; managerId: string;
contractId?: number; executionUnit?: string; launchDate?: Date; deliveryDate?: Date;
}> = [
// DRAFT (3)
{ orderCode: 'MK-2026-001', productName: 'Ролл-контейнер РК-800', quantity: 200, status: 'DRAFT', clientId: cAshan.id, managerId: anna.id, contractId: cc1.id },
{ orderCode: 'MK-2026-002', productName: 'Стеллаж палетный СП-3000', quantity: 50, status: 'DRAFT', clientId: cLerua.id, managerId: anna.id, contractId: cc2.id },
{ orderCode: 'MK-2026-003', productName: 'Корзина торговая КТ-30', quantity: 500, status: 'DRAFT', clientId: cMagnit.id, managerId: anna.id },
// IN_PRODUCTION (3)
{ orderCode: 'MK-2026-004', productName: 'Грузовая тележка ГТ-500', quantity: 100, status: 'IN_PRODUCTION', clientId: cAshan.id, managerId: anna.id, contractId: cc1.id, executionUnit: 'MAIN_PRODUCTION', launchDate: new Date('2026-03-01') },
{ orderCode: 'MK-2026-005', productName: 'Контейнер сетчатый КС-1200', quantity: 150, status: 'IN_PRODUCTION', clientId: cWB.id, managerId: anna.id, contractId: cc3.id, executionUnit: 'TLT', launchDate: new Date('2026-03-15') },
{ orderCode: 'MK-2026-006', productName: 'Полка складская ПС-600', quantity: 300, status: 'IN_PRODUCTION', clientId: cOzon.id, managerId: anna.id, contractId: cc4.id, executionUnit: 'MAIN_PRODUCTION', launchDate: new Date('2026-04-01') },
// SHIPPED (2) — using SHIPPED status as "in delivery"
{ orderCode: 'MK-2026-007', productName: 'Стеллаж консольный СК-2500', quantity: 30, status: 'SHIPPED', clientId: cLerua.id, managerId: anna.id, contractId: cc2.id, executionUnit: 'MAIN_PRODUCTION', launchDate: new Date('2026-02-01'), deliveryDate: new Date('2026-05-10') },
{ orderCode: 'MK-2026-008', productName: 'Поддон металлический ПМ-1200', quantity: 400, status: 'SHIPPED', clientId: cKomus.id, managerId: anna.id, executionUnit: 'SEVER', launchDate: new Date('2026-02-15'), deliveryDate: new Date('2026-05-15') },
// DELIVERED (2) — closed
{ orderCode: 'MK-2026-009', productName: 'Решётка сварная РС-100', quantity: 1000, status: 'DELIVERED', clientId: cRMS.id, managerId: anna.id, executionUnit: 'TLT', launchDate: new Date('2026-01-15'), deliveryDate: new Date('2026-04-20') },
{ orderCode: 'MK-2026-010', productName: 'Контейнер ёмкостный КЕ-800', quantity: 80, status: 'DELIVERED', clientId: cTVZ.id, managerId: anna.id, executionUnit: 'MAIN_PRODUCTION', launchDate: new Date('2026-01-20'), deliveryDate: new Date('2026-04-25') },
];
const orderRecords = [];
for (const def of orderDefs) {
const o = await prisma.order.create({
data: {
orderCode: def.orderCode,
productName: def.productName,
quantity: def.quantity,
status: def.status as any,
clientId: def.clientId,
managerId: def.managerId,
contractId: def.contractId,
executionUnit: (def.executionUnit ?? 'MAIN_PRODUCTION') as any,
launchDate: def.launchDate,
deliveryDate: def.deliveryDate,
},
});
orderRecords.push(o);
}
const [o1, o2, o3, o4, o5, o6, o7, o8, o9, o10] = orderRecords;
console.log(`[ORDERS] ${orderRecords.length} заказов créés (3 DRAFT, 3 PRODUCTION, 2 SHIPPED, 2 DELIVERED)`);
// Status history for non-DRAFT orders
const statusHistoryData = [
// IN_PRODUCTION orders
{ orderId: o4.id, fromStatus: 'VALIDATED', toStatus: 'IN_PRODUCTION', changedById: nikita.id },
{ orderId: o5.id, fromStatus: 'VALIDATED', toStatus: 'IN_PRODUCTION', changedById: nikita.id },
{ orderId: o6.id, fromStatus: 'VALIDATED', toStatus: 'IN_PRODUCTION', changedById: nikita.id },
// SHIPPED
{ orderId: o7.id, fromStatus: 'IN_PRODUCTION', toStatus: 'SHIPPED', changedById: nikita.id },
{ orderId: o8.id, fromStatus: 'IN_PRODUCTION', toStatus: 'SHIPPED', changedById: nikita.id },
// DELIVERED
{ orderId: o9.id, fromStatus: 'SHIPPED', toStatus: 'DELIVERED', changedById: petrov.id },
{ orderId: o10.id, fromStatus: 'SHIPPED', toStatus: 'DELIVERED', changedById: petrov.id },
];
for (const sh of statusHistoryData) {
await prisma.orderStatusHistory.create({ data: sh as any });
}
// ─── 7. SMETAS (7 — for orders 4-10, all LOCKED) ──────────
const smetaDefs: Array<{
orderId: number; sellingPrice: number;
lines: Array<{ lineType: string; lineCategory: string; description: string; unit: string; quantity: number; unitPrice: number; position: number }>;
}> = [
{ orderId: o4.id, sellingPrice: 3500000, lines: [
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Труба профильная 40x40x3', unit: 'м', quantity: 400, unitPrice: 550, position: 1 },
{ lineType: 'MATERIAL', lineCategory: 'Крепёж', description: 'Болт М12x40 цинк.', unit: 'шт', quantity: 2000, unitPrice: 12, position: 2 },
{ lineType: 'SERVICE', lineCategory: 'Лазер', description: 'Лазерная резка листового металла', unit: 'час', quantity: 40, unitPrice: 3500, position: 3 },
{ lineType: 'MATERIAL', lineCategory: 'Покраска', description: 'Порошковая окраска RAL-7035', unit: 'м²', quantity: 300, unitPrice: 180, position: 4 },
]},
{ orderId: o5.id, sellingPrice: 6000000, lines: [
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Лист стальной S235 3мм', unit: 'т', quantity: 5, unitPrice: 68000, position: 1 },
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Пруток круглый Ø12мм', unit: 'м', quantity: 600, unitPrice: 85, position: 2 },
{ lineType: 'SERVICE', lineCategory: 'Лазер', description: 'Лазерная резка трубы', unit: 'час', quantity: 60, unitPrice: 4000, position: 3 },
{ lineType: 'MATERIAL', lineCategory: 'Покраска', description: 'Оцинковка горячая', unit: 'м²', quantity: 500, unitPrice: 220, position: 4 },
{ lineType: 'MATERIAL', lineCategory: 'Крепёж', description: 'Гайка M10 оцинк.', unit: 'шт', quantity: 5000, unitPrice: 5, position: 5 },
]},
{ orderId: o6.id, sellingPrice: 4200000, lines: [
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Профиль Z 200x80x3', unit: 'м', quantity: 900, unitPrice: 620, position: 1 },
{ lineType: 'MATERIAL', lineCategory: 'Крепёж', description: 'Анкер забивной М8', unit: 'шт', quantity: 3000, unitPrice: 18, position: 2 },
{ lineType: 'SERVICE', lineCategory: 'Гибка', description: 'Гибка профиля', unit: 'шт', quantity: 900, unitPrice: 120, position: 3 },
{ lineType: 'MATERIAL', lineCategory: 'Покраска', description: 'Порошковая окраска RAL-5002', unit: 'м²', quantity: 400, unitPrice: 190, position: 4 },
]},
{ orderId: o7.id, sellingPrice: 2800000, lines: [
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Труба профильная 60x60x4', unit: 'м', quantity: 180, unitPrice: 780, position: 1 },
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Лист 5мм горячекатаный', unit: 'т', quantity: 2, unitPrice: 72000, position: 2 },
{ lineType: 'SERVICE', lineCategory: 'Лазер', description: 'Лазерная резка', unit: 'час', quantity: 25, unitPrice: 3500, position: 3 },
]},
{ orderId: o8.id, sellingPrice: 5500000, lines: [
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Лист стальной S355 4мм', unit: 'т', quantity: 8, unitPrice: 75000, position: 1 },
{ lineType: 'SERVICE', lineCategory: 'Сварка', description: 'Сварка полуавтомат', unit: 'час', quantity: 200, unitPrice: 1800, position: 2 },
{ lineType: 'MATERIAL', lineCategory: 'Покраска', description: 'Порошковая окраска RAL-6005', unit: 'м²', quantity: 600, unitPrice: 175, position: 3 },
]},
{ orderId: o9.id, sellingPrice: 8000000, lines: [
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Проволока Вр-1 Ø5мм', unit: 'т', quantity: 3, unitPrice: 82000, position: 1 },
{ lineType: 'SERVICE', lineCategory: 'Сварка', description: 'Контактная сварка сетки', unit: 'шт', quantity: 1000, unitPrice: 450, position: 2 },
{ lineType: 'MATERIAL', lineCategory: 'Покраска', description: 'Оцинковка гальваническая', unit: 'м²', quantity: 800, unitPrice: 280, position: 3 },
]},
{ orderId: o10.id, sellingPrice: 3200000, lines: [
{ lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Лист стальной 08ПС 2мм', unit: 'т', quantity: 4, unitPrice: 65000, position: 1 },
{ lineType: 'MATERIAL', lineCategory: 'Крепёж', description: 'Заклёпка вытяжная 4.8x12', unit: 'шт', quantity: 4000, unitPrice: 3, position: 2 },
{ lineType: 'SERVICE', lineCategory: 'Лазер', description: 'Лазерная резка + гибка', unit: 'час', quantity: 35, unitPrice: 4200, position: 3 },
{ lineType: 'MATERIAL', lineCategory: 'Покраска', description: 'Порошковая окраска RAL-9003', unit: 'м²', quantity: 250, unitPrice: 185, position: 4 },
]},
];
for (const sd of smetaDefs) {
const totalMaterials = sd.lines.filter(l => l.lineType === 'MATERIAL').reduce((s, l) => s + l.quantity * l.unitPrice, 0);
const totalServices = sd.lines.filter(l => l.lineType === 'SERVICE').reduce((s, l) => s + l.quantity * l.unitPrice, 0);
const totalLabor = sd.lines.filter(l => l.lineType === 'LABOR').reduce((s, l) => s + l.quantity * l.unitPrice, 0);
const smeta = await prisma.smeta.create({
data: {
orderId: sd.orderId,
status: 'LOCKED',
isLocked: true,
lockedAt: new Date('2026-04-01'),
sellingPrice: new Prisma.Decimal(sd.sellingPrice),
totalMaterials: new Prisma.Decimal(totalMaterials),
totalServices: new Prisma.Decimal(totalServices),
totalLabor: new Prisma.Decimal(totalLabor),
importSource: 'EXCEL',
importedFile: `smeta-${sd.orderId}.xlsx`,
},
});
await prisma.smetaLine.createMany({
data: sd.lines.map(l => ({
smetaId: smeta.id,
lineType: l.lineType as any,
lineCategory: l.lineCategory,
description: l.description,
unit: l.unit,
quantity: new Prisma.Decimal(l.quantity),
unitPrice: new Prisma.Decimal(l.unitPrice),
totalPrice: new Prisma.Decimal(l.quantity * l.unitPrice),
position: l.position,
})),
});
}
console.log(`[SMETAS] ${smetaDefs.length} смет créées (toutes LOCKED)`);
// ─── 8. PURCHASE REQUIREMENTS (20) ─────────────────────────
// Mix of statuses across orders 4-8 (production + shipped)
const prDefs: Array<{
orderId: number; materialCategory: string; description: string;
quantity: number; unit: string; estimatedPrice: number;
status: string; priority?: string; sourceType?: string;
}> = [
// Order 4 — ГТ-500
{ orderId: o4.id, materialCategory: 'METAL', description: 'Труба профильная 40x40x3', quantity: 400, unit: 'м', estimatedPrice: 550, status: 'CONFIRMED', sourceType: 'SMETA_IMPORT' },
{ orderId: o4.id, materialCategory: 'FASTENERS', description: 'Болт М12x40 цинк.', quantity: 2000, unit: 'шт', estimatedPrice: 12, status: 'CONFIRMED', sourceType: 'SMETA_IMPORT' },
{ orderId: o4.id, materialCategory: 'LASER_FLAT', description: 'Лазерная резка листового металла', quantity: 40, unit: 'час', estimatedPrice: 3500, status: 'DRAFT', sourceType: 'SMETA_IMPORT' },
{ orderId: o4.id, materialCategory: 'PAINT', description: 'Порошковая окраска RAL-7035', quantity: 300, unit: 'м²', estimatedPrice: 180, status: 'CONFIRMED', sourceType: 'SMETA_IMPORT' },
// Order 5 — КС-1200
{ orderId: o5.id, materialCategory: 'METAL', description: 'Лист стальной S235 3мм', quantity: 5, unit: 'т', estimatedPrice: 68000, status: 'ORDERED', sourceType: 'SMETA_IMPORT' },
{ orderId: o5.id, materialCategory: 'METAL', description: 'Пруток круглый Ø12мм', quantity: 600, unit: 'м', estimatedPrice: 85, status: 'ORDERED', sourceType: 'SMETA_IMPORT' },
{ orderId: o5.id, materialCategory: 'LASER_TUBE', description: 'Лазерная резка трубы', quantity: 60, unit: 'час', estimatedPrice: 4000, status: 'IN_TENDER', sourceType: 'SMETA_IMPORT' },
{ orderId: o5.id, materialCategory: 'PAINT', description: 'Оцинковка горячая', quantity: 500, unit: 'м²', estimatedPrice: 220, status: 'CONFIRMED', sourceType: 'SMETA_IMPORT' },
{ orderId: o5.id, materialCategory: 'FASTENERS', description: 'Гайка M10 оцинк.', quantity: 5000, unit: 'шт', estimatedPrice: 5, status: 'RECEIVED', sourceType: 'SMETA_IMPORT' },
// Order 6 — ПС-600
{ orderId: o6.id, materialCategory: 'METAL', description: 'Профиль Z 200x80x3', quantity: 900, unit: 'м', estimatedPrice: 620, status: 'CONFIRMED', sourceType: 'SMETA_IMPORT' },
{ orderId: o6.id, materialCategory: 'FASTENERS', description: 'Анкер забивной М8', quantity: 3000, unit: 'шт', estimatedPrice: 18, status: 'DRAFT', sourceType: 'SMETA_IMPORT' },
{ orderId: o6.id, materialCategory: 'TUBE_BENDING', description: 'Гибка профиля', quantity: 900, unit: 'шт', estimatedPrice: 120, status: 'CONFIRMED', sourceType: 'SMETA_IMPORT' },
{ orderId: o6.id, materialCategory: 'PAINT', description: 'Порошковая окраска RAL-5002', quantity: 400, unit: 'м²', estimatedPrice: 190, status: 'IN_TENDER', sourceType: 'SMETA_IMPORT' },
// Order 7 — СК-2500
{ orderId: o7.id, materialCategory: 'METAL', description: 'Труба профильная 60x60x4', quantity: 180, unit: 'м', estimatedPrice: 780, status: 'RECEIVED', sourceType: 'SMETA_IMPORT' },
{ orderId: o7.id, materialCategory: 'METAL', description: 'Лист 5мм горячекатаный', quantity: 2, unit: 'т', estimatedPrice: 72000, status: 'RECEIVED', sourceType: 'SMETA_IMPORT' },
{ orderId: o7.id, materialCategory: 'LASER_FLAT', description: 'Лазерная резка', quantity: 25, unit: 'час', estimatedPrice: 3500, status: 'ORDERED', sourceType: 'SMETA_IMPORT' },
// Order 8 — ПМ-1200
{ orderId: o8.id, materialCategory: 'METAL', description: 'Лист стальной S355 4мм', quantity: 8, unit: 'т', estimatedPrice: 75000, status: 'RECEIVED', sourceType: 'SMETA_IMPORT' },
{ orderId: o8.id, materialCategory: 'PAINT', description: 'Порошковая окраска RAL-6005', quantity: 600, unit: 'м²', estimatedPrice: 175, status: 'ORDERED', sourceType: 'SMETA_IMPORT' },
// Urgent PR
{ orderId: o4.id, materialCategory: 'COMPONENTS', description: 'Колёса поворотные Ø125мм с тормозом', quantity: 400, unit: 'шт', estimatedPrice: 350, status: 'CONFIRMED', priority: 'URGENT', sourceType: 'MANUAL' },
{ orderId: o6.id, materialCategory: 'COMPONENTS', description: 'Ножки регулируемые М10x80', quantity: 1200, unit: 'шт', estimatedPrice: 45, status: 'DRAFT', priority: 'HIGH', sourceType: 'MANUAL' },
];
for (const pr of prDefs) {
await prisma.purchaseRequirement.create({
data: {
orderId: pr.orderId,
materialCategory: pr.materialCategory as any,
description: pr.description,
quantity: new Prisma.Decimal(pr.quantity),
originalQuantity: new Prisma.Decimal(pr.quantity),
unit: pr.unit,
estimatedPrice: new Prisma.Decimal(pr.estimatedPrice),
budgetAmount: new Prisma.Decimal(pr.quantity * pr.estimatedPrice),
status: pr.status as any,
priority: (pr.priority ?? 'NORMAL') as any,
sourceType: pr.sourceType,
},
});
}
console.log(`[PR] ${prDefs.length} заявок créées (mix statuts)`);
// ─── 9. AUDIT LOGS (20) ────────────────────────────────────
const now = new Date();
const auditDefs: Array<{ daysAgo: number; userId: string; action: string; entityType: string; entityId: string; details?: object }> = [
{ daysAgo: 14, userId: anna.id, action: 'ORDER_CREATE', entityType: 'ORDER', entityId: String(o1.id), details: { orderCode: 'MK-2026-001', productName: 'Ролл-контейнер РК-800' } },
{ daysAgo: 13, userId: anna.id, action: 'ORDER_CREATE', entityType: 'ORDER', entityId: String(o2.id), details: { orderCode: 'MK-2026-002', productName: 'Стеллаж палетный СП-3000' } },
{ daysAgo: 12, userId: anna.id, action: 'ORDER_CREATE', entityType: 'ORDER', entityId: String(o3.id), details: { orderCode: 'MK-2026-003', productName: 'Корзина торговая КТ-30' } },
{ daysAgo: 11, userId: anna.id, action: 'ORDER_CREATE', entityType: 'ORDER', entityId: String(o4.id), details: { orderCode: 'MK-2026-004', productName: 'Грузовая тележка ГТ-500' } },
{ daysAgo: 10, userId: vasilieva.id, action: 'SMETA_IMPORT', entityType: 'ORDER', entityId: String(o4.id), details: { file: 'smeta-ГТ-500.xlsx' } },
{ daysAgo: 10, userId: vasilieva.id, action: 'SMETA_IMPORT', entityType: 'ORDER', entityId: String(o5.id), details: { file: 'smeta-КС-1200.xlsx' } },
{ daysAgo: 9, userId: martyanov.id, action: 'SMETA_REVISION_APPROVE', entityType: 'ORDER', entityId: String(o4.id), details: { version: 1 } },
{ daysAgo: 9, userId: martyanov.id, action: 'SMETA_REVISION_APPROVE', entityType: 'ORDER', entityId: String(o5.id), details: { version: 1 } },
{ daysAgo: 8, userId: evgeniy.id, action: 'PR_SUBMIT', entityType: 'ORDER', entityId: String(o4.id), details: { count: 4 } },
{ daysAgo: 8, userId: evgeniy.id, action: 'PR_SUBMIT', entityType: 'ORDER', entityId: String(o5.id), details: { count: 5 } },
{ daysAgo: 7, userId: evgeniy.id, action: 'TENDER_CREATE', entityType: 'TENDER', entityId: '1', details: { referenceCode: 'T-2026-001', description: 'Тендер на металл для ГТ-500' } },
{ daysAgo: 6, userId: evgeniy.id, action: 'TENDER_AWARD', entityType: 'TENDER', entityId: '1', details: { supplier: 'Северсталь-Метиз' } },
{ daysAgo: 5, userId: evgeniy.id, action: 'PO_CONFIRM', entityType: 'PURCHASE_ORDER', entityId: '1', details: { orderCode: 'PO-2026-001', total: 220000 } },
{ daysAgo: 4, userId: nikita.id, action: 'ORDER_STATUS_CHANGE', entityType: 'ORDER', entityId: String(o4.id), details: { from: 'VALIDATED', to: 'IN_PRODUCTION' } },
{ daysAgo: 3, userId: nikita.id, action: 'ORDER_STATUS_CHANGE', entityType: 'ORDER', entityId: String(o5.id), details: { from: 'VALIDATED', to: 'IN_PRODUCTION' } },
{ daysAgo: 3, userId: vasilieva.id, action: 'SMETA_IMPORT', entityType: 'ORDER', entityId: String(o6.id), details: { file: 'smeta-ПС-600.xlsx' } },
{ daysAgo: 2, userId: evgeniy.id, action: 'PR_SUBMIT', entityType: 'ORDER', entityId: String(o6.id), details: { count: 4 } },
{ daysAgo: 1, userId: anna.id, action: 'ORDER_CREATE', entityType: 'ORDER', entityId: String(o6.id), details: { orderCode: 'MK-2026-006', productName: 'Полка складская ПС-600' } },
{ daysAgo: 1, userId: nikita.id, action: 'ORDER_STATUS_CHANGE', entityType: 'ORDER', entityId: String(o6.id), details: { from: 'VALIDATED', to: 'IN_PRODUCTION' } },
{ daysAgo: 0, userId: louis.id, action: 'UPD_SIGN', entityType: 'ORDER', entityId: String(o7.id), details: { orderCode: 'MK-2026-007' } },
];
for (const a of auditDefs) {
const d = new Date(now);
d.setDate(d.getDate() - a.daysAgo);
d.setHours(9 + Math.floor(Math.random() * 8), Math.floor(Math.random() * 60));
await prisma.auditLog.create({
data: {
userId: a.userId,
action: a.action,
entityType: a.entityType,
entityId: a.entityId,
details: a.details as any,
createdAt: d,
},
});
}
console.log(`[AUDIT] ${auditDefs.length} событий créés`);
// ─── 10. HELP ARTICLES (59) ────────────────────────────────
console.log('[HELP] Seed des 59 articles d\'aide...');
await seedHelpArticles(prisma);
const helpCount = await prisma.helpArticle.count();
console.log(`[HELP] OK — ${helpCount} articles`);
// ─── 11. DOCUMENTS for contracts ───────────────────────────
// Sync documents for client contracts (like prod would)
for (const cc of [cc1, cc2, cc3, cc4]) {
await prisma.document.create({
data: {
documentType: 'CONTRACT',
direction: 'CLIENT',
title: `Договор ${cc.contractNumber}`,
clientContractId: cc.id,
responsibleId: louis.id,
},
});
}
// ═══════════════════════════════════════════════════════════
console.log('');
console.log('═══════════════════════════════════════════');
console.log(' STAGING SEED OK ✓');
console.log('═══════════════════════════════════════════');
console.log(' Login: any user@metallcart.ru / MK_Staging_2026!');
console.log(' Exemples:');
console.log(' - louis@metallcart.ru (ADMIN)');
console.log(' - maksim@metallcart.ru (DIRECTION_FIN)');
console.log(' - evgeniy@metallcart.ru (ACHETEUR)');
console.log(' - tamara@metallcart.ru (COMPTABLE)');
console.log('═══════════════════════════════════════════');
}
// ─── Runner ─────────────────────────────────────────────────
const url = process.env.DATABASE_URL;
if (!url) {
console.error('DATABASE_URL required. Usage:\n DATABASE_URL="postgresql://...erp_staging..." npx tsx scripts/seed-staging.ts');
process.exit(1);
}
assertSafeDatabase(url);
const prisma = new PrismaClient({ datasources: { db: { url } } });
seedStaging(prisma)
.catch((e) => { console.error('[STAGING SEED] FAILED:', e); process.exit(1); })
.finally(() => prisma.$disconnect());