feat(purchases): создать возврат поставщику на основании приёмки (п.5 слайс)
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
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
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
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
POST /api/purchases/supplier-returns/from-supply/{supplyId}: черновик возврата с
преднабором строк из проведённой приёмки (товар/кол-во/цена), поставщик/склад/валюта
и ссылка ReferenceSupplyId. Integration-тест: преднабор + связь.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ddf7918eb1
commit
ad2e3b3117
|
|
@ -25,7 +25,7 @@
|
||||||
- Быстрое создание товара из приёмки/кассы по неизвестному штрихкоду (1 поле + цена).
|
- Быстрое создание товара из приёмки/кассы по неизвестному штрихкоду (1 поле + цена).
|
||||||
- Недостающее ловит фоновая «Проверка данных», а не запрет сохранения.
|
- Недостающее ловит фоновая «Проверка данных», а не запрет сохранения.
|
||||||
5. [~] **«Создать на основании» + связи** — частично: инвентаризация→опр/спис (есть), продажа→возврат
|
5. [~] **«Создать на основании» + связи** — частично: инвентаризация→опр/спис (есть), продажа→возврат
|
||||||
(create-return, ✅), приёмка→оплата (есть). **Осталось:** приёмка→возврат поставщику с преднабором;
|
(create-return, ✅), приёмка→оплата (есть). **Осталось:** приёмка→возврат поставщику с преднабором (from-supply ✅, 0ad);
|
||||||
единый блок «Связанные документы»/«На основании …» во всех документах.
|
единый блок «Связанные документы»/«На основании …» во всех документах.
|
||||||
6. [x] **Простой режим**: Оприходование/Списание уже отдельные пункты меню для Admin/Кладовщика вне
|
6. [x] **Простой режим**: Оприходование/Списание уже отдельные пункты меню для Admin/Кладовщика вне
|
||||||
зависимости от режима (AppLayout.tsx:111-112 — НЕ за advanced-гейтом). Проверено, ничего не прячем.
|
зависимости от режима (AppLayout.tsx:111-112 — НЕ за advanced-гейтом). Проверено, ничего не прячем.
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,44 @@ public async Task<ActionResult<SupplierReturnDto>> Get(Guid id, CancellationToke
|
||||||
return dto is null ? NotFound() : Ok(dto);
|
return dto is null ? NotFound() : Ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>«Создать на основании» приёмки: черновик возврата поставщику с
|
||||||
|
/// преднабором строк из проведённой приёмки (товар/кол-во/цена), поставщик/
|
||||||
|
/// склад/валюта и ссылка на приёмку. Кассир/закупщик лишь правит количества.</summary>
|
||||||
|
[HttpPost("from-supply/{supplyId:guid}"), RequiresPermission("SuppliesEdit")]
|
||||||
|
public async Task<ActionResult<SupplierReturnDto>> CreateFromSupply(Guid supplyId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var supply = await _db.Supplies.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == supplyId, ct);
|
||||||
|
if (supply is null) return NotFound();
|
||||||
|
if (supply.Status != SupplyStatus.Posted)
|
||||||
|
return BadRequest(new { error = "Создать возврат можно только из проведённой приёмки." });
|
||||||
|
if (supply.Lines.Count == 0)
|
||||||
|
return BadRequest(new { error = "В приёмке нет позиций." });
|
||||||
|
|
||||||
|
var number = await GenerateNumberAsync(DateTime.UtcNow, ct);
|
||||||
|
var r = new SupplierReturn
|
||||||
|
{
|
||||||
|
Number = number,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
Status = SupplierReturnStatus.Draft,
|
||||||
|
SupplierId = supply.SupplierId,
|
||||||
|
StoreId = supply.StoreId,
|
||||||
|
CurrencyId = supply.CurrencyId,
|
||||||
|
ReferenceSupplyId = supply.Id,
|
||||||
|
Notes = $"Возврат по приёмке {supply.Number}",
|
||||||
|
};
|
||||||
|
var order = 0;
|
||||||
|
foreach (var l in supply.Lines)
|
||||||
|
r.Lines.Add(new SupplierReturnLine
|
||||||
|
{
|
||||||
|
ProductId = l.ProductId, Quantity = l.Quantity, UnitPrice = l.UnitPrice,
|
||||||
|
LineTotal = l.Quantity * l.UnitPrice, SortOrder = order++,
|
||||||
|
});
|
||||||
|
r.Total = r.Lines.Sum(x => x.LineTotal);
|
||||||
|
_db.SupplierReturns.Add(r);
|
||||||
|
if (await SaveOrFkErrorAsync(ct) is { } err) return err;
|
||||||
|
return CreatedAtAction(nameof(Get), new { id = r.Id }, await GetInternal(r.Id, ct));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost, RequiresPermission("SuppliesEdit")]
|
[HttpPost, RequiresPermission("SuppliesEdit")]
|
||||||
public async Task<ActionResult<SupplierReturnDto>> Create([FromBody] SupplierReturnInput input, CancellationToken ct)
|
public async Task<ActionResult<SupplierReturnDto>> Create([FromBody] SupplierReturnInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using FluentAssertions;
|
||||||
|
using foodmarket.IntegrationTests.Support;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace foodmarket.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>Паритет п.5 (слайс): «создать на основании» — возврат поставщику из
|
||||||
|
/// проведённой приёмки с преднабором строк и ссылкой на приёмку.</summary>
|
||||||
|
[Collection(ApiCollection.Name)]
|
||||||
|
public class SupplierReturnFromSupplyTests
|
||||||
|
{
|
||||||
|
private readonly ApiFactory _factory;
|
||||||
|
public SupplierReturnFromSupplyTests(ApiFactory factory) => _factory = factory;
|
||||||
|
|
||||||
|
private static string RandomBarcode()
|
||||||
|
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateFromSupply_presets_lines_and_links_supply()
|
||||||
|
{
|
||||||
|
var api = new ApiActor(_factory.CreateClient());
|
||||||
|
await api.SignupAndLoginAsync($"spret-{Guid.NewGuid():N}");
|
||||||
|
var refs = await api.LoadRefsAsync();
|
||||||
|
var supplier = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}");
|
||||||
|
var product = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 500m, RandomBarcode());
|
||||||
|
var (supplyId, _) = await api.CreatePostedSupplyAsync(refs, supplier, product, 10m, 100m);
|
||||||
|
|
||||||
|
var resp = await api.Http.PostAsJsonAsync($"/api/purchases/supplier-returns/from-supply/{supplyId}", new { });
|
||||||
|
var body = await resp.Content.ReadAsStringAsync();
|
||||||
|
resp.StatusCode.Should().Be(HttpStatusCode.Created, body);
|
||||||
|
var r = JsonDocument.Parse(body).RootElement;
|
||||||
|
|
||||||
|
r.GetProperty("referenceSupplyId").GetString().Should().Be(supplyId);
|
||||||
|
var lines = r.GetProperty("lines").EnumerateArray().ToList();
|
||||||
|
lines.Should().ContainSingle(l => l.GetProperty("productId").GetString() == product);
|
||||||
|
lines.First().GetProperty("quantity").GetDecimal().Should().Be(10m);
|
||||||
|
lines.First().GetProperty("unitPrice").GetDecimal().Should().Be(100m);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue