671 lines
38 KiB
TypeScript
671 lines
38 KiB
TypeScript
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());
|