feat(pos): окно выбора товара при скане ШК с несколькими карточками (дубли ШК)
- 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:
parent
30b39a7bcb
commit
b04d3f0d0c
|
|
@ -90,8 +90,9 @@ Part 1 (инвентаризация) и Part 3 (орг) — выполнены.
|
|||
дублем ШК (MergeByBarcode=false, dupeInInput не пропускает, артикул-дубль дизамбигуируется
|
||||
суффиксом msId). Перенос вернул **49 ранее слитых карточек** (каталог 30993 = МойСклад). Дубли
|
||||
ШК видны в «Проверке данных» → dup-barcode (чек уже есть). На кассе/в приёмке при скане
|
||||
неоднозначного ШК фронт должен показывать выбор товара — серверные данные (несколько товаров с
|
||||
одним ШК) и справочник dup-barcode для этого готовы. **Остатки пересчитаны**: см. раздел сверки —
|
||||
неоднозначного ШК показывается ОКНО ВЫБОРА товара: сервер `by-barcode` отдаёт список совпадений
|
||||
(цена/остаток), веб-приёмка (`SupplyLineQuickAdd`) и касса (POS `SaleView`/`FindAllByBarcode`) —
|
||||
показывают выбор. **Остатки пересчитаны**: см. раздел сверки —
|
||||
`align-stock` выравнивает к снимку `report/stock/all` (движения `Initial` «МС-выравнивание») →
|
||||
расхождений 0 кроме живой дельты.
|
||||
2. **Долги — на уровне КОНТРАГЕНТА** (SupplierDebts переписан): Σприёмок − Σвозвратов − Σисходящих
|
||||
|
|
|
|||
|
|
@ -173,6 +173,21 @@ public int ProductCount()
|
|||
return r.Read() ? Map(r) : null;
|
||||
}
|
||||
|
||||
/// <summary>Все товары с таким штрихкодом (ШК не уникален — «минимум как МойСклад»).
|
||||
/// Если вернулось >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>
|
||||
public IReadOnlyList<PosProduct> GetFavorites(int limit = 12)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -531,6 +531,12 @@ public partial class SaleViewModel : ViewModelBase
|
|||
[ObservableProperty] private bool _favoritesOpen = true;
|
||||
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 _shiftNumber = "С-2026-000001";
|
||||
[ObservableProperty] private string _message = "Отсканируйте товар или введите штрихкод";
|
||||
|
|
@ -677,8 +683,21 @@ private async Task Scan()
|
|||
if (raw.Length == 0) return;
|
||||
// Разбор этикеток по настройкам организации (по умолчанию ВЫКЛ → точный код).
|
||||
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 как запасной путь.
|
||||
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);
|
||||
if (p is null)
|
||||
{
|
||||
|
|
@ -708,6 +727,17 @@ private async Task Scan()
|
|||
|
||||
[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>
|
||||
[RelayCommand]
|
||||
private async Task QuickCreateConfirmAsync()
|
||||
|
|
|
|||
|
|
@ -238,6 +238,34 @@
|
|||
</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 Width="440" Background="{DynamicResource PosCard}" CornerRadius="14" Padding="24"
|
||||
|
|
|
|||
Loading…
Reference in a new issue