0
0

Update products class

This commit is contained in:
Sergey Chebotar 2023-05-11 07:55:26 +03:00
parent 49381b06a7
commit 3ab838e27f
15 changed files with 115 additions and 265 deletions

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using RhSolutions.Api.Models; using RhSolutions.Models;
using RhSolutions.Api.Services; using RhSolutions.Api.Services;
using System.Linq;
namespace RhSolutions.Api.Controllers namespace RhSolutions.Api.Controllers
{ {
@ -31,17 +32,25 @@ namespace RhSolutions.Api.Controllers
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> PostProductsFromXls() public IActionResult PostProductsFromXls()
{ {
try try
{ {
var products = parser.GetProducts(HttpContext); var products = parser.GetProducts(HttpContext).GroupBy(p => p.ProductSku)
await foreach (var p in products) .Select(g => new Product(g.Key)
{
using (p)
{ {
dbContext.Add<Product>(p); Name = g.First().Name,
} DeprecatedSkus = g.SelectMany(p => p.DeprecatedSkus).ToList(),
ProductLines = g.SelectMany(p => p.ProductLines).Distinct().ToList(),
IsOnWarehouse = g.Any(p => p.IsOnWarehouse == true),
ProductMeasure = g.First().ProductMeasure,
DeliveryMakeUp = g.First().DeliveryMakeUp,
Price = g.First().Price
});
foreach (var p in products)
{
dbContext.Add<Product>(p);
} }
dbContext.SaveChanges(); dbContext.SaveChanges();

View File

@ -1,28 +1,27 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RhSolutions.Api.Models; using RhSolutions.Models;
namespace RhSolutions.Api.Controllers namespace RhSolutions.Api.Controllers
{ {
[Route("api/[controller]")] [Route("api/[controller]")]
public class SearchController : ControllerBase public class SearchController : ControllerBase
{ {
private RhSolutionsContext context; private RhSolutionsContext context;
public SearchController(RhSolutionsContext context) public SearchController(RhSolutionsContext context)
{ {
this.context = context; this.context = context;
} }
[HttpGet] [HttpGet]
public IAsyncEnumerable<Product> SearchProducts([FromQuery] string query) public IAsyncEnumerable<Product> SearchProducts([FromQuery] string query)
{ {
return context.Products return context.Products
.Where(p => EF.Functions.ToTsVector( .Where(p => EF.Functions.ToTsVector(
"russian", string.Join(' ', "russian", string.Join(' ', new[] { p.Name, string.Join(' ', p.ProductLines)}))
new [] {p.ProductLine ?? string.Empty, p.Name ?? string.Empty })) .Matches(EF.Functions.WebSearchToTsQuery("russian", query)))
.Matches(EF.Functions.WebSearchToTsQuery("russian", query))) .AsAsyncEnumerable();
.AsAsyncEnumerable(); }
} }
}
} }

View File

@ -7,19 +7,19 @@ CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
START TRANSACTION; START TRANSACTION;
CREATE TABLE "Products" ( CREATE TABLE "Products" (
"Id" integer GENERATED BY DEFAULT AS IDENTITY, "Id" text NOT NULL,
"ProductSku" text NULL,
"DeprecatedSkus" text[] NOT NULL,
"Name" text NOT NULL, "Name" text NOT NULL,
"ProductLine" text NULL, "ProductSku" text NOT NULL,
"IsOnWarehouse" boolean NULL, "DeprecatedSkus" text[] NOT NULL,
"ProductLines" text[] NOT NULL,
"IsOnWarehouse" boolean NOT NULL,
"ProductMeasure" integer NOT NULL, "ProductMeasure" integer NOT NULL,
"DeliveryMakeUp" double precision NULL, "DeliveryMakeUp" double precision NULL,
"Price" numeric(8,2) NOT NULL, "Price" numeric NOT NULL,
CONSTRAINT "PK_Products" PRIMARY KEY ("Id") CONSTRAINT "PK_Products" PRIMARY KEY ("Id")
); );
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20221201071323_Init', '7.0.0'); VALUES ('20230511043408_Init', '7.0.5');
COMMIT; COMMIT;

View File

@ -6,14 +6,14 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using RhSolutions.Api.Models; using RhSolutions.Models;
#nullable disable #nullable disable
namespace RhSolutions.Api.Migrations namespace RhSolutions.Api.Migrations
{ {
[DbContext(typeof(RhSolutionsContext))] [DbContext(typeof(RhSolutionsContext))]
[Migration("20221201071323_Init")] [Migration("20230511043408_Init")]
partial class Init partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -21,18 +21,15 @@ namespace RhSolutions.Api.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "7.0.0") .HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("RhSolutions.Api.Models.Product", b => modelBuilder.Entity("RhSolutions.Models.Product", b =>
{ {
b.Property<int>("Id") b.Property<string>("Id")
.ValueGeneratedOnAdd() .HasColumnType("text");
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double?>("DeliveryMakeUp") b.Property<double?>("DeliveryMakeUp")
.HasColumnType("double precision"); .HasColumnType("double precision");
@ -41,7 +38,7 @@ namespace RhSolutions.Api.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<bool?>("IsOnWarehouse") b.Property<bool>("IsOnWarehouse")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("Name") b.Property<string>("Name")
@ -49,15 +46,17 @@ namespace RhSolutions.Api.Migrations
.HasColumnType("text"); .HasColumnType("text");
b.Property<decimal>("Price") b.Property<decimal>("Price")
.HasColumnType("decimal(8,2)"); .HasColumnType("numeric");
b.Property<string>("ProductLine") b.Property<List<string>>("ProductLines")
.HasColumnType("text"); .IsRequired()
.HasColumnType("text[]");
b.Property<int>("ProductMeasure") b.Property<int>("ProductMeasure")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("ProductSku") b.Property<string>("ProductSku")
.IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");

View File

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
@ -16,16 +15,15 @@ namespace RhSolutions.Api.Migrations
name: "Products", name: "Products",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(type: "integer", nullable: false) Id = table.Column<string>(type: "text", nullable: false),
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ProductSku = table.Column<string>(type: "text", nullable: true),
DeprecatedSkus = table.Column<List<string>>(type: "text[]", nullable: false),
Name = table.Column<string>(type: "text", nullable: false), Name = table.Column<string>(type: "text", nullable: false),
ProductLine = table.Column<string>(type: "text", nullable: true), ProductSku = table.Column<string>(type: "text", nullable: false),
IsOnWarehouse = table.Column<bool>(type: "boolean", nullable: true), DeprecatedSkus = table.Column<List<string>>(type: "text[]", nullable: false),
ProductLines = table.Column<List<string>>(type: "text[]", nullable: false),
IsOnWarehouse = table.Column<bool>(type: "boolean", nullable: false),
ProductMeasure = table.Column<int>(type: "integer", nullable: false), ProductMeasure = table.Column<int>(type: "integer", nullable: false),
DeliveryMakeUp = table.Column<double>(type: "double precision", nullable: true), DeliveryMakeUp = table.Column<double>(type: "double precision", nullable: true),
Price = table.Column<decimal>(type: "numeric(8,2)", nullable: false) Price = table.Column<decimal>(type: "numeric", nullable: false)
}, },
constraints: table => constraints: table =>
{ {

View File

@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using RhSolutions.Api.Models; using RhSolutions.Models;
#nullable disable #nullable disable
@ -18,18 +18,15 @@ namespace RhSolutions.Api.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "7.0.0") .HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("RhSolutions.Api.Models.Product", b => modelBuilder.Entity("RhSolutions.Models.Product", b =>
{ {
b.Property<int>("Id") b.Property<string>("Id")
.ValueGeneratedOnAdd() .HasColumnType("text");
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double?>("DeliveryMakeUp") b.Property<double?>("DeliveryMakeUp")
.HasColumnType("double precision"); .HasColumnType("double precision");
@ -38,7 +35,7 @@ namespace RhSolutions.Api.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<bool?>("IsOnWarehouse") b.Property<bool>("IsOnWarehouse")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("Name") b.Property<string>("Name")
@ -46,15 +43,17 @@ namespace RhSolutions.Api.Migrations
.HasColumnType("text"); .HasColumnType("text");
b.Property<decimal>("Price") b.Property<decimal>("Price")
.HasColumnType("decimal(8,2)"); .HasColumnType("numeric");
b.Property<string>("ProductLine") b.Property<List<string>>("ProductLines")
.HasColumnType("text"); .IsRequired()
.HasColumnType("text[]");
b.Property<int>("ProductMeasure") b.Property<int>("ProductMeasure")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("ProductSku") b.Property<string>("ProductSku")
.IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");

View File

@ -1,4 +0,0 @@
namespace RhSolutions.Api.Models
{
public enum Measure { Kg, M, M2, P }
}

View File

@ -1,34 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Text.Json.Serialization;
namespace RhSolutions.Api.Models
{
public partial class Product : IDisposable
{
[Key]
[JsonIgnore]
public int Id { get; set; }
public string ProductSku { get; set; } = string.Empty;
public List<string> DeprecatedSkus { get; set; } = new();
public string Name { get; set; } = string.Empty;
public string ProductLine { get; set; } = string.Empty;
public bool? IsOnWarehouse { get; set; }
public Measure ProductMeasure { get; set; }
public double? DeliveryMakeUp { get; set; }
[Column(TypeName = "decimal(8,2)")]
public decimal Price { get; set; }
public void Dispose()
{
Debug.WriteLine($"{this} disposed");
}
public override string ToString()
{
return $"({ProductSku}) {Name}";
}
}
}

View File

@ -1,11 +1,21 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace RhSolutions.Api.Models; namespace RhSolutions.Models;
public class RhSolutionsContext : DbContext public class RhSolutionsContext : DbContext
{ {
public RhSolutionsContext(DbContextOptions<RhSolutionsContext> options) public RhSolutionsContext(DbContextOptions<RhSolutionsContext> options)
: base(options) { } : base(options) { }
public DbSet<Product> Products => Set<Product>(); public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Product>()
.Property(e => e.ProductSku)
.HasConversion(v => v.ToString(), v => new ProductSku(v));
builder.Entity<Product>()
.Property(e => e.DeprecatedSkus)
.HasPostgresArrayConversion<ProductSku, string>(v => v.ToString(), v => new ProductSku(v));
}
} }

View File

@ -1,127 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace RhSolutions.Api.Models
{
public class Sku
{
private const string matchPattern = @"([1\D]|\b)(?<Article>\d{6})([1\s-]|)(?<Variant>\d{3})\b";
private string? _article;
private string? _variant;
public Sku(string article, string variant)
{
Article = article;
Variant = variant;
}
[Key]
public string Id
{
get
{
return $"1{Article}1{Variant}";
}
set
{
if (TryParse(value, out IEnumerable<Sku> skus))
{
if (skus.Count() > 1)
{
throw new ArgumentException($"More than one valid sku detected: {value}");
}
else
{
this.Article = skus.First().Article;
this.Variant = skus.First().Variant;
}
}
else
{
throw new ArgumentException($"Invalid sku input: {value}");
}
}
}
public string? Article
{
get
{
return _article;
}
set
{
if (value == null || value.Length != 6 || value.Where(c => char.IsDigit(c)).Count() != 6)
{
throw new ArgumentException($"Wrong Article: {Article}");
}
else
{
_article = value;
}
}
}
public string? Variant
{
get
{
return _variant;
}
set
{
if (value == null || value.Length != 3 || value.Where(c => char.IsDigit(c)).Count() != 3)
{
throw new ArgumentException($"Wrong Variant: {Variant}");
}
else _variant = value;
}
}
public static IEnumerable<Sku> GetValidSkus(string line)
{
MatchCollection matches = Regex.Matches(line, matchPattern);
if (matches.Count == 0)
{
yield break;
}
else
{
foreach (Match m in matches)
{
yield return new Sku(m.Groups["Article"].Value, m.Groups["Variant"].Value);
}
}
}
public static bool TryParse(string line, out IEnumerable<Sku> skus)
{
MatchCollection matches = Regex.Matches(line, matchPattern);
if (matches.Count == 0)
{
skus = Enumerable.Empty<Sku>();
return false;
}
else
{
skus = GetValidSkus(line);
return true;
}
}
public override bool Equals(object? obj)
{
return obj is Sku sku &&
Article!.Equals(sku.Article) &&
Variant!.Equals(sku.Variant);
}
public override int GetHashCode()
{
return HashCode.Combine(Article, Variant);
}
public override string ToString()
{
return $"1{Article}1{Variant}";
}
}
}

View File

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RhSolutions.Api.Models; using RhSolutions.Models;
using RhSolutions.Api.Services; using RhSolutions.Api.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);

View File

@ -8,13 +8,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.97.0" /> <PackageReference Include="ClosedXML" Version="0.101.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="Rhsolutions.ProductSku" Version="0.2.13" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,35 +1,36 @@
using ClosedXML.Excel; using ClosedXML.Excel;
using RhSolutions.Api.Models; using RhSolutions.Models;
namespace RhSolutions.Api.Services namespace RhSolutions.Api.Services
{ {
public class ClosedXMLParser : IPricelistParser public class ClosedXMLParser : IPricelistParser
{ {
public async IAsyncEnumerable<Product> GetProducts(HttpContext context) public List<Product> GetProducts(HttpContext context)
{ {
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {
if (context == null) if (context == null)
{ {
yield break; return new List<Product>();
} }
await context.Request.Body.CopyToAsync(memoryStream); context.Request.Body.CopyToAsync(memoryStream).GetAwaiter().GetResult();
List<Product> products = new();
using (var wb = new XLWorkbook(memoryStream)) using (var wb = new XLWorkbook(memoryStream))
{ {
Dictionary<ProductSku, Product> collected = new();
var table = GetTable(wb); var table = GetTable(wb);
var rows = table.DataRange.Rows(); var rows = table.DataRange.Rows();
var enumerator = rows.GetEnumerator(); foreach (var row in rows)
while (enumerator.MoveNext())
{ {
if (Sku.TryParse(enumerator.Current.Field("Актуальный материал") if (ProductSku.TryParse(row.Field("Актуальный материал")
.GetString(), out IEnumerable<Sku> skus)) .GetString(), out _))
{ {
yield return ParseRow(enumerator.Current); products.Add(ParseRow(row));
} }
} }
yield break;
} }
return products;
} }
} }
private IXLTable GetTable(XLWorkbook wb) private IXLTable GetTable(XLWorkbook wb)
@ -60,10 +61,10 @@ namespace RhSolutions.Api.Services
.GetString() .GetString()
.Split('\n') .Split('\n')
.First(); .First();
Sku.TryParse(row.Field("Актуальный материал") ProductSku.TryParse(row.Field("Актуальный материал")
.GetString(), out IEnumerable<Sku> productSkus); .GetString(), out IEnumerable<ProductSku> productSkus);
Sku.TryParse(row.Field("Прежний материал") ProductSku.TryParse(row.Field("Прежний материал")
.GetString(), out IEnumerable<Sku> deprecatedSkus); .GetString(), out IEnumerable<ProductSku> deprecatedSkus);
string measureField = new string(row.Field("Ед. изм.") string measureField = new string(row.Field("Ед. изм.")
.GetString() .GetString()
@ -100,7 +101,7 @@ namespace RhSolutions.Api.Services
string onWarehouseField = row.Field("Складская программа") string onWarehouseField = row.Field("Складская программа")
.GetString(); .GetString();
bool? IsOnWarehouse; bool IsOnWarehouse;
switch (onWarehouseField) switch (onWarehouseField)
{ {
@ -111,7 +112,7 @@ namespace RhSolutions.Api.Services
IsOnWarehouse = false; IsOnWarehouse = false;
break; break;
default: default:
IsOnWarehouse = null; IsOnWarehouse = false;
break; break;
} }
@ -123,12 +124,11 @@ namespace RhSolutions.Api.Services
price = 0.0M; price = 0.0M;
} }
return new Product return new Product(productSkus.First())
{ {
ProductLine = productLine, ProductLines = new List<string>() { productLine },
Name = productName, Name = productName,
ProductSku = productSkus.First().Id, DeprecatedSkus = deprecatedSkus.ToList(),
DeprecatedSkus = deprecatedSkus.Select(s => s.Id).ToList(),
ProductMeasure = productMeasure, ProductMeasure = productMeasure,
DeliveryMakeUp = productWarehouseCount, DeliveryMakeUp = productWarehouseCount,
IsOnWarehouse = IsOnWarehouse, IsOnWarehouse = IsOnWarehouse,

View File

@ -1,9 +1,9 @@
using RhSolutions.Api.Models; using RhSolutions.Models;
namespace RhSolutions.Api.Services namespace RhSolutions.Api.Services
{ {
public interface IPricelistParser public interface IPricelistParser
{ {
public IAsyncEnumerable<Product> GetProducts(HttpContext context); public List<Product> GetProducts(HttpContext context);
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"sdk": { "sdk": {
"version": "6.0.402" "version": "6.0.311"
} }
} }