feat(生產/庫存): 實作生產管理模組與批號追溯功能
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 53s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-21 17:19:36 +08:00
parent fc20c6d813
commit 1ae21febb5
17 changed files with 1753 additions and 33 deletions

View File

@@ -0,0 +1,90 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* 新增批號追溯相關欄位至 inventories 資料表。
* 批號格式:{商品代號}-{來源國家}-{入庫日期}-{批次流水號}
* 完整格式(含箱號):{商品代號}-{來源國家}-{入庫日期}-{批次流水號}-{箱號}
*/
public function up(): void
{
// Step 1: 新增批號相關欄位
Schema::table('inventories', function (Blueprint $table) {
// 批號組成:{商品代號}-{來源國家}-{入庫日期}-{批次流水號}
$table->string('batch_number', 50)->nullable()->after('location')
->comment('批號 (格式: AB-VN-20260119-01)');
$table->string('box_number', 10)->nullable()->after('batch_number')
->comment('箱號 (如: 01, 02)');
// 批號解析欄位(方便查詢與排序)
$table->string('origin_country', 10)->nullable()->after('box_number')
->comment('來源國家代碼 (如: VN, TW)');
$table->date('arrival_date')->nullable()->after('origin_country')
->comment('入庫日期');
$table->date('expiry_date')->nullable()->after('arrival_date')
->comment('效期');
// 來源追溯
$table->foreignId('source_purchase_order_id')->nullable()->after('expiry_date')
->constrained('purchase_orders')->nullOnDelete()
->comment('來源採購單');
// 品質狀態
$table->enum('quality_status', ['normal', 'frozen', 'rejected'])
->default('normal')->after('source_purchase_order_id')
->comment('品質狀態:正常/凍結/退貨');
$table->text('quality_remark')->nullable()->after('quality_status')
->comment('品質異常備註');
});
// Step 2: 為現有資料設定預設批號 (LEGACY-{id})
DB::statement("UPDATE inventories SET batch_number = CONCAT('LEGACY-', id) WHERE batch_number IS NULL");
// Step 3: 將 batch_number 改為必填
Schema::table('inventories', function (Blueprint $table) {
$table->string('batch_number', 50)->nullable(false)->change();
});
// Step 4: 新增批號相關索引 (不刪除舊索引,因為有外鍵依賴)
// 舊的 warehouse_product_unique 保留,新增更精確的批號索引
Schema::table('inventories', function (Blueprint $table) {
$table->index(['warehouse_id', 'product_id', 'batch_number'], 'inventories_batch_lookup');
$table->index(['arrival_date'], 'inventories_arrival_date');
$table->index(['quality_status'], 'inventories_quality_status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('inventories', function (Blueprint $table) {
// 移除索引
$table->dropIndex('inventories_batch_lookup');
$table->dropIndex('inventories_arrival_date');
$table->dropIndex('inventories_quality_status');
// 移除新增欄位
$table->dropForeign(['source_purchase_order_id']);
$table->dropColumn([
'batch_number',
'box_number',
'origin_country',
'arrival_date',
'expiry_date',
'source_purchase_order_id',
'quality_status',
'quality_remark',
]);
});
}
};

View File

@@ -0,0 +1,58 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 生產工單主表,記錄每次生產的成品資訊。
*/
public function up(): void
{
Schema::create('production_orders', function (Blueprint $table) {
$table->id();
$table->string('code', 50)->unique()->comment('生產單號 (如: PRO-20260121-001)');
// 成品資訊
$table->foreignId('product_id')->constrained()->onDelete('restrict')
->comment('成品商品');
$table->string('output_batch_number', 50)->comment('成品批號');
$table->string('output_box_count', 10)->nullable()->comment('成品箱數');
$table->decimal('output_quantity', 10, 2)->comment('生產數量');
// 入庫倉庫
$table->foreignId('warehouse_id')->constrained()->onDelete('restrict')
->comment('入庫倉庫');
// 生產資訊
$table->date('production_date')->comment('生產日期');
$table->date('expiry_date')->nullable()->comment('成品效期');
// 操作人員
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete()
->comment('操作人員');
// 狀態與備註
$table->enum('status', ['draft', 'completed', 'cancelled'])
->default('completed')->comment('狀態:草稿/完成/取消');
$table->text('remark')->nullable()->comment('備註');
$table->timestamps();
// 索引
$table->index(['production_date', 'product_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('production_orders');
}
};

View File

@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 生產工單明細表 (BOM),記錄使用的原物料。
*/
public function up(): void
{
Schema::create('production_order_items', function (Blueprint $table) {
$table->id();
// 所屬生產工單
$table->foreignId('production_order_id')->constrained()->onDelete('cascade')
->comment('所屬生產工單');
// 使用的庫存(含商品與批號)
$table->foreignId('inventory_id')->constrained()->onDelete('restrict')
->comment('使用的庫存紀錄 (含 product, batch)');
// 使用量
$table->decimal('quantity_used', 10, 4)->comment('使用量');
$table->foreignId('unit_id')->nullable()->constrained('units')->nullOnDelete()
->comment('單位');
$table->timestamps();
// 索引
$table->index(['production_order_id', 'inventory_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('production_order_items');
}
};

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
return new class extends Migration
{
/**
* Run the migrations.
*
* 新增生產管理權限
*/
public function up(): void
{
$guard = 'web';
// 建立生產管理權限
$permissions = [
'production_orders.view' => '檢視生產工單',
'production_orders.create' => '建立生產工單',
'production_orders.edit' => '編輯生產工單',
'production_orders.delete' => '刪除生產工單',
];
foreach ($permissions as $name => $description) {
Permission::firstOrCreate(
['name' => $name, 'guard_name' => $guard],
['name' => $name, 'guard_name' => $guard]
);
}
// 授予 super-admin 所有新權限
$superAdmin = Role::where('name', 'super-admin')->first();
if ($superAdmin) {
$superAdmin->givePermissionTo(array_keys($permissions));
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$permissions = [
'production_orders.view',
'production_orders.create',
'production_orders.edit',
'production_orders.delete',
];
foreach ($permissions as $name) {
Permission::where('name', $name)->delete();
}
}
};