add day-end and change item model
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user