feat(products): авто-генерация числового артикула при создании
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 29s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 41s
Docker Images / Web image (push) Successful in 26s
Docker Images / Deploy stage (push) Successful in 18s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 29s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 41s
Docker Images / Web image (push) Successful in 26s
Docker Images / Deploy stage (push) Successful in 18s
При POST /api/catalog/products если Article пустой — сервер берёт max(Article::int) среди артикулов текущей организации и ставит +1. Если числовых артикулов нет — «1». Пользователь может указать артикул руками, тогда используется его значение. Конфликт по unique index IX_products_OrganizationId_Article на SaveChanges перехватывается — возвращается 400 с текстом «Артикул «X» уже занят в этой организации.» вместо 500. Та же обработка добавлена в Update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
08816c60ca
commit
bed30f68bd
|
|
@ -23,6 +23,23 @@ public ProductsController(AppDbContext db, ITenantContext tenant)
|
||||||
_tenant = tenant;
|
_tenant = tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Следующий числовой артикул для организации. Находит max(Article::int)
|
||||||
|
// среди артикулов, которые полностью состоят из цифр, и прибавляет 1.
|
||||||
|
// Если числовых артикулов нет — возвращает "1".
|
||||||
|
private async Task<string> GenerateNextArticleAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var articles = await _db.Products
|
||||||
|
.Where(p => p.Article != null && p.Article != "")
|
||||||
|
.Select(p => p.Article!)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
var next = 1;
|
||||||
|
foreach (var a in articles)
|
||||||
|
{
|
||||||
|
if (int.TryParse(a, out var n) && n >= next) next = n + 1;
|
||||||
|
}
|
||||||
|
return next.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
// Дефолт Vat для нового товара — из страны организации (Country.VatRate).
|
// Дефолт Vat для нового товара — из страны организации (Country.VatRate).
|
||||||
private async Task<decimal> ResolveDefaultVatAsync(CancellationToken ct)
|
private async Task<decimal> ResolveDefaultVatAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|
@ -124,6 +141,8 @@ public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input
|
||||||
Apply(e, input);
|
Apply(e, input);
|
||||||
// Если UI скрывает поле Vat (showVatEnabledOnProduct=false) и прислал null — дефолт из страны.
|
// Если UI скрывает поле Vat (showVatEnabledOnProduct=false) и прислал null — дефолт из страны.
|
||||||
if (input.Vat is null) e.Vat = await ResolveDefaultVatAsync(ct);
|
if (input.Vat is null) e.Vat = await ResolveDefaultVatAsync(ct);
|
||||||
|
// Авто-артикул: если пользователь не указал — генерируем числовой.
|
||||||
|
if (string.IsNullOrWhiteSpace(e.Article)) e.Article = await GenerateNextArticleAsync(ct);
|
||||||
|
|
||||||
foreach (var b in input.Barcodes ?? [])
|
foreach (var b in input.Barcodes ?? [])
|
||||||
e.Barcodes.Add(new ProductBarcode { Code = b.Code, Type = b.Type, IsPrimary = b.IsPrimary });
|
e.Barcodes.Add(new ProductBarcode { Code = b.Code, Type = b.Type, IsPrimary = b.IsPrimary });
|
||||||
|
|
@ -131,7 +150,14 @@ public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input
|
||||||
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId });
|
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId });
|
||||||
|
|
||||||
_db.Products.Add(e);
|
_db.Products.Add(e);
|
||||||
|
try
|
||||||
|
{
|
||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_products_OrganizationId_Article") == true)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = $"Артикул «{e.Article}» уже занят в этой организации." });
|
||||||
|
}
|
||||||
var dto = await GetInternalAsync(e.Id, ct);
|
var dto = await GetInternalAsync(e.Id, ct);
|
||||||
return CreatedAtAction(nameof(Get), new { id = e.Id }, dto);
|
return CreatedAtAction(nameof(Get), new { id = e.Id }, dto);
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +186,14 @@ public async Task<IActionResult> Update(Guid id, [FromBody] ProductInput input,
|
||||||
foreach (var pr in input.Prices ?? [])
|
foreach (var pr in input.Prices ?? [])
|
||||||
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId });
|
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_products_OrganizationId_Article") == true)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = $"Артикул «{e.Article}» уже занят в этой организации." });
|
||||||
|
}
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue