bug fixers 10/20
This commit is contained in:
@@ -92,8 +92,9 @@ public sealed class SalesInvoicesController : ApiControllerBase
|
||||
[FromQuery] DateOnly? to,
|
||||
[FromQuery] int? createdBy,
|
||||
[FromQuery] bool? paid,
|
||||
[FromQuery] bool? overdue,
|
||||
CancellationToken ct)
|
||||
=> Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, branchId, from, to, createdBy, paid, ct));
|
||||
=> Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, branchId, from, to, createdBy, paid, overdue, ct));
|
||||
|
||||
// Totals for the list's stat cards: same filters as List, but over every match rather than one page.
|
||||
[HttpGet("totals")]
|
||||
@@ -108,8 +109,9 @@ public sealed class SalesInvoicesController : ApiControllerBase
|
||||
[FromQuery] DateOnly? to,
|
||||
[FromQuery] int? createdBy,
|
||||
[FromQuery] bool? paid,
|
||||
[FromQuery] bool? overdue,
|
||||
CancellationToken ct)
|
||||
=> Ok(await _invoices.TotalsAsync(q, status, customerId, warehouseId, branchId, from, to, createdBy, paid, ct));
|
||||
=> Ok(await _invoices.TotalsAsync(q, status, customerId, warehouseId, branchId, from, to, createdBy, paid, overdue, ct));
|
||||
|
||||
[HttpGet("{salesInvoiceId:int}")]
|
||||
[ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -51,6 +51,31 @@ public class ProductConfig
|
||||
/// </summary>
|
||||
public bool CustomWarrantyPeriodsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Shows the stock-nature choice (Stocked / Non-stocked / Service) on item screens. Off:
|
||||
/// new items are Stocked. Enforced on item writes - an item keeps the nature it already
|
||||
/// has, only a change <em>to</em> a non-Stocked nature is refused.
|
||||
/// </summary>
|
||||
public bool StockNatureEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the tracking-mode choice on item screens. Off: new items are untracked (None).
|
||||
/// Enforced on item writes like <see cref="StockNatureEnabled"/> - existing modes are kept.
|
||||
/// </summary>
|
||||
public bool TrackingModeEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the low-stock alert (reorder point / qty) section on item create and the item page.
|
||||
/// Advisory: it gates the item screens; reorder rules already saved keep working.
|
||||
/// </summary>
|
||||
public bool LowStockAlertsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Lets the low-stock alert be pinned to one warehouse. Off: the alert watches every
|
||||
/// warehouse. Depends on <see cref="LowStockAlertsEnabled"/>; advisory like it.
|
||||
/// </summary>
|
||||
public bool AlertWarehouseEnabled { get; set; } = true;
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public int? UpdatedBy { get; set; }
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed record ProductConfigDto(
|
||||
bool SellingPricesEnabled, bool SerialNumbersEnabled, bool BatchListsEnabled, bool WarrantyPeriodsEnabled,
|
||||
bool CustomVariantsEnabled, bool ItemImagesEnabled,
|
||||
bool CustomWarrantyPeriodsEnabled,
|
||||
bool StockNatureEnabled, bool TrackingModeEnabled, bool LowStockAlertsEnabled, bool AlertWarehouseEnabled,
|
||||
DateTime? UpdatedAt, int? UpdatedBy);
|
||||
|
||||
/// <summary>
|
||||
@@ -37,4 +38,8 @@ public sealed class UpdateProductConfigRequest
|
||||
[Required] public bool? CustomVariantsEnabled { get; set; }
|
||||
[Required] public bool? ItemImagesEnabled { get; set; }
|
||||
[Required] public bool? CustomWarrantyPeriodsEnabled { get; set; }
|
||||
[Required] public bool? StockNatureEnabled { get; set; }
|
||||
[Required] public bool? TrackingModeEnabled { get; set; }
|
||||
[Required] public bool? LowStockAlertsEnabled { get; set; }
|
||||
[Required] public bool? AlertWarehouseEnabled { get; set; }
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ public sealed class ProductConfigConfiguration : IEntityTypeConfiguration<Produc
|
||||
builder.Property(c => c.CustomVariantsEnabled).IsRequired().HasDefaultValue(false);
|
||||
builder.Property(c => c.ItemImagesEnabled).IsRequired().HasDefaultValue(false);
|
||||
builder.Property(c => c.CustomWarrantyPeriodsEnabled).IsRequired().HasDefaultValue(false);
|
||||
builder.Property(c => c.StockNatureEnabled).IsRequired().HasDefaultValue(true);
|
||||
builder.Property(c => c.TrackingModeEnabled).IsRequired().HasDefaultValue(true);
|
||||
builder.Property(c => c.LowStockAlertsEnabled).IsRequired().HasDefaultValue(true);
|
||||
builder.Property(c => c.AlertWarehouseEnabled).IsRequired().HasDefaultValue(true);
|
||||
|
||||
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||
|
||||
@@ -213,7 +213,11 @@ public static class DataSeeder
|
||||
WarrantyPeriodsEnabled = true,
|
||||
CustomVariantsEnabled = false,
|
||||
ItemImagesEnabled = false,
|
||||
CustomWarrantyPeriodsEnabled = false
|
||||
CustomWarrantyPeriodsEnabled = false,
|
||||
StockNatureEnabled = true,
|
||||
TrackingModeEnabled = true,
|
||||
LowStockAlertsEnabled = true,
|
||||
AlertWarehouseEnabled = true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
+11566
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddItemFieldSwitches : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AlertWarehouseEnabled",
|
||||
table: "product_config",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "LowStockAlertsEnabled",
|
||||
table: "product_config",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "StockNatureEnabled",
|
||||
table: "product_config",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "TrackingModeEnabled",
|
||||
table: "product_config",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlertWarehouseEnabled",
|
||||
table: "product_config");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LowStockAlertsEnabled",
|
||||
table: "product_config");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StockNatureEnabled",
|
||||
table: "product_config");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TrackingModeEnabled",
|
||||
table: "product_config");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4862,6 +4862,11 @@ namespace ERPCore.Migrations
|
||||
b.Property<int>("ConfigId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("AlertWarehouseEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("BatchListsEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
@@ -4892,6 +4897,11 @@ namespace ERPCore.Migrations
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("LowStockAlertsEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("int unsigned");
|
||||
@@ -4906,11 +4916,21 @@ namespace ERPCore.Migrations
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("StockNatureEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("SubcategoriesEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("TrackingModeEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ namespace ERPCore.Services.Interfaces;
|
||||
|
||||
public interface ISalesInvoiceService
|
||||
{
|
||||
Task<PagedResponse<SalesInvoiceSummaryDto>> ListAsync(PageQuery query, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default);
|
||||
Task<PagedResponse<SalesInvoiceSummaryDto>> ListAsync(PageQuery query, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, bool? overdue = null, CancellationToken ct = default);
|
||||
/// <summary>Count and amount totals over every invoice matching the same filters as <see cref="ListAsync"/>.</summary>
|
||||
Task<SalesInvoiceListTotalsDto> TotalsAsync(string? q, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default);
|
||||
Task<SalesInvoiceListTotalsDto> TotalsAsync(string? q, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, bool? overdue = null, CancellationToken ct = default);
|
||||
Task<ETagged<SalesInvoiceDto>?> GetAsync(int salesInvoiceId, CancellationToken ct = default);
|
||||
Task<SalesInvoicePostingCheckDto> CheckPostingAsync(int salesInvoiceId, CancellationToken ct = default);
|
||||
Task<ETagged<SalesInvoiceDto>> CreateAsync(CreateSalesInvoiceRequest request, CancellationToken ct = default);
|
||||
|
||||
@@ -321,7 +321,7 @@ public sealed class ItemService : IItemService
|
||||
await ValidateReferencesAsync(
|
||||
request.CategoryId, request.SubCategoryId, request.BrandId,
|
||||
request.BaseUomId, request.DefaultVendorId, request.TaxRateId, config, ct);
|
||||
ValidateFeatureConfiguration(request, config, item.TrackingMode);
|
||||
ValidateFeatureConfiguration(request, config, item.TrackingMode, item.StockNature);
|
||||
|
||||
// The base UOM is the sole meaning of every quantity recorded against this item —
|
||||
// stock layers, ledger rows and document lines are all plain counts of it. Once any
|
||||
@@ -520,22 +520,38 @@ public sealed class ItemService : IItemService
|
||||
private static void ValidateFeatureConfiguration(CreateItemRequest request, ProductConfigDto config)
|
||||
=> ValidateFeatureConfiguration(
|
||||
request.SellingMinPrice, request.SellingMaxPrice, request.SellingFlatPrice,
|
||||
request.TrackingMode, request.Warranty, config);
|
||||
request.TrackingMode, request.StockNature, request.Warranty, config);
|
||||
|
||||
/// <summary>
|
||||
/// An item keeps the tracking mode it already has even after that feature is switched off —
|
||||
/// otherwise it could not be edited at all; only a change <em>to</em> a disabled mode is refused.
|
||||
/// An item keeps the tracking mode and stock nature it already has even after that feature is
|
||||
/// switched off — otherwise it could not be edited at all; only a change <em>to</em> a disabled
|
||||
/// value is refused.
|
||||
/// </summary>
|
||||
private static void ValidateFeatureConfiguration(UpdateItemRequest request, ProductConfigDto config, TrackingMode currentTrackingMode)
|
||||
private static void ValidateFeatureConfiguration(
|
||||
UpdateItemRequest request, ProductConfigDto config, TrackingMode currentTrackingMode, StockNature currentStockNature)
|
||||
=> ValidateFeatureConfiguration(
|
||||
request.SellingMinPrice, request.SellingMaxPrice, request.SellingFlatPrice,
|
||||
request.TrackingMode, request.Warranty, config, currentTrackingMode);
|
||||
request.TrackingMode, request.StockNature, request.Warranty, config, currentTrackingMode, currentStockNature);
|
||||
|
||||
private static void ValidateFeatureConfiguration(
|
||||
decimal? sellingMinPrice, decimal? sellingMaxPrice, decimal? sellingFlatPrice,
|
||||
TrackingMode trackingMode, Warranty warranty, ProductConfigDto config, TrackingMode? currentTrackingMode = null)
|
||||
TrackingMode trackingMode, StockNature stockNature, Warranty warranty, ProductConfigDto config,
|
||||
TrackingMode? currentTrackingMode = null, StockNature? currentStockNature = null)
|
||||
{
|
||||
var trackingUnchanged = trackingMode == currentTrackingMode;
|
||||
|
||||
if (!config.StockNatureEnabled && stockNature != StockNature.Stocked && stockNature != currentStockNature)
|
||||
throw new DomainException(
|
||||
ErrorCodes.ConfigDisabled,
|
||||
"Stock nature is disabled in the product configuration; stockNature must be Stocked.",
|
||||
422);
|
||||
|
||||
if (!config.TrackingModeEnabled && trackingMode != TrackingMode.None && !trackingUnchanged)
|
||||
throw new DomainException(
|
||||
ErrorCodes.ConfigDisabled,
|
||||
"Tracking mode is disabled in the product configuration; trackingMode must be None.",
|
||||
422);
|
||||
|
||||
if (!config.SellingPricesEnabled &&
|
||||
(sellingMinPrice is not null || sellingMaxPrice is not null || sellingFlatPrice is not null))
|
||||
throw new DomainException(
|
||||
|
||||
@@ -62,6 +62,12 @@ public sealed class ProductConfigService : IProductConfigService
|
||||
// Custom warranty periods depend on warranty periods - switching the parent off
|
||||
// switches this off too, so it can never be left on by itself.
|
||||
config.CustomWarrantyPeriodsEnabled = config.WarrantyPeriodsEnabled && request.CustomWarrantyPeriodsEnabled!.Value;
|
||||
config.StockNatureEnabled = request.StockNatureEnabled!.Value;
|
||||
config.TrackingModeEnabled = request.TrackingModeEnabled!.Value;
|
||||
config.LowStockAlertsEnabled = request.LowStockAlertsEnabled!.Value;
|
||||
// The warehouse picker lives in the low-stock alert section - switching alerts off
|
||||
// switches this off too, so it can never be left on by itself.
|
||||
config.AlertWarehouseEnabled = config.LowStockAlertsEnabled && request.AlertWarehouseEnabled!.Value;
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
config.UpdatedBy = _currentUser.AuditUserId;
|
||||
|
||||
@@ -82,5 +88,6 @@ public sealed class ProductConfigService : IProductConfigService
|
||||
c.SellingPricesEnabled, c.SerialNumbersEnabled, c.BatchListsEnabled, c.WarrantyPeriodsEnabled,
|
||||
c.CustomVariantsEnabled, c.ItemImagesEnabled,
|
||||
c.CustomWarrantyPeriodsEnabled,
|
||||
c.StockNatureEnabled, c.TrackingModeEnabled, c.LowStockAlertsEnabled, c.AlertWarehouseEnabled,
|
||||
c.UpdatedAt, c.UpdatedBy);
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<SalesInvoiceSummaryDto>> ListAsync(PageQuery query, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default)
|
||||
public async Task<PagedResponse<SalesInvoiceSummaryDto>> ListAsync(PageQuery query, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, bool? overdue = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = Filter(_invoices.Query().AsNoTracking().Include(x => x.Lines), query.Q, status, customerId, warehouseId, branchId, from, to, createdBy, paid);
|
||||
var q = Filter(_invoices.Query().AsNoTracking().Include(x => x.Lines), query.Q, status, customerId, warehouseId, branchId, from, to, createdBy, paid, overdue);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(x => x.SalesInvoiceId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
|
||||
@@ -61,9 +61,9 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
return PagedResponse<SalesInvoiceSummaryDto>.Create(rows.Select(MapSummary).ToList(), query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<SalesInvoiceListTotalsDto> TotalsAsync(string? q, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default)
|
||||
public async Task<SalesInvoiceListTotalsDto> TotalsAsync(string? q, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, bool? overdue = null, CancellationToken ct = default)
|
||||
{
|
||||
var filtered = Filter(_invoices.Query().AsNoTracking(), q, status, customerId, warehouseId, branchId, from, to, createdBy, paid);
|
||||
var filtered = Filter(_invoices.Query().AsNoTracking(), q, status, customerId, warehouseId, branchId, from, to, createdBy, paid, overdue);
|
||||
// Overdue = due date (InvoiceDate) before today with money still owed — the list's red "Due" rule.
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
||||
@@ -86,7 +86,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
}
|
||||
|
||||
/// <summary>The list's filters, shared with <see cref="TotalsAsync"/> so the totals always cover exactly the listed rows.</summary>
|
||||
private static IQueryable<SalesInvoice> Filter(IQueryable<SalesInvoice> q, string? search, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from, DateOnly? to, int? createdBy, bool? paid)
|
||||
private static IQueryable<SalesInvoice> Filter(IQueryable<SalesInvoice> q, string? search, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from, DateOnly? to, int? createdBy, bool? paid, bool? overdue = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
@@ -106,6 +106,12 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
||||
// paid=true: nothing left to pay; paid=false: a balance is still due.
|
||||
if (paid == true) q = q.Where(x => x.BalanceAmount <= 0);
|
||||
if (paid == false) q = q.Where(x => x.BalanceAmount > 0);
|
||||
// overdue=true: past its due date (InvoiceDate) with money still owed — the Overdue card's rule.
|
||||
if (overdue == true)
|
||||
{
|
||||
var today = DateTime.UtcNow.Date;
|
||||
q = q.Where(x => x.BalanceAmount > 0 && x.InvoiceDate < today);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user