513 lines
39 KiB
TypeScript
513 lines
39 KiB
TypeScript
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());
|