All files / src/domains/manufacturing/services mrp-engine.service.ts

0% Statements 0/51
0% Branches 0/34
0% Functions 0/7
0% Lines 0/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126                                                                                                                                                                                                                                                           
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { MrpRun, PlannedOrder, Forecast, MasterProductionSchedule } from '../schemas';
import { BomRoutingService } from './bom-routing.service';
import { InventoryService } from '../../inventory/services/inventory.service';
 
@Injectable()
export class MrpEngineService {
  constructor(
    @InjectModel(MrpRun.name) private readonly mrpRunModel: Model<MrpRun>,
    @InjectModel(PlannedOrder.name) private readonly plannedOrderModel: Model<PlannedOrder>,
    @InjectModel(Forecast.name) private readonly forecastModel: Model<Forecast>,
    @InjectModel(MasterProductionSchedule.name) private readonly mpsModel: Model<MasterProductionSchedule>,
    private readonly bomRoutingService: BomRoutingService,
    private readonly inventoryService: InventoryService
  ) {}
 
  async createForecast(tenantId: string, payload: any): Promise<Forecast> {
    const fc = new this.forecastModel({
      tenantId,
      forecastCode: payload.forecastCode,
      itemCode: payload.itemCode,
      forecastDate: new Date(payload.forecastDate),
      quantity: payload.quantity,
      status: 'Active',
    });
    return fc.save();
  }
 
  async runMrp(tenantId: string, userId: string): Promise<MrpRun> {
    const run = new this.mrpRunModel({
      tenantId,
      mrpRunCode: `MRP-${Date.now()}`,
      runDate: new Date(),
      runByUserId: userId,
      status: 'Running',
    });
    await run.save();
 
    try {
      // 1. Gather all active demand (forecasts + MPS lines)
      const forecasts = await this.forecastModel.find({ tenantId, status: 'Active' }).exec();
      const mpsLines = await this.mpsModel.find({ tenantId, status: 'Active' }).exec();
 
      const demands: { itemCode: string; qty: number; date: Date }[] = [];
      forecasts.forEach(f => demands.push({ itemCode: f.itemCode, qty: f.quantity, date: f.forecastDate }));
      mpsLines.forEach(m => demands.push({ itemCode: m.itemCode, qty: m.quantity, date: m.scheduleDate }));
 
      // 2. Process each demand line
      for (const demand of demands) {
        // Fetch current on hand balance
        const balances = await this.inventoryService.ledgerService.getBalances(tenantId, { itemCode: demand.itemCode });
        const onHandQty = balances.reduce((sum, b) => sum + (b.quantityOnHand || 0), 0);
 
        // Fetch reorder and safety stock settings from item definition
        const itemDef = await this.inventoryService.itemService.getItemByCode(tenantId, demand.itemCode);
        const safetyStock = itemDef?.safetyStock || 0;
 
        const netRequirement = demand.qty + safetyStock - onHandQty;
 
        if (netRequirement > 0) {
          // Explode BOM lines
          const explodedComponents = await this.bomRoutingService.explodeBom(tenantId, demand.itemCode, netRequirement);
 
          if (explodedComponents.length > 0) {
            // Exploded components become Planned Purchase or Production Orders
            for (const comp of explodedComponents) {
              const compOrder = new this.plannedOrderModel({
                tenantId,
                mrpRunId: run._id,
                itemCode: comp.componentItemCode,
                orderType: comp.isPhantom ? 'Production' : 'Purchase',
                quantity: comp.quantityNeeded,
                suggestedReleaseDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // 5 days lead time assumption
                suggestedDueDate: demand.date,
                status: 'Suggested',
              });
              await compOrder.save();
            }
 
            // Also create planned production order for the output item itself
            const outputOrder = new this.plannedOrderModel({
              tenantId,
              mrpRunId: run._id,
              itemCode: demand.itemCode,
              orderType: 'Production',
              quantity: netRequirement,
              suggestedReleaseDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // 3 days lead time
              suggestedDueDate: demand.date,
              status: 'Suggested',
            });
            await outputOrder.save();
          } else {
            // No BOM, suggest purchase order
            const purchaseOrder = new this.plannedOrderModel({
              tenantId,
              mrpRunId: run._id,
              itemCode: demand.itemCode,
              orderType: 'Purchase',
              quantity: netRequirement,
              suggestedReleaseDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days purchase lead time
              suggestedDueDate: demand.date,
              status: 'Suggested',
            });
            await purchaseOrder.save();
          }
        }
      }
 
      run.status = 'Completed';
      await run.save();
    } catch (e: any) {
      run.status = 'Failed';
      run.errorMessage = e.message;
      await run.save();
    }
 
    return run;
  }
 
  async getPlannedOrders(tenantId: string): Promise<PlannedOrder[]> {
    return this.plannedOrderModel.find({ tenantId, status: 'Suggested' }).exec();
  }
}