feat(products): авто-генерация числового артикула при создании
При 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
9c70de9b3d
commit
6468186ed5
|
|
@ -23,6 +23,23 @@ public ProductsController(AppDbContext db, ITenantContext 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).
|
||||
private async Task<decimal> ResolveDefaultVatAsync(CancellationToken ct)
|
||||
{
|
||||
|
|
@ -124,6 +141,8 @@ public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input
|
|||
Apply(e, input);
|
||||
// Если UI скрывает поле Vat (showVatEnabledOnProduct=false) и прислал null — дефолт из страны.
|
||||
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 ?? [])
|
||||
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 });
|
||||
|
||||
_db.Products.Add(e);
|
||||
try
|
||||
{
|
||||
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);
|
||||
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 ?? [])
|
||||
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = pr.Amount, CurrencyId = pr.CurrencyId });
|
||||
|
||||
try
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue