initiating db migration
This commit is contained in:
@@ -29,6 +29,9 @@ public sealed class ProductConfigConfiguration : IEntityTypeConfiguration<Produc
|
|||||||
builder.Property(c => c.SerialNumbersEnabled).IsRequired().HasDefaultValue(true);
|
builder.Property(c => c.SerialNumbersEnabled).IsRequired().HasDefaultValue(true);
|
||||||
builder.Property(c => c.BatchListsEnabled).IsRequired().HasDefaultValue(true);
|
builder.Property(c => c.BatchListsEnabled).IsRequired().HasDefaultValue(true);
|
||||||
builder.Property(c => c.WarrantyPeriodsEnabled).IsRequired().HasDefaultValue(true);
|
builder.Property(c => c.WarrantyPeriodsEnabled).IsRequired().HasDefaultValue(true);
|
||||||
|
builder.Property(c => c.CustomVariantsEnabled).IsRequired().HasDefaultValue(false);
|
||||||
|
builder.Property(c => c.ItemImagesEnabled).IsRequired().HasDefaultValue(false);
|
||||||
|
builder.Property(c => c.CustomWarrantyPeriodsEnabled).IsRequired().HasDefaultValue(false);
|
||||||
|
|
||||||
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||||
builder.Property(c => c.RowVersion).IsRowVersion();
|
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||||
|
|||||||
Generated
+8916
File diff suppressed because it is too large
Load Diff
+225
@@ -0,0 +1,225 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSalesGrnConfigItemImagesWarrantyPeriods : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CustomVariantsEnabled",
|
||||||
|
table: "product_config",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CustomWarrantyPeriodsEnabled",
|
||||||
|
table: "product_config",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ItemImagesEnabled",
|
||||||
|
table: "product_config",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "grn_config",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ConfigId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AllowWarehouseSelection = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
|
||||||
|
DefaultWarehouseId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
AllowDiscounts = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
|
||||||
|
AllowVat = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<int>(type: "int", nullable: true),
|
||||||
|
RowVersion = table.Column<uint>(type: "int unsigned", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_grn_config", x => x.ConfigId);
|
||||||
|
table.CheckConstraint("ck_grn_config_singleton", "\"ConfigId\" = 1");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_grn_config_users_UpdatedBy",
|
||||||
|
column: x => x.UpdatedBy,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "UserId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_grn_config_warehouses_DefaultWarehouseId",
|
||||||
|
column: x => x.DefaultWarehouseId,
|
||||||
|
principalTable: "warehouses",
|
||||||
|
principalColumn: "WarehouseId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
})
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "item_images",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ItemImageId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
ItemId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
OriginalFileName = table.Column<string>(type: "varchar(260)", maxLength: 260, nullable: false, collation: "utf8mb4_unicode_ci"),
|
||||||
|
StoredFileName = table.Column<string>(type: "varchar(260)", maxLength: 260, nullable: false, collation: "utf8mb4_unicode_ci"),
|
||||||
|
RelativePath = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false, collation: "utf8mb4_unicode_ci"),
|
||||||
|
ContentType = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false, collation: "utf8mb4_unicode_ci"),
|
||||||
|
SizeBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UploadedBy = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UploadedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_item_images", x => x.ItemImageId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_item_images_items_ItemId",
|
||||||
|
column: x => x.ItemId,
|
||||||
|
principalTable: "items",
|
||||||
|
principalColumn: "ItemId",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "sales_config",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ConfigId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AllowCashCustomer = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
|
||||||
|
CashCustomerId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
AllowFreeItems = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
|
||||||
|
AllowDiscounts = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
|
||||||
|
AllowOverStockSale = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
|
||||||
|
AllowWarehouseSelection = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
|
||||||
|
DefaultWarehouseId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<int>(type: "int", nullable: true),
|
||||||
|
RowVersion = table.Column<uint>(type: "int unsigned", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_sales_config", x => x.ConfigId);
|
||||||
|
table.CheckConstraint("ck_sales_config_singleton", "\"ConfigId\" = 1");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_sales_config_customers_CashCustomerId",
|
||||||
|
column: x => x.CashCustomerId,
|
||||||
|
principalTable: "customers",
|
||||||
|
principalColumn: "CustomerId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_sales_config_users_UpdatedBy",
|
||||||
|
column: x => x.UpdatedBy,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "UserId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_sales_config_warehouses_DefaultWarehouseId",
|
||||||
|
column: x => x.DefaultWarehouseId,
|
||||||
|
principalTable: "warehouses",
|
||||||
|
principalColumn: "WarehouseId",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
})
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "warranty_periods",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
WarrantyPeriodId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Months = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false, defaultValue: "Active", collation: "utf8mb4_unicode_ci"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_warranty_periods", x => x.WarrantyPeriodId);
|
||||||
|
table.CheckConstraint("ck_warranty_periods_months", "\"Months\" BETWEEN 1 AND 600");
|
||||||
|
})
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_grn_config_DefaultWarehouseId",
|
||||||
|
table: "grn_config",
|
||||||
|
column: "DefaultWarehouseId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_grn_config_UpdatedBy",
|
||||||
|
table: "grn_config",
|
||||||
|
column: "UpdatedBy");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_item_images_ItemId_SortOrder",
|
||||||
|
table: "item_images",
|
||||||
|
columns: new[] { "ItemId", "SortOrder" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_sales_config_CashCustomerId",
|
||||||
|
table: "sales_config",
|
||||||
|
column: "CashCustomerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_sales_config_DefaultWarehouseId",
|
||||||
|
table: "sales_config",
|
||||||
|
column: "DefaultWarehouseId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_sales_config_UpdatedBy",
|
||||||
|
table: "sales_config",
|
||||||
|
column: "UpdatedBy");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_warranty_periods_Months",
|
||||||
|
table: "warranty_periods",
|
||||||
|
column: "Months",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_warranty_periods_Status",
|
||||||
|
table: "warranty_periods",
|
||||||
|
column: "Status");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "grn_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "item_images");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "sales_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "warranty_periods");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CustomVariantsEnabled",
|
||||||
|
table: "product_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CustomWarrantyPeriodsEnabled",
|
||||||
|
table: "product_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ItemImagesEnabled",
|
||||||
|
table: "product_config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1496,6 +1496,51 @@ namespace ERPCore.Migrations
|
|||||||
b.ToTable("grns", (string)null);
|
b.ToTable("grns", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.GrnConfig", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ConfigId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("AllowDiscounts")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("AllowVat")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("AllowWarehouseSelection")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<int?>("DefaultWarehouseId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("int unsigned");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int?>("UpdatedBy")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("ConfigId");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultWarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("UpdatedBy");
|
||||||
|
|
||||||
|
b.ToTable("grn_config", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("ck_grn_config_singleton", "\"ConfigId\" = 1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b =>
|
modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("GrnLineId")
|
b.Property<int>("GrnLineId")
|
||||||
@@ -1792,6 +1837,56 @@ namespace ERPCore.Migrations
|
|||||||
b.ToTable("items", (string)null);
|
b.ToTable("items", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemImage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ItemImageId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ItemImageId"));
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<int>("ItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("varchar(260)");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<long>("SizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("StoredFileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("varchar(260)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UploadedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("UploadedBy")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("ItemImageId");
|
||||||
|
|
||||||
|
b.HasIndex("ItemId", "SortOrder");
|
||||||
|
|
||||||
|
b.ToTable("item_images", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("ReorderId")
|
b.Property<int>("ReorderId")
|
||||||
@@ -3208,6 +3303,21 @@ namespace ERPCore.Migrations
|
|||||||
.HasColumnType("tinyint(1)")
|
.HasColumnType("tinyint(1)")
|
||||||
.HasDefaultValue(true);
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("CustomVariantsEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("CustomWarrantyPeriodsEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("ItemImagesEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<bool>("ItemTypesEnabled")
|
b.Property<bool>("ItemTypesEnabled")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("tinyint(1)")
|
.HasColumnType("tinyint(1)")
|
||||||
@@ -4277,6 +4387,66 @@ namespace ERPCore.Migrations
|
|||||||
b.ToTable("hr_salary_components", (string)null);
|
b.ToTable("hr_salary_components", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.SalesConfig", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ConfigId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("AllowCashCustomer")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("AllowDiscounts")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("AllowFreeItems")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("AllowOverStockSale")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("AllowWarehouseSelection")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<int?>("CashCustomerId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("DefaultWarehouseId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("int unsigned");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int?>("UpdatedBy")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("ConfigId");
|
||||||
|
|
||||||
|
b.HasIndex("CashCustomerId");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultWarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("UpdatedBy");
|
||||||
|
|
||||||
|
b.ToTable("sales_config", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("ck_sales_config_singleton", "\"ConfigId\" = 1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b =>
|
modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("SalesDayEndId")
|
b.Property<int>("SalesDayEndId")
|
||||||
@@ -6507,6 +6677,43 @@ namespace ERPCore.Migrations
|
|||||||
b.ToTable("warehouses", (string)null);
|
b.ToTable("warehouses", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.WarrantyPeriod", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("WarrantyPeriodId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("WarrantyPeriodId"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("Months")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasDefaultValue("Active");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("WarrantyPeriodId");
|
||||||
|
|
||||||
|
b.HasIndex("Months")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("warranty_periods", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("ck_warranty_periods_months", "\"Months\" BETWEEN 1 AND 600");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ERPCore.Domain.Entities.WorkShift", b =>
|
modelBuilder.Entity("ERPCore.Domain.Entities.WorkShift", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("WorkShiftId")
|
b.Property<int>("WorkShiftId")
|
||||||
@@ -6959,6 +7166,23 @@ namespace ERPCore.Migrations
|
|||||||
b.Navigation("Warehouse");
|
b.Navigation("Warehouse");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.GrnConfig", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "DefaultWarehouse")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DefaultWarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UpdatedBy")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("DefaultWarehouse");
|
||||||
|
|
||||||
|
b.Navigation("UpdatedByUser");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b =>
|
modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ERPCore.Domain.Entities.Batch", "Batch")
|
b.HasOne("ERPCore.Domain.Entities.Batch", "Batch")
|
||||||
@@ -7057,6 +7281,17 @@ namespace ERPCore.Migrations
|
|||||||
b.Navigation("TaxRate");
|
b.Navigation("TaxRate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemImage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
@@ -7747,6 +7982,30 @@ namespace ERPCore.Migrations
|
|||||||
b.Navigation("Uom");
|
b.Navigation("Uom");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.SalesConfig", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Customer", "CashCustomer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CashCustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "DefaultWarehouse")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DefaultWarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UpdatedBy")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("CashCustomer");
|
||||||
|
|
||||||
|
b.Navigation("DefaultWarehouse");
|
||||||
|
|
||||||
|
b.Navigation("UpdatedByUser");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b =>
|
modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ERPCore.Domain.Entities.User", "CashierUser")
|
b.HasOne("ERPCore.Domain.Entities.User", "CashierUser")
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ public interface IRepository<T> where T : class
|
|||||||
IQueryable<T> Query();
|
IQueryable<T> Query();
|
||||||
|
|
||||||
Task AddAsync(T entity, CancellationToken ct = default);
|
Task AddAsync(T entity, CancellationToken ct = default);
|
||||||
|
void Update(T entity);
|
||||||
void Remove(T entity);
|
void Remove(T entity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,5 +31,7 @@ public class Repository<T> : IRepository<T> where T : class
|
|||||||
public virtual async Task AddAsync(T entity, CancellationToken ct = default)
|
public virtual async Task AddAsync(T entity, CancellationToken ct = default)
|
||||||
=> await Set.AddAsync(entity, ct);
|
=> await Set.AddAsync(entity, ct);
|
||||||
|
|
||||||
|
public virtual void Update(T entity) => Set.Update(entity);
|
||||||
|
|
||||||
public virtual void Remove(T entity) => Set.Remove(entity);
|
public virtual void Remove(T entity) => Set.Remove(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ public sealed class FifoCostingService : IFifoCostingService
|
|||||||
$"Batch {batchId} expired on {expiry:yyyy-MM-dd} and cannot be issued.", 409);
|
$"Batch {batchId} expired on {expiry:yyyy-MM-dd} and cannot be issued.", 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row-lock the issuable open layers oldest-first (NFR-02), in two steps.
|
// Row-lock the issuable open layers oldest-first (NFR-02), plus any shortfall (negative)
|
||||||
|
// layers left by an over-stock sale, in two steps.
|
||||||
//
|
//
|
||||||
// Why two steps: InnoDB's locking clause applies to EVERY table a statement reads,
|
// Why two steps: InnoDB's locking clause applies to EVERY table a statement reads,
|
||||||
// including rows pulled in by the NOT EXISTS subqueries. A single-statement
|
// including rows pulled in by the NOT EXISTS subqueries. A single-statement
|
||||||
@@ -91,23 +92,27 @@ public sealed class FifoCostingService : IFifoCostingService
|
|||||||
//
|
//
|
||||||
// Excludes on-hold and rejected (grn line) and expired-batch stock — rejected stock
|
// Excludes on-hold and rejected (grn line) and expired-batch stock — rejected stock
|
||||||
// sits quarantined until a Purchase Return consumes it directly (see
|
// sits quarantined until a Purchase Return consumes it directly (see
|
||||||
// PurchaseReturnService), never through this general-issue path.
|
// PurchaseReturnService), never through this general-issue path. The exclusions apply
|
||||||
|
// to positive layers only: a shortfall layer carries no GrnLineId and no BatchId, and
|
||||||
|
// must be settled no matter which batch the caller asked for, so it is never filtered
|
||||||
|
// out here. The batch filter is deliberately NOT in this query for the same reason —
|
||||||
|
// it is applied after settlement, below.
|
||||||
var candidateIds = await _db.Database
|
var candidateIds = await _db.Database
|
||||||
.SqlQuery<int>($"""
|
.SqlQuery<int>($"""
|
||||||
SELECT sl.`LayerId` AS `Value` FROM `stock_layers` sl
|
SELECT sl.`LayerId` AS `Value` FROM `stock_layers` sl
|
||||||
WHERE sl.`ItemId` = {itemId} AND sl.`WarehouseId` = {warehouseId} AND sl.`QtyRemaining` > 0
|
WHERE sl.`ItemId` = {itemId} AND sl.`WarehouseId` = {warehouseId}
|
||||||
AND ({batchId} IS NULL OR sl.`BatchId` = {batchId})
|
AND (sl.`QtyRemaining` < 0 OR (sl.`QtyRemaining` > 0
|
||||||
AND NOT EXISTS (SELECT 1 FROM `grn_lines` gl WHERE gl.`GrnLineId` = sl.`GrnLineId` AND gl.`HoldStatus` IN ('OnHold', 'Rejected'))
|
AND NOT EXISTS (SELECT 1 FROM `grn_lines` gl WHERE gl.`GrnLineId` = sl.`GrnLineId` AND gl.`HoldStatus` IN ('OnHold', 'Rejected'))
|
||||||
AND NOT EXISTS (SELECT 1 FROM `batches` b WHERE b.`BatchId` = sl.`BatchId` AND b.`ExpiryDate` < {today})
|
AND NOT EXISTS (SELECT 1 FROM `batches` b WHERE b.`BatchId` = sl.`BatchId` AND b.`ExpiryDate` < {today})))
|
||||||
ORDER BY sl.`ReceiptDate`, sl.`LayerId`
|
ORDER BY sl.`ReceiptDate`, sl.`LayerId`
|
||||||
""")
|
""")
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
// `IN ()` is a syntax error, so short-circuit to the normal insufficient-stock path.
|
// `IN ()` is a syntax error, so short-circuit to the normal insufficient-stock path.
|
||||||
List<StockLayer> layers;
|
List<StockLayer> locked;
|
||||||
if (candidateIds.Count == 0)
|
if (candidateIds.Count == 0)
|
||||||
{
|
{
|
||||||
layers = new List<StockLayer>();
|
locked = new List<StockLayer>();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -123,34 +128,63 @@ public sealed class FifoCostingService : IFifoCostingService
|
|||||||
// not let the `FOR UPDATE` survive. idList is built from List<int>, so it cannot
|
// not let the `FOR UPDATE` survive. idList is built from List<int>, so it cannot
|
||||||
// contain anything but digits and commas.
|
// contain anything but digits and commas.
|
||||||
#pragma warning disable EF1002 // Possible SQL injection vulnerability — see above.
|
#pragma warning disable EF1002 // Possible SQL injection vulnerability — see above.
|
||||||
layers = await _db.StockLayers
|
locked = await _db.StockLayers
|
||||||
.FromSqlRaw($"""
|
.FromSqlRaw($"""
|
||||||
SELECT sl.* FROM `stock_layers` sl
|
SELECT sl.* FROM `stock_layers` sl
|
||||||
WHERE sl.`LayerId` IN ({idList}) AND sl.`QtyRemaining` > 0
|
WHERE sl.`LayerId` IN ({idList}) AND sl.`QtyRemaining` <> 0
|
||||||
ORDER BY sl.`ReceiptDate`, sl.`LayerId`
|
ORDER BY sl.`ReceiptDate`, sl.`LayerId`
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
""")
|
""")
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
#pragma warning restore EF1002
|
|
||||||
|
|
||||||
// Re-validate under the locks. Between step 1 and step 2 a layer could have gone
|
// Re-validate under the locks. Between step 1 and step 2 a layer could have gone
|
||||||
// on-hold or had its batch expire; now that the rows are locked nothing can change
|
// on-hold or had its batch expire; now that the rows are locked nothing can change
|
||||||
// under us, so this restores the single-statement guarantee.
|
// under us, so this restores the single-statement guarantee.
|
||||||
if (layers.Count > 0)
|
//
|
||||||
|
// SqlQueryRaw, NOT SqlQuery: the interpolating overload takes a FormattableString
|
||||||
|
// and turns every hole into a DbParameter, so `IN ({idList})` would bind the whole
|
||||||
|
// list as ONE string — MariaDB coerces '3,4,5' to the integer 3 and silently keeps
|
||||||
|
// only the first layer. Raw keeps the list as SQL; `today` is the one real value
|
||||||
|
// here and goes through SqlQueryRaw's positional placeholder {0}. Note the $$ raw
|
||||||
|
// string: with two $ the interpolation holes are {{...}}, which leaves the literal
|
||||||
|
// {0} alone for EF to bind rather than C# swallowing it.
|
||||||
|
if (locked.Count > 0)
|
||||||
{
|
{
|
||||||
var stillIssuable = await _db.Database
|
var stillIssuable = await _db.Database
|
||||||
.SqlQuery<int>($"""
|
.SqlQueryRaw<int>($$"""
|
||||||
SELECT sl.`LayerId` AS `Value` FROM `stock_layers` sl
|
SELECT sl.`LayerId` AS `Value` FROM `stock_layers` sl
|
||||||
WHERE sl.`LayerId` IN ({idList})
|
WHERE sl.`LayerId` IN ({{idList}})
|
||||||
AND NOT EXISTS (SELECT 1 FROM `grn_lines` gl WHERE gl.`GrnLineId` = sl.`GrnLineId` AND gl.`HoldStatus` IN ('OnHold', 'Rejected'))
|
AND (sl.`QtyRemaining` < 0 OR (
|
||||||
AND NOT EXISTS (SELECT 1 FROM `batches` b WHERE b.`BatchId` = sl.`BatchId` AND b.`ExpiryDate` < {today})
|
NOT EXISTS (SELECT 1 FROM `grn_lines` gl WHERE gl.`GrnLineId` = sl.`GrnLineId` AND gl.`HoldStatus` IN ('OnHold', 'Rejected'))
|
||||||
""")
|
AND NOT EXISTS (SELECT 1 FROM `batches` b WHERE b.`BatchId` = sl.`BatchId` AND b.`ExpiryDate` < {0})))
|
||||||
|
""", today)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
var issuable = stillIssuable.ToHashSet();
|
var issuable = stillIssuable.ToHashSet();
|
||||||
layers = layers.Where(l => issuable.Contains(l.LayerId)).ToList();
|
locked = locked.Where(l => issuable.Contains(l.LayerId)).ToList();
|
||||||
|
}
|
||||||
|
#pragma warning restore EF1002
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock sold before it arrived is owed first: net shortfall layers off against issuable
|
||||||
|
// stock (oldest-first) before anything new is issued, so it can't be sold twice. The
|
||||||
|
// sale already posted its ledger Out, so settling writes no ledger entry of its own.
|
||||||
|
var owed = locked.Where(l => l.QtyRemaining < 0).ToList();
|
||||||
|
var open = locked.Where(l => l.QtyRemaining > 0).ToList();
|
||||||
|
foreach (var shortfall in owed)
|
||||||
|
{
|
||||||
|
foreach (var layer in open)
|
||||||
|
{
|
||||||
|
if (shortfall.QtyRemaining >= 0) break;
|
||||||
|
var settle = Math.Min(-shortfall.QtyRemaining, layer.QtyRemaining);
|
||||||
|
layer.QtyRemaining -= settle;
|
||||||
|
shortfall.QtyRemaining += settle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only now apply the caller's batch filter: settlement above is deliberately
|
||||||
|
// batch-agnostic (a shortfall layer has no batch), but the issue itself is not.
|
||||||
|
var layers = open.Where(l => l.QtyRemaining > 0 && (batchId == null || l.BatchId == batchId)).ToList();
|
||||||
|
|
||||||
var available = layers.Sum(l => l.QtyRemaining);
|
var available = layers.Sum(l => l.QtyRemaining);
|
||||||
if (available < qtyBase && !allowShortfall)
|
if (available < qtyBase && !allowShortfall)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user