metallkart-erp/scripts/seed-extended.ts
2026-04-08 15:48:24 +03:00

671 lines
38 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 } from '@prisma/client';
const prisma = new PrismaClient();
/**
* seed-extended.ts — Seed données avancées pour test ERP MetallKart
* Prérequis: seed.ts et seed-steel-references.ts déjà exécutés
*
* Crée: Tenders, PurchaseOrders, PO lines, PO invoices, PO payment schedules,
* Smeta lines, Notifications, Audit logs, Order status history, Budget validations
*/
async function main() {
// ── Fetch existing data ───────────────────────────────────
const users = await prisma.user.findMany();
const userByEmail = Object.fromEntries(users.map(u => [u.email, u]));
const orders = await prisma.order.findMany({ include: { smetas: true } });
const orderByCode = Object.fromEntries(orders.map(o => [o.orderCode, o]));
const suppliers = await prisma.supplier.findMany();
const supplierByInn = Object.fromEntries(suppliers.map(s => [s.inn, s]));
const requirements = await prisma.purchaseRequirement.findMany();
const clients = await prisma.client.findMany();
const clientByAbbrev = Object.fromEntries(clients.map(c => [c.abbreviation, c]));
const louis = userByEmail['louis@metallcart.ru'];
const maxim = userByEmail['maxim@metallcart.ru'];
const nikita = userByEmail['nikita@metallcart.ru'];
const annaV = userByEmail['anna.v@metallcart.ru'];
const timofey = userByEmail['timofey@metallcart.ru'];
const evgeny = userByEmail['evgeny@metallcart.ru'];
const tamara = userByEmail['tamara@metallcart.ru'];
const darya = userByEmail['darya@metallcart.ru'];
const tmk = supplierByInn['7700000102'];
const severstal = supplierByInn['3500000001'];
const kraskaprom = supplierByInn['6900000201'];
const komplekt = supplierByInn['7800000501'];
const tlt = supplierByInn['6900000701'];
const orderLC = orderByCode['ЛЦ2603СП-50'];
const orderNTS = orderByCode['НТС2603М-1'];
const orderPP = orderByCode['ПП2603Т-100'];
const orderFS = orderByCode[С2603СШ-200'];
const orderMT = orderByCode['МТ2603К-50'];
const orderAZP = orderByCode['АЗП2603В-25'];
// ── SMETA LINES ───────────────────────────────────────────
console.log('Seeding smeta lines...');
const smetaLC = orderLC?.smetas?.[0];
const smetaPP = orderPP?.smetas?.[0];
if (smetaLC) {
const existing = await prisma.smetaLine.count({ where: { smetaId: smetaLC.id } });
if (existing === 0) {
await prisma.smetaLine.createMany({
data: [
{ smetaId: smetaLC.id, lineType: 'SERVICE', lineCategory: 'Лазерная резка', description: 'Лазерная резка листа 3мм — стойки', unit: 'м.п.', quantity: 500, unitPrice: 120, totalPrice: 60000, position: 1 },
{ smetaId: smetaLC.id, lineType: 'SERVICE', lineCategory: 'Лазерная резка', description: 'Лазерный труборез — труба 80x80x4', unit: 'рез', quantity: 800, unitPrice: 50, totalPrice: 40000, position: 2 },
{ smetaId: smetaLC.id, lineType: 'SERVICE', lineCategory: 'ЧПУ гибка', description: 'Гибка листа 3мм — поперечины', unit: 'шт', quantity: 200, unitPrice: 250, totalPrice: 50000, position: 3 },
{ smetaId: smetaLC.id, lineType: 'SERVICE', lineCategory: 'Покраска', description: 'Порошковая покраска RAL 5015', unit: 'м²', quantity: 200, unitPrice: 800, totalPrice: 160000, position: 4 },
{ smetaId: smetaLC.id, lineType: 'SERVICE', lineCategory: 'Гальваника', description: 'Цинкование крепежа', unit: 'кг', quantity: 50, unitPrice: 1300, totalPrice: 65000, position: 5 },
{ smetaId: smetaLC.id, lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Профильная труба 80x80x4 С245', unit: 'кг', quantity: 2500, unitPrice: 85, totalPrice: 212500, position: 6 },
{ smetaId: smetaLC.id, lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Лист г/к 3мм С245 1500x6000', unit: 'шт', quantity: 40, unitPrice: 8000, totalPrice: 320000, position: 7 },
{ smetaId: smetaLC.id, lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Полоса 40x4 С245', unit: 'м.п.', quantity: 300, unitPrice: 120, totalPrice: 36000, position: 8 },
{ smetaId: smetaLC.id, lineType: 'MATERIAL', lineCategory: 'Крепеж', description: 'Болт М12x40 оц. DIN 933', unit: 'шт', quantity: 2000, unitPrice: 9, totalPrice: 18000, position: 9 },
{ smetaId: smetaLC.id, lineType: 'MATERIAL', lineCategory: 'Крепеж', description: 'Гайка М12 оц. DIN 934', unit: 'шт', quantity: 2000, unitPrice: 4, totalPrice: 8000, position: 10 },
{ smetaId: smetaLC.id, lineType: 'MATERIAL', lineCategory: 'Комплектующие', description: 'Балка полочная 2700мм', unit: 'шт', quantity: 400, unitPrice: 2140, totalPrice: 856000, position: 11 },
{ smetaId: smetaLC.id, lineType: 'LABOR', lineCategory: 'Сборка', description: 'Сварка каркасов стоек', unit: 'н/ч', quantity: 200, normHours: 200, unitPrice: 1500, totalPrice: 300000, position: 12 },
{ smetaId: smetaLC.id, lineType: 'LABOR', lineCategory: 'Сборка', description: 'Контрольная сборка стеллажей', unit: 'н/ч', quantity: 100, normHours: 100, unitPrice: 2000, totalPrice: 200000, position: 13 },
],
});
console.log(' ✓ 13 smeta lines for ЛЦ2603СП-50');
}
}
if (smetaPP) {
const existing = await prisma.smetaLine.count({ where: { smetaId: smetaPP.id } });
if (existing === 0) {
await prisma.smetaLine.createMany({
data: [
{ smetaId: smetaPP.id, lineType: 'SERVICE', lineCategory: 'Лазерная резка', description: 'Лазерная резка листа 2мм — платформы', unit: 'м.п.', quantity: 300, unitPrice: 100, totalPrice: 30000, position: 1 },
{ smetaId: smetaPP.id, lineType: 'SERVICE', lineCategory: 'ЧПУ гибка', description: 'Гибка листа — борта тележки', unit: 'шт', quantity: 400, unitPrice: 180, totalPrice: 72000, position: 2 },
{ smetaId: smetaPP.id, lineType: 'SERVICE', lineCategory: 'Покраска', description: 'Порошковая покраска RAL 7035', unit: 'м²', quantity: 350, unitPrice: 800, totalPrice: 280000, position: 3 },
{ smetaId: smetaPP.id, lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Труба профильная 40x40x3 С245', unit: 'кг', quantity: 1800, unitPrice: 75, totalPrice: 135000, position: 4 },
{ smetaId: smetaPP.id, lineType: 'MATERIAL', lineCategory: 'Металл', description: 'Лист г/к 2мм С245 1250x2500', unit: 'шт', quantity: 100, unitPrice: 3500, totalPrice: 350000, position: 5 },
{ smetaId: smetaPP.id, lineType: 'MATERIAL', lineCategory: 'Комплектующие', description: 'Колесо поворотное Ø160 резина', unit: 'шт', quantity: 400, unitPrice: 650, totalPrice: 260000, position: 6 },
{ smetaId: smetaPP.id, lineType: 'LABOR', lineCategory: 'Сборка', description: 'Сварка рам тележек', unit: 'н/ч', quantity: 150, normHours: 150, unitPrice: 1500, totalPrice: 225000, position: 7 },
{ smetaId: smetaPP.id, lineType: 'LABOR', lineCategory: 'Сборка', description: 'Монтаж колёс и ручек', unit: 'н/ч', quantity: 50, normHours: 50, unitPrice: 1500, totalPrice: 75000, position: 8 },
],
});
console.log(' ✓ 8 smeta lines for ПП2603Т-100');
}
}
// ── TENDERS ────────────────────────────────────────────────
console.log('\nSeeding tenders...');
const reqsMetal = requirements.filter(r => r.orderId === orderLC?.id && r.materialCategory === 'METAL');
const reqsFasteners = requirements.filter(r => r.orderId === orderLC?.id && r.materialCategory === 'FASTENERS');
const reqsPaint = requirements.filter(r => r.orderId === orderLC?.id && r.materialCategory === 'PAINT');
// Tender 1: Metal for ЛЦ order
let tender1 = await prisma.tender.findFirst({ where: { referenceCode: 'TND-2026/03-001' } });
if (!tender1) {
tender1 = await prisma.tender.create({
data: {
referenceCode: 'TND-2026/03-001',
description: 'Металлопрокат для заказа ЛЦ2603СП-50 — труба 80x80x4, лист 3мм',
status: 'AWARDED',
createdById: evgeny.id,
deadline: new Date('2026-03-15'),
notes: 'Срочно, производство стартовало',
},
});
// Link requirements to tender
for (const r of reqsMetal) {
await prisma.purchaseRequirement.update({ where: { id: r.id }, data: { tenderId: tender1.id } });
}
console.log(' ✓ TND-2026/03-001 (металл ЛЦ) — AWARDED');
}
// Tender 2: Components for ПП order
let tender2 = await prisma.tender.findFirst({ where: { referenceCode: 'TND-2026/03-002' } });
if (!tender2) {
const reqsComponents = requirements.filter(r => r.orderId === orderPP?.id && r.materialCategory === 'COMPONENTS');
tender2 = await prisma.tender.create({
data: {
referenceCode: 'TND-2026/03-002',
description: 'Колёса поворотные Ø160 для тележек ТП-500 (100 шт)',
status: 'COMPARING',
createdById: evgeny.id,
deadline: new Date('2026-03-25'),
notes: 'Сравнение 3 поставщиков',
},
});
for (const r of reqsComponents) {
await prisma.purchaseRequirement.update({ where: { id: r.id }, data: { tenderId: tender2.id } });
}
console.log(' ✓ TND-2026/03-002 (колёса ПП) — COMPARING');
}
// Tender 3: Metal for ФС order — draft
let tender3 = await prisma.tender.findFirst({ where: { referenceCode: 'TND-2026/04-001' } });
if (!tender3) {
const reqsMetalFS = requirements.filter(r => r.orderId === orderFS?.id && r.materialCategory === 'METAL');
tender3 = await prisma.tender.create({
data: {
referenceCode: 'TND-2026/04-001',
description: 'Профильная труба 60x60x3 для стеллажей шинных ФС2603СШ-200',
status: 'SENT',
createdById: evgeny.id,
deadline: new Date('2026-04-10'),
},
});
for (const r of reqsMetalFS) {
await prisma.purchaseRequirement.update({ where: { id: r.id }, data: { tenderId: tender3.id } });
}
console.log(' ✓ TND-2026/04-001 (металл ФС) — SENT');
}
// ── TENDER OFFERS ──────────────────────────────────────────
console.log('\nSeeding tender offers...');
if (tender1) {
const existingOffers = await prisma.tenderOffer.count({ where: { tenderId: tender1.id } });
if (existingOffers === 0) {
const offer1 = await prisma.tenderOffer.create({
data: {
tenderId: tender1.id,
supplierId: tmk.id,
proposedPrice: 395000,
deliveryDays: 3,
conditions: 'Предоплата 100%. Доставка со склада Москва.',
receivedAt: new Date('2026-03-10'),
isSelected: true,
receivedVia: 'email',
validUntil: new Date('2026-04-10'),
ranking: 1,
},
});
// Create offer lines
for (const r of reqsMetal) {
await prisma.tenderOfferLine.create({
data: {
offerId: offer1.id,
requirementId: r.id,
unitPrice: r.materialCategory === 'METAL' && r.description.includes('труба') ? 34 : 8000,
quantity: Number(r.quantity),
unit: r.unit ?? 'кг',
totalLinePrice: r.materialCategory === 'METAL' && r.description.includes('труба') ? 85000 : 320000,
available: true,
},
});
}
// Award
await prisma.tender.update({
where: { id: tender1.id },
data: { awardedOfferId: offer1.id, awardedAt: new Date('2026-03-12') },
});
console.log(' ✓ Offer TMK (AWARDED) — 395 000 ₽, 3 jours');
// Second offer (not selected)
await prisma.tenderOffer.create({
data: {
tenderId: tender1.id,
supplierId: severstal.id,
proposedPrice: 420000,
deliveryDays: 5,
conditions: 'Предоплата 50%, остаток по факту. Доставка из Череповца.',
receivedAt: new Date('2026-03-11'),
isSelected: false,
receivedVia: 'email',
validUntil: new Date('2026-04-11'),
ranking: 2,
},
});
console.log(' ✓ Offer Северсталь — 420 000 ₽, 5 jours');
}
}
if (tender2) {
const existingOffers = await prisma.tenderOffer.count({ where: { tenderId: tender2.id } });
if (existingOffers === 0) {
await prisma.tenderOffer.create({
data: {
tenderId: tender2.id,
supplierId: komplekt.id,
proposedPrice: 115000,
deliveryDays: 7,
conditions: 'Оплата по факту поставки. Гарантия 1 год.',
receivedAt: new Date('2026-03-18'),
isSelected: false,
receivedVia: 'email',
ranking: 1,
},
});
await prisma.tenderOffer.create({
data: {
tenderId: tender2.id,
supplierId: severstal.id,
proposedPrice: 128000,
deliveryDays: 5,
conditions: 'Предоплата 100%. Немецкие колёса Blickle.',
receivedAt: new Date('2026-03-19'),
isSelected: false,
receivedVia: 'email',
ranking: 2,
},
});
console.log(' ✓ 2 offers for TND-2026/03-002 (comparing)');
}
}
// ── TENDER DISPATCHES ──────────────────────────────────────
console.log('\nSeeding tender dispatches...');
if (tender3) {
const existingDisp = await prisma.tenderDispatch.count({ where: { tenderId: tender3.id } });
if (existingDisp === 0) {
await prisma.tenderDispatch.createMany({
data: [
{ tenderId: tender3.id, supplierId: tmk.id, supplierEmail: 'lebedev@tmk-market.ru', sentAt: new Date('2026-04-01'), status: 'SENT' },
{ tenderId: tender3.id, supplierId: severstal.id, supplierEmail: 'volkov@severstal-metiz.ru', sentAt: new Date('2026-04-01'), status: 'SENT' },
],
});
console.log(' ✓ 2 dispatches for TND-2026/04-001');
}
}
// ── PURCHASE ORDERS ────────────────────────────────────────
console.log('\nSeeding purchase orders...');
// PO 1: Metal from TMK for ЛЦ order (awarded from tender 1)
let po1 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2026/03-001' } });
if (!po1 && orderLC && tender1) {
po1 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2026/03-001',
supplierId: tmk.id,
clientOrderId: orderLC.id,
totalAmount: 395000,
status: 'SENT',
emailSentAt: new Date('2026-03-13'),
tenderId: tender1.id,
createdById: evgeny.id,
expectedDeliveryDate: new Date('2026-03-18'),
notes: 'Привязан к тендеру TND-2026/03-001. Доставка ТК Деловые Линии.',
items: JSON.stringify([
{ description: 'Профильная труба 80x80x4 С245', qty: 2500, unit: 'кг', price: 85000 },
{ description: 'Лист г/к 3мм С245 1500x6000', qty: 40, unit: 'шт', price: 320000 },
]),
},
});
// PO lines
await prisma.purchaseOrderLine.createMany({
data: [
{ purchaseOrderId: po1.id, description: 'Профильная труба 80x80x4 С245', material: 'С245', quantity: 2500, unit: 'кг', estimatedPrice: 85000, finalPrice: 85000 },
{ purchaseOrderId: po1.id, description: 'Лист г/к 3мм С245 1500x6000', material: 'С245', quantity: 40, unit: 'шт', estimatedPrice: 320000, finalPrice: 310000 },
],
});
console.log(' ✓ PO-2026/03-001 (TMK → ЛЦ) — 395 000 ₽ SENT');
}
// PO 2: Paint from KraskaProm for ЛЦ order
let po2 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2026/03-002' } });
if (!po2 && orderLC) {
po2 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2026/03-002',
supplierId: kraskaprom.id,
clientOrderId: orderLC.id,
totalAmount: 160000,
status: 'CONFIRMED',
createdById: evgeny.id,
expectedDeliveryDate: new Date('2026-04-01'),
notes: 'Покраска стоек стеллажей. Предоплата 50% после подтверждения.',
items: JSON.stringify([
{ description: 'Порошковая покраска RAL 5015', qty: 200, unit: 'м²', price: 160000 },
]),
},
});
await prisma.purchaseOrderLine.create({
data: { purchaseOrderId: po2.id, description: 'Порошковая покраска RAL 5015', quantity: 200, unit: 'м²', estimatedPrice: 160000, finalPrice: 160000 },
});
console.log(' ✓ PO-2026/03-002 (КраскаПром → ЛЦ) — 160 000 ₽ CONFIRMED');
}
// PO 3: Fasteners from Severstal for ЛЦ order
let po3 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2026/03-003' } });
if (!po3 && orderLC) {
po3 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2026/03-003',
supplierId: severstal.id,
clientOrderId: orderLC.id,
totalAmount: 26000,
status: 'RECEIVED',
createdById: evgeny.id,
expectedDeliveryDate: new Date('2026-03-20'),
actualDeliveryDate: new Date('2026-03-19'),
deliveryNotes: 'Принято полностью. ТТН №154 от 19.03.2026.',
items: JSON.stringify([
{ description: 'Болт М12x40 оц. DIN 933', qty: 2000, unit: 'шт', price: 18000 },
{ description: 'Гайка М12 оц. DIN 934', qty: 2000, unit: 'шт', price: 8000 },
]),
},
});
await prisma.purchaseOrderLine.createMany({
data: [
{ purchaseOrderId: po3.id, description: 'Болт М12x40 оц. DIN 933', quantity: 2000, unit: 'шт', estimatedPrice: 18000, finalPrice: 18000, receivedQuantity: 2000, receivedAt: new Date('2026-03-19') },
{ purchaseOrderId: po3.id, description: 'Гайка М12 оц. DIN 934', quantity: 2000, unit: 'шт', estimatedPrice: 8000, finalPrice: 8000, receivedQuantity: 2000, receivedAt: new Date('2026-03-19') },
],
});
console.log(' ✓ PO-2026/03-003 (Северсталь → ЛЦ) — 26 000 ₽ RECEIVED');
}
// PO 4: Internal TLT order
let po4 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2026/03-004' } });
if (!po4 && orderLC && tlt) {
po4 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2026/03-004',
supplierId: tlt.id,
clientOrderId: orderLC.id,
totalAmount: 100000,
status: 'SHIPPED',
isInternalTlt: true,
tltPaymentMode: 'DEFERRED',
createdById: evgeny.id,
expectedDeliveryDate: new Date('2026-03-22'),
notes: 'Лазерная резка листа и труборез — внутренний заказ ТЛТ',
items: JSON.stringify([
{ description: 'Лазерная резка листа 3мм', qty: 500, unit: 'м.п.', price: 60000 },
{ description: 'Лазерный труборез 80x80x4', qty: 800, unit: 'рез', price: 40000 },
]),
},
});
await prisma.purchaseOrderLine.createMany({
data: [
{ purchaseOrderId: po4.id, description: 'Лазерная резка листа 3мм — стойки', quantity: 500, unit: 'м.п.', estimatedPrice: 60000, finalPrice: 60000 },
{ purchaseOrderId: po4.id, description: 'Лазерный труборез труба 80x80x4', quantity: 800, unit: 'рез', estimatedPrice: 40000, finalPrice: 40000 },
],
});
console.log(' ✓ PO-2026/03-004 (ТЛТ interne → ЛЦ) — 100 000 ₽ SHIPPED');
}
// PO 5: Metal for ПП order
let po5 = await prisma.purchaseOrder.findFirst({ where: { orderCode: 'PO-2026/03-005' } });
if (!po5 && orderPP) {
po5 = await prisma.purchaseOrder.create({
data: {
orderCode: 'PO-2026/03-005',
supplierId: tmk.id,
clientOrderId: orderPP.id,
totalAmount: 54000,
status: 'DRAFT',
createdById: evgeny.id,
expectedDeliveryDate: new Date('2026-04-05'),
notes: 'Труба для тележек. Ожидает подтверждения бюджета.',
items: JSON.stringify([
{ description: 'Труба профильная 40x40x3 С245', qty: 1800, unit: 'кг', price: 54000 },
]),
},
});
await prisma.purchaseOrderLine.create({
data: { purchaseOrderId: po5.id, description: 'Труба профильная 40x40x3 С245', material: 'С245', quantity: 1800, unit: 'кг', estimatedPrice: 54000 },
});
console.log(' ✓ PO-2026/03-005 (TMK → ПП) — 54 000 ₽ DRAFT');
}
// ── PO INVOICES ────────────────────────────────────────────
console.log('\nSeeding PO invoices...');
if (po1) {
const existingInv = await prisma.pOInvoice.count({ where: { purchaseOrderId: po1.id } });
if (existingInv === 0) {
await prisma.pOInvoice.create({
data: {
purchaseOrderId: po1.id,
invoiceNumber: 'СЧ-ТМК-2026/03-0147',
amount: 395000,
status: 'APPROVED_AWAITING_PAYMENT',
filePath: '\\\\fs01\\Счета\\2026\\СЧ_ТМК_0147.pdf',
fileName: 'СЧ_ТМК_0147.pdf',
dueDate: new Date('2026-03-20'),
uploadedById: evgeny.id,
notes: 'Счёт на предоплату 100%',
},
});
console.log(' ✓ Invoice СЧ-ТМК-2026/03-0147 — 395 000 ₽');
}
}
if (po3) {
const existingInv = await prisma.pOInvoice.count({ where: { purchaseOrderId: po3.id } });
if (existingInv === 0) {
await prisma.pOInvoice.create({
data: {
purchaseOrderId: po3.id,
invoiceNumber: 'СЧ-СМ-2026/03-0089',
amount: 26000,
status: 'PAID',
filePath: '\\\\fs01\\Счета\\2026\\СЧ_СМ_0089.pdf',
fileName: 'СЧ_СМ_0089.pdf',
dueDate: new Date('2026-03-25'),
paidAt: new Date('2026-03-22'),
paidAmount: 26000,
uploadedById: evgeny.id,
notes: 'Оплачено. Крепёж получен.',
},
});
console.log(' ✓ Invoice СЧ-СМ-2026/03-0089 — 26 000 ₽ (PAID)');
}
}
// ── PO PAYMENT SCHEDULE ────────────────────────────────────
console.log('\nSeeding PO payment schedules...');
if (po2) {
const existingSched = await prisma.pOPaymentSchedule.findUnique({ where: { purchaseOrderId: po2.id } });
if (!existingSched) {
const schedule = await prisma.pOPaymentSchedule.create({
data: {
purchaseOrderId: po2.id,
paymentType: 'INSTALLMENT',
},
});
await prisma.pOInstallment.createMany({
data: [
{ scheduleId: schedule.id, percentage: 50, dueDate: new Date('2026-03-25'), label: 'Аванс 50%', sortOrder: 1 },
{ scheduleId: schedule.id, percentage: 50, dueDate: new Date('2026-04-10'), label: 'Остаток 50% по факту', sortOrder: 2 },
],
});
console.log(' ✓ Payment schedule PO-002: 50/50 (аванс + остаток)');
}
}
// ── PURCHASE LOTS ──────────────────────────────────────────
console.log('\nSeeding purchase lots...');
const reqTube80 = requirements.find(r => r.description?.includes('80x80x4') && r.orderId === orderLC?.id);
if (reqTube80 && po1) {
const existingLot = await prisma.purchaseLot.count({ where: { requirementId: reqTube80.id } });
if (existingLot === 0) {
await prisma.purchaseLot.create({
data: {
requirementId: reqTube80.id,
lotNumber: 1,
quantity: 2500,
plannedDate: new Date('2026-03-15'),
status: 'ORDERED',
purchaseOrderId: po1.id,
validatedByFinId: maxim.id,
validatedByTechId: nikita.id,
validatedAt: new Date('2026-03-13'),
},
});
console.log(' ✓ Lot for труба 80x80x4 → PO-001');
}
}
// ── BUDGET VALIDATIONS ─────────────────────────────────────
console.log('\nSeeding budget validations...');
if (orderLC) {
const existing = await prisma.budgetValidation.findUnique({ where: { orderId: orderLC.id } });
if (!existing) {
await prisma.budgetValidation.create({
data: {
orderId: orderLC.id,
totalEstimated: 2500000,
totalAwarded: 521000,
totalPO: 681000,
variance: -160000,
variancePercent: -6.4,
status: 'FULLY_APPROVED',
finValidatedBy: maxim.id,
finValidatedAt: new Date('2026-03-14'),
finComment: 'Бюджет в рамках допуска',
techValidatedBy: nikita.id,
techValidatedAt: new Date('2026-03-14'),
techComment: 'Технически корректно',
},
});
console.log(' ✓ Budget validation ЛЦ2603СП-50 — FULLY_APPROVED');
}
}
if (orderPP) {
const existing = await prisma.budgetValidation.findUnique({ where: { orderId: orderPP.id } });
if (!existing) {
await prisma.budgetValidation.create({
data: {
orderId: orderPP.id,
totalEstimated: 1500000,
totalAwarded: 0,
totalPO: 54000,
variance: 0,
variancePercent: 0,
status: 'PENDING',
},
});
console.log(' ✓ Budget validation ПП2603Т-100 — PENDING');
}
}
// ── ORDER STATUS HISTORY ───────────────────────────────────
console.log('\nSeeding order status history...');
if (orderLC) {
const existingHist = await prisma.orderStatusHistory.count({ where: { orderId: orderLC.id } });
if (existingHist === 0) {
await prisma.orderStatusHistory.createMany({
data: [
{ orderId: orderLC.id, fromStatus: 'DRAFT', toStatus: 'AWAITING_ESTIMATE', changedById: annaV.id, createdAt: new Date('2026-02-10'), comment: 'Заказ передан в ПТО' },
{ orderId: orderLC.id, fromStatus: 'AWAITING_ESTIMATE', toStatus: 'AWAITING_VALIDATION', changedById: timofey.id, createdAt: new Date('2026-02-14'), comment: 'Смета готова, отправлена на валидацию' },
{ orderId: orderLC.id, fromStatus: 'AWAITING_VALIDATION', toStatus: 'VALIDATED', changedById: maxim.id, createdAt: new Date('2026-02-17'), comment: 'Бюджет утверждён' },
{ orderId: orderLC.id, fromStatus: 'VALIDATED', toStatus: 'LAUNCHED', changedById: nikita.id, createdAt: new Date('2026-02-20'), comment: 'Запуск в производство' },
{ orderId: orderLC.id, fromStatus: 'LAUNCHED', toStatus: 'IN_PRODUCTION', changedById: darya.id, createdAt: new Date('2026-02-25'), comment: 'Материалы закуплены, производство начато' },
],
});
console.log(' ✓ 5 status transitions for ЛЦ2603СП-50');
}
}
if (orderAZP) {
const existingHist = await prisma.orderStatusHistory.count({ where: { orderId: orderAZP.id } });
if (existingHist === 0) {
await prisma.orderStatusHistory.createMany({
data: [
{ orderId: orderAZP.id, fromStatus: 'DRAFT', toStatus: 'VALIDATED', changedById: maxim.id, createdAt: new Date('2026-01-10') },
{ orderId: orderAZP.id, fromStatus: 'VALIDATED', toStatus: 'LAUNCHED', changedById: nikita.id, createdAt: new Date('2026-01-15') },
{ orderId: orderAZP.id, fromStatus: 'LAUNCHED', toStatus: 'IN_PRODUCTION', changedById: darya.id, createdAt: new Date('2026-01-20') },
{ orderId: orderAZP.id, fromStatus: 'IN_PRODUCTION', toStatus: 'SHIPPED', changedById: darya.id, createdAt: new Date('2026-02-25') },
{ orderId: orderAZP.id, fromStatus: 'SHIPPED', toStatus: 'DELIVERED', changedById: annaV.id, createdAt: new Date('2026-02-28'), comment: 'Клиент подтвердил приёмку' },
],
});
console.log(' ✓ 5 status transitions for АЗП2603В-25');
}
}
// ── NOTIFICATIONS ──────────────────────────────────────────
console.log('\nSeeding notifications...');
const existingNotifs = await prisma.notification.count();
if (existingNotifs === 0) {
await prisma.notification.createMany({
data: [
{ recipientId: evgeny.id, type: 'PAYMENT_REQUEST', message: 'Запрос PR-202603-001 одобрен — 320 000 ₽ металлопрокат', entityType: 'PaymentRequest', priority: 'NORMAL', actionRequired: false, createdAt: new Date('2026-03-12') },
{ recipientId: maxim.id, type: 'PAYMENT_PENDING', message: 'Новая заявка PR-202603-003 — покраска 160 000 ₽ ожидает одобрения', entityType: 'PaymentRequest', priority: 'HIGH', actionRequired: true, actionUrl: '/payments', createdAt: new Date('2026-03-15') },
{ recipientId: nikita.id, type: 'BUDGET_ALERT', message: 'Заказ МТ2603К-50 — расход материалов 98% бюджета (YELLOW)', entityType: 'FinancialTracking', priority: 'HIGH', actionRequired: true, actionUrl: '/orders', createdAt: new Date('2026-03-14') },
{ recipientId: annaV.id, type: 'DOCUMENT_REMINDER', message: 'Договор ДОГ-ФС-2026/03-004 — 1-е напоминание отправлено', entityType: 'Document', priority: 'NORMAL', actionRequired: false, createdAt: new Date('2026-03-11') },
{ recipientId: annaV.id, type: 'ORDER_STATUS', message: 'Заказ НТС2603М-1 ожидает валидации сметы', entityType: 'Order', priority: 'NORMAL', actionRequired: true, actionUrl: '/orders', createdAt: new Date('2026-03-16') },
{ recipientId: tamara.id, type: 'PAYMENT_REQUEST', message: 'Заявка PR-202603-002 — расходники 45 000 ₽ ожидает проверки', entityType: 'PaymentRequest', priority: 'NORMAL', actionRequired: true, actionUrl: '/payments', createdAt: new Date('2026-03-13') },
{ recipientId: evgeny.id, type: 'PO_DELIVERY', message: 'PO-2026/03-003 — крепёж получен, оприходовать', entityType: 'PurchaseOrder', priority: 'NORMAL', actionRequired: true, createdAt: new Date('2026-03-19') },
{ recipientId: louis.id, type: 'SYSTEM', message: 'Ежедневный backup выполнен — 45 таблиц, PG dump OK', entityType: 'System', priority: 'LOW', actionRequired: false, createdAt: new Date('2026-04-06') },
{ recipientId: darya.id, type: 'ORDER_STATUS', message: 'Тележки ПП2603Т-100 — заготовки готовы, передать на сварку', entityType: 'Order', priority: 'HIGH', actionRequired: true, actionUrl: '/orders', createdAt: new Date('2026-03-20') },
{ recipientId: nikita.id, type: 'TENDER', message: 'Тендер TND-2026/03-002 — 2 предложения получены, ожидает сравнения', entityType: 'Tender', priority: 'NORMAL', actionRequired: true, createdAt: new Date('2026-03-20') },
],
});
console.log(' ✓ 10 notifications');
}
// ── AUDIT LOGS ─────────────────────────────────────────────
console.log('\nSeeding audit logs...');
const existingAudit = await prisma.auditLog.count();
if (existingAudit === 0) {
await prisma.auditLog.createMany({
data: [
{ userId: louis.id, action: 'LOGIN', entityType: 'User', entityId: louis.id, details: { ip: '192.168.1.10' }, createdAt: new Date('2026-04-06T08:00:00') },
{ userId: annaV.id, action: 'CREATE', entityType: 'Order', entityId: String(orderLC?.id), details: { orderCode: 'ЛЦ2603СП-50' }, createdAt: new Date('2026-02-10T10:30:00') },
{ userId: maxim.id, action: 'APPROVE', entityType: 'Smeta', entityId: String(orderLC?.smetas?.[0]?.id ?? '0'), details: { orderCode: 'ЛЦ2603СП-50', version: 1 }, createdAt: new Date('2026-02-17T14:15:00') },
{ userId: evgeny.id, action: 'CREATE', entityType: 'Tender', entityId: String(tender1?.id ?? '0'), details: { referenceCode: 'TND-2026/03-001' }, createdAt: new Date('2026-03-08T09:00:00') },
{ userId: evgeny.id, action: 'AWARD', entityType: 'Tender', entityId: String(tender1?.id ?? '0'), details: { referenceCode: 'TND-2026/03-001', supplier: 'ТМК-Маркет' }, createdAt: new Date('2026-03-12T16:00:00') },
{ userId: evgeny.id, action: 'CREATE', entityType: 'PurchaseOrder', entityId: String(po1?.id ?? '0'), details: { orderCode: 'PO-2026/03-001', amount: 395000 }, createdAt: new Date('2026-03-13T10:00:00') },
{ userId: maxim.id, action: 'APPROVE', entityType: 'PaymentRequest', entityId: '1', details: { requestNumber: 'PR-202603-001', amount: 320000 }, createdAt: new Date('2026-03-12T11:30:00') },
{ userId: tamara.id, action: 'CREATE', entityType: 'CashflowEntry', entityId: '1', details: { type: 'INCOME', amount: 1250000, client: 'Логистик-Центр' }, createdAt: new Date('2026-03-01T12:00:00') },
{ userId: darya.id, action: 'UPDATE', entityType: 'Order', entityId: String(orderLC?.id ?? '0'), details: { status: 'IN_PRODUCTION' }, createdAt: new Date('2026-02-25T08:30:00') },
{ userId: nikita.id, action: 'APPROVE', entityType: 'BudgetValidation', entityId: String(orderLC?.id ?? '0'), details: { orderCode: 'ЛЦ2603СП-50', type: 'TECH' }, createdAt: new Date('2026-03-14T15:00:00') },
{ userId: louis.id, action: 'EXPORT', entityType: 'Report', entityId: '0', details: { type: 'cashflow_monthly', month: '03/2026' }, createdAt: new Date('2026-03-31T18:00:00') },
{ userId: annaV.id, action: 'CREATE', entityType: 'Document', entityId: '1', details: { type: 'CONTRACT', ref: 'ДОГ-НТС-2026/03-002' }, createdAt: new Date('2026-03-03T09:45:00') },
],
});
console.log(' ✓ 12 audit logs');
}
// ── APRIL BUDGETS ──────────────────────────────────────────
console.log('\nSeeding April budgets...');
const aprilBudgets = [
{ category: 'PRODUCTION_SUPPLY', expenseCode: 'ZH14', month: 4, year: 2026, allocated: 600000 },
{ category: 'PRODUCTION_SUPPLY', expenseCode: 'ZH15', month: 4, year: 2026, allocated: 250000 },
{ category: 'ADMINISTRATIVE', expenseCode: 'A', month: 4, year: 2026, allocated: 350000 },
{ category: 'EXCEPTIONAL', expenseCode: 'B', month: 4, year: 2026, allocated: 150000 },
{ category: 'DIRECT_PROJECT', expenseCode: 'E11', month: 4, year: 2026, allocated: 3500000 },
{ category: 'DIRECT_PROJECT', expenseCode: 'E13', month: 4, year: 2026, allocated: 900000 },
{ category: 'DIRECT_PROJECT', expenseCode: 'E14', month: 4, year: 2026, allocated: 500000 },
];
for (const b of aprilBudgets) {
await prisma.monthlyBudget.upsert({
where: { category_expenseCode_month_year: { category: b.category as any, expenseCode: b.expenseCode as any, month: b.month, year: b.year } },
update: { allocatedAmount: b.allocated },
create: { category: b.category as any, expenseCode: b.expenseCode as any, month: b.month, year: b.year, allocatedAmount: b.allocated, consumedAmount: 0, alertThreshold: 70 },
});
}
console.log(`${aprilBudgets.length} April budgets`);
// ── MORE CASHFLOW (APRIL) ─────────────────────────────────
console.log('\nSeeding April cashflow entries...');
const aprilCf = [
{ date: '2026-04-05', type: 'EXPENSE', category: 'Аренда', desc: 'Аренда цеха апрель 2026', amount: 150000, status: 'PLANNED', expenseCode: 'A' },
{ date: '2026-04-05', type: 'EXPENSE', category: 'ЗП', desc: 'Заработная плата апрель 2026', amount: 2500000, status: 'PLANNED', expenseCode: 'A' },
{ date: '2026-04-10', type: 'INCOME', category: 'Оплата от заказчика', desc: 'Аванс 50% — ООО "Фарма-Склад" заказ ФС2603СШ-200', amount: 1800000, status: 'PLANNED' },
{ date: '2026-04-15', type: 'EXPENSE', category: 'Оплата поставщику', desc: 'Оплата покраски PO-2026/03-002 аванс 50%', amount: 80000, status: 'PLANNED', expenseCode: 'E13' },
{ date: '2026-04-20', type: 'INCOME', category: 'Оплата от заказчика', desc: 'Остаток 50% — ООО "ПищеПром" заказ ПП2603Т-100', amount: 1050000, status: 'PLANNED' },
];
for (const cf of aprilCf) {
const existing = await prisma.cashflowEntry.findFirst({ where: { description: cf.desc } });
if (!existing) {
await prisma.cashflowEntry.create({
data: {
date: new Date(cf.date),
type: cf.type as any,
category: cf.category,
description: cf.desc,
amount: cf.amount,
status: cf.status as any,
expenseCode: (cf as any).expenseCode as any ?? undefined,
source: 'MANUAL',
createdById: tamara.id,
},
});
}
console.log(`${cf.date} ${cf.type} ${cf.amount.toLocaleString('ru')}`);
}
console.log('\n✅ Extended seed complete.');
}
main()
.catch((e) => {
console.error('Extended seed error:', e);
process.exit(1);
})
.finally(() => prisma.$disconnect());