add day-end and change item model

This commit is contained in:
Dhananjaya99
2026-08-20 14:10:39 +05:30
parent 8835ba9500
commit 4cc6a546c7
43 changed files with 8955 additions and 1680 deletions
@@ -85,6 +85,45 @@ public sealed class BundleSaleService : IBundleSaleService
x.BundleSaleTemplateLineId, x.ItemId, x.WarehouseId, x.Qty, x.UnitPrice, x.IncludeInBundle, x.SortOrder)).ToList());
}
public async Task<BundleSaleTemplateDto> CreateTemplateAsync(CreateBundleSaleTemplateRequest request, CancellationToken ct = default)
{
var template = new BundleSaleTemplate
{
TemplateCode = await _numbers.NextAsync(DocumentTypes.BundleSaleTemplate, ct),
TemplateName = request.TemplateName,
Description = request.Description,
Status = EntityStatus.Active,
CreatedAt = DateTime.UtcNow
};
foreach (var line in request.Lines)
{
if (!await _items.Query().AsNoTracking().AnyAsync(x => x.ItemId == line.ItemId, ct))
throw new NotFoundException($"Item {line.ItemId} was not found.");
if (!await _warehouses.Query().AsNoTracking().AnyAsync(x => x.WarehouseId == line.WarehouseId, ct))
throw new NotFoundException($"Warehouse {line.WarehouseId} was not found.");
template.Lines.Add(new BundleSaleTemplateLine
{
ItemId = line.ItemId,
WarehouseId = line.WarehouseId,
Qty = line.Qty,
UnitPrice = line.UnitPrice,
IncludeInBundle = line.IncludeInBundle,
SortOrder = line.SortOrder
});
}
await _templates.AddAsync(template, ct);
await _uow.SaveChangesAsync(ct);
return new BundleSaleTemplateDto(
template.BundleSaleTemplateId, template.TemplateCode, template.TemplateName, template.Description,
template.Status, template.CreatedAt, template.UpdatedAt,
template.Lines.OrderBy(x => x.SortOrder).Select(x => new BundleSaleTemplateLineDto(
x.BundleSaleTemplateLineId, x.ItemId, x.WarehouseId, x.Qty, x.UnitPrice, x.IncludeInBundle, x.SortOrder)).ToList());
}
public async Task<PagedResponse<BundleSaleSummaryDto>> ListAsync(PageQuery query, BundleSaleStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default)
{
IQueryable<BundleSale> q = _bundles.Query().AsNoTracking().Include(x => x.Lines);
@@ -9,6 +9,7 @@ public interface IBundleSaleService
{
Task<PagedResponse<BundleSaleTemplateSummaryDto>> ListTemplatesAsync(PageQuery query, CancellationToken ct = default);
Task<BundleSaleTemplateDto?> GetTemplateAsync(int bundleSaleTemplateId, CancellationToken ct = default);
Task<BundleSaleTemplateDto> CreateTemplateAsync(CreateBundleSaleTemplateRequest request, CancellationToken ct = default);
Task<PagedResponse<BundleSaleSummaryDto>> ListAsync(PageQuery query, BundleSaleStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default);
Task<BundleSaleDto?> GetAsync(int bundleSaleId, CancellationToken ct = default);
Task<BundleSalePostingCheckDto> CheckPostingAsync(int bundleSaleId, CancellationToken ct = default);
+10 -4
View File
@@ -85,7 +85,8 @@ public sealed class ItemService : IItemService
.Select(i => new ItemListItemDto(
i.ItemId, i.Sku, i.Name, i.CategoryId, i.SubCategoryId, i.BrandId,
i.BaseUomId, i.DefaultVendorId,
i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, i.SalePrice,
i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass,
i.SellingMinPrice, i.SellingMaxPrice, i.SellingFlatPrice,
i.ContentQty, i.ContentUnit, i.ContentBaseQty, i.ContentBaseUnit,
i.Status))
.ToListAsync(ct);
@@ -130,7 +131,9 @@ public sealed class ItemService : IItemService
Warranty = request.Warranty,
WarrantyPeriodMonths = warrantyPeriodMonths,
TaxClass = request.TaxClass,
SalePrice = request.SalePrice,
SellingMinPrice = request.SellingMinPrice,
SellingMaxPrice = request.SellingMaxPrice,
SellingFlatPrice = request.SellingFlatPrice,
ContentQty = request.ContentQty,
ContentUnit = request.ContentUnit,
ContentBaseQty = contentBaseQty,
@@ -190,7 +193,9 @@ public sealed class ItemService : IItemService
item.Warranty = request.Warranty;
item.WarrantyPeriodMonths = warrantyPeriodMonths;
item.TaxClass = request.TaxClass;
item.SalePrice = request.SalePrice;
item.SellingMinPrice = request.SellingMinPrice;
item.SellingMaxPrice = request.SellingMaxPrice;
item.SellingFlatPrice = request.SellingFlatPrice;
item.ContentQty = request.ContentQty;
item.ContentUnit = request.ContentUnit;
item.ContentBaseQty = contentBaseQty;
@@ -364,7 +369,8 @@ public sealed class ItemService : IItemService
private static ItemDetailDto ToDetail(Item i) => new(
i.ItemId, i.Sku, i.Name, i.Description, i.CategoryId, i.SubCategoryId, i.BrandId,
i.BaseUomId, i.DefaultVendorId,
i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, i.SalePrice,
i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass,
i.SellingMinPrice, i.SellingMaxPrice, i.SellingFlatPrice,
i.ContentQty, i.ContentUnit, i.ContentBaseQty, i.ContentBaseUnit,
i.Status,
i.ReorderSettings
+30 -4
View File
@@ -34,6 +34,7 @@ public sealed class SalesDayEndService : ISalesDayEndService
private readonly IRepository<SalesDayEnd> _dayEnds;
private readonly IRepository<SalesSlip> _slips;
private readonly IRepository<BundleSale> _bundles;
private readonly IRepository<SalesInvoice> _invoices;
private readonly IRepository<StockLedger> _ledger;
private readonly IRepository<Item> _items;
private readonly INumberSequenceService _numbers;
@@ -49,12 +50,13 @@ public sealed class SalesDayEndService : ISalesDayEndService
public SalesDayEndService(
IRepository<SalesDayEnd> dayEnds, IRepository<SalesSlip> slips, IRepository<BundleSale> bundles,
IRepository<StockLedger> ledger, IRepository<Item> items, INumberSequenceService numbers,
IRepository<SalesInvoice> invoices, IRepository<StockLedger> ledger, IRepository<Item> items, INumberSequenceService numbers,
ICurrentUser currentUser, IUnitOfWork uow, IGeneralLedgerService gl, IConfiguration configuration)
{
_dayEnds = dayEnds;
_slips = slips;
_bundles = bundles;
_invoices = invoices;
_ledger = ledger;
_items = items;
_numbers = numbers;
@@ -71,6 +73,8 @@ public sealed class SalesDayEndService : ISalesDayEndService
public async Task<SalesDayEndPreviewDto> PreviewAsync(int cashierUserId, DateOnly businessDate, CancellationToken ct = default)
{
var invoiceSummary = await LoadInvoiceSummaryAsync(cashierUserId, businessDate, ct);
var existing = await _dayEnds.Query().AsNoTracking()
.FirstOrDefaultAsync(x => x.CashierUserId == cashierUserId && x.BusinessDate == businessDate, ct);
if (existing is not null)
@@ -89,7 +93,7 @@ public sealed class SalesDayEndService : ISalesDayEndService
cashierUserId, businessDate, true,
existing.SlipCount, existing.BundleCount, existing.Subtotal, existing.DiscountTotal, existing.TaxTotal, existing.GrandTotal,
closedSlipNumbers, closedBundleNumbers,
Array.Empty<DraftSlipBlockingCloseDto>(), Array.Empty<DraftBundleBlockingCloseDto>());
Array.Empty<DraftSlipBlockingCloseDto>(), Array.Empty<DraftBundleBlockingCloseDto>(), invoiceSummary);
}
var openSlips = await LoadOpenSlipsAsync(cashierUserId, businessDate, ct);
@@ -111,7 +115,8 @@ public sealed class SalesDayEndService : ISalesDayEndService
postedSlips.OrderBy(x => x.SlipNo).Select(x => x.SlipNo).ToList(),
postedBundles.OrderBy(x => x.BundleNo).Select(x => x.BundleNo).ToList(),
draftSlips.OrderBy(x => x.SlipNo).Select(x => new DraftSlipBlockingCloseDto(x.SalesSlipId, x.SlipNo, x.GrandTotal)).ToList(),
draftBundles.OrderBy(x => x.BundleNo).Select(x => new DraftBundleBlockingCloseDto(x.BundleSaleId, x.BundleNo, x.GrandTotal)).ToList());
draftBundles.OrderBy(x => x.BundleNo).Select(x => new DraftBundleBlockingCloseDto(x.BundleSaleId, x.BundleNo, x.GrandTotal)).ToList(),
invoiceSummary);
}
public async Task<SalesDayEndDto> CloseAsync(CreateSalesDayEndRequest request, CancellationToken ct = default)
@@ -265,6 +270,25 @@ public sealed class SalesDayEndService : ISalesDayEndService
.ToListAsync(ct);
}
// Sales Invoices have no CashierUserId (they aren't a cashier document — SalesInvoiceService
// doesn't take one) so CreatedBy is the closest equivalent: whoever was logged in when they
// made the sale. Matched against Posted invoices only, same as the Slip/Bundle totals above.
private async Task<SalesDayEndInvoiceSummaryDto> LoadInvoiceSummaryAsync(int cashierUserId, DateOnly businessDate, CancellationToken ct)
{
var (dayStart, dayEnd) = DayRangeUtc(businessDate);
var invoices = await _invoices.Query().AsNoTracking()
.Where(x => x.CreatedBy == cashierUserId && x.Status == SalesInvoiceStatus.Posted
&& x.InvoiceDate >= dayStart && x.InvoiceDate < dayEnd)
.ToListAsync(ct);
return new SalesDayEndInvoiceSummaryDto(
invoices.Count,
invoices.Sum(x => x.Subtotal),
invoices.Sum(x => x.DiscountTotal),
invoices.Sum(x => x.TaxTotal),
invoices.Sum(x => x.GrandTotal),
invoices.OrderBy(x => x.InvoiceNo).Select(x => x.InvoiceNo).ToList());
}
// SlipDate/BundleDate are stored as DateTime.UtcNow (Kind=Utc) throughout — Npgsql requires
// a matching Kind=Utc here too, or comparisons against the `timestamptz` column throw.
private static (DateTime Start, DateTime End) DayRangeUtc(DateOnly businessDate)
@@ -310,10 +334,12 @@ public sealed class SalesDayEndService : ISalesDayEndService
.OrderByDescending(x => x.Revenue)
.ToList();
var invoiceSummary = await LoadInvoiceSummaryAsync(entity.CashierUserId, entity.BusinessDate, ct);
return new SalesDayEndDto(
entity.SalesDayEndId, entity.DocNo, entity.CashierUserId, entity.BusinessDate,
entity.SlipCount, entity.BundleCount, entity.Subtotal, entity.DiscountTotal, entity.TaxTotal, entity.GrandTotal,
entity.CostOfGoodsSold, entity.GlJournalNo, entity.GlPostedAt, entity.ClosedBy, entity.ClosedAt,
slipNumbers, bundleNumbers, breakdown);
slipNumbers, bundleNumbers, breakdown, invoiceSummary);
}
}
@@ -31,8 +31,8 @@ public sealed class SalesPricingService : ISalesPricingService
{
if (!allowManualOverride)
{
if (item.SalePrice.HasValue)
return new SalesPriceResolution(item.SalePrice.Value, "SALE_PRICE", item.SalePrice.Value);
if (item.SellingFlatPrice.HasValue)
return new SalesPriceResolution(item.SellingFlatPrice.Value, "SALE_PRICE", item.SellingFlatPrice.Value);
var grnPrice = await GetWeightedGrnPriceAsync(itemId, warehouseId, ct);
if (grnPrice is not null)
@@ -44,8 +44,8 @@ public sealed class SalesPricingService : ISalesPricingService
return new SalesPriceResolution(requestedUnitPrice.Value, "MANUAL", requestedUnitPrice.Value);
}
if (item.SalePrice.HasValue)
return new SalesPriceResolution(item.SalePrice.Value, "SALE_PRICE", item.SalePrice.Value);
if (item.SellingFlatPrice.HasValue)
return new SalesPriceResolution(item.SellingFlatPrice.Value, "SALE_PRICE", item.SellingFlatPrice.Value);
var weighted = await GetWeightedGrnPriceAsync(itemId, warehouseId, ct);
if (weighted is not null)
@@ -46,14 +46,14 @@ public sealed class SalesPromotionSuggestionService : ISalesPromotionSuggestionS
var rewardOptions = new List<SalesFreeIssueRewardOptionDto>
{
new(item.ItemId, item.Sku, item.Name, item.SalePrice)
new(item.ItemId, item.Sku, item.Name, item.SellingFlatPrice)
};
var alternates = await _items.Query().AsNoTracking()
.Where(x => x.Status == EntityStatus.Active && x.CategoryId == item.CategoryId && x.ItemId != item.ItemId)
.OrderBy(x => x.Name)
.Take(3)
.Select(x => new SalesFreeIssueRewardOptionDto(x.ItemId, x.Sku, x.Name, x.SalePrice))
.Select(x => new SalesFreeIssueRewardOptionDto(x.ItemId, x.Sku, x.Name, x.SellingFlatPrice))
.ToListAsync(ct);
rewardOptions.AddRange(alternates.Where(x => rewardOptions.All(r => r.ItemId != x.ItemId)));