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

1. 修復倉庫統計數據加總與樣式。
2. 修正可用庫存計算邏輯(排除不可銷售倉庫)。
3. 撥補單商品列表加入批號與效期顯示。
4. 修正撥補單儲存邏輯以支援精確批號轉移。
5. 整合 FEATURES.md 至 README.md。
This commit is contained in:
2026-01-26 14:59:24 +08:00
parent b0848a6bb8
commit 106de4e945
81 changed files with 4118 additions and 1023 deletions

View File

@@ -11,6 +11,13 @@ use Illuminate\Support\Str;
*/
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = \App\Modules\Core\Models\User::class;
/**
* The current password being used by the factory.
*/
@@ -25,6 +32,7 @@ class UserFactory extends Factory
{
return [
'name' => fake()->name(),
'username' => fake()->unique()->userName(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),

View File

@@ -0,0 +1,45 @@
<?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('recipes', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')->constrained('products')->onDelete('cascade')->comment('連結的成品商品 ID');
$table->string('code')->unique()->comment('配方代號');
$table->string('name')->comment('配方名稱');
$table->text('description')->nullable()->comment('配方描述');
$table->decimal('yield_quantity', 10, 2)->default(1.00)->comment('標準產出數量');
$table->boolean('is_active')->default(true)->comment('是否啟用');
$table->timestamps();
$table->softDeletes();
});
Schema::create('recipe_items', function (Blueprint $table) {
$table->id();
$table->foreignId('recipe_id')->constrained('recipes')->onDelete('cascade');
$table->foreignId('product_id')->constrained('products')->comment('原物料商品 ID');
$table->decimal('quantity', 10, 4)->comment('標準用量');
$table->foreignId('unit_id')->nullable()->constrained('units')->comment('單位 ID');
$table->string('remark')->nullable()->comment('備註');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('recipe_items');
Schema::dropIfExists('recipes');
}
};

View File

@@ -0,0 +1,68 @@
<?php
use Illuminate\Database\Migrations\Migration;
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 = [
'recipes.view' => '檢視配方',
'recipes.create' => '建立配方',
'recipes.edit' => '編輯配方',
'recipes.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));
}
// 授予 admin 所有新權限
$admin = Role::where('name', 'admin')->first();
if ($admin) {
$admin->givePermissionTo(array_keys($permissions));
}
// 授予 warehouse-manager 檢視權限 (配方通常與庫存相關)
$whManager = Role::where('name', 'warehouse-manager')->first();
if ($whManager) {
$whManager->givePermissionTo(['recipes.view']);
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$permissions = [
'recipes.view',
'recipes.create',
'recipes.edit',
'recipes.delete',
];
foreach ($permissions as $name) {
Permission::where('name', $name)->delete();
}
}
};

View File

@@ -0,0 +1,28 @@
<?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::table('warehouses', function (Blueprint $table) {
$table->boolean('is_sellable')->default(true)->after('description')->comment('是否可銷售');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('warehouses', function (Blueprint $table) {
$table->dropColumn('is_sellable');
});
}
};

View File

@@ -13,21 +13,21 @@ class UnitSeeder extends Seeder
public function run(): void
{
$units = [
['name' => '個', 'code' => 'pc'],
['name' => '箱', 'code' => 'box'],
['name' => '瓶', 'code' => 'btl'],
['name' => '包', 'code' => 'pkg'],
['name' => '公斤', 'code' => 'kg'],
['name' => '公克', 'code' => 'g'],
['name' => '公升', 'code' => 'l'],
['name' => '毫升', 'code' => 'ml'],
['name' => '籃', 'code' => 'bsk'],
['name' => '桶', 'code' => 'bucket'],
['name' => '罐', 'code' => 'can'],
['name' => '個', 'code' => 'PCE'], // Piece
['name' => '箱', 'code' => 'BX'], // Box
['name' => '瓶', 'code' => 'BO'], // Bottle
['name' => '包', 'code' => 'PK'], // Package
['name' => '公斤', 'code' => 'KGM'], // Kilogram
['name' => '公克', 'code' => 'GRM'], // Gram
['name' => '公升', 'code' => 'LTR'], // Litre
['name' => '毫升', 'code' => 'MLT'], // Millilitre
['name' => '籃', 'code' => 'BK'], // Basket
['name' => '桶', 'code' => 'BJ'], // Bucket
['name' => '罐', 'code' => 'CA'], // Can
];
foreach ($units as $unit) {
Unit::firstOrCreate(
Unit::updateOrCreate(
['name' => $unit['name']],
['code' => $unit['code']]
);