From 8fc9ef1a2e9e2edb8f55cc85f2e8ff262534b8e2 Mon Sep 17 00:00:00 2001 From: nurdotnet <278048682+nurdotnet@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:32:02 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20strict=20MoySklad=20schema=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D0=BF=D0=BB=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D1=8F=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20f7087e?= =?UTF-8?q?9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main расходился с БД стейджа (Phase2c3_MsStrict в history, но код ещё ссылался на VatRate etc.) — деплой ломался. Реплицирую удаление сущностей вручную, чтобы код совпадал с таблицами. Убрано (нет в MoySklad — не выдумываем): - Domain: VatRate сущность целиком; Counterparty.Kind + enum CounterpartyKind; Store.Kind + enum StoreKind; Product.IsAlcohol; UnitOfMeasure.Symbol/DecimalPlaces/IsBase. - EF: DbSet, ConfigureVatRate, Product.VatRate navigation, индекс Counterparty.Kind. - DTO/Input: соответствующие поля и VatRateDto/Input. - API: VatRatesController удалён; references в Products/Counterparties/Stores/UoM/Supplies/Retail/Stock. Добавлено как в MoySklad: - Product.Vat (int) + Product.VatEnabled — MoySklad держит НДС числом на товаре. - KZ default VAT 16% — applied в сидерах и в MoySkladImportService когда товар не принёс свой vat. MoySkladImportService: - ResolveKind убран; CompanyType=entrepreneur→Individual (как и было). - VatRates lookup → прямой p.Vat ?? 16 + p.Vat > 0 для VatEnabled. - baseUnit ищется по code="796" вместо IsBase. Web: - types.ts: убраны CounterpartyKind/StoreKind/VatRate/Product.vatRateId/vatPercent/isAlcohol/UoM.symbol/decimalPlaces/isBase; добавлено Product.vat/vatEnabled; унифицировано unitSymbol→unitName. - VatRatesPage удалён, роут из App.tsx тоже. - CounterpartiesPage/StoresPage/UnitsOfMeasurePage: убраны соответствующие поля в формах. - ProductEditPage: select "Ставка НДС" теперь с фиксированными 0/10/12/16/20 + чекбокс VatEnabled. - Stock/RetailSale/Supply pages: unitSymbol → unitName. deploy-stage unguarded — теперь код соответствует DB, авто-deploy безопасен. --- .claude/scheduled_tasks.lock | 1 + .forgejo/workflows/docker.yml | 7 -- .../Catalog/CounterpartiesController.cs | 12 +- .../Controllers/Catalog/ProductsController.cs | 11 +- .../Controllers/Catalog/StoresController.cs | 10 +- .../Catalog/UnitsOfMeasureController.cs | 28 ++--- .../Controllers/Catalog/VatRatesController.cs | 101 --------------- .../Controllers/Inventory/StockController.cs | 2 +- .../Purchases/SuppliesController.cs | 2 +- .../Sales/RetailSalesController.cs | 2 +- src/food-market.api/Seed/DemoCatalogSeeder.cs | 99 ++++++++------- src/food-market.api/Seed/DevDataSeeder.cs | 20 +-- .../Catalog/CatalogDtos.cs | 26 ++-- .../Catalog/Counterparty.cs | 1 - src/food-market.domain/Catalog/Enums.cs | 17 --- src/food-market.domain/Catalog/Product.cs | 7 +- src/food-market.domain/Catalog/Store.cs | 4 +- .../Catalog/UnitOfMeasure.cs | 6 +- src/food-market.domain/Catalog/VatRate.cs | 13 -- .../MoySklad/MoySkladImportService.cs | 30 ++--- .../Persistence/AppDbContext.cs | 1 - .../Configurations/CatalogConfigurations.cs | 13 +- src/food-market.web/src/App.tsx | 2 - .../src/components/ProductPicker.tsx | 2 +- src/food-market.web/src/lib/types.ts | 25 ++-- src/food-market.web/src/lib/useLookups.ts | 3 +- .../src/pages/CounterpartiesPage.tsx | 27 ++-- .../src/pages/ProductEditPage.tsx | 43 ++++--- .../src/pages/ProductsPage.tsx | 5 +- .../src/pages/RetailSaleEditPage.tsx | 10 +- src/food-market.web/src/pages/StockPage.tsx | 2 +- src/food-market.web/src/pages/StoresPage.tsx | 16 +-- .../src/pages/SupplyEditPage.tsx | 8 +- .../src/pages/UnitsOfMeasurePage.tsx | 37 ++---- .../src/pages/VatRatesPage.tsx | 116 ------------------ 35 files changed, 178 insertions(+), 531 deletions(-) create mode 100644 .claude/scheduled_tasks.lock delete mode 100644 src/food-market.api/Controllers/Catalog/VatRatesController.cs delete mode 100644 src/food-market.domain/Catalog/VatRate.cs delete mode 100644 src/food-market.web/src/pages/VatRatesPage.tsx diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..9df4eab --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"791848bb-ad06-4ccc-9c5a-13da4ee36524","pid":353363,"acquiredAt":1776883641374} \ No newline at end of file diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml index 53fa553..84c8e81 100644 --- a/.forgejo/workflows/docker.yml +++ b/.forgejo/workflows/docker.yml @@ -56,13 +56,6 @@ jobs: name: Deploy stage runs-on: [self-hosted, linux] needs: [api, web] - # Temporary guard: main currently contains references to VatRate etc. while - # the stage DB is already on Phase2c3_MsStrict (vat_rates dropped). Until - # the user lands the matching code changes on main, an auto-deploy of a - # freshly built image from main would break the API. Run deploy only on - # tag/dispatch; normal pushes still build + push images to the local - # registry, but don't roll the stage forward. - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v4 diff --git a/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs b/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs index f3040a2..217cf6f 100644 --- a/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs +++ b/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs @@ -20,14 +20,9 @@ public class CounterpartiesController : ControllerBase [HttpGet] public async Task>> List( [FromQuery] PagedRequest req, - [FromQuery] CounterpartyKind? kind, CancellationToken ct) { var q = _db.Counterparties.Include(c => c.Country).AsNoTracking().AsQueryable(); - if (kind is not null) - { - q = q.Where(c => c.Kind == kind || c.Kind == CounterpartyKind.Both); - } if (!string.IsNullOrWhiteSpace(req.Search)) { var s = req.Search.Trim().ToLower(); @@ -43,7 +38,7 @@ public class CounterpartiesController : ControllerBase .OrderBy(c => c.Name) .Skip(req.Skip).Take(req.Take) .Select(c => new CounterpartyDto( - c.Id, c.Name, c.LegalName, c.Kind, c.Type, + c.Id, c.Name, c.LegalName, c.Type, c.Bin, c.Iin, c.TaxNumber, c.CountryId, c.Country != null ? c.Country.Name : null, c.Address, c.Phone, c.Email, c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes, c.IsActive)) @@ -56,7 +51,7 @@ public async Task> Get(Guid id, CancellationToken { var c = await _db.Counterparties.Include(x => x.Country).AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct); return c is null ? NotFound() : new CounterpartyDto( - c.Id, c.Name, c.LegalName, c.Kind, c.Type, + c.Id, c.Name, c.LegalName, c.Type, c.Bin, c.Iin, c.TaxNumber, c.CountryId, c.Country?.Name, c.Address, c.Phone, c.Email, c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes, c.IsActive); @@ -95,7 +90,6 @@ private static Counterparty Apply(Counterparty e, CounterpartyInput i) { e.Name = i.Name; e.LegalName = i.LegalName; - e.Kind = i.Kind; e.Type = i.Type; e.Bin = i.Bin; e.Iin = i.Iin; @@ -117,7 +111,7 @@ private async Task ProjectAsync(Guid id, CancellationToken ct) { var c = await _db.Counterparties.Include(x => x.Country).AsNoTracking().FirstAsync(x => x.Id == id, ct); return new CounterpartyDto( - c.Id, c.Name, c.LegalName, c.Kind, c.Type, + c.Id, c.Name, c.LegalName, c.Type, c.Bin, c.Iin, c.TaxNumber, c.CountryId, c.Country?.Name, c.Address, c.Phone, c.Email, c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes, c.IsActive); diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs index d6b2d73..0972da3 100644 --- a/src/food-market.api/Controllers/Catalog/ProductsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs @@ -111,7 +111,6 @@ public async Task Delete(Guid id, CancellationToken ct) private IQueryable QueryIncludes() => _db.Products .Include(p => p.UnitOfMeasure) - .Include(p => p.VatRate) .Include(p => p.ProductGroup) .Include(p => p.DefaultSupplier) .Include(p => p.CountryOfOrigin) @@ -126,12 +125,12 @@ public async Task Delete(Guid id, CancellationToken ct) private static readonly System.Linq.Expressions.Expression> Projection = p => new ProductDto( p.Id, p.Name, p.Article, p.Description, - p.UnitOfMeasureId, p.UnitOfMeasure!.Symbol, - p.VatRateId, p.VatRate!.Percent, + p.UnitOfMeasureId, p.UnitOfMeasure!.Name, + p.Vat, p.VatEnabled, p.ProductGroupId, p.ProductGroup != null ? p.ProductGroup.Name : null, p.DefaultSupplierId, p.DefaultSupplier != null ? p.DefaultSupplier.Name : null, p.CountryOfOriginId, p.CountryOfOrigin != null ? p.CountryOfOrigin.Name : null, - p.IsService, p.IsWeighed, p.IsAlcohol, p.IsMarked, + p.IsService, p.IsWeighed, p.IsMarked, p.MinStock, p.MaxStock, p.PurchasePrice, p.PurchaseCurrencyId, p.PurchaseCurrency != null ? p.PurchaseCurrency.Code : null, p.ImageUrl, p.IsActive, @@ -144,13 +143,13 @@ private static void Apply(Product e, ProductInput i) e.Article = i.Article; e.Description = i.Description; e.UnitOfMeasureId = i.UnitOfMeasureId; - e.VatRateId = i.VatRateId; + e.Vat = i.Vat; + e.VatEnabled = i.VatEnabled; e.ProductGroupId = i.ProductGroupId; e.DefaultSupplierId = i.DefaultSupplierId; e.CountryOfOriginId = i.CountryOfOriginId; e.IsService = i.IsService; e.IsWeighed = i.IsWeighed; - e.IsAlcohol = i.IsAlcohol; e.IsMarked = i.IsMarked; e.MinStock = i.MinStock; e.MaxStock = i.MaxStock; diff --git a/src/food-market.api/Controllers/Catalog/StoresController.cs b/src/food-market.api/Controllers/Catalog/StoresController.cs index 8e638f6..54bb980 100644 --- a/src/food-market.api/Controllers/Catalog/StoresController.cs +++ b/src/food-market.api/Controllers/Catalog/StoresController.cs @@ -30,7 +30,7 @@ public async Task>> List([FromQuery] PagedReq var items = await q .OrderByDescending(x => x.IsMain).ThenBy(x => x.Name) .Skip(req.Skip).Take(req.Take) - .Select(x => new StoreDto(x.Id, x.Name, x.Code, x.Kind, x.Address, x.Phone, x.ManagerName, x.IsMain, x.IsActive)) + .Select(x => new StoreDto(x.Id, x.Name, x.Code, x.Address, x.Phone, x.ManagerName, x.IsMain, x.IsActive)) .ToListAsync(ct); return new PagedResult { Items = items, Total = total, Page = req.Page, PageSize = req.Take }; } @@ -39,7 +39,7 @@ public async Task>> List([FromQuery] PagedReq public async Task> Get(Guid id, CancellationToken ct) { var x = await _db.Stores.AsNoTracking().FirstOrDefaultAsync(s => s.Id == id, ct); - return x is null ? NotFound() : new StoreDto(x.Id, x.Name, x.Code, x.Kind, x.Address, x.Phone, x.ManagerName, x.IsMain, x.IsActive); + return x is null ? NotFound() : new StoreDto(x.Id, x.Name, x.Code, x.Address, x.Phone, x.ManagerName, x.IsMain, x.IsActive); } [HttpPost, Authorize(Roles = "Admin,Manager")] @@ -51,13 +51,13 @@ public async Task> Create([FromBody] StoreInput input, Ca } var e = new Store { - Name = input.Name, Code = input.Code, Kind = input.Kind, Address = input.Address, + Name = input.Name, Code = input.Code,Address = input.Address, Phone = input.Phone, ManagerName = input.ManagerName, IsMain = input.IsMain, IsActive = input.IsActive, }; _db.Stores.Add(e); await _db.SaveChangesAsync(ct); return CreatedAtAction(nameof(Get), new { id = e.Id }, - new StoreDto(e.Id, e.Name, e.Code, e.Kind, e.Address, e.Phone, e.ManagerName, e.IsMain, e.IsActive)); + new StoreDto(e.Id, e.Name, e.Code, e.Address, e.Phone, e.ManagerName, e.IsMain, e.IsActive)); } [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")] @@ -71,7 +71,7 @@ public async Task Update(Guid id, [FromBody] StoreInput input, Ca } e.Name = input.Name; e.Code = input.Code; - e.Kind = input.Kind; + e.Address = input.Address; e.Phone = input.Phone; e.ManagerName = input.ManagerName; diff --git a/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs b/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs index 63881ab..58bac55 100644 --- a/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs +++ b/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs @@ -24,13 +24,13 @@ public async Task>> List([FromQuery] if (!string.IsNullOrWhiteSpace(req.Search)) { var s = req.Search.Trim().ToLower(); - q = q.Where(u => u.Name.ToLower().Contains(s) || u.Symbol.ToLower().Contains(s) || u.Code.ToLower().Contains(s)); + q = q.Where(u => u.Name.ToLower().Contains(s) || u.Code.ToLower().Contains(s)); } var total = await q.CountAsync(ct); var items = await q - .OrderByDescending(u => u.IsBase).ThenBy(u => u.Name) + .OrderBy(u => u.Name) .Skip(req.Skip).Take(req.Take) - .Select(u => new UnitOfMeasureDto(u.Id, u.Code, u.Symbol, u.Name, u.DecimalPlaces, u.IsBase, u.IsActive)) + .Select(u => new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.Description, u.IsActive)) .ToListAsync(ct); return new PagedResult { Items = items, Total = total, Page = req.Page, PageSize = req.Take }; } @@ -39,30 +39,23 @@ public async Task>> List([FromQuery] public async Task> Get(Guid id, CancellationToken ct) { var u = await _db.UnitsOfMeasure.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct); - return u is null ? NotFound() : new UnitOfMeasureDto(u.Id, u.Code, u.Symbol, u.Name, u.DecimalPlaces, u.IsBase, u.IsActive); + return u is null ? NotFound() : new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.Description, u.IsActive); } [HttpPost, Authorize(Roles = "Admin,Manager")] public async Task> Create([FromBody] UnitOfMeasureInput input, CancellationToken ct) { - if (input.IsBase) - { - await _db.UnitsOfMeasure.Where(u => u.IsBase).ExecuteUpdateAsync(s => s.SetProperty(u => u.IsBase, false), ct); - } - var e = new UnitOfMeasure { Code = input.Code, - Symbol = input.Symbol, Name = input.Name, - DecimalPlaces = input.DecimalPlaces, - IsBase = input.IsBase, + Description = input.Description, IsActive = input.IsActive, }; _db.UnitsOfMeasure.Add(e); await _db.SaveChangesAsync(ct); return CreatedAtAction(nameof(Get), new { id = e.Id }, - new UnitOfMeasureDto(e.Id, e.Code, e.Symbol, e.Name, e.DecimalPlaces, e.IsBase, e.IsActive)); + new UnitOfMeasureDto(e.Id, e.Code, e.Name, e.Description, e.IsActive)); } [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")] @@ -71,16 +64,9 @@ public async Task Update(Guid id, [FromBody] UnitOfMeasureInput i var e = await _db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == id, ct); if (e is null) return NotFound(); - if (input.IsBase && !e.IsBase) - { - await _db.UnitsOfMeasure.Where(u => u.IsBase && u.Id != id).ExecuteUpdateAsync(s => s.SetProperty(u => u.IsBase, false), ct); - } - e.Code = input.Code; - e.Symbol = input.Symbol; e.Name = input.Name; - e.DecimalPlaces = input.DecimalPlaces; - e.IsBase = input.IsBase; + e.Description = input.Description; e.IsActive = input.IsActive; await _db.SaveChangesAsync(ct); return NoContent(); diff --git a/src/food-market.api/Controllers/Catalog/VatRatesController.cs b/src/food-market.api/Controllers/Catalog/VatRatesController.cs deleted file mode 100644 index d72b01b..0000000 --- a/src/food-market.api/Controllers/Catalog/VatRatesController.cs +++ /dev/null @@ -1,101 +0,0 @@ -using foodmarket.Application.Catalog; -using foodmarket.Application.Common; -using foodmarket.Domain.Catalog; -using foodmarket.Infrastructure.Persistence; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace foodmarket.Api.Controllers.Catalog; - -[ApiController] -[Authorize] -[Route("api/catalog/vat-rates")] -public class VatRatesController : ControllerBase -{ - private readonly AppDbContext _db; - - public VatRatesController(AppDbContext db) => _db = db; - - [HttpGet] - public async Task>> List([FromQuery] PagedRequest req, CancellationToken ct) - { - var q = _db.VatRates.AsNoTracking().AsQueryable(); - if (!string.IsNullOrWhiteSpace(req.Search)) - { - var s = req.Search.Trim().ToLower(); - q = q.Where(v => v.Name.ToLower().Contains(s)); - } - var total = await q.CountAsync(ct); - var items = await q - .OrderByDescending(v => v.IsDefault).ThenBy(v => v.Percent) - .Skip(req.Skip).Take(req.Take) - .Select(v => new VatRateDto(v.Id, v.Name, v.Percent, v.IsIncludedInPrice, v.IsDefault, v.IsActive)) - .ToListAsync(ct); - return new PagedResult { Items = items, Total = total, Page = req.Page, PageSize = req.Take }; - } - - [HttpGet("{id:guid}")] - public async Task> Get(Guid id, CancellationToken ct) - { - var v = await _db.VatRates.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct); - return v is null ? NotFound() : new VatRateDto(v.Id, v.Name, v.Percent, v.IsIncludedInPrice, v.IsDefault, v.IsActive); - } - - [HttpPost, Authorize(Roles = "Admin,Manager")] - public async Task> Create([FromBody] VatRateInput input, CancellationToken ct) - { - if (input.IsDefault) - { - await ResetDefaultsAsync(ct); - } - - var e = new VatRate - { - Name = input.Name, - Percent = input.Percent, - IsIncludedInPrice = input.IsIncludedInPrice, - IsDefault = input.IsDefault, - IsActive = input.IsActive, - }; - _db.VatRates.Add(e); - await _db.SaveChangesAsync(ct); - return CreatedAtAction(nameof(Get), new { id = e.Id }, - new VatRateDto(e.Id, e.Name, e.Percent, e.IsIncludedInPrice, e.IsDefault, e.IsActive)); - } - - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")] - public async Task Update(Guid id, [FromBody] VatRateInput input, CancellationToken ct) - { - var e = await _db.VatRates.FirstOrDefaultAsync(x => x.Id == id, ct); - if (e is null) return NotFound(); - - if (input.IsDefault && !e.IsDefault) - { - await ResetDefaultsAsync(ct); - } - - e.Name = input.Name; - e.Percent = input.Percent; - e.IsIncludedInPrice = input.IsIncludedInPrice; - e.IsDefault = input.IsDefault; - e.IsActive = input.IsActive; - await _db.SaveChangesAsync(ct); - return NoContent(); - } - - [HttpDelete("{id:guid}"), Authorize(Roles = "Admin")] - public async Task Delete(Guid id, CancellationToken ct) - { - var e = await _db.VatRates.FirstOrDefaultAsync(x => x.Id == id, ct); - if (e is null) return NotFound(); - _db.VatRates.Remove(e); - await _db.SaveChangesAsync(ct); - return NoContent(); - } - - private async Task ResetDefaultsAsync(CancellationToken ct) - { - await _db.VatRates.Where(v => v.IsDefault).ExecuteUpdateAsync(s => s.SetProperty(v => v.IsDefault, false), ct); - } -} diff --git a/src/food-market.api/Controllers/Inventory/StockController.cs b/src/food-market.api/Controllers/Inventory/StockController.cs index 6cc550b..d798bcf 100644 --- a/src/food-market.api/Controllers/Inventory/StockController.cs +++ b/src/food-market.api/Controllers/Inventory/StockController.cs @@ -50,7 +50,7 @@ public record StockRow( .OrderBy(x => x.p.Name) .Skip((page - 1) * pageSize).Take(pageSize) .Select(x => new StockRow( - x.p.Id, x.p.Name, x.p.Article, x.u.Symbol, + x.p.Id, x.p.Name, x.p.Article, x.u.Name, x.st.Id, x.st.Name, x.s.Quantity, x.s.ReservedQuantity, x.s.Quantity - x.s.ReservedQuantity)) .ToListAsync(ct); diff --git a/src/food-market.api/Controllers/Purchases/SuppliesController.cs b/src/food-market.api/Controllers/Purchases/SuppliesController.cs index 94ec84c..85ca766 100644 --- a/src/food-market.api/Controllers/Purchases/SuppliesController.cs +++ b/src/food-market.api/Controllers/Purchases/SuppliesController.cs @@ -276,7 +276,7 @@ private async Task GenerateNumberAsync(DateTime date, CancellationToken where l.SupplyId == id orderby l.SortOrder select new SupplyLineDto( - l.Id, l.ProductId, p.Name, p.Article, u.Symbol, + l.Id, l.ProductId, p.Name, p.Article, u.Name, l.Quantity, l.UnitPrice, l.LineTotal, l.SortOrder)) .ToListAsync(ct); diff --git a/src/food-market.api/Controllers/Sales/RetailSalesController.cs b/src/food-market.api/Controllers/Sales/RetailSalesController.cs index 17bec60..3631ed1 100644 --- a/src/food-market.api/Controllers/Sales/RetailSalesController.cs +++ b/src/food-market.api/Controllers/Sales/RetailSalesController.cs @@ -352,7 +352,7 @@ private async Task GenerateNumberAsync(DateTime date, CancellationToken where l.RetailSaleId == id orderby l.SortOrder select new RetailSaleLineDto( - l.Id, l.ProductId, p.Name, p.Article, u.Symbol, + l.Id, l.ProductId, p.Name, p.Article, u.Name, l.Quantity, l.UnitPrice, l.Discount, l.LineTotal, l.VatPercent, l.SortOrder)) .ToListAsync(ct); diff --git a/src/food-market.api/Seed/DemoCatalogSeeder.cs b/src/food-market.api/Seed/DemoCatalogSeeder.cs index e6fabdd..178db6d 100644 --- a/src/food-market.api/Seed/DemoCatalogSeeder.cs +++ b/src/food-market.api/Seed/DemoCatalogSeeder.cs @@ -32,21 +32,18 @@ public async Task StartAsync(CancellationToken ct) var hasProducts = await db.Products.IgnoreQueryFilters().AnyAsync(p => p.OrganizationId == orgId, ct); if (hasProducts) return; - var defaultVat = await db.VatRates.IgnoreQueryFilters() - .FirstOrDefaultAsync(v => v.OrganizationId == orgId && v.IsDefault, ct); - var noVat = await db.VatRates.IgnoreQueryFilters() - .FirstOrDefaultAsync(v => v.OrganizationId == orgId && v.Percent == 0m, ct); + // KZ default VAT is 16% (applies as int on Product). + const int vatDefault = 16; + const int vat0 = 0; var unitSht = await db.UnitsOfMeasure.IgnoreQueryFilters() - .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Symbol == "шт", ct); + .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Code == "796", ct); var unitKg = await db.UnitsOfMeasure.IgnoreQueryFilters() - .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Symbol == "кг", ct); + .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Code == "166", ct); var unitL = await db.UnitsOfMeasure.IgnoreQueryFilters() - .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Symbol == "л", ct); + .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Code == "112", ct); - if (defaultVat is null || unitSht is null) return; - var vat = defaultVat.Id; - var vat0 = noVat?.Id ?? vat; + if (unitSht is null) return; var retailPriceType = await db.PriceTypes.IgnoreQueryFilters() .FirstOrDefaultAsync(p => p.OrganizationId == orgId && p.IsRetail, ct); @@ -88,7 +85,7 @@ Guid AddGroup(string name, Guid? parentId) var supplier1 = new Counterparty { OrganizationId = orgId, Name = "ТОО «Продтрейд»", LegalName = "Товарищество с ограниченной ответственностью «Продтрейд»", - Kind = CounterpartyKind.Supplier, Type = CounterpartyType.LegalEntity, + Type = CounterpartyType.LegalEntity, Bin = "100140005678", CountryId = kz?.Id, Address = "Алматы, ул. Абая 15", Phone = "+7 (727) 100-00-01", Email = "order@prodtrade.kz", BankName = "Kaspi Bank", BankAccount = "KZ000000000000000001", Bik = "CASPKZKA", @@ -97,7 +94,7 @@ Guid AddGroup(string name, Guid? parentId) var supplier2 = new Counterparty { OrganizationId = orgId, Name = "ИП Иванов А.С.", - Kind = CounterpartyKind.Supplier, Type = CounterpartyType.Individual, + Type = CounterpartyType.Individual, Iin = "850101300000", CountryId = kz?.Id, Phone = "+7 (777) 100-00-02", ContactPerson = "Иванов Алексей", IsActive = true, @@ -106,49 +103,49 @@ Guid AddGroup(string name, Guid? parentId) // Barcodes use generic internal range (2xxxxxxxxxxxx) to avoid colliding with real products. // When user does real приёмка, real barcodes will overwrite. - var demo = new (string Name, Guid Group, decimal RetailPrice, string Article, string Barcode, Guid? Country, bool IsWeighed, Guid Unit, bool IsAlcohol)[] + var demo = new (string Name, Guid Group, decimal RetailPrice, string Article, string Barcode, Guid? Country, bool IsWeighed, Guid Unit)[] { // Напитки — безалкогольные - ("Вода питьевая «Тассай» 0.5л", gDrinksNon, 220, "DR-WAT-001", "2000000000018", kz?.Id, false, unitSht.Id, false), - ("Вода питьевая «Тассай» 1.5л", gDrinksNon, 380, "DR-WAT-002", "2000000000025", kz?.Id, false, unitSht.Id, false), - ("Сок «Да-Да» яблоко 1л", gDrinksNon, 650, "DR-JUC-001", "2000000000032", kz?.Id, false, unitSht.Id, false), - ("Сок «Да-Да» апельсин 1л", gDrinksNon, 650, "DR-JUC-002", "2000000000049", kz?.Id, false, unitSht.Id, false), - ("Кока-кола 0.5л", gDrinksNon, 420, "DR-SOD-001", "2000000000056", ru?.Id, false, unitSht.Id, false), - ("Пепси 0.5л", gDrinksNon, 420, "DR-SOD-002", "2000000000063", ru?.Id, false, unitSht.Id, false), - ("Спрайт 0.5л", gDrinksNon, 420, "DR-SOD-003", "2000000000070", ru?.Id, false, unitSht.Id, false), - ("Чай чёрный «Пиала» пакетированный, 100шт", gDrinksNon, 1250, "DR-TEA-001", "2000000000087", kz?.Id, false, unitSht.Id, false), + ("Вода питьевая «Тассай» 0.5л", gDrinksNon, 220, "DR-WAT-001", "2000000000018", kz?.Id, false, unitSht.Id), + ("Вода питьевая «Тассай» 1.5л", gDrinksNon, 380, "DR-WAT-002", "2000000000025", kz?.Id, false, unitSht.Id), + ("Сок «Да-Да» яблоко 1л", gDrinksNon, 650, "DR-JUC-001", "2000000000032", kz?.Id, false, unitSht.Id), + ("Сок «Да-Да» апельсин 1л", gDrinksNon, 650, "DR-JUC-002", "2000000000049", kz?.Id, false, unitSht.Id), + ("Кока-кола 0.5л", gDrinksNon, 420, "DR-SOD-001", "2000000000056", ru?.Id, false, unitSht.Id), + ("Пепси 0.5л", gDrinksNon, 420, "DR-SOD-002", "2000000000063", ru?.Id, false, unitSht.Id), + ("Спрайт 0.5л", gDrinksNon, 420, "DR-SOD-003", "2000000000070", ru?.Id, false, unitSht.Id), + ("Чай чёрный «Пиала» пакетированный, 100шт", gDrinksNon, 1250, "DR-TEA-001", "2000000000087", kz?.Id, false, unitSht.Id), // Молочные - ("Молоко «Простоквашино» 2.5% 930мл", gDairy, 720, "DAI-MLK-001", "2000000000094", ru?.Id, false, unitSht.Id, false), - ("Молоко «Food Master» 3.2% 1л", gDairy, 620, "DAI-MLK-002", "2000000000100", kz?.Id, false, unitSht.Id, false), - ("Кефир «Food Master» 2.5% 1л", gDairy, 590, "DAI-KEF-001", "2000000000117", kz?.Id, false, unitSht.Id, false), - ("Йогурт «Актимель» клубника 100мл", gDairy, 180, "DAI-YOG-001", "2000000000124", null, false, unitSht.Id, false), - ("Творог «Простоквашино» 5% 200г", gDairy, 590, "DAI-CRD-001", "2000000000131", ru?.Id, false, unitSht.Id, false), - ("Сметана «Food Master» 20% 400г", gDairy, 850, "DAI-SCR-001", "2000000000148", kz?.Id, false, unitSht.Id, false), - ("Сыр «Российский» 45%", gDairy, 3200, "DAI-CHE-001", "2000000000155", kz?.Id, true, unitKg?.Id ?? unitSht.Id, false), + ("Молоко «Простоквашино» 2.5% 930мл", gDairy, 720, "DAI-MLK-001", "2000000000094", ru?.Id, false, unitSht.Id), + ("Молоко «Food Master» 3.2% 1л", gDairy, 620, "DAI-MLK-002", "2000000000100", kz?.Id, false, unitSht.Id), + ("Кефир «Food Master» 2.5% 1л", gDairy, 590, "DAI-KEF-001", "2000000000117", kz?.Id, false, unitSht.Id), + ("Йогурт «Актимель» клубника 100мл", gDairy, 180, "DAI-YOG-001", "2000000000124", null, false, unitSht.Id), + ("Творог «Простоквашино» 5% 200г", gDairy, 590, "DAI-CRD-001", "2000000000131", ru?.Id, false, unitSht.Id), + ("Сметана «Food Master» 20% 400г", gDairy, 850, "DAI-SCR-001", "2000000000148", kz?.Id, false, unitSht.Id), + ("Сыр «Российский» 45%", gDairy, 3200, "DAI-CHE-001", "2000000000155", kz?.Id, true, unitKg?.Id ?? unitSht.Id), // Хлеб и выпечка - ("Хлеб «Семейный» нарезной 500г", gBakery, 180, "BAK-BRD-001", "2000000000162", kz?.Id, false, unitSht.Id, false), - ("Батон «Столичный» 400г", gBakery, 170, "BAK-BRD-002", "2000000000179", kz?.Id, false, unitSht.Id, false), - ("Лепёшка домашняя", gBakery, 220, "BAK-LEP-001", "2000000000186", kz?.Id, false, unitSht.Id, false), - ("Круассан шоколадный", gBakery, 320, "BAK-CRO-001", "2000000000193", kz?.Id, false, unitSht.Id, false), + ("Хлеб «Семейный» нарезной 500г", gBakery, 180, "BAK-BRD-001", "2000000000162", kz?.Id, false, unitSht.Id), + ("Батон «Столичный» 400г", gBakery, 170, "BAK-BRD-002", "2000000000179", kz?.Id, false, unitSht.Id), + ("Лепёшка домашняя", gBakery, 220, "BAK-LEP-001", "2000000000186", kz?.Id, false, unitSht.Id), + ("Круассан шоколадный", gBakery, 320, "BAK-CRO-001", "2000000000193", kz?.Id, false, unitSht.Id), // Кондитерские - ("Шоколад «Рахат» молочный 100г", gSweets, 650, "SW-CHO-001", "2000000000209", kz?.Id, false, unitSht.Id, false), - ("Шоколад «Алёнка» 100г", gSweets, 620, "SW-CHO-002", "2000000000216", ru?.Id, false, unitSht.Id, false), - ("Конфеты «Казахстанские» 200г", gSweets, 890, "SW-CND-001", "2000000000223", kz?.Id, false, unitSht.Id, false), - ("Печенье «Юбилейное» 250г", gSweets, 540, "SW-CKE-001", "2000000000230", ru?.Id, false, unitSht.Id, false), - ("Вафли «Артек» 250г", gSweets, 480, "SW-WAF-001", "2000000000247", kz?.Id, false, unitSht.Id, false), + ("Шоколад «Рахат» молочный 100г", gSweets, 650, "SW-CHO-001", "2000000000209", kz?.Id, false, unitSht.Id), + ("Шоколад «Алёнка» 100г", gSweets, 620, "SW-CHO-002", "2000000000216", ru?.Id, false, unitSht.Id), + ("Конфеты «Казахстанские» 200г", gSweets, 890, "SW-CND-001", "2000000000223", kz?.Id, false, unitSht.Id), + ("Печенье «Юбилейное» 250г", gSweets, 540, "SW-CKE-001", "2000000000230", ru?.Id, false, unitSht.Id), + ("Вафли «Артек» 250г", gSweets, 480, "SW-WAF-001", "2000000000247", kz?.Id, false, unitSht.Id), // Бакалея - ("Сахар-песок 1кг", gGrocery, 480, "GRO-SUG-001", "2000000000254", kz?.Id, false, unitSht.Id, false), - ("Соль поваренная 1кг", gGrocery, 180, "GRO-SLT-001", "2000000000261", kz?.Id, false, unitSht.Id, false), - ("Рис круглозёрный 1кг", gGrocery, 850, "GRO-RCE-001", "2000000000278", kz?.Id, false, unitSht.Id, false), - ("Гречка «Мистраль» 900г", gGrocery, 990, "GRO-GRE-001", "2000000000285", ru?.Id, false, unitSht.Id, false), - ("Макароны «Corona» 500г", gGrocery, 420, "GRO-PAS-001", "2000000000292", kz?.Id, false, unitSht.Id, false), - ("Масло подсолнечное «Шедевр» 1л", gGrocery, 920, "GRO-OIL-001", "2000000000308", kz?.Id, false, unitL?.Id ?? unitSht.Id, false), - ("Кофе «Якобс Монарх» 95г растворимый", gGrocery, 2890, "GRO-COF-001", "2000000000315", null, false, unitSht.Id, false), + ("Сахар-песок 1кг", gGrocery, 480, "GRO-SUG-001", "2000000000254", kz?.Id, false, unitSht.Id), + ("Соль поваренная 1кг", gGrocery, 180, "GRO-SLT-001", "2000000000261", kz?.Id, false, unitSht.Id), + ("Рис круглозёрный 1кг", gGrocery, 850, "GRO-RCE-001", "2000000000278", kz?.Id, false, unitSht.Id), + ("Гречка «Мистраль» 900г", gGrocery, 990, "GRO-GRE-001", "2000000000285", ru?.Id, false, unitSht.Id), + ("Макароны «Corona» 500г", gGrocery, 420, "GRO-PAS-001", "2000000000292", kz?.Id, false, unitSht.Id), + ("Масло подсолнечное «Шедевр» 1л", gGrocery, 920, "GRO-OIL-001", "2000000000308", kz?.Id, false, unitL?.Id ?? unitSht.Id), + ("Кофе «Якобс Монарх» 95г растворимый", gGrocery, 2890, "GRO-COF-001", "2000000000315", null, false, unitSht.Id), // Снеки - ("Чипсы «Lays» сметана-лук 80г", gSnacks, 380, "SN-CHP-001", "2000000000322", kz?.Id, false, unitSht.Id, false), - ("Сухарики «3 корочки» ржаные 40г", gSnacks, 190, "SN-SBR-001", "2000000000339", ru?.Id, false, unitSht.Id, false), - ("Орешки фисташки 100г", gSnacks, 1190, "SN-NUT-001", "2000000000346", kz?.Id, false, unitSht.Id, false), - ("Семечки «Мартин» 100г", gSnacks, 290, "SN-SED-001", "2000000000353", ru?.Id, false, unitSht.Id, false), + ("Чипсы «Lays» сметана-лук 80г", gSnacks, 380, "SN-CHP-001", "2000000000322", kz?.Id, false, unitSht.Id), + ("Сухарики «3 корочки» ржаные 40г", gSnacks, 190, "SN-SBR-001", "2000000000339", ru?.Id, false, unitSht.Id), + ("Орешки фисташки 100г", gSnacks, 1190, "SN-NUT-001", "2000000000346", kz?.Id, false, unitSht.Id), + ("Семечки «Мартин» 100г", gSnacks, 290, "SN-SED-001", "2000000000353", ru?.Id, false, unitSht.Id), }; var products = demo.Select(d => @@ -159,12 +156,12 @@ Guid AddGroup(string name, Guid? parentId) Name = d.Name, Article = d.Article, UnitOfMeasureId = d.Unit, - VatRateId = d.Name.Contains("Хлеб") || d.Name.Contains("Батон") || d.Name.Contains("Лепёшка") - ? vat0 : vat, + Vat = d.Name.Contains("Хлеб") || d.Name.Contains("Батон") || d.Name.Contains("Лепёшка") + ? vat0 : vatDefault, + VatEnabled = !(d.Name.Contains("Хлеб") || d.Name.Contains("Батон") || d.Name.Contains("Лепёшка")), ProductGroupId = d.Group, CountryOfOriginId = d.Country, IsWeighed = d.IsWeighed, - IsAlcohol = d.IsAlcohol, IsActive = true, PurchasePrice = Math.Round(d.RetailPrice * 0.72m, 2), PurchaseCurrencyId = kzt.Id, diff --git a/src/food-market.api/Seed/DevDataSeeder.cs b/src/food-market.api/Seed/DevDataSeeder.cs index db68a8a..9300f59 100644 --- a/src/food-market.api/Seed/DevDataSeeder.cs +++ b/src/food-market.api/Seed/DevDataSeeder.cs @@ -78,24 +78,15 @@ public async Task StartAsync(CancellationToken ct) private static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, CancellationToken ct) { - var anyVat = await db.VatRates.IgnoreQueryFilters().AnyAsync(v => v.OrganizationId == orgId, ct); - if (!anyVat) - { - db.VatRates.AddRange( - new VatRate { OrganizationId = orgId, Name = "Без НДС", Percent = 0m, IsDefault = false }, - new VatRate { OrganizationId = orgId, Name = "НДС 12%", Percent = 12m, IsIncludedInPrice = true, IsDefault = true } - ); - } - var anyUnit = await db.UnitsOfMeasure.IgnoreQueryFilters().AnyAsync(u => u.OrganizationId == orgId, ct); if (!anyUnit) { db.UnitsOfMeasure.AddRange( - new UnitOfMeasure { OrganizationId = orgId, Code = "796", Symbol = "шт", Name = "штука", DecimalPlaces = 0, IsBase = true }, - new UnitOfMeasure { OrganizationId = orgId, Code = "166", Symbol = "кг", Name = "килограмм", DecimalPlaces = 3 }, - new UnitOfMeasure { OrganizationId = orgId, Code = "112", Symbol = "л", Name = "литр", DecimalPlaces = 3 }, - new UnitOfMeasure { OrganizationId = orgId, Code = "006", Symbol = "м", Name = "метр", DecimalPlaces = 3 }, - new UnitOfMeasure { OrganizationId = orgId, Code = "625", Symbol = "уп", Name = "упаковка", DecimalPlaces = 0 } + new UnitOfMeasure { OrganizationId = orgId, Code = "796", Name = "штука" }, + new UnitOfMeasure { OrganizationId = orgId, Code = "166", Name = "килограмм" }, + new UnitOfMeasure { OrganizationId = orgId, Code = "112", Name = "литр" }, + new UnitOfMeasure { OrganizationId = orgId, Code = "006", Name = "метр" }, + new UnitOfMeasure { OrganizationId = orgId, Code = "625", Name = "упаковка" } ); } @@ -116,7 +107,6 @@ private static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, OrganizationId = orgId, Name = "Основной склад", Code = "MAIN", - Kind = StoreKind.Warehouse, IsMain = true, Address = "Алматы, ул. Пример 1", }; diff --git a/src/food-market.application/Catalog/CatalogDtos.cs b/src/food-market.application/Catalog/CatalogDtos.cs index 859cc9f..1dc86fa 100644 --- a/src/food-market.application/Catalog/CatalogDtos.cs +++ b/src/food-market.application/Catalog/CatalogDtos.cs @@ -6,17 +6,14 @@ public record CountryDto(Guid Id, string Code, string Name, int SortOrder); public record CurrencyDto(Guid Id, string Code, string Name, string Symbol, int MinorUnit, bool IsActive); -public record VatRateDto( - Guid Id, string Name, decimal Percent, bool IsIncludedInPrice, bool IsDefault, bool IsActive); - public record UnitOfMeasureDto( - Guid Id, string Code, string Symbol, string Name, int DecimalPlaces, bool IsBase, bool IsActive); + Guid Id, string Code, string Name, string? Description, bool IsActive); public record PriceTypeDto( Guid Id, string Name, bool IsDefault, bool IsRetail, int SortOrder, bool IsActive); public record StoreDto( - Guid Id, string Name, string? Code, StoreKind Kind, string? Address, string? Phone, + Guid Id, string Name, string? Code, string? Address, string? Phone, string? ManagerName, bool IsMain, bool IsActive); public record RetailPointDto( @@ -27,7 +24,7 @@ public record ProductGroupDto( Guid Id, string Name, Guid? ParentId, string Path, int SortOrder, bool IsActive); public record CounterpartyDto( - Guid Id, string Name, string? LegalName, CounterpartyKind Kind, CounterpartyType Type, + Guid Id, string Name, string? LegalName, CounterpartyType Type, string? Bin, string? Iin, string? TaxNumber, Guid? CountryId, string? CountryName, string? Address, string? Phone, string? Email, string? BankName, string? BankAccount, string? Bik, string? ContactPerson, string? Notes, bool IsActive); @@ -38,12 +35,12 @@ public record ProductPriceDto(Guid Id, Guid PriceTypeId, string PriceTypeName, d public record ProductDto( Guid Id, string Name, string? Article, string? Description, - Guid UnitOfMeasureId, string UnitSymbol, - Guid VatRateId, decimal VatPercent, + Guid UnitOfMeasureId, string UnitName, + int Vat, bool VatEnabled, Guid? ProductGroupId, string? ProductGroupName, Guid? DefaultSupplierId, string? DefaultSupplierName, Guid? CountryOfOriginId, string? CountryOfOriginName, - bool IsService, bool IsWeighed, bool IsAlcohol, bool IsMarked, + bool IsService, bool IsWeighed, bool IsMarked, decimal? MinStock, decimal? MaxStock, decimal? PurchasePrice, Guid? PurchaseCurrencyId, string? PurchaseCurrencyCode, string? ImageUrl, bool IsActive, @@ -53,11 +50,10 @@ public record ProductDto( // Upsert payloads (input) public record CountryInput(string Code, string Name, int SortOrder = 0); public record CurrencyInput(string Code, string Name, string Symbol, int MinorUnit = 2, bool IsActive = true); -public record VatRateInput(string Name, decimal Percent, bool IsIncludedInPrice, bool IsDefault = false, bool IsActive = true); -public record UnitOfMeasureInput(string Code, string Symbol, string Name, int DecimalPlaces, bool IsBase = false, bool IsActive = true); +public record UnitOfMeasureInput(string Code, string Name, string? Description = null, bool IsActive = true); public record PriceTypeInput(string Name, bool IsDefault = false, bool IsRetail = false, int SortOrder = 0, bool IsActive = true); public record StoreInput( - string Name, string? Code, StoreKind Kind = StoreKind.Warehouse, + string Name, string? Code, string? Address = null, string? Phone = null, string? ManagerName = null, bool IsMain = false, bool IsActive = true); public record RetailPointInput( @@ -66,7 +62,7 @@ public record RetailPointInput( string? FiscalSerial = null, string? FiscalRegNumber = null, bool IsActive = true); public record ProductGroupInput(string Name, Guid? ParentId, int SortOrder = 0, bool IsActive = true); public record CounterpartyInput( - string Name, string? LegalName, CounterpartyKind Kind, CounterpartyType Type, + string Name, string? LegalName, CounterpartyType Type, string? Bin, string? Iin, string? TaxNumber, Guid? CountryId, string? Address, string? Phone, string? Email, string? BankName, string? BankAccount, string? Bik, string? ContactPerson, string? Notes, bool IsActive = true); @@ -74,9 +70,9 @@ public record ProductBarcodeInput(string Code, BarcodeType Type = BarcodeType.Ea public record ProductPriceInput(Guid PriceTypeId, decimal Amount, Guid CurrencyId); public record ProductInput( string Name, string? Article, string? Description, - Guid UnitOfMeasureId, Guid VatRateId, + Guid UnitOfMeasureId, int Vat, bool VatEnabled, Guid? ProductGroupId, Guid? DefaultSupplierId, Guid? CountryOfOriginId, - bool IsService = false, bool IsWeighed = false, bool IsAlcohol = false, bool IsMarked = false, + bool IsService = false, bool IsWeighed = false, bool IsMarked = false, decimal? MinStock = null, decimal? MaxStock = null, decimal? PurchasePrice = null, Guid? PurchaseCurrencyId = null, string? ImageUrl = null, bool IsActive = true, diff --git a/src/food-market.domain/Catalog/Counterparty.cs b/src/food-market.domain/Catalog/Counterparty.cs index db25617..0549580 100644 --- a/src/food-market.domain/Catalog/Counterparty.cs +++ b/src/food-market.domain/Catalog/Counterparty.cs @@ -6,7 +6,6 @@ public class Counterparty : TenantEntity { public string Name { get; set; } = null!; // отображаемое имя public string? LegalName { get; set; } // полное юридическое имя - public CounterpartyKind Kind { get; set; } // Supplier / Customer / Both public CounterpartyType Type { get; set; } // Юрлицо / Физлицо public string? Bin { get; set; } // БИН (для юрлиц РК) public string? Iin { get; set; } // ИИН (для физлиц РК) diff --git a/src/food-market.domain/Catalog/Enums.cs b/src/food-market.domain/Catalog/Enums.cs index d886eb4..58f7c74 100644 --- a/src/food-market.domain/Catalog/Enums.cs +++ b/src/food-market.domain/Catalog/Enums.cs @@ -1,28 +1,11 @@ namespace foodmarket.Domain.Catalog; -public enum CounterpartyKind -{ - /// Не указано — дефолт для импортированных без явной классификации. - /// MoySklad сам не имеет встроенного поля Supplier/Customer, оно ставится - /// через теги или группы, и часто отсутствует. Не выдумываем за пользователя. - Unspecified = 0, - Supplier = 1, - Customer = 2, - Both = 3, -} - public enum CounterpartyType { LegalEntity = 1, Individual = 2, } -public enum StoreKind -{ - Warehouse = 1, - RetailFloor = 2, -} - public enum BarcodeType { Ean13 = 1, diff --git a/src/food-market.domain/Catalog/Product.cs b/src/food-market.domain/Catalog/Product.cs index a0c7c8a..e63c9fe 100644 --- a/src/food-market.domain/Catalog/Product.cs +++ b/src/food-market.domain/Catalog/Product.cs @@ -11,8 +11,10 @@ public class Product : TenantEntity public Guid UnitOfMeasureId { get; set; } public UnitOfMeasure? UnitOfMeasure { get; set; } - public Guid VatRateId { get; set; } - public VatRate? VatRate { get; set; } + // Ставка НДС на товаре — число 0/10/12/16/20 как в MoySklad. + // VatEnabled=true → НДС применяется, false → без НДС. + public int Vat { get; set; } + public bool VatEnabled { get; set; } = true; public Guid? ProductGroupId { get; set; } public ProductGroup? ProductGroup { get; set; } @@ -25,7 +27,6 @@ public class Product : TenantEntity public bool IsService { get; set; } // услуга, а не физический товар public bool IsWeighed { get; set; } // весовой (продаётся с весов) - public bool IsAlcohol { get; set; } // алкоголь (подакцизный) public bool IsMarked { get; set; } // маркируемый (Datamatrix) public decimal? MinStock { get; set; } // минимальный остаток (для уведомлений) diff --git a/src/food-market.domain/Catalog/Store.cs b/src/food-market.domain/Catalog/Store.cs index 00b20ee..6e7866f 100644 --- a/src/food-market.domain/Catalog/Store.cs +++ b/src/food-market.domain/Catalog/Store.cs @@ -2,12 +2,12 @@ namespace foodmarket.Domain.Catalog; -// Склад: физическое место хранения товаров. Может быть чисто склад или торговый зал. +// Склад: физическое место хранения товаров. MoySklad не различает "склад" и +// "торговый зал" — это одна сущность entity/store, опираемся на это. public class Store : TenantEntity { public string Name { get; set; } = null!; public string? Code { get; set; } // внутренний код склада - public StoreKind Kind { get; set; } = StoreKind.Warehouse; public string? Address { get; set; } public string? Phone { get; set; } public string? ManagerName { get; set; } diff --git a/src/food-market.domain/Catalog/UnitOfMeasure.cs b/src/food-market.domain/Catalog/UnitOfMeasure.cs index 562ceb9..06d42d4 100644 --- a/src/food-market.domain/Catalog/UnitOfMeasure.cs +++ b/src/food-market.domain/Catalog/UnitOfMeasure.cs @@ -2,13 +2,11 @@ namespace foodmarket.Domain.Catalog; -// Tenant-scoped справочник единиц измерения. +// Единица измерения как в MoySklad entity/uom: code + name + description. public class UnitOfMeasure : TenantEntity { public string Code { get; set; } = null!; // ОКЕИ код: "796" (шт), "166" (кг), "112" (л) - public string Symbol { get; set; } = null!; // "шт", "кг", "л", "м" public string Name { get; set; } = null!; // "штука", "килограмм", "литр" - public int DecimalPlaces { get; set; } // 0 для шт, 3 для кг/л - public bool IsBase { get; set; } // базовая единица этой организации + public string? Description { get; set; } public bool IsActive { get; set; } = true; } diff --git a/src/food-market.domain/Catalog/VatRate.cs b/src/food-market.domain/Catalog/VatRate.cs deleted file mode 100644 index 23dfce9..0000000 --- a/src/food-market.domain/Catalog/VatRate.cs +++ /dev/null @@ -1,13 +0,0 @@ -using foodmarket.Domain.Common; - -namespace foodmarket.Domain.Catalog; - -// Tenant-scoped: разные организации могут работать в разных режимах (с НДС / упрощёнка). -public class VatRate : TenantEntity -{ - public string Name { get; set; } = null!; // "НДС 12%", "Без НДС" - public decimal Percent { get; set; } // 12.00, 0.00 - public bool IsIncludedInPrice { get; set; } // входит ли в цену или начисляется сверху - public bool IsDefault { get; set; } - public bool IsActive { get; set; } = true; -} diff --git a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs index 2953411..51cb1f5 100644 --- a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs +++ b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs @@ -39,14 +39,11 @@ public async Task ImportCounterpartiesAsync(string token, { var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant organization in context."); - // MoySklad НЕ имеет поля "Поставщик/Покупатель" у контрагентов вообще — это - // не наша выдумка, проверено через API: counterparty entity содержит только - // group (группа доступа), tags (произвольные), state (пользовательская цепочка - // статусов), companyType (legal/individual/entrepreneur). Никакой role/kind. - // Поэтому при импорте ВСЕГДА ставим Unspecified — пользователь сам решит. - // Параметр tags оставлен ради совместимости сигнатуры, не используется. - static foodmarket.Domain.Catalog.CounterpartyKind ResolveKind(IReadOnlyList? tags) - => foodmarket.Domain.Catalog.CounterpartyKind.Unspecified; + // MoySklad НЕ имеет поля "Поставщик/Покупатель" у контрагентов вообще — + // counterparty entity содержит только group (группа доступа), tags + // (произвольные), state (пользовательская цепочка статусов), companyType + // (legal/individual/entrepreneur). Никакого role/kind. Поэтому у нас тоже + // этого поля нет — пусть пользователь сам решит. static foodmarket.Domain.Catalog.CounterpartyType ResolveType(string? companyType) => companyType switch @@ -83,7 +80,6 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct)) OrganizationId = orgId, Name = Trim(c.Name, 255) ?? c.Name, LegalName = Trim(c.LegalTitle, 500), - Kind = ResolveKind(c.Tags), Type = ResolveType(c.CompanyType), Bin = Trim(c.Inn, 20), TaxNumber = Trim(c.Kpp, 20), @@ -121,11 +117,10 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct)) { var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant organization in context."); - // Pre-load tenant defaults. - var defaultVat = await _db.VatRates.FirstOrDefaultAsync(v => v.IsDefault, ct) - ?? await _db.VatRates.FirstAsync(ct); - var vatByPercent = await _db.VatRates.ToDictionaryAsync(v => (int)v.Percent, v => v.Id, ct); - var baseUnit = await _db.UnitsOfMeasure.FirstOrDefaultAsync(u => u.IsBase, ct) + // Pre-load tenant defaults. KZ default VAT is 16% — applied when product didn't + // carry its own vat from MoySklad. + const int kzDefaultVat = 16; + var baseUnit = await _db.UnitsOfMeasure.FirstOrDefaultAsync(u => u.Code == "796", ct) ?? await _db.UnitsOfMeasure.FirstAsync(ct); var retailType = await _db.PriceTypes.FirstOrDefaultAsync(p => p.IsRetail, ct) ?? await _db.PriceTypes.FirstAsync(ct); @@ -194,7 +189,8 @@ await foreach (var p in _client.StreamProductsAsync(token, ct)) try { - var vatId = p.Vat is int vp && vatByPercent.TryGetValue(vp, out var id) ? id : defaultVat.Id; + var vat = p.Vat ?? kzDefaultVat; + var vatEnabled = (p.Vat ?? kzDefaultVat) > 0; Guid? groupId = p.ProductFolder?.Meta?.Href is { } href && TryExtractId(href) is { } msGroupId && localGroupByMsId.TryGetValue(msGroupId, out var gId) ? gId : null; Guid? countryId = p.Country?.Name is { } cn && countriesByName.TryGetValue(cn, out var cId) ? cId : null; @@ -209,11 +205,11 @@ await foreach (var p in _client.StreamProductsAsync(token, ct)) Article = Trim(article, 500), Description = p.Description, UnitOfMeasureId = baseUnit.Id, - VatRateId = vatId, + Vat = vat, + VatEnabled = vatEnabled, ProductGroupId = groupId, CountryOfOriginId = countryId, IsWeighed = p.Weighed, - IsAlcohol = p.Alcoholic is not null, IsMarked = !string.IsNullOrEmpty(p.TrackingType) && p.TrackingType != "NOT_TRACKED", IsActive = !p.Archived, PurchasePrice = p.BuyPrice is null ? null : p.BuyPrice.Value / 100m, diff --git a/src/food-market.infrastructure/Persistence/AppDbContext.cs b/src/food-market.infrastructure/Persistence/AppDbContext.cs index 6256a4e..cef1341 100644 --- a/src/food-market.infrastructure/Persistence/AppDbContext.cs +++ b/src/food-market.infrastructure/Persistence/AppDbContext.cs @@ -26,7 +26,6 @@ public AppDbContext(DbContextOptions options, ITenantContext tenan public DbSet Countries => Set(); public DbSet Currencies => Set(); - public DbSet VatRates => Set(); public DbSet UnitsOfMeasure => Set(); public DbSet Counterparties => Set(); public DbSet Stores => Set(); diff --git a/src/food-market.infrastructure/Persistence/Configurations/CatalogConfigurations.cs b/src/food-market.infrastructure/Persistence/Configurations/CatalogConfigurations.cs index 80a09ea..034175a 100644 --- a/src/food-market.infrastructure/Persistence/Configurations/CatalogConfigurations.cs +++ b/src/food-market.infrastructure/Persistence/Configurations/CatalogConfigurations.cs @@ -10,7 +10,6 @@ public static void ConfigureCatalog(this ModelBuilder b) { b.Entity(ConfigureCountry); b.Entity(ConfigureCurrency); - b.Entity(ConfigureVatRate); b.Entity(ConfigureUnit); b.Entity(ConfigureCounterparty); b.Entity(ConfigureStore); @@ -40,20 +39,12 @@ private static void ConfigureCurrency(EntityTypeBuilder b) b.HasIndex(x => x.Code).IsUnique(); } - private static void ConfigureVatRate(EntityTypeBuilder b) - { - b.ToTable("vat_rates"); - b.Property(x => x.Name).HasMaxLength(100).IsRequired(); - b.Property(x => x.Percent).HasPrecision(5, 2); - b.HasIndex(x => new { x.OrganizationId, x.Name }).IsUnique(); - } - private static void ConfigureUnit(EntityTypeBuilder b) { b.ToTable("units_of_measure"); b.Property(x => x.Code).HasMaxLength(10).IsRequired(); - b.Property(x => x.Symbol).HasMaxLength(20).IsRequired(); b.Property(x => x.Name).HasMaxLength(100).IsRequired(); + b.Property(x => x.Description).HasMaxLength(500); b.HasIndex(x => new { x.OrganizationId, x.Code }).IsUnique(); } @@ -74,7 +65,6 @@ private static void ConfigureCounterparty(EntityTypeBuilder b) b.HasOne(x => x.Country).WithMany().HasForeignKey(x => x.CountryId).OnDelete(DeleteBehavior.Restrict); b.HasIndex(x => new { x.OrganizationId, x.Name }); b.HasIndex(x => new { x.OrganizationId, x.Bin }); - b.HasIndex(x => new { x.OrganizationId, x.Kind }); } private static void ConfigureStore(EntityTypeBuilder b) @@ -130,7 +120,6 @@ private static void ConfigureProduct(EntityTypeBuilder b) b.Property(x => x.ImageUrl).HasMaxLength(1000); b.HasOne(x => x.UnitOfMeasure).WithMany().HasForeignKey(x => x.UnitOfMeasureId).OnDelete(DeleteBehavior.Restrict); - b.HasOne(x => x.VatRate).WithMany().HasForeignKey(x => x.VatRateId).OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.ProductGroup).WithMany().HasForeignKey(x => x.ProductGroupId).OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.DefaultSupplier).WithMany().HasForeignKey(x => x.DefaultSupplierId).OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.CountryOfOrigin).WithMany().HasForeignKey(x => x.CountryOfOriginId).OnDelete(DeleteBehavior.Restrict); diff --git a/src/food-market.web/src/App.tsx b/src/food-market.web/src/App.tsx index 8820ace..0950c83 100644 --- a/src/food-market.web/src/App.tsx +++ b/src/food-market.web/src/App.tsx @@ -4,7 +4,6 @@ import { LoginPage } from '@/pages/LoginPage' import { DashboardPage } from '@/pages/DashboardPage' import { CountriesPage } from '@/pages/CountriesPage' import { CurrenciesPage } from '@/pages/CurrenciesPage' -import { VatRatesPage } from '@/pages/VatRatesPage' import { UnitsOfMeasurePage } from '@/pages/UnitsOfMeasurePage' import { PriceTypesPage } from '@/pages/PriceTypesPage' import { StoresPage } from '@/pages/StoresPage' @@ -46,7 +45,6 @@ export default function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/src/food-market.web/src/components/ProductPicker.tsx b/src/food-market.web/src/components/ProductPicker.tsx index bbc626a..a2453da 100644 --- a/src/food-market.web/src/components/ProductPicker.tsx +++ b/src/food-market.web/src/components/ProductPicker.tsx @@ -71,7 +71,7 @@ export function ProductPicker({ open, onClose, onPick, title = 'Выбор то
{p.article && {p.article}} {p.barcodes[0] && · {p.barcodes[0].code}} - · {p.unitSymbol} + · {p.unitName}
{p.purchasePrice !== null && ( diff --git a/src/food-market.web/src/lib/types.ts b/src/food-market.web/src/lib/types.ts index f728570..ccaf777 100644 --- a/src/food-market.web/src/lib/types.ts +++ b/src/food-market.web/src/lib/types.ts @@ -6,25 +6,18 @@ export interface PagedResult { totalPages: number } -export const CounterpartyKind = { Unspecified: 0, Supplier: 1, Customer: 2, Both: 3 } as const -export type CounterpartyKind = (typeof CounterpartyKind)[keyof typeof CounterpartyKind] - export const CounterpartyType = { LegalEntity: 1, Individual: 2 } as const export type CounterpartyType = (typeof CounterpartyType)[keyof typeof CounterpartyType] -export const StoreKind = { Warehouse: 1, RetailFloor: 2 } as const -export type StoreKind = (typeof StoreKind)[keyof typeof StoreKind] - export const BarcodeType = { Ean13: 1, Ean8: 2, Code128: 3, Code39: 4, Upca: 5, Upce: 6, Other: 99 } as const export type BarcodeType = (typeof BarcodeType)[keyof typeof BarcodeType] export interface Country { id: string; code: string; name: string; sortOrder: number } export interface Currency { id: string; code: string; name: string; symbol: string; minorUnit: number; isActive: boolean } -export interface VatRate { id: string; name: string; percent: number; isIncludedInPrice: boolean; isDefault: boolean; isActive: boolean } -export interface UnitOfMeasure { id: string; code: string; symbol: string; name: string; decimalPlaces: number; isBase: boolean; isActive: boolean } +export interface UnitOfMeasure { id: string; code: string; name: string; description: string | null; isActive: boolean } export interface PriceType { id: string; name: string; isDefault: boolean; isRetail: boolean; sortOrder: number; isActive: boolean } export interface Store { - id: string; name: string; code: string | null; kind: StoreKind; address: string | null; phone: string | null; + id: string; name: string; code: string | null; address: string | null; phone: string | null; managerName: string | null; isMain: boolean; isActive: boolean } export interface RetailPoint { @@ -33,7 +26,7 @@ export interface RetailPoint { } export interface ProductGroup { id: string; name: string; parentId: string | null; path: string; sortOrder: number; isActive: boolean } export interface Counterparty { - id: string; name: string; legalName: string | null; kind: CounterpartyKind; type: CounterpartyType; + id: string; name: string; legalName: string | null; type: CounterpartyType; bin: string | null; iin: string | null; taxNumber: string | null; countryId: string | null; countryName: string | null; address: string | null; phone: string | null; email: string | null; bankName: string | null; bankAccount: string | null; bik: string | null; @@ -43,12 +36,12 @@ export interface ProductBarcode { id: string; code: string; type: BarcodeType; i export interface ProductPrice { id: string; priceTypeId: string; priceTypeName: string; amount: number; currencyId: string; currencyCode: string } export interface Product { id: string; name: string; article: string | null; description: string | null; - unitOfMeasureId: string; unitSymbol: string; - vatRateId: string; vatPercent: number; + unitOfMeasureId: string; unitName: string; + vat: number; vatEnabled: boolean; productGroupId: string | null; productGroupName: string | null; defaultSupplierId: string | null; defaultSupplierName: string | null; countryOfOriginId: string | null; countryOfOriginName: string | null; - isService: boolean; isWeighed: boolean; isAlcohol: boolean; isMarked: boolean; + isService: boolean; isWeighed: boolean; isMarked: boolean; minStock: number | null; maxStock: number | null; purchasePrice: number | null; purchaseCurrencyId: string | null; purchaseCurrencyCode: string | null; imageUrl: string | null; isActive: boolean; @@ -56,7 +49,7 @@ export interface Product { } export interface StockRow { - productId: string; productName: string; article: string | null; unitSymbol: string; + productId: string; productName: string; article: string | null; unitName: string; storeId: string; storeName: string; quantity: number; reservedQuantity: number; available: number; } @@ -83,7 +76,7 @@ export interface SupplyListRow { export interface SupplyLineDto { id: string | null; productId: string; - productName: string | null; productArticle: string | null; unitSymbol: string | null; + productName: string | null; productArticle: string | null; unitName: string | null; quantity: number; unitPrice: number; lineTotal: number; sortOrder: number; } @@ -116,7 +109,7 @@ export interface RetailSaleListRow { export interface RetailSaleLineDto { id: string | null; productId: string; - productName: string | null; productArticle: string | null; unitSymbol: string | null; + productName: string | null; productArticle: string | null; unitName: string | null; quantity: number; unitPrice: number; discount: number; lineTotal: number; vatPercent: number; sortOrder: number; } diff --git a/src/food-market.web/src/lib/useLookups.ts b/src/food-market.web/src/lib/useLookups.ts index 9d715f8..a1be6fe 100644 --- a/src/food-market.web/src/lib/useLookups.ts +++ b/src/food-market.web/src/lib/useLookups.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query' import { api } from '@/lib/api' import type { - PagedResult, UnitOfMeasure, VatRate, ProductGroup, Counterparty, + PagedResult, UnitOfMeasure, ProductGroup, Counterparty, Country, Currency, Store, PriceType, } from '@/lib/types' @@ -14,7 +14,6 @@ function useLookup(key: string, url: string) { } export const useUnits = () => useLookup('units', '/api/catalog/units-of-measure') -export const useVatRates = () => useLookup('vat', '/api/catalog/vat-rates') export const useProductGroups = () => useLookup('groups', '/api/catalog/product-groups') export const useCountries = () => useLookup('countries', '/api/catalog/countries') export const useCurrencies = () => useLookup('currencies', '/api/catalog/currencies') diff --git a/src/food-market.web/src/pages/CounterpartiesPage.tsx b/src/food-market.web/src/pages/CounterpartiesPage.tsx index dcf16ac..7382984 100644 --- a/src/food-market.web/src/pages/CounterpartiesPage.tsx +++ b/src/food-market.web/src/pages/CounterpartiesPage.tsx @@ -10,7 +10,7 @@ import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, TextArea, Select, Checkbox } from '@/components/Field' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' -import { type Counterparty, type Country, type PagedResult, CounterpartyKind, CounterpartyType } from '@/lib/types' +import { type Counterparty, type Country, type PagedResult, CounterpartyType } from '@/lib/types' const URL = '/api/catalog/counterparties' @@ -18,7 +18,6 @@ interface Form { id?: string name: string legalName: string - kind: CounterpartyKind type: CounterpartyType bin: string iin: string @@ -36,20 +35,16 @@ interface Form { } const blankForm: Form = { - // Kind по умолчанию Unspecified — MoySklad не имеет такого поля у контрагентов, - // не выдумываем за пользователя. Пусть выберет вручную если нужно. - name: '', legalName: '', kind: CounterpartyKind.Unspecified, type: CounterpartyType.LegalEntity, + name: '', legalName: '', type: CounterpartyType.LegalEntity, bin: '', iin: '', taxNumber: '', countryId: '', address: '', phone: '', email: '', bankName: '', bankAccount: '', bik: '', contactPerson: '', notes: '', isActive: true, } -const kindLabel: Record = { - [CounterpartyKind.Unspecified]: '—', - [CounterpartyKind.Supplier]: 'Поставщик', - [CounterpartyKind.Customer]: 'Покупатель', - [CounterpartyKind.Both]: 'Поставщик + Покупатель', +const typeLabel: Record = { + [CounterpartyType.LegalEntity]: 'Юрлицо', + [CounterpartyType.Individual]: 'Физлицо', } export function CounterpartiesPage() { @@ -92,7 +87,7 @@ export function CounterpartiesPage() { isLoading={isLoading} rowKey={(r) => r.id} onRowClick={(r) => setForm({ - id: r.id, name: r.name, legalName: r.legalName ?? '', kind: r.kind, type: r.type, + id: r.id, name: r.name, legalName: r.legalName ?? '', type: r.type, bin: r.bin ?? '', iin: r.iin ?? '', taxNumber: r.taxNumber ?? '', countryId: r.countryId ?? '', address: r.address ?? '', phone: r.phone ?? '', email: r.email ?? '', bankName: r.bankName ?? '', bankAccount: r.bankAccount ?? '', bik: r.bik ?? '', @@ -100,7 +95,7 @@ export function CounterpartiesPage() { })} columns={[ { header: 'Название', cell: (r) => r.name }, - { header: 'Тип', width: '120px', cell: (r) => kindLabel[r.kind] }, + { header: 'Тип', width: '120px', cell: (r) => typeLabel[r.type] }, { header: 'БИН/ИИН', width: '140px', cell: (r) => {r.bin ?? r.iin ?? '—'} }, { header: 'Телефон', width: '160px', cell: (r) => r.phone ?? '—' }, { header: 'Страна', width: '120px', cell: (r) => r.countryName ?? '—' }, @@ -139,14 +134,6 @@ export function CounterpartiesPage() { setForm({ ...form, legalName: e.target.value })} /> - - - setForm({ ...form, unitOfMeasureId: e.target.value })}> - {units.data?.map((u) => )} + {units.data?.map((u) => )} - - setForm({ ...form, vat: Number(e.target.value) })}> + {vatChoices.map((v) => )} @@ -266,9 +265,9 @@ export function ProductEditPage() {
+ setForm({ ...form, vatEnabled: v })} /> setForm({ ...form, isService: v })} /> setForm({ ...form, isWeighed: v })} /> - setForm({ ...form, isAlcohol: v })} /> setForm({ ...form, isMarked: v })} /> setForm({ ...form, isActive: v })} />
diff --git a/src/food-market.web/src/pages/ProductsPage.tsx b/src/food-market.web/src/pages/ProductsPage.tsx index 2a3222c..0d1cad1 100644 --- a/src/food-market.web/src/pages/ProductsPage.tsx +++ b/src/food-market.web/src/pages/ProductsPage.tsx @@ -45,13 +45,12 @@ export function ProductsPage() { )}, { header: 'Группа', width: '200px', cell: (r) => r.productGroupName ?? '—' }, - { header: 'Ед.', width: '70px', cell: (r) => r.unitSymbol }, - { header: 'НДС', width: '80px', className: 'text-right', cell: (r) => `${r.vatPercent}%` }, + { header: 'Ед.', width: '70px', cell: (r) => r.unitName }, + { header: 'НДС', width: '80px', className: 'text-right', cell: (r) => `${r.vat}%` }, { header: 'Тип', width: '140px', cell: (r) => (
{r.isService && Услуга} {r.isWeighed && Весовой} - {r.isAlcohol && Алкоголь} {r.isMarked && Маркир.}
)}, diff --git a/src/food-market.web/src/pages/RetailSaleEditPage.tsx b/src/food-market.web/src/pages/RetailSaleEditPage.tsx index d84c8cc..79a1ee9 100644 --- a/src/food-market.web/src/pages/RetailSaleEditPage.tsx +++ b/src/food-market.web/src/pages/RetailSaleEditPage.tsx @@ -14,7 +14,7 @@ interface LineRow { productId: string productName: string productArticle: string | null - unitSymbol: string | null + unitName: string | null quantity: number unitPrice: number discount: number @@ -81,7 +81,7 @@ export function RetailSaleEditPage() { productId: l.productId, productName: l.productName ?? '', productArticle: l.productArticle, - unitSymbol: l.unitSymbol, + unitName: l.unitName, quantity: l.quantity, unitPrice: l.unitPrice, discount: l.discount, vatPercent: l.vatPercent, })), @@ -179,11 +179,11 @@ export function RetailSaleEditPage() { productId: p.id, productName: p.name, productArticle: p.article, - unitSymbol: p.unitSymbol, + unitName: p.unitName, quantity: 1, unitPrice: retail?.amount ?? 0, discount: 0, - vatPercent: p.vatPercent, + vatPercent: p.vat * 1, }], }) } @@ -323,7 +323,7 @@ export function RetailSaleEditPage() {
{l.productName}
{l.productArticle &&
{l.productArticle}
} - {l.unitSymbol} + {l.unitName} )}, { header: 'Склад', width: '220px', cell: (r) => r.storeName }, - { header: 'Ед.', width: '80px', cell: (r) => r.unitSymbol }, + { header: 'Ед.', width: '80px', cell: (r) => r.unitName }, { header: 'Остаток', width: '130px', className: 'text-right font-mono', cell: (r) => r.quantity.toLocaleString('ru', { maximumFractionDigits: 3 }) }, { header: 'Резерв', width: '110px', className: 'text-right font-mono text-slate-400', cell: (r) => r.reservedQuantity ? r.reservedQuantity.toLocaleString('ru') : '—' }, { header: 'Доступно', width: '130px', className: 'text-right font-mono font-semibold', cell: (r) => ( diff --git a/src/food-market.web/src/pages/StoresPage.tsx b/src/food-market.web/src/pages/StoresPage.tsx index 67794da..b6f5714 100644 --- a/src/food-market.web/src/pages/StoresPage.tsx +++ b/src/food-market.web/src/pages/StoresPage.tsx @@ -6,9 +6,9 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' -import { Field, TextInput, Select, Checkbox } from '@/components/Field' +import { Field, TextInput, Checkbox } from '@/components/Field' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' -import { type Store, StoreKind } from '@/lib/types' +import { type Store } from '@/lib/types' const URL = '/api/catalog/stores' @@ -16,7 +16,6 @@ interface Form { id?: string name: string code: string - kind: StoreKind address: string phone: string managerName: string @@ -25,7 +24,7 @@ interface Form { } const blankForm: Form = { - name: '', code: '', kind: StoreKind.Warehouse, address: '', phone: '', + name: '', code: '', address: '', phone: '', managerName: '', isMain: false, isActive: true, } @@ -62,14 +61,13 @@ export function StoresPage() { isLoading={isLoading} rowKey={(r) => r.id} onRowClick={(r) => setForm({ - id: r.id, name: r.name, code: r.code ?? '', kind: r.kind, + id: r.id, name: r.name, code: r.code ?? '', address: r.address ?? '', phone: r.phone ?? '', managerName: r.managerName ?? '', isMain: r.isMain, isActive: r.isActive, })} columns={[ { header: 'Название', cell: (r) => r.name }, { header: 'Код', width: '120px', cell: (r) => {r.code ?? '—'} }, - { header: 'Тип', width: '150px', cell: (r) => r.kind === StoreKind.Warehouse ? 'Склад' : 'Торговый зал' }, { header: 'Адрес', cell: (r) => r.address ?? '—' }, { header: 'Основной', width: '110px', cell: (r) => r.isMain ? '✓' : '—' }, { header: 'Активен', width: '100px', cell: (r) => r.isActive ? '✓' : '—' }, @@ -108,12 +106,6 @@ export function StoresPage() { setForm({ ...form, code: e.target.value })} /> - - - setForm({ ...form, address: e.target.value })} /> diff --git a/src/food-market.web/src/pages/SupplyEditPage.tsx b/src/food-market.web/src/pages/SupplyEditPage.tsx index c26c002..94d035e 100644 --- a/src/food-market.web/src/pages/SupplyEditPage.tsx +++ b/src/food-market.web/src/pages/SupplyEditPage.tsx @@ -14,7 +14,7 @@ interface LineRow { productId: string productName: string productArticle: string | null - unitSymbol: string | null + unitName: string | null quantity: number unitPrice: number } @@ -76,7 +76,7 @@ export function SupplyEditPage() { productId: l.productId, productName: l.productName ?? '', productArticle: l.productArticle, - unitSymbol: l.unitSymbol, + unitName: l.unitName, quantity: l.quantity, unitPrice: l.unitPrice, })), @@ -169,7 +169,7 @@ export function SupplyEditPage() { productId: p.id, productName: p.name, productArticle: p.article, - unitSymbol: p.unitSymbol, + unitName: p.unitName, quantity: 1, unitPrice: p.purchasePrice ?? 0, }], @@ -304,7 +304,7 @@ export function SupplyEditPage() {
{l.productName}
{l.productArticle &&
{l.productArticle}
} - {l.unitSymbol} + {l.unitName} (URL) @@ -41,7 +39,7 @@ export function UnitsOfMeasurePage() { <> @@ -56,13 +54,14 @@ export function UnitsOfMeasurePage() { rows={data?.items ?? []} isLoading={isLoading} rowKey={(r) => r.id} - onRowClick={(r) => setForm({ ...r })} + onRowClick={(r) => setForm({ + id: r.id, code: r.code, name: r.name, + description: r.description ?? '', isActive: r.isActive, + })} columns={[ { header: 'Код', width: '90px', cell: (r) => {r.code} }, - { header: 'Символ', width: '100px', cell: (r) => r.symbol }, { header: 'Название', cell: (r) => r.name }, - { header: 'Дробность', width: '120px', className: 'text-right', cell: (r) => r.decimalPlaces }, - { header: 'Базовая', width: '100px', cell: (r) => r.isBase ? '✓' : '—' }, + { header: 'Описание', cell: (r) => r.description ?? '—' }, { header: 'Активна', width: '100px', cell: (r) => r.isActive ? '✓' : '—' }, ]} /> @@ -91,25 +90,15 @@ export function UnitsOfMeasurePage() { > {form && (
-
- - setForm({ ...form, code: e.target.value })} /> - - - setForm({ ...form, symbol: e.target.value })} /> - -
+ + setForm({ ...form, code: e.target.value })} /> + setForm({ ...form, name: e.target.value })} /> - - setForm({ ...form, decimalPlaces: Number(e.target.value) })} - /> + + setForm({ ...form, description: e.target.value })} /> - setForm({ ...form, isBase: v })} /> setForm({ ...form, isActive: v })} />
)} diff --git a/src/food-market.web/src/pages/VatRatesPage.tsx b/src/food-market.web/src/pages/VatRatesPage.tsx deleted file mode 100644 index 5bc4de1..0000000 --- a/src/food-market.web/src/pages/VatRatesPage.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState } from 'react' -import { Plus, Trash2 } from 'lucide-react' -import { ListPageShell } from '@/components/ListPageShell' -import { DataTable } from '@/components/DataTable' -import { Pagination } from '@/components/Pagination' -import { SearchBar } from '@/components/SearchBar' -import { Button } from '@/components/Button' -import { Modal } from '@/components/Modal' -import { Field, TextInput, Checkbox } from '@/components/Field' -import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' -import type { VatRate } from '@/lib/types' - -const URL = '/api/catalog/vat-rates' - -interface Form { - id?: string - name: string - percent: number - isIncludedInPrice: boolean - isDefault: boolean - isActive: boolean -} - -const blankForm: Form = { name: '', percent: 0, isIncludedInPrice: true, isDefault: false, isActive: true } - -export function VatRatesPage() { - const { data, isLoading, page, setPage, search, setSearch } = useCatalogList(URL) - const { create, update, remove } = useCatalogMutations(URL, URL) - const [form, setForm] = useState
(null) - - const save = async () => { - if (!form) return - const payload = { - name: form.name, percent: form.percent, - isIncludedInPrice: form.isIncludedInPrice, isDefault: form.isDefault, isActive: form.isActive, - } - if (form.id) await update.mutateAsync({ id: form.id, input: payload }) - else await create.mutateAsync(payload) - setForm(null) - } - - return ( - <> - - - - - } - footer={data && data.total > 0 && ( - - )} - > - r.id} - onRowClick={(r) => setForm({ - id: r.id, name: r.name, percent: r.percent, - isIncludedInPrice: r.isIncludedInPrice, isDefault: r.isDefault, isActive: r.isActive, - })} - columns={[ - { header: 'Название', cell: (r) => r.name }, - { header: '%', width: '90px', className: 'text-right font-mono', cell: (r) => r.percent.toFixed(2) }, - { header: 'В цене', width: '100px', cell: (r) => r.isIncludedInPrice ? '✓' : '—' }, - { header: 'По умолчанию', width: '140px', cell: (r) => r.isDefault ? '✓' : '—' }, - { header: 'Активна', width: '100px', cell: (r) => r.isActive ? '✓' : '—' }, - ]} - /> - - - setForm(null)} - title={form?.id ? 'Редактировать ставку НДС' : 'Новая ставка НДС'} - footer={ - <> - {form?.id && ( - - )} - - - - } - > - {form && ( -
- - setForm({ ...form, name: e.target.value })} /> - - - setForm({ ...form, percent: Number(e.target.value) })} - /> - - setForm({ ...form, isIncludedInPrice: v })} /> - setForm({ ...form, isDefault: v })} /> - setForm({ ...form, isActive: v })} /> -
- )} -
- - ) -}