first commit
This commit is contained in:
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
70
database/factories/ProductFactory.php
Normal file
70
database/factories/ProductFactory.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
|
||||
*/
|
||||
class ProductFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
// 定義豐富的中文測試資料庫
|
||||
$productNames = [
|
||||
'高山烏龍茶', '茉莉綠茶', '特級紅茶', '四季春茶', '阿薩姆紅茶', '錫蘭紅茶', '普洱茶',
|
||||
'珍珠粉圓', '波霸粉圓', '椰果', '仙草凍', '愛玉凍', '布丁', '紅豆', '綠豆',
|
||||
'果糖', '黑糖漿', '蜂蜜', '蔗糖液', '煉乳',
|
||||
'奶精粉', '鮮奶油', '全脂鮮乳', '低脂鮮乳', '燕麥奶',
|
||||
'檸檬原汁', '百香果漿', '芒果果泥', '草莓果醬', '柳橙濃縮汁',
|
||||
'塑膠杯 (700cc)', '紙杯 (500cc)', '封口膜', '粗吸管', '細吸管', '杯蓋',
|
||||
'外帶提袋 (單杯)', '外帶提袋 (雙杯)', '吸管套', '杯架'
|
||||
];
|
||||
|
||||
$brands = [
|
||||
'統一', '光泉', '味全', '雀巢', '立頓', '天仁', '台糖', '義美', '開元', '瑞穗', '六甲田莊', '南亞塑膠'
|
||||
];
|
||||
|
||||
$specs = [
|
||||
'業務用 5kg裝', '100入/袋', '業務號 3L', '特級品', 'A級', '家庭號 2L', '10kg/箱', '500g/罐'
|
||||
];
|
||||
|
||||
$units = ['包', '罐', '瓶', '箱', '袋', '個', '支', '公斤', '公升'];
|
||||
$baseUnit = $this->faker->randomElement($units);
|
||||
|
||||
$largeUnits = ['箱', '袋', '桶', '磨', '組'];
|
||||
$largeUnit = $this->faker->randomElement($largeUnits);
|
||||
|
||||
$purchaseUnits = ['箱', '棧板', '車'];
|
||||
$purchaseUnit = $this->faker->randomElement($purchaseUnits);
|
||||
|
||||
// 隨機取得一個 Category,若沒有則略過
|
||||
$category = Category::inRandomOrder()->first();
|
||||
|
||||
// 為了讓名稱不重複,可以加個隨機形容詞或編號,或者直接用 faker 但指定語系 (若 env 不能改)
|
||||
// 這裡我們混搭:隨機選一個品名 + 隨機品牌 + 隨機規格,組成較真實的商品描述概念
|
||||
|
||||
$name = $this->faker->randomElement($productNames);
|
||||
$brand = $this->faker->randomElement($brands);
|
||||
$spec = $this->faker->randomElement($specs);
|
||||
|
||||
return [
|
||||
'code' => strtoupper($this->faker->unique()->bothify('??###')),
|
||||
'name' => $name . ' (' . $brand . ')', // 例如:高山烏龍茶 (天仁)
|
||||
'category_id' => $category ? $category->id : null,
|
||||
'brand' => $brand,
|
||||
'specification' => $spec,
|
||||
'base_unit' => $baseUnit,
|
||||
'large_unit' => $largeUnit,
|
||||
'conversion_rate' => $this->faker->numberBetween(10, 1000),
|
||||
'purchase_unit' => $purchaseUnit,
|
||||
];
|
||||
}
|
||||
}
|
||||
44
database/factories/UserFactory.php
Normal file
44
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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('categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('description')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique()->comment('商品編號');
|
||||
$table->string('name')->comment('商品名稱');
|
||||
$table->foreignId('category_id')->constrained('categories')->onDelete('restrict')->comment('商品分類');
|
||||
$table->string('brand')->nullable()->comment('品牌');
|
||||
$table->text('specification')->nullable()->comment('規格描述');
|
||||
|
||||
// Unit conversion
|
||||
$table->string('base_unit')->comment('基本庫存單位 (e.g. g, ml)');
|
||||
$table->string('large_unit')->nullable()->comment('大單位 (e.g. 桶, 箱)');
|
||||
$table->decimal('conversion_rate', 10, 4)->default(1)->comment('換算率 (1 大單位 = ? 基本單位)');
|
||||
$table->string('purchase_unit')->nullable()->comment('採購單位');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('vendors', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique()->comment('廠商編號');
|
||||
$table->string('name')->comment('廠商名稱');
|
||||
$table->string('short_name')->nullable()->comment('廠商簡稱');
|
||||
$table->string('tax_id')->nullable()->comment('統一編號');
|
||||
$table->string('owner')->nullable()->comment('負責人');
|
||||
$table->string('contact_name')->nullable()->comment('聯絡人');
|
||||
$table->string('tel')->nullable()->comment('市話');
|
||||
$table->string('phone')->nullable()->comment('連絡電話');
|
||||
$table->string('email')->nullable()->comment('電子郵件');
|
||||
$table->text('address')->nullable()->comment('地址');
|
||||
$table->text('remark')->nullable()->comment('備註');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('vendors');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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('vendors', function (Blueprint $table) {
|
||||
$table->string('code', 50)->change();
|
||||
$table->string('name', 100)->change();
|
||||
$table->string('short_name', 50)->nullable()->change();
|
||||
$table->string('tax_id', 20)->nullable()->change();
|
||||
$table->string('owner', 50)->nullable()->change();
|
||||
$table->string('contact_name', 50)->nullable()->change();
|
||||
$table->string('tel', 20)->nullable()->change();
|
||||
$table->string('phone', 20)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->string('code', 50)->change();
|
||||
$table->string('name', 100)->change();
|
||||
$table->string('brand', 50)->nullable()->change();
|
||||
$table->string('base_unit', 20)->change();
|
||||
$table->string('large_unit', 20)->nullable()->change();
|
||||
$table->string('purchase_unit', 20)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
$table->string('name', 50)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('vendors', function (Blueprint $table) {
|
||||
$table->string('code', 255)->change();
|
||||
$table->string('name', 255)->change();
|
||||
$table->string('short_name', 255)->nullable()->change();
|
||||
$table->string('tax_id', 255)->nullable()->change();
|
||||
$table->string('owner', 255)->nullable()->change();
|
||||
$table->string('contact_name', 255)->nullable()->change();
|
||||
$table->string('tel', 255)->nullable()->change();
|
||||
$table->string('phone', 255)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->string('code', 255)->change();
|
||||
$table->string('name', 255)->change();
|
||||
$table->string('brand', 255)->nullable()->change();
|
||||
$table->string('base_unit', 255)->change();
|
||||
$table->string('large_unit', 255)->nullable()->change();
|
||||
$table->string('purchase_unit', 255)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
$table->string('name', 255)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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('warehouses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 50)->unique()->comment('倉庫編號');
|
||||
$table->string('name', 50)->comment('倉庫名稱');
|
||||
$table->string('address')->nullable()->comment('地址');
|
||||
$table->text('description')->nullable()->comment('描述');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 商品庫存詳情
|
||||
Schema::create('inventories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('warehouse_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('product_id')->constrained()->onDelete('restrict');
|
||||
$table->decimal('quantity', 10, 2)->default(0)->comment('當前庫存數量');
|
||||
$table->decimal('safety_stock', 10, 2)->default(0)->nullable()->comment('安全存量');
|
||||
$table->string('location', 50)->nullable()->comment('儲位/貨架號');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['warehouse_id', 'product_id'], 'warehouse_product_unique');
|
||||
});
|
||||
|
||||
// 庫存異動紀錄
|
||||
Schema::create('inventory_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('inventory_id')->constrained()->onDelete('cascade');
|
||||
$table->string('type', 20)->comment('異動類型: purchase_in, sales_out, adjustment, transfer_in, transfer_out');
|
||||
$table->decimal('quantity', 10, 2)->comment('異動數量 (+/-)');
|
||||
$table->decimal('balance_before', 10, 2)->comment('異動前餘額');
|
||||
$table->decimal('balance_after', 10, 2)->comment('異動後餘額');
|
||||
$table->string('reason')->nullable()->comment('異動原因/備註');
|
||||
$table->nullableMorphs('reference'); // reference_type, reference_id
|
||||
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null')->comment('操作人員');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('inventory_transactions');
|
||||
Schema::dropIfExists('inventories');
|
||||
Schema::dropIfExists('warehouses');
|
||||
}
|
||||
};
|
||||
@@ -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('inventory_transactions', function (Blueprint $table) {
|
||||
$table->string('type', 20)->comment('異動類型: 採購進貨, 銷售出庫, 盤點調整, 撥補入庫, 撥補出庫, 手動入庫')->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('inventory_transactions', function (Blueprint $table) {
|
||||
$table->string('type', 20)->comment('異動類型: purchase_in, sales_out, adjustment, transfer_in, transfer_out')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('inventory_transactions', function (Blueprint $table) {
|
||||
$table->dateTime('actual_time')->nullable()->after('reason')->comment('實際異動發生時間');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('inventory_transactions', function (Blueprint $table) {
|
||||
$table->dropColumn('actual_time');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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('product_vendor', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('vendor_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
|
||||
$table->decimal('last_price', 10, 2)->nullable()->comment('最近一次進價');
|
||||
$table->timestamps();
|
||||
|
||||
// 確保同一個廠商對同一個商品只有一筆記錄
|
||||
$table->unique(['vendor_id', 'product_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('product_vendor');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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('inventories', function (Blueprint $table) {
|
||||
// Change default value of safety_stock to NULL
|
||||
$table->decimal('safety_stock', 10, 2)->nullable()->default(null)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('inventories', function (Blueprint $table) {
|
||||
// Revert default value to 0
|
||||
$table->decimal('safety_stock', 10, 2)->nullable()->default(0)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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('purchase_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique()->comment('採購單號');
|
||||
$table->foreignId('vendor_id')->constrained()->onDelete('restrict');
|
||||
$table->foreignId('warehouse_id')->nullable()->constrained()->onDelete('set null');
|
||||
$table->foreignId('user_id')->constrained()->onDelete('restrict');
|
||||
$table->string('status')->default('draft')->comment('草稿, 待審核, 處理中, 運送中, 待確認, 已完成');
|
||||
$table->date('expected_delivery_date')->nullable();
|
||||
$table->decimal('total_amount', 12, 2)->default(0);
|
||||
$table->decimal('tax_amount', 12, 2)->default(0);
|
||||
$table->decimal('grand_total', 12, 2)->default(0);
|
||||
$table->text('remark')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('purchase_orders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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('purchase_order_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('purchase_order_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('product_id')->constrained()->onDelete('restrict');
|
||||
$table->decimal('quantity', 10, 2);
|
||||
$table->decimal('unit_price', 10, 2);
|
||||
$table->decimal('subtotal', 12, 2);
|
||||
$table->decimal('received_quantity', 10, 2)->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('purchase_order_items');
|
||||
}
|
||||
};
|
||||
47
database/seeders/CategorySeeder.php
Normal file
47
database/seeders/CategorySeeder.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CategorySeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$categories = [
|
||||
[
|
||||
'name' => '原物料',
|
||||
'description' => '製作飲品或餐點的基礎原料',
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'name' => '半成品',
|
||||
'description' => '已經過初步加工的原料',
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'name' => '包材',
|
||||
'description' => '杯子、吸管、封膜等消耗品',
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'name' => '設備',
|
||||
'description' => '店面或廚房使用的機器設備',
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'name' => '清潔用品',
|
||||
'description' => '清潔與環境維護用品',
|
||||
'is_active' => true,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
Category::create($category);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
database/seeders/DatabaseSeeder.php
Normal file
30
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$this->call([
|
||||
CategorySeeder::class,
|
||||
ProductSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
88
database/seeders/ProductSeeder.php
Normal file
88
database/seeders/ProductSeeder.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ProductSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// 取得分類 ID
|
||||
$rawMaterialId = Category::where('name', '原物料')->first()->id;
|
||||
$packagingId = Category::where('name', '包材')->first()->id;
|
||||
|
||||
$products = [
|
||||
// 原物料
|
||||
[
|
||||
'code' => 'RM001',
|
||||
'name' => '二砂糖',
|
||||
'category_id' => $rawMaterialId,
|
||||
'brand' => '台糖',
|
||||
'specification' => '業務用 50kg/袋',
|
||||
'base_unit' => 'g',
|
||||
'large_unit' => '袋',
|
||||
'conversion_rate' => 50000,
|
||||
'purchase_unit' => '袋',
|
||||
],
|
||||
[
|
||||
'code' => 'RM002',
|
||||
'name' => '錫蘭紅茶',
|
||||
'category_id' => $rawMaterialId,
|
||||
'brand' => '天仁',
|
||||
'specification' => '特級茶葉',
|
||||
'base_unit' => 'g',
|
||||
'large_unit' => '箱',
|
||||
'conversion_rate' => 30000, // 假設一箱 30kg
|
||||
'purchase_unit' => '箱',
|
||||
],
|
||||
[
|
||||
'code' => 'RM003',
|
||||
'name' => '鮮乳',
|
||||
'category_id' => $rawMaterialId,
|
||||
'brand' => '光泉',
|
||||
'specification' => '業務用鮮乳 1公升',
|
||||
'base_unit' => 'ml',
|
||||
'large_unit' => '瓶',
|
||||
'conversion_rate' => 960, // 960ml
|
||||
'purchase_unit' => '瓶',
|
||||
],
|
||||
// 包材
|
||||
[
|
||||
'code' => 'PC001',
|
||||
'name' => 'PP飲料杯 700cc',
|
||||
'category_id' => $packagingId,
|
||||
'brand' => '南亞',
|
||||
'specification' => '透明, 95口徑',
|
||||
'base_unit' => '個',
|
||||
'large_unit' => '箱',
|
||||
'conversion_rate' => 1000, // 1箱1000個
|
||||
'purchase_unit' => '箱',
|
||||
],
|
||||
[
|
||||
'code' => 'PC002',
|
||||
'name' => '粗吸管',
|
||||
'category_id' => $packagingId,
|
||||
'brand' => '',
|
||||
'specification' => '獨立包裝, 12mm',
|
||||
'base_unit' => '支',
|
||||
'large_unit' => '包',
|
||||
'conversion_rate' => 100, // 1包100支
|
||||
'purchase_unit' => '箱', // 假設採購單位是箱,這裡可能需要邏輯調整,但先跟著轉換率
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($products as $product) {
|
||||
// 使用 firstOrCreate 避免重複建立
|
||||
Product::firstOrCreate(['code' => $product['code']], $product);
|
||||
}
|
||||
|
||||
// 額外隨機建立 25 筆商品以測試分頁
|
||||
Product::factory()->count(25)->create();
|
||||
}
|
||||
}
|
||||
38
database/seeders/VendorSeeder.php
Normal file
38
database/seeders/VendorSeeder.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Vendor;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class VendorSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$vendors = [
|
||||
[
|
||||
'code' => 'V001',
|
||||
'name' => '萬源食糧行',
|
||||
'contact_name' => '王老闆',
|
||||
'phone' => '02-23456789',
|
||||
'address' => '台北市萬華區某路123號',
|
||||
'tax_id' => '12345678',
|
||||
],
|
||||
[
|
||||
'code' => 'V002',
|
||||
'name' => '新竹農產批發',
|
||||
'contact_name' => '李經理',
|
||||
'phone' => '03-5566778',
|
||||
'address' => '新竹市北區某路456號',
|
||||
'tax_id' => '87654321',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($vendors as $vendor) {
|
||||
Vendor::create($vendor);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user