feat(pos): окно выбора товара при скане ШК с несколькими карточками (дубли ШК)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run

- PosStore.FindAllByBarcode; SaleViewModel.ScanCandidates/IsScanPickerOpen + PickScanCandidate
- SaleView: модалка выбора; сервер by-barcode уже отдаёт items, веб-приёмка уже показывает выбор

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 21:04:20 +05:00
parent 30b39a7bcb
commit b04d3f0d0c
4 changed files with 77 additions and 3 deletions

View file

@ -90,8 +90,9 @@ Part 1 (инвентаризация) и Part 3 (орг) — выполнены.
дублем ШК (MergeByBarcode=false, dupeInInput не пропускает, артикул-дубль дизамбигуируется дублем ШК (MergeByBarcode=false, dupeInInput не пропускает, артикул-дубль дизамбигуируется
суффиксом msId). Перенос вернул **49 ранее слитых карточек** (каталог 30993 = МойСклад). Дубли суффиксом msId). Перенос вернул **49 ранее слитых карточек** (каталог 30993 = МойСклад). Дубли
ШК видны в «Проверке данных» → dup-barcode (чек уже есть). На кассе/в приёмке при скане ШК видны в «Проверке данных» → dup-barcode (чек уже есть). На кассе/в приёмке при скане
неоднозначного ШК фронт должен показывать выбор товара — серверные данные (несколько товаров с неоднозначного ШК показывается ОКНО ВЫБОРА товара: сервер `by-barcode` отдаёт список совпадений
одним ШК) и справочник dup-barcode для этого готовы. **Остатки пересчитаны**: см. раздел сверки — (цена/остаток), веб-приёмка (`SupplyLineQuickAdd`) и касса (POS `SaleView`/`FindAllByBarcode`) —
показывают выбор. **Остатки пересчитаны**: см. раздел сверки —
`align-stock` выравнивает к снимку `report/stock/all` (движения `Initial` «МС-выравнивание») → `align-stock` выравнивает к снимку `report/stock/all` (движения `Initial` «МС-выравнивание») →
расхождений 0 кроме живой дельты. расхождений 0 кроме живой дельты.
2. **Долги — на уровне КОНТРАГЕНТА** (SupplierDebts переписан): Σприёмок − Σвозвратов − Σисходящих 2. **Долги — на уровне КОНТРАГЕНТА** (SupplierDebts переписан): Σприёмок − Σвозвратов − Σисходящих

View file

