id(); $t->foreignId('company_id')->constrained()->cascadeOnDelete(); $t->string('code', 32); $t->string('name', 120); $t->string('address')->nullable(); $t->boolean('is_default')->default(false); $t->boolean('is_active')->default(true); $t->timestamps(); $t->softDeletes(); $t->unique(['company_id', 'code']); $t->index(['company_id', 'is_active']); }); Schema::create('part_batches', function (Blueprint $t) { $t->id(); $t->foreignId('company_id')->constrained()->cascadeOnDelete(); $t->foreignId('part_id')->constrained()->cascadeOnDelete(); $t->foreignId('warehouse_id')->constrained()->cascadeOnDelete(); $t->foreignId('supplier_id')->nullable()->constrained()->nullOnDelete(); $t->string('batch_ref', 64)->nullable(); // PO number / invoice ref $t->decimal('qty_in', 12, 3); // initial intake $t->decimal('qty_remaining', 12, 3); // current available $t->decimal('buy_price', 12, 2); // unit cost for this batch $t->dateTime('received_at'); $t->text('notes')->nullable(); $t->timestamps(); // FIFO consume = ORDER BY received_at ASC, id ASC $t->index(['company_id', 'part_id', 'warehouse_id', 'received_at']); $t->index(['company_id', 'qty_remaining']); }); Schema::create('warehouse_events', function (Blueprint $t) { $t->id(); $t->foreignId('company_id')->constrained()->cascadeOnDelete(); $t->foreignId('part_id')->constrained()->cascadeOnDelete(); $t->foreignId('batch_id')->nullable()->constrained('part_batches')->nullOnDelete(); $t->foreignId('warehouse_id')->constrained()->cascadeOnDelete(); $t->string('type', 24); // opening / receipt / issue / transfer_out / transfer_in / adjustment / write_off / return $t->decimal('qty_delta', 12, 3); // signed: + intake, − issue $t->decimal('unit_cost', 12, 2)->nullable(); $t->nullableMorphs('ref'); // ref_type/ref_id — link la WorkOrder/Purchase/Transfer/Inventory $t->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); $t->dateTime('occurred_at'); $t->text('notes')->nullable(); $t->timestamps(); $t->index(['company_id', 'part_id', 'occurred_at']); $t->index(['company_id', 'warehouse_id', 'occurred_at']); $t->index(['company_id', 'type']); }); Schema::create('part_reservations', function (Blueprint $t) { $t->id(); $t->foreignId('company_id')->constrained()->cascadeOnDelete(); $t->foreignId('work_order_id')->constrained()->cascadeOnDelete(); $t->foreignId('work_order_part_id')->nullable()->constrained('wo_parts')->cascadeOnDelete(); $t->foreignId('part_id')->constrained()->cascadeOnDelete(); $t->foreignId('batch_id')->constrained('part_batches')->cascadeOnDelete(); $t->decimal('qty', 12, 3); $t->string('status', 16)->default('active'); // active / consumed / released $t->dateTime('reserved_at'); $t->dateTime('consumed_at')->nullable(); $t->timestamps(); $t->index(['company_id', 'work_order_id']); $t->index(['company_id', 'part_id', 'status']); $t->index(['company_id', 'batch_id', 'status']); }); Schema::table('companies', function (Blueprint $t) { $t->foreignId('default_warehouse_id')->nullable()->after('plan_id') ->constrained('warehouses')->nullOnDelete(); }); Schema::table('parts', function (Blueprint $t) { $t->decimal('qty_reserved', 12, 3)->default(0)->after('qty'); }); // ── Backfill ────────────────────────────────────────────────── // For each existing company: create 1 default warehouse + open // one batch per part with the current parts.qty as opening stock. $now = now(); foreach (Company::withoutGlobalScopes()->cursor() as $company) { $whId = DB::table('warehouses')->insertGetId([ 'company_id' => $company->id, 'code' => 'MAIN', 'name' => 'Depozit principal', 'is_default' => true, 'is_active' => true, 'created_at' => $now, 'updated_at' => $now, ]); DB::table('companies')->where('id', $company->id) ->update(['default_warehouse_id' => $whId]); $parts = DB::table('parts') ->where('company_id', $company->id) ->whereNull('deleted_at') ->where('qty', '>', 0) ->get(); foreach ($parts as $p) { $batchId = DB::table('part_batches')->insertGetId([ 'company_id' => $company->id, 'part_id' => $p->id, 'warehouse_id' => $whId, 'qty_in' => $p->qty, 'qty_remaining' => $p->qty, 'buy_price' => $p->buy_price, 'received_at' => $p->created_at ?: $now, 'notes' => 'Stoc inițial (migrare)', 'created_at' => $now, 'updated_at' => $now, ]); DB::table('warehouse_events')->insert([ 'company_id' => $company->id, 'part_id' => $p->id, 'batch_id' => $batchId, 'warehouse_id' => $whId, 'type' => 'opening', 'qty_delta' => $p->qty, 'unit_cost' => $p->buy_price, 'occurred_at' => $p->created_at ?: $now, 'notes' => 'Stoc inițial la migrare', 'created_at' => $now, 'updated_at' => $now, ]); } } } public function down(): void { Schema::table('parts', function (Blueprint $t) { $t->dropColumn('qty_reserved'); }); Schema::table('companies', function (Blueprint $t) { $t->dropForeign(['default_warehouse_id']); $t->dropColumn('default_warehouse_id'); }); Schema::dropIfExists('part_reservations'); Schema::dropIfExists('warehouse_events'); Schema::dropIfExists('part_batches'); Schema::dropIfExists('warehouses'); } };