Add sales report functions

This commit is contained in:
2026-07-27 16:28:26 +05:30
committed by ImanThiyanga
parent 2dab7051b3
commit 74d3e684d2
9 changed files with 440 additions and 8 deletions
@@ -0,0 +1,49 @@
using ERPCore.Dtos.Sales;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
[Route("api/v1/reports/sales")]
public sealed class SalesReportsController : ApiControllerBase
{
private readonly ISalesReportService _reports;
public SalesReportsController(ISalesReportService reports) => _reports = reports;
[HttpGet("daily-summary")]
[ProducesResponseType(typeof(IReadOnlyList<SalesDailySummaryRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SalesDailySummaryRowDto>>> DailySummary(
[FromQuery] DateOnly from, [FromQuery] DateOnly to, CancellationToken ct)
=> Ok(await _reports.DailySummaryAsync(from, to, ct));
[HttpGet("item-wise")]
[ProducesResponseType(typeof(IReadOnlyList<SalesItemSummaryRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SalesItemSummaryRowDto>>> ItemWise(
[FromQuery] DateOnly from, [FromQuery] DateOnly to, [FromQuery] int? itemId, [FromQuery] int? warehouseId, CancellationToken ct)
=> Ok(await _reports.ItemSummaryAsync(from, to, itemId, warehouseId, ct));
[HttpGet("customer-wise")]
[ProducesResponseType(typeof(IReadOnlyList<SalesCustomerSummaryRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SalesCustomerSummaryRowDto>>> CustomerWise(
[FromQuery] DateOnly from, [FromQuery] DateOnly to, [FromQuery] int? customerId, CancellationToken ct)
=> Ok(await _reports.CustomerSummaryAsync(from, to, customerId, ct));
[HttpGet("warehouse-wise")]
[ProducesResponseType(typeof(IReadOnlyList<SalesWarehouseSummaryRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SalesWarehouseSummaryRowDto>>> WarehouseWise(
[FromQuery] DateOnly from, [FromQuery] DateOnly to, [FromQuery] int? warehouseId, CancellationToken ct)
=> Ok(await _reports.WarehouseSummaryAsync(from, to, warehouseId, ct));
[HttpGet("discount-summary")]
[ProducesResponseType(typeof(IReadOnlyList<SalesDiscountSummaryRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SalesDiscountSummaryRowDto>>> DiscountSummary(
[FromQuery] DateOnly from, [FromQuery] DateOnly to, CancellationToken ct)
=> Ok(await _reports.DiscountSummaryAsync(from, to, ct));
[HttpGet("free-issue-summary")]
[ProducesResponseType(typeof(IReadOnlyList<SalesFreeIssueSummaryRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SalesFreeIssueSummaryRowDto>>> FreeIssueSummary(
[FromQuery] DateOnly from, [FromQuery] DateOnly to, CancellationToken ct)
=> Ok(await _reports.FreeIssueSummaryAsync(from, to, ct));
}
@@ -10,7 +10,7 @@ public sealed record SalesInvoiceLineDto(
decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId); decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId);
public sealed record SalesInvoiceTotalsDto( public sealed record SalesInvoiceTotalsDto(
decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal,
decimal RoundOff, decimal NetPayable, decimal PaidAmount, decimal BalanceAmount); decimal RoundOff, decimal NetPayable, decimal PaidAmount, decimal BalanceAmount);
public sealed record SalesInvoiceDto( public sealed record SalesInvoiceDto(
@@ -0,0 +1,68 @@
namespace ERPCore.Dtos.Sales;
public sealed record SalesDailySummaryRowDto(
DateOnly Date,
int InvoiceCount,
int SlipCount,
decimal InvoiceSubtotal,
decimal SlipSubtotal,
decimal DiscountTotal,
decimal FreeQtyTotal,
decimal TaxTotal,
decimal GrandTotal);
public sealed record SalesItemSummaryRowDto(
int ItemId,
string ItemName,
decimal SoldQty,
decimal FreeQty,
decimal GrossAmount,
decimal DiscountTotal,
decimal TaxTotal,
decimal NetAmount);
public sealed record SalesCustomerSummaryRowDto(
int CustomerId,
string CustomerName,
int InvoiceCount,
int SlipCount,
decimal SoldQty,
decimal FreeQty,
decimal GrossAmount,
decimal DiscountTotal,
decimal TaxTotal,
decimal NetAmount);
public sealed record SalesWarehouseSummaryRowDto(
int WarehouseId,
string WarehouseName,
int InvoiceCount,
int SlipCount,
decimal SoldQty,
decimal FreeQty,
decimal GrossAmount,
decimal DiscountTotal,
decimal TaxTotal,
decimal NetAmount);
public sealed record SalesDiscountSummaryRowDto(
string DocumentType,
string DocumentNo,
DateTime DocumentDate,
string CustomerName,
decimal Subtotal,
decimal DiscountTotal,
decimal TaxTotal,
decimal NetAmount);
public sealed record SalesFreeIssueSummaryRowDto(
string DocumentType,
string DocumentNo,
DateTime DocumentDate,
string CustomerName,
int ItemId,
string ItemName,
decimal FreeQty,
decimal FreeValue,
int WarehouseId,
string WarehouseName);
+1 -1
View File
@@ -10,7 +10,7 @@ public sealed record SalesSlipLineDto(
decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId); decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId);
public sealed record SalesSlipTotalsDto( public sealed record SalesSlipTotalsDto(
decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal,
decimal PaidAmount, decimal BalanceAmount); decimal PaidAmount, decimal BalanceAmount);
public sealed record SalesSlipDto( public sealed record SalesSlipDto(
+1
View File
@@ -93,6 +93,7 @@ builder.Services.AddScoped<IGrnService, GrnService>();
builder.Services.AddScoped<ISalesPricingService, SalesPricingService>(); builder.Services.AddScoped<ISalesPricingService, SalesPricingService>();
builder.Services.AddScoped<ISalesInvoiceService, SalesInvoiceService>(); builder.Services.AddScoped<ISalesInvoiceService, SalesInvoiceService>();
builder.Services.AddScoped<ISalesSlipService, SalesSlipService>(); builder.Services.AddScoped<ISalesSlipService, SalesSlipService>();
builder.Services.AddScoped<ISalesReportService, SalesReportService>();
// Stock transactions + reference data (docs/11 §56) // Stock transactions + reference data (docs/11 §56)
builder.Services.AddScoped<IStockMutator, StockMutator>(); builder.Services.AddScoped<IStockMutator, StockMutator>();
@@ -0,0 +1,13 @@
using ERPCore.Dtos.Sales;
namespace ERPCore.Services.Interfaces;
public interface ISalesReportService
{
Task<IReadOnlyList<SalesDailySummaryRowDto>> DailySummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default);
Task<IReadOnlyList<SalesItemSummaryRowDto>> ItemSummaryAsync(DateOnly from, DateOnly to, int? itemId, int? warehouseId, CancellationToken ct = default);
Task<IReadOnlyList<SalesCustomerSummaryRowDto>> CustomerSummaryAsync(DateOnly from, DateOnly to, int? customerId, CancellationToken ct = default);
Task<IReadOnlyList<SalesWarehouseSummaryRowDto>> WarehouseSummaryAsync(DateOnly from, DateOnly to, int? warehouseId, CancellationToken ct = default);
Task<IReadOnlyList<SalesDiscountSummaryRowDto>> DiscountSummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default);
Task<IReadOnlyList<SalesFreeIssueSummaryRowDto>> FreeIssueSummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default);
}
@@ -182,7 +182,9 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
var priceSource = resolved.PriceSource; var priceSource = resolved.PriceSource;
var gross = r.Qty * unitPrice; var gross = r.Qty * unitPrice;
var discountTotal = CalculateDiscount(gross, r.DiscountMode, r.DiscountPct, r.DiscountValue, r.DiscountAmount); var discountTotal = r.IsFreeIssue
? 0m
: CalculateDiscount(gross, r.DiscountMode, r.DiscountPct, r.DiscountValue, r.DiscountAmount);
var netUnit = r.Qty > 0 ? (gross - discountTotal) / r.Qty : 0m; var netUnit = r.Qty > 0 ? (gross - discountTotal) / r.Qty : 0m;
var lineTotal = gross - discountTotal; var lineTotal = gross - discountTotal;
var taxAmount = lineTotal * (r.TaxPct / 100m); var taxAmount = lineTotal * (r.TaxPct / 100m);
@@ -239,11 +241,11 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
private static SalesInvoiceSummaryDto MapSummary(SalesInvoice x) => new( private static SalesInvoiceSummaryDto MapSummary(SalesInvoice x) => new(
x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName, x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName,
x.WarehouseId, x.InvoiceType, x.Status, new SalesInvoiceTotalsDto( x.WarehouseId, x.InvoiceType, x.Status, new SalesInvoiceTotalsDto(
x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal, x.RoundOff, x.NetPayable, x.PaidAmount, x.BalanceAmount), x.CreatedAt); x.Subtotal, x.DiscountTotal, x.Lines.Sum(l => l.FreeQty), x.TaxTotal, x.GrandTotal, x.RoundOff, x.NetPayable, x.PaidAmount, x.BalanceAmount), x.CreatedAt);
private static SalesInvoiceDto Map(SalesInvoice x) => new( private static SalesInvoiceDto Map(SalesInvoice x) => new(
x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName, x.CustomerSnapshotTaxNo, x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName, x.CustomerSnapshotTaxNo,
x.WarehouseId, x.InvoiceType, x.Status, x.CreatedBy, x.CreatedAt, x.UpdatedAt, x.WarehouseId, x.InvoiceType, x.Status, x.CreatedBy, x.CreatedAt, x.UpdatedAt,
new SalesInvoiceTotalsDto(x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal, x.RoundOff, x.NetPayable, x.PaidAmount, x.BalanceAmount), new SalesInvoiceTotalsDto(x.Subtotal, x.DiscountTotal, x.Lines.Sum(l => l.FreeQty), x.TaxTotal, x.GrandTotal, x.RoundOff, x.NetPayable, x.PaidAmount, x.BalanceAmount),
x.Lines.Select(l => new SalesInvoiceLineDto(l.SalesInvoiceLineId, l.ItemId, l.Description, l.Qty, l.FreeQty, l.UomId, l.WarehouseId, l.UnitPrice, l.BaseCost, l.PriceSource, l.DiscountPct, l.DiscountAmount, l.DiscountMode, l.NetUnitPrice, l.LineTotal, l.TaxPct, l.TaxAmount, l.IsFreeIssue, l.ParentLineId)).ToList()); x.Lines.Select(l => new SalesInvoiceLineDto(l.SalesInvoiceLineId, l.ItemId, l.Description, l.Qty, l.FreeQty, l.UomId, l.WarehouseId, l.UnitPrice, l.BaseCost, l.PriceSource, l.DiscountPct, l.DiscountAmount, l.DiscountMode, l.NetUnitPrice, l.LineTotal, l.TaxPct, l.TaxAmount, l.IsFreeIssue, l.ParentLineId)).ToList());
} }
@@ -0,0 +1,297 @@
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Sales;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace ERPCore.Services;
public sealed class SalesReportService : ISalesReportService
{
private readonly IRepository<SalesInvoice> _invoices;
private readonly IRepository<SalesSlip> _slips;
public SalesReportService(IRepository<SalesInvoice> invoices, IRepository<SalesSlip> slips)
{
_invoices = invoices;
_slips = slips;
}
public async Task<IReadOnlyList<SalesDailySummaryRowDto>> DailySummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default)
{
var invoiceRows = await _invoices.Query().AsNoTracking()
.Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
.GroupBy(x => DateOnly.FromDateTime(x.InvoiceDate))
.Select(g => new
{
Date = g.Key,
InvoiceCount = g.Count(),
SlipCount = 0,
InvoiceSubtotal = g.Sum(x => x.Subtotal),
SlipSubtotal = 0m,
DiscountTotal = g.Sum(x => x.DiscountTotal),
FreeQtyTotal = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
TaxTotal = g.Sum(x => x.TaxTotal),
GrandTotal = g.Sum(x => x.GrandTotal)
})
.ToListAsync(ct);
var slipRows = await _slips.Query().AsNoTracking()
.Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
.GroupBy(x => DateOnly.FromDateTime(x.SlipDate))
.Select(g => new
{
Date = g.Key,
InvoiceCount = 0,
SlipCount = g.Count(),
InvoiceSubtotal = 0m,
SlipSubtotal = g.Sum(x => x.Subtotal),
DiscountTotal = g.Sum(x => x.DiscountTotal),
FreeQtyTotal = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
TaxTotal = g.Sum(x => x.TaxTotal),
GrandTotal = g.Sum(x => x.GrandTotal)
})
.ToListAsync(ct);
return invoiceRows.Concat(slipRows)
.GroupBy(x => x.Date)
.OrderBy(x => x.Key)
.Select(g => new SalesDailySummaryRowDto(
g.Key,
g.Sum(x => x.InvoiceCount),
g.Sum(x => x.SlipCount),
g.Sum(x => x.InvoiceSubtotal),
g.Sum(x => x.SlipSubtotal),
g.Sum(x => x.DiscountTotal),
g.Sum(x => x.FreeQtyTotal),
g.Sum(x => x.TaxTotal),
g.Sum(x => x.GrandTotal)))
.ToList();
}
public async Task<IReadOnlyList<SalesItemSummaryRowDto>> ItemSummaryAsync(DateOnly from, DateOnly to, int? itemId, int? warehouseId, CancellationToken ct = default)
{
var invoiceLines = _invoices.Query().AsNoTracking()
.Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
if (warehouseId is not null) invoiceLines = invoiceLines.Where(x => x.WarehouseId == warehouseId);
var invoiceQuery = invoiceLines.SelectMany(x => x.Lines.Select(l => new
{
l.ItemId,
l.Description,
l.Qty,
l.FreeQty,
Gross = l.Qty * l.UnitPrice,
l.DiscountAmount,
l.TaxAmount,
l.LineTotal,
l.WarehouseId
}));
if (itemId is not null) invoiceQuery = invoiceQuery.Where(x => x.ItemId == itemId);
var slipLines = _slips.Query().AsNoTracking()
.Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
if (warehouseId is not null) slipLines = slipLines.Where(x => x.WarehouseId == warehouseId);
var slipQuery = slipLines.SelectMany(x => x.Lines.Select(l => new
{
l.ItemId,
l.Description,
l.Qty,
l.FreeQty,
Gross = l.Qty * l.UnitPrice,
l.DiscountAmount,
l.TaxAmount,
l.LineTotal,
l.WarehouseId
}));
if (itemId is not null) slipQuery = slipQuery.Where(x => x.ItemId == itemId);
var rows = await invoiceQuery.Concat(slipQuery)
.GroupBy(x => new { x.ItemId, x.Description })
.Select(g => new SalesItemSummaryRowDto(
g.Key.ItemId,
g.Key.Description,
g.Sum(x => x.Qty),
g.Sum(x => x.FreeQty),
g.Sum(x => x.Gross),
g.Sum(x => x.DiscountAmount),
g.Sum(x => x.TaxAmount),
g.Sum(x => x.LineTotal + x.TaxAmount)))
.OrderByDescending(x => x.NetAmount)
.ToListAsync(ct);
return rows;
}
public async Task<IReadOnlyList<SalesCustomerSummaryRowDto>> CustomerSummaryAsync(DateOnly from, DateOnly to, int? customerId, CancellationToken ct = default)
{
var invoiceQuery = _invoices.Query().AsNoTracking()
.Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
if (customerId is not null) invoiceQuery = invoiceQuery.Where(x => x.CustomerId == customerId);
var slipQuery = _slips.Query().AsNoTracking()
.Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
if (customerId is not null) slipQuery = slipQuery.Where(x => x.CustomerId == customerId);
var invoiceRows = await invoiceQuery
.GroupBy(x => new { x.CustomerId, x.CustomerSnapshotName })
.Select(g => new
{
g.Key.CustomerId,
CustomerName = g.Key.CustomerSnapshotName,
InvoiceCount = g.Count(),
SlipCount = 0,
SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
GrossAmount = g.Sum(x => x.Subtotal),
DiscountTotal = g.Sum(x => x.DiscountTotal),
TaxTotal = g.Sum(x => x.TaxTotal),
NetAmount = g.Sum(x => x.GrandTotal)
})
.ToListAsync(ct);
var slipRows = await slipQuery
.GroupBy(x => new { x.CustomerId, x.CustomerSnapshotName })
.Select(g => new
{
g.Key.CustomerId,
CustomerName = g.Key.CustomerSnapshotName,
InvoiceCount = 0,
SlipCount = g.Count(),
SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
GrossAmount = g.Sum(x => x.Subtotal),
DiscountTotal = g.Sum(x => x.DiscountTotal),
TaxTotal = g.Sum(x => x.TaxTotal),
NetAmount = g.Sum(x => x.GrandTotal)
})
.ToListAsync(ct);
return invoiceRows.Concat(slipRows)
.GroupBy(x => new { x.CustomerId, x.CustomerName })
.OrderByDescending(g => g.Sum(x => x.NetAmount))
.Select(g => new SalesCustomerSummaryRowDto(
g.Key.CustomerId,
g.Key.CustomerName,
g.Sum(x => x.InvoiceCount),
g.Sum(x => x.SlipCount),
g.Sum(x => x.SoldQty),
g.Sum(x => x.FreeQty),
g.Sum(x => x.GrossAmount),
g.Sum(x => x.DiscountTotal),
g.Sum(x => x.TaxTotal),
g.Sum(x => x.NetAmount)))
.ToList();
}
public async Task<IReadOnlyList<SalesWarehouseSummaryRowDto>> WarehouseSummaryAsync(DateOnly from, DateOnly to, int? warehouseId, CancellationToken ct = default)
{
var invoiceQuery = _invoices.Query().AsNoTracking()
.Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
if (warehouseId is not null) invoiceQuery = invoiceQuery.Where(x => x.WarehouseId == warehouseId);
var slipQuery = _slips.Query().AsNoTracking()
.Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue));
if (warehouseId is not null) slipQuery = slipQuery.Where(x => x.WarehouseId == warehouseId);
var invoiceRows = await invoiceQuery
.GroupBy(x => new { x.WarehouseId, x.Warehouse!.Name })
.Select(g => new
{
g.Key.WarehouseId,
WarehouseName = g.Key.Name,
InvoiceCount = g.Count(),
SlipCount = 0,
SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
GrossAmount = g.Sum(x => x.Subtotal),
DiscountTotal = g.Sum(x => x.DiscountTotal),
TaxTotal = g.Sum(x => x.TaxTotal),
NetAmount = g.Sum(x => x.GrandTotal)
})
.ToListAsync(ct);
var slipRows = await slipQuery
.GroupBy(x => new { x.WarehouseId, x.Warehouse!.Name })
.Select(g => new
{
g.Key.WarehouseId,
WarehouseName = g.Key.Name,
InvoiceCount = 0,
SlipCount = g.Count(),
SoldQty = g.Sum(x => x.Lines.Sum(l => l.Qty)),
FreeQty = g.Sum(x => x.Lines.Sum(l => l.FreeQty)),
GrossAmount = g.Sum(x => x.Subtotal),
DiscountTotal = g.Sum(x => x.DiscountTotal),
TaxTotal = g.Sum(x => x.TaxTotal),
NetAmount = g.Sum(x => x.GrandTotal)
})
.ToListAsync(ct);
return invoiceRows.Concat(slipRows)
.GroupBy(x => new { x.WarehouseId, x.WarehouseName })
.OrderByDescending(g => g.Sum(x => x.NetAmount))
.Select(g => new SalesWarehouseSummaryRowDto(
g.Key.WarehouseId,
g.Key.WarehouseName,
g.Sum(x => x.InvoiceCount),
g.Sum(x => x.SlipCount),
g.Sum(x => x.SoldQty),
g.Sum(x => x.FreeQty),
g.Sum(x => x.GrossAmount),
g.Sum(x => x.DiscountTotal),
g.Sum(x => x.TaxTotal),
g.Sum(x => x.NetAmount)))
.ToList();
}
public async Task<IReadOnlyList<SalesDiscountSummaryRowDto>> DiscountSummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default)
{
var invoices = await _invoices.Query().AsNoTracking()
.Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue) && x.DiscountTotal > 0m)
.Select(x => new SalesDiscountSummaryRowDto("Invoice", x.InvoiceNo, x.InvoiceDate, x.CustomerSnapshotName, x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal))
.ToListAsync(ct);
var slips = await _slips.Query().AsNoTracking()
.Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue) && x.DiscountTotal > 0m)
.Select(x => new SalesDiscountSummaryRowDto("Slip", x.SlipNo, x.SlipDate, x.CustomerSnapshotName, x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal))
.ToListAsync(ct);
return invoices.Concat(slips).OrderByDescending(x => x.DiscountTotal).ToList();
}
public async Task<IReadOnlyList<SalesFreeIssueSummaryRowDto>> FreeIssueSummaryAsync(DateOnly from, DateOnly to, CancellationToken ct = default)
{
var invoiceRows = await _invoices.Query().AsNoTracking()
.Where(x => x.Status == SalesInvoiceStatus.Posted && x.InvoiceDate >= from.ToDateTime(TimeOnly.MinValue) && x.InvoiceDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
.SelectMany(x => x.Lines.Where(l => l.FreeQty > 0m).Select(l => new SalesFreeIssueSummaryRowDto(
"Invoice",
x.InvoiceNo,
x.InvoiceDate,
x.CustomerSnapshotName,
l.ItemId,
l.Description,
l.FreeQty,
l.FreeQty * l.UnitPrice,
l.WarehouseId,
x.Warehouse!.Name)))
.ToListAsync(ct);
var slipRows = await _slips.Query().AsNoTracking()
.Where(x => x.Status == SalesSlipStatus.Posted && x.SlipDate >= from.ToDateTime(TimeOnly.MinValue) && x.SlipDate < to.AddDays(1).ToDateTime(TimeOnly.MinValue))
.SelectMany(x => x.Lines.Where(l => l.FreeQty > 0m).Select(l => new SalesFreeIssueSummaryRowDto(
"Slip",
x.SlipNo,
x.SlipDate,
x.CustomerSnapshotName,
l.ItemId,
l.Description,
l.FreeQty,
l.FreeQty * l.UnitPrice,
l.WarehouseId,
x.Warehouse!.Name)))
.ToListAsync(ct);
return invoiceRows.Concat(slipRows).OrderByDescending(x => x.FreeQty).ToList();
}
}
+5 -3
View File
@@ -183,7 +183,9 @@ public sealed class SalesSlipService : ISalesSlipService
var priceSource = resolved.PriceSource; var priceSource = resolved.PriceSource;
var gross = r.Qty * unitPrice; var gross = r.Qty * unitPrice;
var discountTotal = CalculateDiscount(gross, r.DiscountMode, r.DiscountPct, r.DiscountValue, r.DiscountAmount); var discountTotal = r.IsFreeIssue
? 0m
: CalculateDiscount(gross, r.DiscountMode, r.DiscountPct, r.DiscountValue, r.DiscountAmount);
var netUnit = r.Qty > 0 ? (gross - discountTotal) / r.Qty : 0m; var netUnit = r.Qty > 0 ? (gross - discountTotal) / r.Qty : 0m;
var lineTotal = gross - discountTotal; var lineTotal = gross - discountTotal;
var taxAmount = lineTotal * (r.TaxPct / 100m); var taxAmount = lineTotal * (r.TaxPct / 100m);
@@ -237,10 +239,10 @@ public sealed class SalesSlipService : ISalesSlipService
private static SalesSlipSummaryDto MapSummary(SalesSlip x) => new( private static SalesSlipSummaryDto MapSummary(SalesSlip x) => new(
x.SalesSlipId, x.SlipNo, x.SlipDate, x.CustomerId, x.CustomerSnapshotName, x.WarehouseId, x.Status, x.SalesSlipId, x.SlipNo, x.SlipDate, x.CustomerId, x.CustomerSnapshotName, x.WarehouseId, x.Status,
new SalesSlipTotalsDto(x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal, x.PaidAmount, x.BalanceAmount), x.CreatedAt); new SalesSlipTotalsDto(x.Subtotal, x.DiscountTotal, x.Lines.Sum(l => l.FreeQty), x.TaxTotal, x.GrandTotal, x.PaidAmount, x.BalanceAmount), x.CreatedAt);
private static SalesSlipDto Map(SalesSlip x) => new( private static SalesSlipDto Map(SalesSlip x) => new(
x.SalesSlipId, x.SlipNo, x.SlipDate, x.CustomerId, x.CustomerSnapshotName, x.WarehouseId, x.CashierUserId, x.Status, x.CreatedAt, x.UpdatedAt, x.SalesSlipId, x.SlipNo, x.SlipDate, x.CustomerId, x.CustomerSnapshotName, x.WarehouseId, x.CashierUserId, x.Status, x.CreatedAt, x.UpdatedAt,
new SalesSlipTotalsDto(x.Subtotal, x.DiscountTotal, x.TaxTotal, x.GrandTotal, x.PaidAmount, x.BalanceAmount), new SalesSlipTotalsDto(x.Subtotal, x.DiscountTotal, x.Lines.Sum(l => l.FreeQty), x.TaxTotal, x.GrandTotal, x.PaidAmount, x.BalanceAmount),
x.Lines.Select(l => new SalesSlipLineDto(l.SalesSlipLineId, l.ItemId, l.Description, l.Qty, l.FreeQty, l.UomId, l.WarehouseId, l.UnitPrice, l.BaseCost, l.PriceSource, l.DiscountPct, l.DiscountAmount, l.DiscountMode, l.NetUnitPrice, l.LineTotal, l.TaxPct, l.TaxAmount, l.IsFreeIssue, l.ParentLineId)).ToList()); x.Lines.Select(l => new SalesSlipLineDto(l.SalesSlipLineId, l.ItemId, l.Description, l.Qty, l.FreeQty, l.UomId, l.WarehouseId, l.UnitPrice, l.BaseCost, l.PriceSource, l.DiscountPct, l.DiscountAmount, l.DiscountMode, l.NetUnitPrice, l.LineTotal, l.TaxPct, l.TaxAmount, l.IsFreeIssue, l.ParentLineId)).ToList());
} }