Add sales report functions
This commit is contained in:
@@ -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);
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 §5–6)
|
// Stock transactions + reference data (docs/11 §5–6)
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user