V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
namespace Modules.WMS.Picking.Shared;
|
||||
|
||||
public sealed record PickingLineDto(
|
||||
Guid LineId,
|
||||
int LineNo,
|
||||
string LocationCode,
|
||||
Guid ItemId,
|
||||
string ItemCode,
|
||||
string ItemName,
|
||||
string? ItemOption,
|
||||
string Barcode,
|
||||
decimal RequiredQty,
|
||||
decimal PickedQty,
|
||||
decimal RemainingQty);
|
||||
|
||||
public sealed record PickingTaskDto(
|
||||
Guid TaskId,
|
||||
string TaskNo,
|
||||
string Stage,
|
||||
string Status,
|
||||
int CompletedLines,
|
||||
int TotalLines,
|
||||
decimal CompletedQty,
|
||||
decimal TotalQty,
|
||||
long Version,
|
||||
PickingLineDto? CurrentLine,
|
||||
string? Message = null);
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
namespace Modules.WMS.Picking.Shared;
|
||||
|
||||
public enum PickingScanDecisionKind
|
||||
{
|
||||
RejectLocation,
|
||||
AcceptLocation,
|
||||
RejectItem,
|
||||
AcceptItem
|
||||
}
|
||||
|
||||
public sealed record PickingScanState(
|
||||
bool LocationConfirmed,
|
||||
string LocationCode,
|
||||
string LocationBarcode,
|
||||
string ItemBarcode,
|
||||
decimal RequiredQty,
|
||||
decimal PickedQty);
|
||||
|
||||
public sealed record PickingScanDecision(
|
||||
PickingScanDecisionKind Kind,
|
||||
bool Accepted,
|
||||
string Feedback,
|
||||
string Message,
|
||||
decimal? NewPickedQty = null,
|
||||
bool LineCompleted = false)
|
||||
{
|
||||
public string AuditAction => Kind switch
|
||||
{
|
||||
PickingScanDecisionKind.RejectLocation => "LocationRejected",
|
||||
PickingScanDecisionKind.AcceptLocation => "LocationAccepted",
|
||||
PickingScanDecisionKind.RejectItem => "ItemRejected",
|
||||
PickingScanDecisionKind.AcceptItem => "ItemAccepted",
|
||||
_ => "BarcodeScanned"
|
||||
};
|
||||
}
|
||||
|
||||
public static class PickingScanStateMachine
|
||||
{
|
||||
public static PickingScanDecision Decide(PickingScanState state, string scannedBarcode)
|
||||
{
|
||||
var barcode = scannedBarcode.Trim();
|
||||
|
||||
if (!state.LocationConfirmed)
|
||||
{
|
||||
if (!string.Equals(barcode, state.LocationBarcode, StringComparison.OrdinalIgnoreCase))
|
||||
return new(
|
||||
PickingScanDecisionKind.RejectLocation,
|
||||
false,
|
||||
"error",
|
||||
$"잘못된 위치입니다. {state.LocationCode} 위치로 이동하세요.");
|
||||
|
||||
return new(
|
||||
PickingScanDecisionKind.AcceptLocation,
|
||||
true,
|
||||
"success",
|
||||
"위치를 확인했습니다. 상품을 스캔하세요.");
|
||||
}
|
||||
|
||||
if (!string.Equals(barcode, state.ItemBarcode, StringComparison.OrdinalIgnoreCase))
|
||||
return new(
|
||||
PickingScanDecisionKind.RejectItem,
|
||||
false,
|
||||
"error",
|
||||
"다른 상품입니다. 화면의 품목과 바코드를 확인하세요.");
|
||||
|
||||
var nextQty = Math.Min(state.PickedQty + 1m, state.RequiredQty);
|
||||
var completed = nextQty >= state.RequiredQty;
|
||||
return new(
|
||||
PickingScanDecisionKind.AcceptItem,
|
||||
true,
|
||||
"success",
|
||||
completed ? "현재 품목을 완료했습니다. 다음 작업을 진행하세요." : "1개 피킹했습니다.",
|
||||
nextQty,
|
||||
completed);
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
|
||||
namespace Modules.WMS.Picking.Shared;
|
||||
|
||||
public static class PickingTaskQueries
|
||||
{
|
||||
public static async Task<PickingTaskDto?> GetAsync(
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction? transaction,
|
||||
Guid taskId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
const string taskSql = """
|
||||
select id as TaskId,
|
||||
task_no as TaskNo,
|
||||
status as Status,
|
||||
version as Version
|
||||
from wms.picking_tasks
|
||||
where id = @TaskId;
|
||||
""";
|
||||
|
||||
var task = await connection.QuerySingleOrDefaultAsync<TaskRow>(
|
||||
new CommandDefinition(taskSql, new { TaskId = taskId }, transaction, cancellationToken: ct));
|
||||
if (task is null) return null;
|
||||
|
||||
const string linesSql = """
|
||||
select l.id as LineId,
|
||||
l.line_no as LineNo,
|
||||
l.location_code as LocationCode,
|
||||
l.item_id as ItemId,
|
||||
i.code as ItemCode,
|
||||
i.name as ItemName,
|
||||
l.item_option as ItemOption,
|
||||
l.barcode as Barcode,
|
||||
l.required_qty as RequiredQty,
|
||||
l.picked_qty as PickedQty,
|
||||
greatest(l.required_qty - l.picked_qty, 0) as RemainingQty,
|
||||
l.status as Status,
|
||||
l.location_confirmed as LocationConfirmed
|
||||
from wms.picking_lines l
|
||||
join catalog.items i on i.id = l.item_id
|
||||
where l.task_id = @TaskId
|
||||
order by l.line_no;
|
||||
""";
|
||||
|
||||
var lines = (await connection.QueryAsync<LineRow>(
|
||||
new CommandDefinition(linesSql, new { TaskId = taskId }, transaction, cancellationToken: ct))).AsList();
|
||||
|
||||
var current = lines.FirstOrDefault(x => x.Status != "COMPLETED");
|
||||
var completedLines = lines.Count(x => x.Status == "COMPLETED");
|
||||
var totalQty = lines.Sum(x => x.RequiredQty);
|
||||
var completedQty = lines.Sum(x => x.PickedQty);
|
||||
|
||||
var stage = task.Status switch
|
||||
{
|
||||
"READY" => "ready",
|
||||
"COMPLETED" => "completed",
|
||||
"BLOCKED" => "blocked",
|
||||
_ when current is null => "completed",
|
||||
_ when current.LocationConfirmed => "await-item",
|
||||
_ => "await-location"
|
||||
};
|
||||
|
||||
return new PickingTaskDto(
|
||||
task.TaskId,
|
||||
task.TaskNo,
|
||||
stage,
|
||||
task.Status,
|
||||
completedLines,
|
||||
lines.Count,
|
||||
completedQty,
|
||||
totalQty,
|
||||
task.Version,
|
||||
current is null ? null : new PickingLineDto(
|
||||
current.LineId,
|
||||
current.LineNo,
|
||||
current.LocationCode,
|
||||
current.ItemId,
|
||||
current.ItemCode,
|
||||
current.ItemName,
|
||||
current.ItemOption,
|
||||
current.Barcode,
|
||||
current.RequiredQty,
|
||||
current.PickedQty,
|
||||
current.RemainingQty));
|
||||
}
|
||||
|
||||
private sealed record TaskRow(Guid TaskId, string TaskNo, string Status, long Version);
|
||||
private sealed record LineRow(
|
||||
Guid LineId,
|
||||
int LineNo,
|
||||
string LocationCode,
|
||||
Guid ItemId,
|
||||
string ItemCode,
|
||||
string ItemName,
|
||||
string? ItemOption,
|
||||
string Barcode,
|
||||
decimal RequiredQty,
|
||||
decimal PickedQty,
|
||||
decimal RemainingQty,
|
||||
string Status,
|
||||
bool LocationConfirmed = false);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
|
||||
namespace Modules.WMS.Picking.Shared;
|
||||
|
||||
public static class PickingTransitions
|
||||
{
|
||||
public static async Task AutoConfirmNextSameLocationAsync(
|
||||
NpgsqlConnection connection,
|
||||
NpgsqlTransaction transaction,
|
||||
Guid taskId,
|
||||
string completedLocation,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await connection.ExecuteAsync(new CommandDefinition("""
|
||||
update wms.picking_lines
|
||||
set location_confirmed=true, updated_at=now()
|
||||
where id = (
|
||||
select id
|
||||
from wms.picking_lines
|
||||
where task_id=@TaskId
|
||||
and status <> 'COMPLETED'
|
||||
order by line_no
|
||||
limit 1
|
||||
)
|
||||
and location_code=@LocationCode;
|
||||
""", new { TaskId = taskId, LocationCode = completedLocation }, transaction, cancellationToken: ct));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user