Neil67's picture
Deploy Bakery ERP Space
13240d3
Raw
History Blame Contribute Delete
10.2 kB
using BakeryErp.Infrastructure.Database;
namespace BakeryErp.Infrastructure.Repositories;
public sealed class InventoryRepository
{
private readonly DatabaseOptions _options;
public InventoryRepository(DatabaseOptions options)
{
_options = options;
}
public IReadOnlyList<LocalStockBalance> ListBalances()
{
return LocalDataStoreFile.Load(_options)
.StockBalances
.OrderBy(static stock => stock.WarehouseName)
.ThenBy(static stock => stock.ItemSku)
.ToList();
}
public IReadOnlyList<LocalInventoryTransaction> ListTransactions()
{
return LocalDataStoreFile.Load(_options)
.InventoryTransactions
.OrderByDescending(static transaction => transaction.OccurredAt)
.ToList();
}
public LocalInventoryTransaction? FindTransaction(Guid id)
{
return LocalDataStoreFile.Load(_options)
.InventoryTransactions
.FirstOrDefault(transaction => transaction.Id == id);
}
public InventoryReport GetReport()
{
var store = LocalDataStoreFile.Load(_options);
var balances = store.StockBalances;
var transactions = store.InventoryTransactions;
return new InventoryReport(
balances.Count,
balances.Count(static stock => stock.QuantityOnHand <= stock.SafetyStockQuantity),
transactions.Count(static transaction => transaction.Status == LocalInventoryTransactionStatus.PendingApproval),
transactions.Count(static transaction => transaction.Status == LocalInventoryTransactionStatus.Approved),
balances.Sum(static stock => stock.QuantityOnHand));
}
public SaveInventoryTransactionResult SaveTransaction(LocalInventoryTransaction transaction)
{
var store = LocalDataStoreFile.Load(_options);
var transactionNumber = transaction.TransactionNumber.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(transactionNumber) ||
string.IsNullOrWhiteSpace(transaction.WarehouseName) ||
string.IsNullOrWhiteSpace(transaction.ItemName) ||
string.IsNullOrWhiteSpace(transaction.Unit))
{
return SaveInventoryTransactionResult.ValidationError("異動單號、倉庫、品項與單位為必填。");
}
if (transaction.Quantity == 0)
{
return SaveInventoryTransactionResult.ValidationError("數量不可為 0。");
}
var duplicatedNumber = store.InventoryTransactions.Any(existing =>
existing.Id != transaction.Id &&
string.Equals(existing.TransactionNumber, transactionNumber, StringComparison.OrdinalIgnoreCase));
if (duplicatedNumber)
{
return SaveInventoryTransactionResult.ValidationError("異動單號已存在,請改用其他單號。");
}
var existingTransaction = store.InventoryTransactions.FirstOrDefault(existing => existing.Id == transaction.Id);
if (existingTransaction is null)
{
transaction.Id = transaction.Id == Guid.Empty ? Guid.NewGuid() : transaction.Id;
transaction.TransactionNumber = transactionNumber;
transaction.WarehouseName = transaction.WarehouseName.Trim();
transaction.ItemSku = BlankToEmpty(transaction.ItemSku).ToUpperInvariant();
transaction.ItemName = transaction.ItemName.Trim();
transaction.Unit = transaction.Unit.Trim();
transaction.SourceDocumentNumber = BlankToNull(transaction.SourceDocumentNumber);
transaction.Note = BlankToNull(transaction.Note);
transaction.Status = LocalInventoryTransactionStatus.PendingApproval;
transaction.CreatedAt = DateTime.UtcNow;
store.InventoryTransactions.Add(transaction);
}
else
{
if (existingTransaction.Status == LocalInventoryTransactionStatus.Approved)
{
return SaveInventoryTransactionResult.ValidationError("已審核的庫存異動不可編輯。");
}
existingTransaction.TransactionNumber = transactionNumber;
existingTransaction.WarehouseName = transaction.WarehouseName.Trim();
existingTransaction.ItemSku = BlankToEmpty(transaction.ItemSku).ToUpperInvariant();
existingTransaction.ItemName = transaction.ItemName.Trim();
existingTransaction.TransactionType = transaction.TransactionType;
existingTransaction.Quantity = transaction.Quantity;
existingTransaction.Unit = transaction.Unit.Trim();
existingTransaction.SourceDocumentNumber = BlankToNull(transaction.SourceDocumentNumber);
existingTransaction.Note = BlankToNull(transaction.Note);
existingTransaction.OccurredAt = transaction.OccurredAt;
existingTransaction.UpdatedAt = DateTime.UtcNow;
}
LocalDataStoreFile.Save(_options, store);
return SaveInventoryTransactionResult.Success();
}
public ApproveInventoryTransactionResult Approve(Guid id)
{
var store = LocalDataStoreFile.Load(_options);
var transaction = store.InventoryTransactions.FirstOrDefault(existing => existing.Id == id);
if (transaction is null)
{
return ApproveInventoryTransactionResult.ValidationError("找不到庫存異動單。");
}
if (transaction.Status == LocalInventoryTransactionStatus.Approved)
{
return ApproveInventoryTransactionResult.Success();
}
var delta = GetQuantityDelta(transaction);
var balance = store.StockBalances.FirstOrDefault(stock =>
string.Equals(stock.WarehouseName, transaction.WarehouseName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(stock.ItemSku, transaction.ItemSku, StringComparison.OrdinalIgnoreCase));
if (balance is null)
{
balance = new LocalStockBalance
{
Id = Guid.NewGuid(),
WarehouseName = transaction.WarehouseName,
ItemSku = transaction.ItemSku,
ItemName = transaction.ItemName,
Unit = transaction.Unit,
SafetyStockQuantity = 0
};
store.StockBalances.Add(balance);
}
if (balance.QuantityOnHand + delta < 0)
{
return ApproveInventoryTransactionResult.ValidationError("審核後庫存會小於 0,請確認數量。");
}
balance.ItemName = transaction.ItemName;
balance.Unit = transaction.Unit;
balance.QuantityOnHand += delta;
balance.LastTransactionAt = DateTime.UtcNow;
transaction.Status = LocalInventoryTransactionStatus.Approved;
transaction.ApprovedAt = DateTime.UtcNow;
transaction.UpdatedAt = DateTime.UtcNow;
LocalDataStoreFile.Save(_options, store);
return ApproveInventoryTransactionResult.Success();
}
public void Cancel(Guid id)
{
var store = LocalDataStoreFile.Load(_options);
var transaction = store.InventoryTransactions.FirstOrDefault(existing => existing.Id == id);
if (transaction is null || transaction.Status == LocalInventoryTransactionStatus.Approved)
{
return;
}
transaction.Status = LocalInventoryTransactionStatus.Cancelled;
transaction.UpdatedAt = DateTime.UtcNow;
LocalDataStoreFile.Save(_options, store);
}
public static string GetTransactionTypeName(LocalInventoryTransactionType type)
{
return type switch
{
LocalInventoryTransactionType.GoodsReceipt => "進貨入庫",
LocalInventoryTransactionType.StockOut => "出庫",
LocalInventoryTransactionType.StockAdjustment => "庫存調整",
LocalInventoryTransactionType.Scrap => "報廢",
LocalInventoryTransactionType.StoreReturn => "門市退貨",
_ => type.ToString()
};
}
public static string GetStatusName(LocalInventoryTransactionStatus status)
{
return status switch
{
LocalInventoryTransactionStatus.Draft => "草稿",
LocalInventoryTransactionStatus.PendingApproval => "待審核",
LocalInventoryTransactionStatus.Approved => "已審核",
LocalInventoryTransactionStatus.Cancelled => "已取消",
_ => status.ToString()
};
}
private static decimal GetQuantityDelta(LocalInventoryTransaction transaction)
{
var absoluteQuantity = Math.Abs(transaction.Quantity);
return transaction.TransactionType switch
{
LocalInventoryTransactionType.GoodsReceipt => absoluteQuantity,
LocalInventoryTransactionType.StoreReturn => absoluteQuantity,
LocalInventoryTransactionType.StockOut => -absoluteQuantity,
LocalInventoryTransactionType.Scrap => -absoluteQuantity,
LocalInventoryTransactionType.StockAdjustment => transaction.Quantity,
_ => transaction.Quantity
};
}
private static string BlankToEmpty(string? value)
{
return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
}
private static string? BlankToNull(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}
public sealed record InventoryReport(
int StockItemCount,
int LowStockCount,
int PendingApprovalCount,
int ApprovedTransactionCount,
decimal TotalQuantityOnHand);
public sealed record SaveInventoryTransactionResult(bool IsSuccess, string? ErrorMessage)
{
public static SaveInventoryTransactionResult Success() => new(true, null);
public static SaveInventoryTransactionResult ValidationError(string message) => new(false, message);
}
public sealed record ApproveInventoryTransactionResult(bool IsSuccess, string? ErrorMessage)
{
public static ApproveInventoryTransactionResult Success() => new(true, null);
public static ApproveInventoryTransactionResult ValidationError(string message) => new(false, message);
}