Compare commits
2 Commits
4bbbde685d
...
649af40919
| Author | SHA1 | Date | |
|---|---|---|---|
| 649af40919 | |||
| 5f8b2a1c2d |
@@ -22,6 +22,7 @@ class ActivityLogController extends Controller
|
||||
'App\Modules\Procurement\Models\PurchaseOrder' => '採購單',
|
||||
'App\Modules\Inventory\Models\Warehouse' => '倉庫',
|
||||
'App\Modules\Inventory\Models\Inventory' => '庫存',
|
||||
'App\Modules\Inventory\Models\InventoryTransaction' => '庫存異動紀錄',
|
||||
'App\Modules\Finance\Models\UtilityFee' => '公共事業費',
|
||||
'App\Modules\Inventory\Models\GoodsReceipt' => '進貨單',
|
||||
'App\Modules\Production\Models\ProductionOrder' => '生產工單',
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Integration\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class InventorySyncController extends Controller
|
||||
{
|
||||
protected $inventoryService;
|
||||
|
||||
public function __construct(InventoryServiceInterface $inventoryService)
|
||||
{
|
||||
$this->inventoryService = $inventoryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供外部 POS 查詢指定倉庫的商品庫存餘額
|
||||
*
|
||||
* @param string $warehouseCode
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function show(string $warehouseCode): JsonResponse
|
||||
{
|
||||
// 透過 Service 調用跨模組庫存查詢功能
|
||||
$inventoryData = $this->inventoryService->getPosInventoryByWarehouseCode($warehouseCode);
|
||||
|
||||
// 若回傳 null,表示尋無此倉庫代碼
|
||||
if (is_null($inventoryData)) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "Warehouse with code '{$warehouseCode}' not found.",
|
||||
], 404);
|
||||
}
|
||||
|
||||
// 以 JSON 格式回傳組合好的商品庫存列表
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'warehouse_code' => $warehouseCode,
|
||||
'data' => $inventoryData->map(function ($item) {
|
||||
return [
|
||||
'external_pos_id' => $item->external_pos_id,
|
||||
'product_code' => $item->product_code,
|
||||
'product_name' => $item->product_name,
|
||||
'quantity' => (float) $item->total_quantity,
|
||||
];
|
||||
})
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Integration\Controllers\ProductSyncController;
|
||||
use App\Modules\Integration\Controllers\OrderSyncController;
|
||||
use App\Modules\Integration\Controllers\VendingOrderSyncController;
|
||||
use App\Modules\Integration\Controllers\InventorySyncController;
|
||||
|
||||
Route::prefix('api/v1/integration')
|
||||
->middleware(['api', 'throttle:integration', 'integration.tenant', 'auth:sanctum'])
|
||||
@@ -11,4 +12,5 @@ Route::prefix('api/v1/integration')
|
||||
Route::post('products/upsert', [ProductSyncController::class, 'upsert']);
|
||||
Route::post('orders', [OrderSyncController::class, 'store']);
|
||||
Route::post('vending/orders', [VendingOrderSyncController::class, 'store']);
|
||||
Route::get('inventory/{warehouse_code}', [InventorySyncController::class, 'show']);
|
||||
});
|
||||
|
||||
@@ -157,4 +157,31 @@ interface InventoryServiceInterface
|
||||
* Get items expiring soon for dashboard.
|
||||
*/
|
||||
public function getExpiringSoon(int $limit = 5): \Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Get inventory summary (group by product) for a specific warehouse code
|
||||
*
|
||||
* @param string $code
|
||||
* @return \Illuminate\Support\Collection|null
|
||||
*/
|
||||
public function getPosInventoryByWarehouseCode(string $code);
|
||||
|
||||
/**
|
||||
* 處理批量入庫邏輯 (含批號產生與現有批號累加)。
|
||||
*
|
||||
* @param \App\Modules\Inventory\Models\Warehouse $warehouse
|
||||
* @param array $items 入庫品項清單
|
||||
* @param array $meta 資料包含 inboundDate, reason, notes
|
||||
* @return void
|
||||
*/
|
||||
public function processIncomingInventory(\App\Modules\Inventory\Models\Warehouse $warehouse, array $items, array $meta): void;
|
||||
|
||||
/**
|
||||
* 處理單一庫存項目的調整。
|
||||
*
|
||||
* @param \App\Modules\Inventory\Models\Inventory $inventory
|
||||
* @param array $data 包含 quantity, operation, type, reason, unit_cost 等
|
||||
* @return void
|
||||
*/
|
||||
public function adjustInventory(\App\Modules\Inventory\Models\Inventory $inventory, array $data): void;
|
||||
}
|
||||
@@ -22,10 +22,14 @@ use App\Modules\Core\Contracts\CoreServiceInterface;
|
||||
class InventoryController extends Controller
|
||||
{
|
||||
protected $coreService;
|
||||
protected $inventoryService;
|
||||
|
||||
public function __construct(CoreServiceInterface $coreService)
|
||||
{
|
||||
public function __construct(
|
||||
CoreServiceInterface $coreService,
|
||||
\App\Modules\Inventory\Contracts\InventoryServiceInterface $inventoryService
|
||||
) {
|
||||
$this->coreService = $coreService;
|
||||
$this->inventoryService = $inventoryService;
|
||||
}
|
||||
|
||||
public function index(Request $request, Warehouse $warehouse)
|
||||
@@ -182,97 +186,11 @@ class InventoryController extends Controller
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($validated, $warehouse) {
|
||||
foreach ($validated['items'] as $item) {
|
||||
// ... (略,傳遞 unit_cost 交給 Service 處理) ...
|
||||
// 這裡需要修改呼叫 Service 的地方或直接更新邏輯
|
||||
// 為求快速,我將在此更新邏輯
|
||||
|
||||
$inventory = null;
|
||||
|
||||
if ($item['batchMode'] === 'existing') {
|
||||
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
||||
$inventory = Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
|
||||
// 更新成本 (若有傳入)
|
||||
if (isset($item['unit_cost'])) {
|
||||
$inventory->unit_cost = $item['unit_cost'];
|
||||
}
|
||||
} elseif ($item['batchMode'] === 'none') {
|
||||
// 模式 C:不使用批號 (自動累加至 NO-BATCH)
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => 'NO-BATCH'
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0,
|
||||
'total_value' => 0,
|
||||
'arrival_date' => $validated['inboundDate'],
|
||||
'expiry_date' => null,
|
||||
'origin_country' => 'TW',
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
} else {
|
||||
// 模式 B:建立新批號
|
||||
$originCountry = $item['originCountry'] ?? 'TW';
|
||||
$product = Product::find($item['productId']);
|
||||
|
||||
$batchNumber = Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
$originCountry,
|
||||
$validated['inboundDate']
|
||||
);
|
||||
|
||||
// 檢查是否存在
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => $batchNumber
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0, // 新增
|
||||
'total_value' => 0, // 稍後計算
|
||||
'location' => $item['location'] ?? null,
|
||||
'arrival_date' => $validated['inboundDate'],
|
||||
'expiry_date' => $item['expiryDate'] ?? null,
|
||||
'origin_country' => $originCountry,
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
}
|
||||
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $currentQty + $item['quantity'];
|
||||
|
||||
$inventory->quantity = $newQty;
|
||||
// 更新總價值
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->save();
|
||||
|
||||
// 寫入異動紀錄
|
||||
$inventory->transactions()->create([
|
||||
'type' => '手動入庫',
|
||||
'quantity' => $item['quantity'],
|
||||
'unit_cost' => $inventory->unit_cost, // 記錄成本
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $validated['reason'] . ($validated['notes'] ? ' - ' . $validated['notes'] : ''),
|
||||
'actual_time' => $validated['inboundDate'],
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
$this->inventoryService->processIncomingInventory($warehouse, $validated['items'], [
|
||||
'inboundDate' => $validated['inboundDate'],
|
||||
'reason' => $validated['reason'],
|
||||
'notes' => $validated['notes'] ?? '',
|
||||
]);
|
||||
|
||||
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
||||
->with('success', '庫存記錄已儲存成功');
|
||||
@@ -401,81 +319,7 @@ class InventoryController extends Controller
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($validated, $inventory) {
|
||||
$currentQty = (float) $inventory->quantity;
|
||||
$newQty = (float) $validated['quantity'];
|
||||
|
||||
// 判斷是否來自調整彈窗 (包含 operation 參數)
|
||||
$isAdjustment = isset($validated['operation']);
|
||||
$changeQty = 0;
|
||||
|
||||
if ($isAdjustment) {
|
||||
switch ($validated['operation']) {
|
||||
case 'add':
|
||||
$changeQty = (float) $validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'subtract':
|
||||
$changeQty = -(float) $validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'set':
|
||||
$changeQty = $newQty - $currentQty;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 來自編輯頁面,直接 Set
|
||||
$changeQty = $newQty - $currentQty;
|
||||
}
|
||||
|
||||
// 更新成本 (若有傳)
|
||||
if (isset($validated['unit_cost'])) {
|
||||
$inventory->unit_cost = $validated['unit_cost'];
|
||||
}
|
||||
|
||||
// 更新庫存
|
||||
$inventory->quantity = $newQty;
|
||||
// 更新總值
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->save();
|
||||
|
||||
// 異動類型映射
|
||||
$type = $validated['type'] ?? ($isAdjustment ? 'manual_adjustment' : 'adjustment');
|
||||
$typeMapping = [
|
||||
'manual_adjustment' => '手動調整庫存',
|
||||
'adjustment' => '盤點調整',
|
||||
'purchase_in' => '採購進貨',
|
||||
'sales_out' => '銷售出庫',
|
||||
'return_in' => '退貨入庫',
|
||||
'return_out' => '退貨出庫',
|
||||
'transfer_in' => '撥補入庫',
|
||||
'transfer_out' => '撥補出庫',
|
||||
];
|
||||
$chineseType = $typeMapping[$type] ?? $type;
|
||||
|
||||
// 如果是編輯頁面來的,且沒傳 type,設為手動編輯
|
||||
if (!$isAdjustment && !isset($validated['type'])) {
|
||||
$chineseType = '手動編輯';
|
||||
}
|
||||
|
||||
// 整理原因
|
||||
$reason = $validated['reason'] ?? ($isAdjustment ? '手動庫存調整' : '編輯頁面更新');
|
||||
if (isset($validated['notes'])) {
|
||||
$reason .= ' - ' . $validated['notes'];
|
||||
}
|
||||
|
||||
// 寫入異動紀錄
|
||||
if (abs($changeQty) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => $chineseType,
|
||||
'quantity' => $changeQty,
|
||||
'unit_cost' => $inventory->unit_cost, // 記錄
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $reason,
|
||||
'actual_time' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
$this->inventoryService->adjustInventory($inventory, $validated);
|
||||
|
||||
return redirect()->route('warehouses.inventory.index', $inventory->warehouse_id)
|
||||
->with('success', '庫存資料已更新');
|
||||
|
||||
@@ -58,8 +58,11 @@ class Inventory extends Model
|
||||
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
$attributes = $properties['attributes'] ?? [];
|
||||
// 核心:轉換為陣列以避免 Indirect modification error
|
||||
$properties = $activity->properties instanceof \Illuminate\Support\Collection
|
||||
? $activity->properties->toArray()
|
||||
: $activity->properties;
|
||||
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
|
||||
// 始終對名稱進行快照以便於上下文顯示,即使 ID 未更改
|
||||
@@ -69,11 +72,28 @@ class Inventory extends Model
|
||||
|
||||
// 如果已設定原因,則進行捕捉
|
||||
if ($this->activityLogReason) {
|
||||
$attributes['_reason'] = $this->activityLogReason;
|
||||
$properties['attributes']['_reason'] = $this->activityLogReason;
|
||||
}
|
||||
|
||||
$properties['attributes'] = $attributes;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
// 全域 ID 轉名稱邏輯
|
||||
$resolver = function (&$data) {
|
||||
if (empty($data) || !is_array($data)) return;
|
||||
|
||||
// 倉庫 ID 轉換
|
||||
if (isset($data['warehouse_id']) && is_numeric($data['warehouse_id'])) {
|
||||
$data['warehouse_id'] = \App\Modules\Inventory\Models\Warehouse::find($data['warehouse_id'])?->name;
|
||||
}
|
||||
// 商品 ID 轉換
|
||||
if (isset($data['product_id']) && is_numeric($data['product_id'])) {
|
||||
$data['product_id'] = \App\Modules\Inventory\Models\Product::find($data['product_id'])?->name;
|
||||
}
|
||||
};
|
||||
|
||||
if (isset($properties['attributes'])) $resolver($properties['attributes']);
|
||||
if (isset($properties['old'])) $resolver($properties['old']);
|
||||
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class InventoryTransaction extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\InventoryTransactionFactory> */
|
||||
use HasFactory;
|
||||
use \Spatie\Activitylog\Traits\LogsActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'inventory_id',
|
||||
@@ -41,4 +42,49 @@ class InventoryTransaction extends Model
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
||||
{
|
||||
return \Spatie\Activitylog\LogOptions::defaults()
|
||||
->logAll()
|
||||
->dontLogIfAttributesChangedOnly(['updated_at'])
|
||||
// 取消 logOnlyDirty,代表新增時(created)也要留紀錄
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
$properties = $activity->properties instanceof \Illuminate\Support\Collection
|
||||
? $activity->properties->toArray()
|
||||
: $activity->properties;
|
||||
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
|
||||
// 試著取得商品與倉庫名稱來作為主要顯示依據
|
||||
$inventory = $this->inventory;
|
||||
if ($inventory) {
|
||||
$snapshot['warehouse_name'] = $inventory->warehouse ? $inventory->warehouse->name : null;
|
||||
$snapshot['product_name'] = $inventory->product ? $inventory->product->name : null;
|
||||
$snapshot['batch_number'] = $inventory->batch_number;
|
||||
}
|
||||
|
||||
// 把異動類型與數量也拉到 snapshot
|
||||
$snapshot['type'] = $this->type;
|
||||
$snapshot['quantity'] = $this->quantity;
|
||||
$snapshot['reason'] = $this->reason;
|
||||
|
||||
// 替換使用者名稱
|
||||
$resolver = function (&$data) {
|
||||
if (empty($data) || !is_array($data)) return;
|
||||
if (isset($data['user_id']) && is_numeric($data['user_id'])) {
|
||||
$data['user_id'] = \App\Modules\Core\Models\User::find($data['user_id'])?->name;
|
||||
}
|
||||
};
|
||||
|
||||
if (isset($properties['attributes'])) $resolver($properties['attributes']);
|
||||
if (isset($properties['old'])) $resolver($properties['old']);
|
||||
|
||||
$properties['snapshot'] = $snapshot;
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,30 +85,50 @@ class Product extends Model
|
||||
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
$attributes = $properties['attributes'] ?? [];
|
||||
// 核心:轉換為陣列以避免 Indirect modification error
|
||||
$properties = $activity->properties instanceof \Illuminate\Support\Collection
|
||||
? $activity->properties->toArray()
|
||||
: $activity->properties;
|
||||
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
|
||||
// 處理分類名稱快照
|
||||
if (isset($attributes['category_id'])) {
|
||||
$category = Category::find($attributes['category_id']);
|
||||
$snapshot['category_name'] = $category ? $category->name : null;
|
||||
}
|
||||
|
||||
// 處理單位名稱快照
|
||||
$unitFields = ['base_unit_id', 'large_unit_id', 'purchase_unit_id'];
|
||||
foreach ($unitFields as $field) {
|
||||
if (isset($attributes[$field])) {
|
||||
$unit = Unit::find($attributes[$field]);
|
||||
$nameKey = str_replace('_id', '_name', $field);
|
||||
$snapshot[$nameKey] = $unit ? $unit->name : null;
|
||||
}
|
||||
}
|
||||
|
||||
// 始終對自身名稱進行快照以便於上下文顯示(這樣日誌總是顯示 "可樂")
|
||||
$snapshot['name'] = $this->name;
|
||||
|
||||
$properties['attributes'] = $attributes;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
// 全域 ID 轉名稱邏輯
|
||||
$resolver = function (&$data) use (&$snapshot) {
|
||||
if (empty($data) || !is_array($data)) return;
|
||||
|
||||
// 處理分類名稱
|
||||
if (isset($data['category_id']) && is_numeric($data['category_id'])) {
|
||||
$categoryName = Category::find($data['category_id'])?->name;
|
||||
$data['category_id'] = $categoryName;
|
||||
if (!isset($snapshot['category_name']) && $categoryName) {
|
||||
$snapshot['category_name'] = $categoryName;
|
||||
}
|
||||
}
|
||||
|
||||
// 處理單位名稱
|
||||
$unitFields = ['base_unit_id', 'large_unit_id', 'purchase_unit_id'];
|
||||
foreach ($unitFields as $field) {
|
||||
if (isset($data[$field]) && is_numeric($data[$field])) {
|
||||
$unitName = Unit::find($data[$field])?->name;
|
||||
$data[$field] = $unitName;
|
||||
|
||||
$nameKey = str_replace('_id', '_name', $field);
|
||||
if (!isset($snapshot[$nameKey]) && $unitName) {
|
||||
$snapshot[$nameKey] = $unitName;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isset($properties['attributes'])) $resolver($properties['attributes']);
|
||||
if (isset($properties['old'])) $resolver($properties['old']);
|
||||
|
||||
// 因為 resolver 內部可能更新了 snapshot,所以再覆寫一次
|
||||
$properties['snapshot'] = $snapshot;
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
|
||||
@@ -644,4 +644,198 @@ class InventoryService implements InventoryServiceInterface
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得特定倉庫代碼的所屬商品總庫存 (給 POS/外部系統同步使用)
|
||||
*
|
||||
* @param string $code
|
||||
* @return \Illuminate\Support\Collection|null
|
||||
*/
|
||||
public function getPosInventoryByWarehouseCode(string $code)
|
||||
{
|
||||
$warehouse = Warehouse::where('code', $code)->first();
|
||||
|
||||
if (!$warehouse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 整理該倉庫的庫存,以 product_id 進行 GROUP BY 並加總 quantity
|
||||
return DB::table('inventories')
|
||||
->join('products', 'inventories.product_id', '=', 'products.id')
|
||||
->where('inventories.warehouse_id', $warehouse->id)
|
||||
->whereNull('inventories.deleted_at')
|
||||
->whereNull('products.deleted_at')
|
||||
->select(
|
||||
'products.external_pos_id',
|
||||
'products.code as product_code',
|
||||
'products.name as product_name',
|
||||
DB::raw('SUM(inventories.quantity) as total_quantity')
|
||||
)
|
||||
->groupBy('inventories.product_id', 'products.external_pos_id', 'products.code', 'products.name')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function processIncomingInventory(Warehouse $warehouse, array $items, array $meta): void
|
||||
{
|
||||
DB::transaction(function () use ($warehouse, $items, $meta) {
|
||||
foreach ($items as $item) {
|
||||
$inventory = null;
|
||||
|
||||
if ($item['batchMode'] === 'existing') {
|
||||
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
||||
$inventory = Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
|
||||
// 更新成本 (若有傳入)
|
||||
if (isset($item['unit_cost'])) {
|
||||
$inventory->unit_cost = $item['unit_cost'];
|
||||
}
|
||||
} elseif ($item['batchMode'] === 'none') {
|
||||
// 模式 C:不使用批號 (自動累加至 NO-BATCH)
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => 'NO-BATCH'
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0,
|
||||
'total_value' => 0,
|
||||
'arrival_date' => $meta['inboundDate'],
|
||||
'origin_country' => 'TW',
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
} else {
|
||||
// 模式 B:建立新批號
|
||||
$originCountry = $item['originCountry'] ?? 'TW';
|
||||
$product = Product::find($item['productId']);
|
||||
|
||||
$batchNumber = Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
$originCountry,
|
||||
$meta['inboundDate']
|
||||
);
|
||||
|
||||
// 檢查是否存在
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => $batchNumber
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0,
|
||||
'total_value' => 0,
|
||||
'location' => $item['location'] ?? null,
|
||||
'arrival_date' => $meta['inboundDate'],
|
||||
'expiry_date' => $item['expiryDate'] ?? null,
|
||||
'origin_country' => $originCountry,
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
}
|
||||
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $currentQty + $item['quantity'];
|
||||
|
||||
$inventory->quantity = $newQty;
|
||||
// 更新總價值
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->saveQuietly();
|
||||
|
||||
// 寫入異動紀錄
|
||||
$inventory->transactions()->create([
|
||||
'type' => '手動入庫',
|
||||
'quantity' => $item['quantity'],
|
||||
'unit_cost' => $inventory->unit_cost,
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $meta['reason'] . (!empty($meta['notes']) ? ' - ' . $meta['notes'] : ''),
|
||||
'actual_time' => $meta['inboundDate'],
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function adjustInventory(Inventory $inventory, array $data): void
|
||||
{
|
||||
DB::transaction(function () use ($inventory, $data) {
|
||||
$currentQty = (float) $inventory->quantity;
|
||||
$newQty = (float) $data['quantity'];
|
||||
|
||||
$isAdjustment = isset($data['operation']);
|
||||
$changeQty = 0;
|
||||
|
||||
if ($isAdjustment) {
|
||||
switch ($data['operation']) {
|
||||
case 'add':
|
||||
$changeQty = (float) $data['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'subtract':
|
||||
$changeQty = -(float) $data['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'set':
|
||||
$changeQty = $newQty - $currentQty;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$changeQty = $newQty - $currentQty;
|
||||
}
|
||||
|
||||
if (isset($data['unit_cost'])) {
|
||||
$inventory->unit_cost = $data['unit_cost'];
|
||||
}
|
||||
|
||||
$inventory->quantity = $newQty;
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->saveQuietly();
|
||||
|
||||
$type = $data['type'] ?? ($isAdjustment ? 'manual_adjustment' : 'adjustment');
|
||||
$typeMapping = [
|
||||
'manual_adjustment' => '手動調整庫存',
|
||||
'adjustment' => '盤點調整',
|
||||
'purchase_in' => '採購進貨',
|
||||
'sales_out' => '銷售出庫',
|
||||
'return_in' => '退貨入庫',
|
||||
'return_out' => '退貨出庫',
|
||||
'transfer_in' => '撥補入庫',
|
||||
'transfer_out' => '撥補出庫',
|
||||
];
|
||||
$chineseType = $typeMapping[$type] ?? $type;
|
||||
|
||||
if (!$isAdjustment && !isset($data['type'])) {
|
||||
$chineseType = '手動編輯';
|
||||
}
|
||||
|
||||
$reason = $data['reason'] ?? ($isAdjustment ? '手動庫存調整' : '編輯頁面更新');
|
||||
if (!empty($data['notes'])) {
|
||||
$reason .= ' - ' . $data['notes'];
|
||||
}
|
||||
|
||||
if (abs($changeQty) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => $chineseType,
|
||||
'quantity' => $changeQty,
|
||||
'unit_cost' => $inventory->unit_cost,
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $reason,
|
||||
'actual_time' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,15 @@ interface Props {
|
||||
|
||||
// 欄位翻譯對照表
|
||||
const fieldLabels: Record<string, string> = {
|
||||
id: 'ID',
|
||||
created_at: '建立時間',
|
||||
updated_at: '更新時間',
|
||||
deleted_at: '刪除時間',
|
||||
name: '名稱',
|
||||
code: '商品代號',
|
||||
username: '登入帳號',
|
||||
description: '描述',
|
||||
// ... (保持原有翻譯)
|
||||
price: '價格',
|
||||
cost: '成本',
|
||||
stock: '庫存',
|
||||
@@ -66,6 +71,11 @@ const fieldLabels: Record<string, string> = {
|
||||
role_id: '角色',
|
||||
email_verified_at: '電子郵件驗證時間',
|
||||
remember_token: '登入權杖',
|
||||
barcode: '條碼',
|
||||
external_pos_id: '外部 POS ID',
|
||||
cost_price: '成本價',
|
||||
member_price: '會員價',
|
||||
wholesale_price: '批發價',
|
||||
// 快照欄位
|
||||
category_name: '分類名稱',
|
||||
base_unit_name: '基本單位名稱',
|
||||
@@ -78,6 +88,9 @@ const fieldLabels: Record<string, string> = {
|
||||
contact_name: '聯絡人',
|
||||
tel: '電話',
|
||||
remark: '備註',
|
||||
license_plate: '車牌號碼',
|
||||
driver_name: '司機姓名',
|
||||
default_transit_warehouse_id: '預設在途倉庫',
|
||||
// 倉庫與庫存欄位
|
||||
warehouse_name: '倉庫名稱',
|
||||
product_name: '商品名稱',
|
||||
@@ -98,6 +111,12 @@ const fieldLabels: Record<string, string> = {
|
||||
quality_status: '品質狀態',
|
||||
quality_remark: '品質備註',
|
||||
purchase_order_id: '來源採購單',
|
||||
inventory_id: '庫存 ID',
|
||||
balance_before: '異動前餘額',
|
||||
balance_after: '異動後餘額',
|
||||
reference_type: '參考單據類型',
|
||||
reference_id: '參考單據 ID',
|
||||
actual_time: '實際時點',
|
||||
// 採購單欄位
|
||||
po_number: '採購單號',
|
||||
vendor_id: '廠商',
|
||||
@@ -173,7 +192,50 @@ const statusMap: Record<string, string> = {
|
||||
in_progress: '生產中',
|
||||
// 調撥單狀態
|
||||
voided: '已作廢',
|
||||
// completed 已定義
|
||||
};
|
||||
|
||||
// 主體類型解析 (Model 類名轉中文)
|
||||
const subjectTypeMap: Record<string, string> = {
|
||||
// 完整路徑映射
|
||||
'App\\Modules\\Inventory\\Models\\Product': '商品資料',
|
||||
'App\\Modules\\Inventory\\Models\\Warehouse': '倉庫資料',
|
||||
'App\\Modules\\Inventory\\Models\\Inventory': '庫存異動',
|
||||
'App\\Modules\\Inventory\\Models\\Category': '商品分類',
|
||||
'App\\Modules\\Inventory\\Models\\Unit': '單位',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransaction': '庫存異動紀錄',
|
||||
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
|
||||
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
|
||||
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
|
||||
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
|
||||
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
|
||||
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
|
||||
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
|
||||
'App\\Modules\\Core\\Models\\User': '使用者帳號',
|
||||
'App\\Modules\\Core\\Models\\Role': '角色權限',
|
||||
// 簡寫映射 (應對後端回傳 class_basename 的情況)
|
||||
'Product': '商品資料',
|
||||
'Warehouse': '倉庫資料',
|
||||
'Inventory': '庫存異動',
|
||||
'InventoryTransaction': '庫存異動紀錄',
|
||||
'Category': '商品分類',
|
||||
'Unit': '單位',
|
||||
'Vendor': '廠商資料',
|
||||
'PurchaseOrder': '採購單',
|
||||
'GoodsReceipt': '進貨單',
|
||||
'ProductionOrder': '生產工單',
|
||||
'Recipe': '生產配方',
|
||||
'InventoryCountDoc': '庫存盤點單',
|
||||
'InventoryAdjustDoc': '庫存盤調單',
|
||||
'InventoryTransferOrder': '庫存調撥單',
|
||||
'StockMovementDoc': '庫存單據',
|
||||
'User': '使用者帳號',
|
||||
'Role': '角色權限',
|
||||
'UtilityFee': '公共事業費',
|
||||
};
|
||||
|
||||
// 庫存品質狀態對照表
|
||||
@@ -202,9 +264,10 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
|
||||
// 自訂欄位排序順序
|
||||
const sortOrder = [
|
||||
'po_number', 'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
|
||||
'doc_no', 'po_number', 'gr_number', 'production_number',
|
||||
'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
|
||||
'invoice_number', 'invoice_date', 'invoice_amount',
|
||||
'total_amount', 'tax_amount', 'grand_total' // 確保金額的特定順序
|
||||
'total_amount', 'tax_amount', 'grand_total'
|
||||
];
|
||||
|
||||
// 過濾掉通常會記錄但對使用者無用的內部鍵
|
||||
@@ -215,30 +278,22 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
.sort((a, b) => {
|
||||
const indexA = sortOrder.indexOf(a);
|
||||
const indexB = sortOrder.indexOf(b);
|
||||
|
||||
// 如果兩者都在排序順序中,比較索引
|
||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
||||
// 如果只有 A 在排序順序中,它排在前面(或根據邏輯,通常將已知欄位排在前面)
|
||||
if (indexA !== -1) return -1;
|
||||
if (indexB !== -1) return 1;
|
||||
// 否則按字母順序或預設
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
// 檢查鍵是否為快照名稱欄位或輔助名稱欄位的輔助函式
|
||||
const isSnapshotField = (key: string) => {
|
||||
// 隱藏快照欄位
|
||||
const snapshotFields = [
|
||||
'category_name', 'base_unit_name', 'large_unit_name', 'purchase_unit_name',
|
||||
'warehouse_name', 'user_name', 'from_warehouse_name', 'to_warehouse_name',
|
||||
'created_by_name', 'updated_by_name', 'completed_by_name', 'posted_by_name'
|
||||
'created_by_name', 'updated_by_name', 'completed_by_name', 'posted_by_name',
|
||||
'vendor_name', 'product_name', 'recipe_name'
|
||||
];
|
||||
|
||||
if (snapshotFields.includes(key)) return true;
|
||||
|
||||
// 隱藏所有以 _name 結尾的欄位(因為它們通常是 ID 欄位的文字補充)
|
||||
if (key.endsWith('_name')) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -262,36 +317,26 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
};
|
||||
|
||||
const formatValue = (key: string, value: any) => {
|
||||
// 遮蔽密碼
|
||||
if (key === 'password') return '******';
|
||||
|
||||
if (value === null || value === undefined) return '-';
|
||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||
if (key === 'is_active') return value ? '啟用' : '停用';
|
||||
|
||||
// 處理採購單狀態
|
||||
if (key === 'status' && typeof value === 'string' && statusMap[value]) {
|
||||
return statusMap[value];
|
||||
}
|
||||
|
||||
// 處理庫存品質狀態
|
||||
if (key === 'quality_status' && typeof value === 'string' && qualityStatusMap[value]) {
|
||||
return qualityStatusMap[value];
|
||||
}
|
||||
|
||||
// 處理入庫類型
|
||||
if (key === 'type' && typeof value === 'string' && typeMap[value]) {
|
||||
return typeMap[value];
|
||||
}
|
||||
|
||||
// 處理日期欄位 (YYYY-MM-DD)
|
||||
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date') && typeof value === 'string') {
|
||||
// 僅取日期部分 (YYYY-MM-DD)
|
||||
// 處理日期與時間
|
||||
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date' || key === 'received_date' || key === 'production_date') && typeof value === 'string') {
|
||||
return value.split('T')[0].split(' ')[0];
|
||||
}
|
||||
|
||||
// 處理日期時間欄位 (YYYY-MM-DD HH:mm:ss)
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at') && typeof value === 'string') {
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at') && typeof value === 'string') {
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleString('zh-TW', {
|
||||
@@ -308,44 +353,31 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const getFormattedValue = (key: string, value: any) => {
|
||||
// 如果是 ID 欄位,嘗試在快照或屬性中尋找對應名稱
|
||||
if (key.endsWith('_id')) {
|
||||
const nameKey = key.replace('_id', '_name');
|
||||
// 先檢查快照,然後檢查屬性
|
||||
const nameValue = snapshot[nameKey] || attributes[nameKey];
|
||||
if (nameValue) {
|
||||
return `${nameValue}`;
|
||||
}
|
||||
if (nameValue) return `${nameValue}`;
|
||||
}
|
||||
return formatValue(key, value);
|
||||
};
|
||||
|
||||
// 取得翻譯欄位標籤的輔助函式
|
||||
const getFieldLabel = (key: string) => {
|
||||
return fieldLabels[key] || key;
|
||||
};
|
||||
const getFieldLabel = (key: string) => fieldLabels[key] || key;
|
||||
const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type;
|
||||
|
||||
// 取得標題的主題名稱
|
||||
const getSubjectName = () => {
|
||||
// 庫存的特殊處理:顯示 "倉庫 - 商品"
|
||||
if ((snapshot.warehouse_name || attributes.warehouse_name) && (snapshot.product_name || attributes.product_name)) {
|
||||
const wName = snapshot.warehouse_name || attributes.warehouse_name;
|
||||
const pName = snapshot.product_name || attributes.product_name;
|
||||
return `${wName} - ${pName}`;
|
||||
return `${snapshot.warehouse_name || attributes.warehouse_name} - ${snapshot.product_name || attributes.product_name}`;
|
||||
}
|
||||
|
||||
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title'];
|
||||
const nameParams = ['doc_no', 'po_number', 'gr_number', 'production_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'title'];
|
||||
for (const param of nameParams) {
|
||||
if (snapshot[param]) return snapshot[param];
|
||||
if (attributes[param]) return attributes[param];
|
||||
if (old[param]) return old[param];
|
||||
}
|
||||
|
||||
if (attributes.id || old.id) return `#${attributes.id || old.id}`;
|
||||
return '';
|
||||
};
|
||||
@@ -365,7 +397,6 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 現代化元數據條 */}
|
||||
<div className="flex flex-wrap items-center gap-6 pt-2 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
@@ -379,19 +410,19 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
<Package className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-medium text-gray-700">
|
||||
{subjectName ? `${subjectName} ` : ''}
|
||||
{activity.properties?.sub_subject || activity.subject_type}
|
||||
({getSubjectTypeLabel(activity.properties?.sub_subject || activity.subject_type)})
|
||||
</span>
|
||||
</div>
|
||||
{/* 僅在描述與事件名稱不同時顯示(不太可能發生但為了安全起見) */}
|
||||
{activity.description !== getEventLabel(activity.event) &&
|
||||
activity.description !== 'created' && activity.description !== 'updated' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<ActivityIcon className="w-4 h-4 text-gray-400" />
|
||||
<span>{activity.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
{/* 僅在描述與事件名稱不同時顯示(不太可能發生但為了安全起見) */}
|
||||
{activity.description !== getEventLabel(activity.event) &&
|
||||
activity.description !== 'created' && activity.description !== 'updated' && (
|
||||
<div className="flex items-center gap-2 px-6 pb-2 -mt-2">
|
||||
<ActivityIcon className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-500">{activity.description}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-gray-50/50 p-6 min-h-[300px]">
|
||||
{activity.event === 'created' ? (
|
||||
@@ -571,7 +602,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DialogContent >
|
||||
</Dialog >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,50 @@ interface LogTableProps {
|
||||
from?: number; // 起始索引編號 (paginator.from)
|
||||
}
|
||||
|
||||
// 主體類型解析 (Model 類名轉中文)
|
||||
const subjectTypeMap: Record<string, string> = {
|
||||
// 完整路徑映射
|
||||
'App\\Modules\\Inventory\\Models\\Product': '商品資料',
|
||||
'App\\Modules\\Inventory\\Models\\Warehouse': '倉庫資料',
|
||||
'App\\Modules\\Inventory\\Models\\Inventory': '庫存異動',
|
||||
'App\\Modules\\Inventory\\Models\\Category': '商品分類',
|
||||
'App\\Modules\\Inventory\\Models\\Unit': '單位',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransaction': '庫存異動紀錄',
|
||||
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
|
||||
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
|
||||
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
|
||||
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
|
||||
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
|
||||
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
|
||||
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
|
||||
'App\\Modules\\Core\\Models\\User': '使用者帳號',
|
||||
'App\\Modules\\Core\\Models\\Role': '角色權限',
|
||||
// 簡寫映射
|
||||
'Product': '商品資料',
|
||||
'Warehouse': '倉庫資料',
|
||||
'Inventory': '庫存異動',
|
||||
'InventoryTransaction': '庫存異動紀錄',
|
||||
'Category': '商品分類',
|
||||
'Unit': '單位',
|
||||
'Vendor': '廠商資料',
|
||||
'PurchaseOrder': '採購單',
|
||||
'GoodsReceipt': '進貨單',
|
||||
'ProductionOrder': '生產工單',
|
||||
'Recipe': '生產配方',
|
||||
'InventoryCountDoc': '庫存盤點單',
|
||||
'InventoryAdjustDoc': '庫存盤調單',
|
||||
'InventoryTransferOrder': '庫存調撥單',
|
||||
'StockMovementDoc': '庫存單據',
|
||||
'User': '使用者帳號',
|
||||
'Role': '角色權限',
|
||||
'UtilityFee': '公共事業費',
|
||||
};
|
||||
|
||||
export default function LogTable({
|
||||
activities,
|
||||
sortField,
|
||||
@@ -37,6 +81,8 @@ export default function LogTable({
|
||||
onViewDetail,
|
||||
from = 1
|
||||
}: LogTableProps) {
|
||||
const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type;
|
||||
|
||||
const getEventBadgeClass = (event: string) => {
|
||||
switch (event) {
|
||||
case 'created': return 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100';
|
||||
@@ -111,7 +157,7 @@ export default function LogTable({
|
||||
{props.sub_subject ? (
|
||||
<span className="text-gray-700">{props.sub_subject}</span>
|
||||
) : (
|
||||
<span className="text-gray-700">{activity.subject_type}</span>
|
||||
<span className="text-gray-700">{getSubjectTypeLabel(activity.subject_type)}</span>
|
||||
)}
|
||||
|
||||
{/* 如果有原因/來源則顯示(例如:來自補貨) */}
|
||||
@@ -185,7 +231,7 @@ export default function LogTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[200px]">
|
||||
<Badge variant="outline" className="bg-slate-50 text-slate-600 border-slate-200 break-all whitespace-normal text-left h-auto py-1">
|
||||
{activity.subject_type}
|
||||
{getSubjectTypeLabel(activity.subject_type)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
|
||||
@@ -73,7 +73,57 @@ Star ERP 系統提供外部整合 API (Integration API) 供電商前台、POS
|
||||
|
||||
---
|
||||
|
||||
## 2. 訂單資料寫入與扣庫 (Create Order)
|
||||
## 2. 門市庫存查詢 (Query Inventory)
|
||||
|
||||
此 API 用於讓外部系統(如 POS)依據特定的「倉庫代碼」,查詢該倉庫目前所有商品的庫存餘額。
|
||||
**注意**:此 API 會回傳該倉庫內的所有商品數量,不論該商品是否已綁定外部 POS ID。
|
||||
|
||||
- **Endpoint**: `/inventory/{warehouse_code}`
|
||||
- **Method**: `GET`
|
||||
|
||||
### URL 路徑參數
|
||||
|
||||
| 參數名稱 | 類型 | 必填 | 說明 |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| `warehouse_code` | String | **是** | 要查詢的倉庫代碼 (例如:`STORE-001`) |
|
||||
|
||||
### Response
|
||||
|
||||
**Success (HTTP 200)**
|
||||
回傳該倉庫內所有的商品目前庫存總數。若商品未建置 `external_pos_id`,該欄位將顯示為 `null`。
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"warehouse_code": "STORE-001",
|
||||
"data": [
|
||||
{
|
||||
"external_pos_id": "POS-ITEM-001",
|
||||
"product_code": "PROD-A001",
|
||||
"product_name": "特級冷壓初榨橄欖油 500ml",
|
||||
"quantity": 15
|
||||
},
|
||||
{
|
||||
"external_pos_id": null,
|
||||
"product_code": "MAT-001",
|
||||
"product_name": "未包裝干貝醬原料",
|
||||
"quantity": 2.5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Error: Warehouse Not Found (HTTP 404)**
|
||||
當傳入系統中不存在的倉庫代碼時發生。
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Warehouse with code 'STORE-999' not found."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 訂單資料寫入與扣庫 (Create Order)
|
||||
|
||||
此 API 用於讓第三方系統(如 POS 結帳後)將「已成交」的訂單推送到 ERP。
|
||||
**重要提醒**:寫入訂單的同時,ERP 會自動且**強制**扣除對應倉庫的庫存(允許扣至負數,以利後續盤點或補單)。
|
||||
|
||||
Reference in New Issue
Block a user