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);
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);
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);
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);
public sealed record SalesSlipDto(
+1
View File
@@ -93,6 +93,7 @@ builder.Services.AddScoped<IGrnService, GrnService>();
builder.Services.AddScoped<ISalesPricingService, SalesPricingService>();
builder.Services.AddScoped<ISalesInvoiceService, SalesInvoiceService>();
builder.Services.AddScoped<ISalesSlipService, SalesSlipService>();
builder.Services.AddScoped<ISalesReportService, SalesReportService>();
// Stock transactions + reference data (docs/11 §56)
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 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 lineTotal = gross - discountTotal;
var taxAmount = lineTotal * (r.TaxPct / 100m);
@@ -239,11 +241,11 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
private static SalesInvoiceSummaryDto MapSummary(SalesInvoice x) => new(
x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName,
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(
x.SalesInvoiceId, x.InvoiceNo, x.InvoiceDate, x.CustomerId, x.CustomerSnapshotName, x.CustomerSnapshotTaxNo,
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());
}
@@ -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 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 lineTotal = gross - discountTotal;
var taxAmount = lineTotal * (r.TaxPct / 100m);
@@ -237,10 +239,10 @@ public sealed class SalesSlipService : ISalesSlipService
private static SalesSlipSummaryDto MapSummary(SalesSlip x) => new(
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(
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());
}