bug fixers 10/20

This commit is contained in:
2026-10-20 15:33:19 +05:30
parent 0e423b806c
commit 47479fb4f2
43 changed files with 13191 additions and 811 deletions
@@ -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);
+23 -7
View File
@@ -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;
}