@ -173,6 +173,21 @@ public int ProductCount()
return r.Read() ? Map(r) : null; return r.Read() ? Map(r) : null;
} }
/// <summary>Все товары с таким штрихкодом (ШК не уникален — «минимум как МойСклад»).
/// Если вернулось &gt;1 — касса показывает окно выбора товара.</summary>
public IReadOnlyList<PosProduct> FindAllByBarcode(string code)
{
var lookup = OnlyDigitsOrRaw(code);
using var cmd = _cn.CreateCommand();
cmd.CommandText = @"SELECT p.* FROM products p JOIN barcodes b ON b.ProductId=p.Id
WHERE b.Code=$c AND p.IsArchived=0";
Bind(cmd, "$c", lookup);
using var r = cmd.ExecuteReader();
var list = new List<PosProduct>();
while (r.Read()) list.Add(Map(r));
return list;
}
/// <summary>Избранные товары для плиток «быстрых товаров» на экране продажи.</summary> /// <summary>Избранные товары для плиток «быстрых товаров» на экране продажи.</summary>
public IReadOnlyList<PosProduct> GetFavorites(int limit = 12) public IReadOnlyList<PosProduct> GetFavorites(int limit = 12)
{ {

View file

@ -531,6 +531,12 @@ public partial class SaleViewModel : ViewModelBase
[ObservableProperty] private bool _favoritesOpen = true; [ObservableProperty] private bool _favoritesOpen = true;
public bool HasFavorites => Favorites.Count > 0; public bool HasFavorites => Favorites.Count > 0;
// Выбор товара при сканировании ШК, который есть у НЕСКОЛЬКИХ карточек
// («минимум как МойСклад»: дубли ШК допустимы). Касса показывает окно выбора.
public ObservableCollection<PosProduct> ScanCandidates { get; } = new();
[ObservableProperty] private bool _isScanPickerOpen;
[ObservableProperty] private string _scanPickerCode = "";
[ObservableProperty] private string _scanInput = ""; [ObservableProperty] private string _scanInput = "";
[ObservableProperty] private string _shiftNumber = "С-2026-000001"; [ObservableProperty] private string _shiftNumber = "С-2026-000001";
[ObservableProperty] private string _message = "Отсканируйте товар или введите штрихкод"; [ObservableProperty] private string _message = "Отсканируйте товар или введите штрихкод";
@ -677,8 +683,21 @@ private async Task Scan()
if (raw.Length == 0) return; if (raw.Length == 0) return;
// Разбор этикеток по настройкам организации (по умолчанию ВЫКЛ → точный код). // Разбор этикеток по настройкам организации (по умолчанию ВЫКЛ → точный код).
var parsed = BarcodeParser.Parse(raw, _svc?.WeightedSettings); var parsed = BarcodeParser.Parse(raw, _svc?.WeightedSettings);
// ШК может быть у НЕСКОЛЬКИХ карточек (дубли допустимы). Если так — окно выбора.
var matches = _svc?.Store.FindAllByBarcode(raw.Trim());
if (matches is null || matches.Count == 0) matches = _svc?.Store.FindAllByBarcode(parsed.Lookup);
if (matches is { Count: > 1 })
{
ScanCandidates.Clear();
foreach (var m in matches) ScanCandidates.Add(m);
ScanPickerCode = parsed.Lookup;
IsScanPickerOpen = true;
Message = $"Штрихкод {parsed.Lookup}: несколько товаров — выберите";
return;
}
// Сначала — точное совпадение штрихкода; этикеточный PLU как запасной путь. // Сначала — точное совпадение штрихкода; этикеточный PLU как запасной путь.
var p = _svc?.Store.FindByBarcode(raw.Trim()) var p = (matches is { Count: 1 } ? matches[0] : null)
?? _svc?.Store.FindByBarcode(raw.Trim())
?? _svc?.Store.FindByBarcode(parsed.Lookup); ?? _svc?.Store.FindByBarcode(parsed.Lookup);
if (p is null) if (p is null)
{ {
@ -708,6 +727,17 @@ private async Task Scan()
[RelayCommand] private void QuickCreateCancel() { IsQuickCreateOpen = false; QuickCreateName = ""; QuickCreatePrice = null; } [RelayCommand] private void QuickCreateCancel() { IsQuickCreateOpen = false; QuickCreateName = ""; QuickCreatePrice = null; }
/// <summary>Выбор товара из окна выбора (ШК у нескольких карточек).</summary>
[RelayCommand] private async Task PickScanCandidate(PosProduct? p)
{
IsScanPickerOpen = false; ScanCandidates.Clear();
if (p is null) return;
if (p.IsWeighed) { await AddWeighedAsync(p); return; }
AddProduct(p, 1m);
}
[RelayCommand] private void ScanPickerCancel() { IsScanPickerOpen = false; ScanCandidates.Clear(); }
/// <summary>Создать товар по неизвестному штрихкоду на сервере и сразу добавить в чек.</summary> /// <summary>Создать товар по неизвестному штрихкоду на сервере и сразу добавить в чек.</summary>
[RelayCommand] [RelayCommand]
private async Task QuickCreateConfirmAsync() private async Task QuickCreateConfirmAsync()

View file

@ -238,6 +238,34 @@
</Border> </Border>
</Border> </Border>
<!-- Выбор товара: один штрихкод у НЕСКОЛЬКИХ карточек (дубли ШК — «минимум как МойСклад») -->
<Border Grid.ColumnSpan="2" Background="#99000000" IsVisible="{Binding IsScanPickerOpen}">
<Border Width="520" MaxHeight="540" Background="{DynamicResource PosCard}" CornerRadius="14" Padding="24"
VerticalAlignment="Center" HorizontalAlignment="Center" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="Выберите товар" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource PosText}"/>
<TextBlock Text="{Binding ScanPickerCode, StringFormat='Штрихкод {0} — несколько карточек'}" Foreground="{DynamicResource PosTextMuted}"/>
<ScrollViewer MaxHeight="380">
<ItemsControl ItemsSource="{Binding ScanCandidates}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="0,0,0,8" Padding="14,12" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="10"
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).PickScanCandidateCommand}" CommandParameter="{Binding}">
<StackPanel>
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource PosText}"/>
<TextBlock Text="{Binding PriceText}" FontSize="14" Foreground="{DynamicResource PosBrandDark}"/>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button Content="Отмена" Padding="18,10" HorizontalAlignment="Right" Command="{Binding ScanPickerCancelCommand}"/>
</StackPanel>
</Border>
</Border>
<!-- Ручной ввод веса для весового товара (весов нет/не ответили) --> <!-- Ручной ввод веса для весового товара (весов нет/не ответили) -->
<Border Grid.ColumnSpan="2" Background="#99000000" IsVisible="{Binding IsWeightEntryOpen}"> <Border Grid.ColumnSpan="2" Background="#99000000" IsVisible="{Binding IsWeightEntryOpen}">
<Border Width="440" Background="{DynamicResource PosCard}" CornerRadius="14" Padding="24" <Border Width="440" Background="{DynamicResource PosCard}" CornerRadius="14" Padding="24"