QR-Single Scanning
Flow
- In Dncn1Create if sysparameter is On then we will make Qty field as readonly and add a placeholder Use Add Qty button below to add quantity
- In Dncn1Create, if sysparameter is On then instead of Save button we will Add Qty button
- Once user clicks on Add Qty then a drawer will get open, in that user can scan Trni-Ids, it will get stored in a UI table and then the Sum of Qty will be taken as Dncn.qty
- Once the qty is added we will show submit button
- Once submit button is clicked records will be create with existing flow
- After that we will insert in scannedsubtrn and update qty in mfgsubtrn
Columns that will be shown in QR single scanning Drawer
| Column Name |
|---|
| Trn Id |
| Sub Trn Id |
| Item Name |
| Qty |
| Delete |
Validations
| Validation Message | Short Description |
|---|---|
| Qty must be between 0.001 and 999999999.999 | Individual scanned item quantity must fall within the allowed numeric range. |
| IIRS is Un-Authorized | The scanned TrnId is not authorized (IsAuth = “N”). |
| Stock is Rejected | The scanned stock is marked as rejected (IsRejected = “Y”). |
| Entered Qty exceeds available balance | Entered quantity is greater than the available balance quantity for the TrnId. |
| Item/Store/Stage/Brand of the Trnid is not matching with Invoice/Bill | The scanned TrnId details (Item, Store, Stage, Brand) do not match the DNCN document configuration. |
| Party mismatch with DNCN | The PartyId of the scanned TrnId does not match the DNCN PartyId. |
| Branch mismatch with DNCN | The BranchId of the scanned TrnId does not match the DNCN BranchId. |
| Unit mismatch with DNCN | The UnitCode of the scanned TrnId does not match the DNCN UnitCode. |
| Qty must be between 0.001 and 999999.999 | Total aggregated quantity of all scanned items must be within allowed document range. |
| SCN/PCN qty cannot exceed the actual available qty for this Trn Id. | For SCN or PCN type documents, the entered quantity cannot exceed actual available quantity for the specific TrnId. |
| Qty cannot be more than Invoice Qty / Bill Qty | Total scanned quantity cannot exceed the original Invoice/Bill document quantity. |
| Invalid TrnId format. Expected format: M000001-001 (7-3 chars). | QR format validation failed. TrnId must be 7 characters and SubTrnId must be 3 characters. |
| QR already scanned: {TrnId}-{SubTrnId} | Duplicate QR scan detected in the current session. |
| No balance found or invalid QR for TrnId: {TrnId}, SubTrnId: {SubTrnId} | No available balance returned from service or QR is invalid. |
| Failed to fetch quantity for {TrnId}-{SubTrnId}. | Exception occurred while retrieving balance quantity from service. |
Code
DncnQrDailog.razor
@inject IDncnService _IDncnService
<div class="d-flex flex-column" style="height: 100%;">
<!-- Header Section: Total Quantity Display -->
<div class="pa-4 flex-shrink-0" style="background-color: var(--mud-palette-surface);">
<MudPaper Elevation="0" Outlined="true" Class="pa-4 mud-background-gray">
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Total Scanned Quantity</MudText>
<MudText Typo="Typo.h4" Color="Color.Primary"><b>@_internalItems.Sum(x => x.Qty).ToString("N2")</b>
</MudText>
</div>
<div class="text-end">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Total Scanned Items</MudText>
<MudText Typo="Typo.h6"><b>@_internalItems.Count</b></MudText>
</div>
</div>
</MudPaper>
<div class="mt-4">
<MudTextField @ref="_qrTextField" Value="@qrInput"
ValueChanged="@(async (string s) => await OnQrTextChanged(s))" Label="Scan QR Code"
Variant="Variant.Outlined" Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.QrCodeScanner" OnKeyDown="HandleQrKeyDown" AutoFocus="true"
Immediate="true" Placeholder="Please Scan here..." HelperText="Ensure scanner is focused here"
TextUpdateSuppression="false" />
</div>
</div>
<!-- Body Section: Scanned Records Grid -->
<div class="flex-grow-1 overflow-auto px-4 pb-4">
@if (_internalItems.Any())
{
<MudTable Items="@_internalItems" Dense="true" Hover="true" Striped="true" Bordered="false" Elevation="0"
Class="border">
<HeaderContent>
<MudTh Style="font-weight: bold;">TrnId</MudTh>
<MudTh Style="font-weight: bold;">SubTrnId</MudTh>
<MudTh Style="font-weight: bold;">ItemName</MudTh>
<MudTh Style="font-weight: bold; width: 100px;">Qty</MudTh>
<MudTh Style="font-weight: bold; width: 50px;" Class="text-center">Delete</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="TrnId">@context.TrnId</MudTd>
<MudTd DataLabel="SubTrnId">@context.SubTrnId</MudTd>
<MudTd DataLabel="ItemName">@context.ItemName</MudTd>
<MudTd DataLabel="Qty">
<MudNumericField Value="@context.Qty" ValueChanged="@((decimal v) => UpdateItemQty(context, v))"
Min="0m" Step="1m" HideSpinButtons="true" Variant="Variant.Text" Margin="Margin.Dense"
Style="width: 80px;" Immediate="true" Error="@(!string.IsNullOrEmpty(context.QtyError))"
ErrorText="@context.QtyError" />
</MudTd>
<MudTd DataLabel="Delete" Class="text-center">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="@(() => RemoveItem(context))" Size="Size.Small" />
</MudTd>
</RowTemplate>
</MudTable>
}
else
{
<div class="d-flex flex-column align-center justify-center mt-10" style="opacity: 0.4;">
<MudIcon Icon="@Icons.Material.Filled.QrCodeScanner" Style="font-size: 5rem;" />
<MudText Typo="Typo.h6">No items scanned yet</MudText>
<MudText Typo="Typo.body2">Use a QR scanner</MudText>
</div>
}
</div>
<!-- Footer Section: Action Buttons -->
<div class="pa-4 border-t flex-shrink-0 d-flex justify-space-between align-center"
style="background-color: var(--mud-palette-surface);">
<MudButton Variant="Variant.Text" Color="Color.Error" OnClick="ClearAll" Disabled="@(!_internalItems.Any())"
StartIcon="@Icons.Material.Filled.DeleteSweep">
Clear All
</MudButton>
<div class="d-flex gap-2">
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="OnCancelClick">Close</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnSubmitClick"
StartIcon="@Icons.Material.Filled.Save">
Save
</MudButton>
</div>
</div>
</div>
@code {
[Parameter]
public List<QrItemModel> Items { get; set; } = new List<QrItemModel>();
[Parameter]
public EventCallback<List<QrItemModel>> OnCommit { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
[Parameter]
public EventCallback<List<QrItemModel>> OnItemsChanged { get; set; }
[Parameter]
public Dncn1 ParentDncn1 { get; set; } = new Dncn1();
[Parameter]
public List<Dncn1> ReferenceLines { get; set; } = new List<Dncn1>();
private List<QrItemModel> _internalItems = new List<QrItemModel>();
private string qrInput = "";
private MudTextField<string> _qrTextField;
private PostLogin? _userInfo;
protected override async Task OnInitializedAsync()
{
_userInfo = await _LocalSession.GetItemAsync<PostLogin>("userinfo");
if (Items != null)
{
_internalItems = Items.Select(x => new QrItemModel
{
TrnId = x.TrnId,
SubTrnId = x.SubTrnId,
ItemName = x.ItemName,
ItemId = x.ItemId,
Qty = x.Qty,
ScanTime = x.ScanTime,
QtyError = x.QtyError
}).ToList();
foreach (var item in _internalItems)
{
item.QtyError = null;
}
}
}
private async Task OnCancelClick()
{
await OnCancel.InvokeAsync();
}
private async Task OnSubmitClick()
{
var dncnRequest = new DncnValidateQRRequest
{
Items = _internalItems,
MainId = ParentDncn1.MainId,
ItemId = ParentDncn1.ItemId,
StoreCode = ParentDncn1.StoreCode,
StageCode = ParentDncn1.StageCode,
BrandId = ParentDncn1.BrandId,
DocQty = ParentDncn1.DocQty
};
var response = await _IDncnService.Dncn1ValidateQR(dncnRequest, _userInfo?.dbname);
if (response.StatusCode != HttpStatusCode.OK)
{
var errors = await response.Content.ReadFromJsonAsync<Dictionary<string, List<string>>>();
if (errors != null)
{
foreach (var item in _internalItems) item.QtyError = null;
foreach (var err in errors)
{
// Pattern matches "Items[0].Qty"
if (err.Key.StartsWith("Items["))
{
var parts = err.Key.Split('[', ']');
if (parts.Length > 1 && int.TryParse(parts[1], out int index))
{
if (index >= 0 && index < _internalItems.Count)
{
_internalItems[index].QtyError = string.Join(", ", err.Value);
}
}
}
}
StateHasChanged();
}
}
else
{
await OnCommit.InvokeAsync(_internalItems);
}
}
private async Task ClearAll()
{
_internalItems.Clear();
await NotifyChanges();
}
private async Task HandleQrKeyDown(KeyboardEventArgs args)
{
if (args.Key == "Enter")
{
await OnQrTextChanged(qrInput);
}
}
private async Task OnQrTextChanged(string value)
{
qrInput = value;
if (!string.IsNullOrWhiteSpace(qrInput))
{
var parts = qrInput.Split('-');
// Strict Validation: TrnId (parts[0]) must be 7 characters, SubTrnId (parts[1]) must be 3 characters
if (parts[0].Length != 7 || parts[1].Length != 3)
{
_ISnackbar.Add("Invalid TrnId format. Expected format: M000001-001 (7-3 chars).", Severity.Error);
await ClearInput();
return;
}
await ParseQrCode(qrInput);
await ClearInput();
StateHasChanged();
}
}
private async Task ClearInput()
{
qrInput = "";
if (_qrTextField != null)
{
await _qrTextField.Clear();
await _qrTextField.FocusAsync();
}
}
private async Task ParseQrCode(string input)
{
if (string.IsNullOrWhiteSpace(input)) return;
var parts = input.Split('-');
if (parts.Length >= 4)
{
var trnId = parts[0];
var subTrnId = parts[1];
// Check if already scanned to avoid duplicate calls/entries
if (_internalItems.Any(x => x.TrnId == trnId && x.SubTrnId == subTrnId))
{
_ISnackbar.Add($"QR already scanned: {trnId}-{subTrnId}", Severity.Warning);
return;
}
try
{
var balanceQty = await _IDncnService.GetQrSubTrnQty(trnId, subTrnId, _userInfo?.dbname);
if (balanceQty <= 0)
{
_ISnackbar.Add($"No balance found or invalid QR for TrnId: {trnId}, SubTrnId: {subTrnId}", Severity.Warning);
return;
}
var newItem = new QrItemModel
{
TrnId = trnId,
SubTrnId = subTrnId,
ItemName = parts[2],
ItemId = parts[3],
Qty = balanceQty,
ScanTime = DateTime.Now
};
_internalItems.Insert(0, newItem);
await NotifyChanges();
StateHasChanged();
}
catch (Exception ex)
{
_ISnackbar.Add($"Failed to fetch quantity for {trnId}-{subTrnId}.", Severity.Error);
Console.WriteLine($"Error parsing QR or fetching Qty: {ex.Message}");
}
}
}
private async Task UpdateItemQty(QrItemModel item, decimal value)
{
item.Qty = value;
item.QtyError = null;
await NotifyChanges();
}
private async Task RemoveItem(QrItemModel item)
{
_internalItems.Remove(item);
await NotifyChanges();
}
private async Task NotifyChanges()
{
if (OnItemsChanged.HasDelegate)
{
await OnItemsChanged.InvokeAsync(_internalItems);
}
StateHasChanged();
}
}Dncn1ValidateQR Method
[HttpPost("{dbname}")]
public ActionResult Dncn1ValidateQR(DncnValidateQRRequest _Dncn1, string dbname)
{
var items = _Dncn1.Items;
// Fetch main DNCN details using MainId
var mainDncn = _IDncnRepository.GetDncnInfo(_Dncn1.MainId, dbname);
string docType = mainDncn.DncnType.Substring(0, 1);
string docName = docType == "S" ? "Invoice" : "Bill";
// Duplicate scan detection (Rule H)
var scanKeys = items.Select(x =>
{
var trnId = (x.TrnId ?? "").Trim();
var subTrnId = (x.SubTrnId ?? "").Trim();
return trnId + subTrnId;
})
.GroupBy(k => k)
.Where(g => !string.IsNullOrEmpty(g.Key) && g.Count() > 1)
.Select(g => g.Key)
.ToHashSet();
// Aggregate: total quantity only
decimal totalQty = items.Sum(x => x.Qty);
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
if (item.Qty < 0.001M || item.Qty > 999999999.999M)
{
ModelState.AddModelError($"Items[{i}].Qty", "Qty must be between 0.001 and 999999999.999");
return BadRequest(ModelState);
}
var trnIdDetails = _IDncnRepository.GetTrnIdDetails(item.TrnId, item.SubTrnId, mainDncn.PartyId, mainDncn.BranchId, dbname);
if (trnIdDetails.IsAuth == "N")
{
ModelState.AddModelError($"Items[{i}].ItemId", "IIRS is Un-Authorized");
return BadRequest(ModelState);
}
if (trnIdDetails.IsRejected == "Y")
{
ModelState.AddModelError($"Items[{i}].ItemId", "Stock is Rejected");
return BadRequest(ModelState);
}
if (item.Qty > trnIdDetails.BalanceQty)
{
ModelState.AddModelError($"Items[{i}].Qty", "Entered Qty exceeds available balance");
return BadRequest(ModelState);
}
if (_Dncn1.ItemId != trnIdDetails.ItemId ||
_Dncn1.StoreCode != trnIdDetails.StoreCode ||
_Dncn1.StageCode != trnIdDetails.StageCode ||
_Dncn1.BrandId != trnIdDetails.BrandId)
{
ModelState.AddModelError($"Items[{i}].ItemId",
$"Item/Store/Stage/Brand of the Trnid is not matching with {docName}");
return BadRequest(ModelState);
}
if (trnIdDetails.PartyId != "" && trnIdDetails.PartyId != mainDncn.PartyId)
{
ModelState.AddModelError($"Items[{i}].Trnid", "Party mismatch with DNCN.");
return BadRequest(ModelState);
}
if (trnIdDetails.BranchId != "" && trnIdDetails.BranchId != mainDncn.BranchId)
{
ModelState.AddModelError($"Items[{i}].Trnid", "Branch mismatch with DNCN.");
return BadRequest(ModelState);
}
if (trnIdDetails.UnitCode != mainDncn.UnitCode)
{
ModelState.AddModelError($"Items[{i}].Trnid", "Unit mismatch with DNCN.");
return BadRequest(ModelState);
}
// Rule D/F: Aggregate quantity validation
if (totalQty < 0.001M || totalQty > 999999.999M)
{
ModelState.AddModelError($"Items[{i}].Qty", "Qty must be between 0.001 and 999999.999.");
return BadRequest(ModelState);
}
if(mainDncn.DncnType == "SCN" || mainDncn.DncnType == "PCN")
{
var balanceQtyCnt = _IDncnRepository.GetBalanceQtyCntForSCNPCN(item.TrnId, item.SubTrnId, item.Qty, dbname);
if (balanceQtyCnt > 0)
{
ModelState.AddModelError($"Items[{i}].Qty", "SCN/PCN qty cannot exceed the actual available qty for this Trn Id.");
return BadRequest(ModelState);
}
}
else if (totalQty > _Dncn1.DocQty)
{
ModelState.AddModelError($"Items[{i}].Qty", $"Qty cannot be more than {docName} Qty.");
return BadRequest(ModelState);
}
}
return Ok(_Dncn1);
}Dncn Repository Methods
----dncn1 insert will happen through existing dncn1Create method
---------------------------------GetTrnIdDetails---------------------------------
--This query will execute in loop for every trnid
WITH A(stage, storecode, brandid, partyid, unitcode, branchid, isauth, isrejected) AS
(
SELECT G1.ItemId,
I1.stage AS stagecode, G1.storecode, I1.brandid, G.partyid, G.unitcode, G.branchid,
'Y' AS isauth, 'N' AS isrejected
FROM Grn1 G1
LEFT JOIN indent1 I1 ON G1.indent1id = I1.id
LEFT JOIN Grn G ON G1.yeargrnno = G.YearGrnNo
WHERE LEFT(Z.trnid,1) = 'G'
UNION ALL
SELECT J1.ItemId,
J1.stage AS stagecode, J1.storecode, J1.brandid, J.partyid, J.unitcode, J.branchid,
'Y' AS isauth, 'N' AS isrejected
FROM jobwork1 J1
LEFT JOIN Jobwork J ON J1.yearjobno = J.YearJobNo
WHERE LEFT(Z.trnid,1) = 'J'
UNION ALL
SELECT M1.ItemId,
M1.stage AS stagecode, M1.storecode, M1.brandid, @partyid AS partyid, M.unitcode, @branchid AS branchid,
isauth, 'N' AS isrejected
FROM Mirs1 M1
LEFT JOIN Mirs M ON M1.yearjobno = M.yearjobno
WHERE LEFT(Z.trnid,1) = 'M'
UNION ALL
SELECT S.ItemId,
S.stagecode, S.storecode, S.brandid, @partyid AS partyid, S.unitcode, @branchid AS branchid,
'Y' AS isauth, IIF(itemstate = 'G', 'N', 'Y') AS isrejected
FROM Qrstock
WHERE LEFT(Z.trnid,1) = 'S'
)
-------------------GetBalanceQtyCntForSCNPCN------------------------
WITH A (trnid, subtrnid, qty) AS
(
SELECT @trnid, @subtrnid, @qty
UNION ALL
----taking records which are already imported
SELECT S.trnid, S.subtrnid, S.qty
FROM scannedsubtrn S
WHERE doctype IN ('SCN', 'PCN') AND isrolledback = 'N'
AND S.trnid = @trnid AND S.subtrnid = @subtrnid
),
B (trnid, subtrnid, qty) AS
(
SELECT trnid, subtrnid, SUM(qty) AS qty
FROM A
GROUP BY trnid, subtrnid
),
C(trnid, subtrnid) AS
(
SELECT B.trnid, B.subtrnid
FROM B
LEFT JOIN QRsubtrn Q ON B.trnid = Q.trnid AND B.subtrnid = Q.subtrnid
WHERE (B.qty + Q.balanceqty) > Q.actualqty
)
SELECT COUNT(*) FROM C
------------------------Insert Into ScannedSubTrn------------------------
--This query will execute in loop for every trnid
INSERT INTO ScannedSubTrn (trnid, subtrnid, qty, yeardocno, doctype, docid, isrolledback)
VALUES (@trnid, @subtrnid, @qty, @yeardocno, @dncntype, @docid, 'N')
---------------------UpdateQrSubTrn-------------------------
--This query will execute in loop for every trnid
WITH A (trnid,subtrnid,qty) AS
(
SELECT @trnid,@subtrnid,IIF(@dncntype IN ('SCN', 'PCN'), @qty, @qty * (-1))
)
UPDATE QrSubTrn SET BalanceQty = BalanceQty + A.qty FROM A
WHERE QrSubTrn.TrnId = A.TrnId AND QrSubTrn.SubTrnId = A.SubTrnId