Merge pull request 'feat(sales): refine sales document creation from delivery notes' (#69) from feature/sales/66-sales-document-backoffice-ui into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 3m11s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 3m11s
Reviewed-on: #69
This commit is contained in:
commit
ec38e44086
@ -17,6 +17,16 @@ namespace Core.Interfaces
|
|||||||
int pageSize = 50);
|
int pageSize = 50);
|
||||||
|
|
||||||
Task<SalesDocumentCreateResponse> CreateAsync(SalesDocumentCreateRequest request);
|
Task<SalesDocumentCreateResponse> CreateAsync(SalesDocumentCreateRequest request);
|
||||||
|
Task<SalesDocumentCreateResponse> CreateFromDeliveryNotesAsync(SalesDocumentCreateFromDeliveryNotesRequest request);
|
||||||
|
Task<PagedResult<SalesDocumentDeliveryNoteCandidateDto>> SearchDeliveryNoteCandidatesAsync(
|
||||||
|
int? customerId,
|
||||||
|
string? customerText,
|
||||||
|
string? deliveryNoteNumber,
|
||||||
|
int? quoteId,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50);
|
||||||
Task<SalesDocumentDto?> GetDtoByIdAsync(int id);
|
Task<SalesDocumentDto?> GetDtoByIdAsync(int id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using Domain.Dtos.Sales;
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Generics;
|
using Domain.Generics;
|
||||||
using Models.Interfaces;
|
using Models.Interfaces;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Core.Services
|
namespace Core.Services
|
||||||
{
|
{
|
||||||
@ -34,6 +35,184 @@ namespace Core.Services
|
|||||||
pageSize);
|
pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task<PagedResult<SalesDocumentDeliveryNoteCandidateDto>> SearchDeliveryNoteCandidatesAsync(
|
||||||
|
int? customerId,
|
||||||
|
string? customerText,
|
||||||
|
string? deliveryNoteNumber,
|
||||||
|
int? quoteId,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50)
|
||||||
|
{
|
||||||
|
return _salesDocumentRepository.SearchDeliveryNoteCandidatesAsync(
|
||||||
|
customerId,
|
||||||
|
customerText,
|
||||||
|
deliveryNoteNumber,
|
||||||
|
quoteId,
|
||||||
|
issueDateFrom,
|
||||||
|
issueDateTo,
|
||||||
|
page,
|
||||||
|
pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SalesDocumentCreateResponse> CreateFromDeliveryNotesAsync(SalesDocumentCreateFromDeliveryNotesRequest request)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
var deliveryNoteIds = request.DeliveryNoteIds
|
||||||
|
.Where(x => x > 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (deliveryNoteIds.Count == 0)
|
||||||
|
throw new InvalidOperationException("Debe seleccionar al menos un remito emitido.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Currency))
|
||||||
|
throw new ArgumentException("La moneda es obligatoria.", nameof(request.Currency));
|
||||||
|
|
||||||
|
var deliveryNotes = await _salesDocumentRepository.GetDeliveryNotesForSalesDocumentAsync(deliveryNoteIds);
|
||||||
|
|
||||||
|
if (deliveryNotes.Count != deliveryNoteIds.Count)
|
||||||
|
throw new InvalidOperationException("Uno o más remitos seleccionados no existen.");
|
||||||
|
|
||||||
|
var notIssued = deliveryNotes.Where(x => !string.Equals(x.Status, "Emitido", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
if (notIssued.Count > 0)
|
||||||
|
throw new InvalidOperationException("Solo se pueden facturar remitos en estado Emitido.");
|
||||||
|
|
||||||
|
var alreadyAssociated = deliveryNotes.Where(x => x.SalesInvoiceId.HasValue && x.SalesInvoiceId.Value > 0).ToList();
|
||||||
|
if (alreadyAssociated.Count > 0)
|
||||||
|
throw new InvalidOperationException("Uno o más remitos ya están asociados a un Sales Document.");
|
||||||
|
|
||||||
|
var fiscalCustomerIds = deliveryNotes.Select(x => x.CustomerId).Distinct().ToList();
|
||||||
|
if (fiscalCustomerIds.Count != 1)
|
||||||
|
throw new InvalidOperationException("No se pueden agrupar remitos de distintos clientes fiscales.");
|
||||||
|
|
||||||
|
if (deliveryNotes.Any(x => x.Items.Count == 0))
|
||||||
|
throw new InvalidOperationException("Todos los remitos seleccionados deben tener ítems.");
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var details = new List<ESalesDocumentDetail>();
|
||||||
|
var coverages = new List<ESalesDocumentCoverage>();
|
||||||
|
var lineNumber = 1;
|
||||||
|
|
||||||
|
foreach (var deliveryNote in deliveryNotes)
|
||||||
|
{
|
||||||
|
foreach (var item in deliveryNote.Items)
|
||||||
|
{
|
||||||
|
var unitPrice = item.ApprovedUnitPrice ?? item.OriginalUnitPrice ?? 0;
|
||||||
|
var approvedAmount = item.ApprovedAmount ?? (unitPrice * item.Quantity);
|
||||||
|
|
||||||
|
if (approvedAmount <= 0)
|
||||||
|
throw new InvalidOperationException($"El remito {deliveryNote.DeliveryNoteNumber} contiene ítems sin precio aprobado.");
|
||||||
|
|
||||||
|
var originSnapshot = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
deliveryNoteId = deliveryNote.Id,
|
||||||
|
deliveryNoteNumber = deliveryNote.DeliveryNoteNumber,
|
||||||
|
deliveryNoteIssueDate = deliveryNote.IssueDate,
|
||||||
|
quoteId = deliveryNote.QuoteId,
|
||||||
|
quoteNumber = deliveryNote.QuoteNumber,
|
||||||
|
customerId = deliveryNote.CustomerId,
|
||||||
|
customerName = deliveryNote.CustomerName,
|
||||||
|
deliveryNoteExtraInfo = deliveryNote.ExtraInfoJson
|
||||||
|
});
|
||||||
|
|
||||||
|
details.Add(new ESalesDocumentDetail
|
||||||
|
{
|
||||||
|
LineNumber = lineNumber++,
|
||||||
|
OriginType = SalesDocumentOriginType.DeliveryNote.ToStorageCode(),
|
||||||
|
OriginId = deliveryNote.Id,
|
||||||
|
QuoteDetailId = item.QuoteDetailId,
|
||||||
|
ProductId = item.ProductId,
|
||||||
|
Description = item.Description.Trim(),
|
||||||
|
Quantity = item.Quantity,
|
||||||
|
AuthorizedUnitPrice = unitPrice,
|
||||||
|
AuthorizedAmount = approvedAmount,
|
||||||
|
BilledPercentage = 100,
|
||||||
|
UnitPrice = unitPrice,
|
||||||
|
NetAmount = approvedAmount,
|
||||||
|
TaxAmount = 0,
|
||||||
|
TotalAmount = approvedAmount,
|
||||||
|
OriginSnapshotJson = originSnapshot,
|
||||||
|
Createdat = now
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deliveryNote.QuoteId.HasValue)
|
||||||
|
{
|
||||||
|
coverages.Add(new ESalesDocumentCoverage
|
||||||
|
{
|
||||||
|
QuoteId = deliveryNote.QuoteId.Value,
|
||||||
|
QuoteDetailId = item.QuoteDetailId,
|
||||||
|
CoverageType = (int)SalesDocumentCoverageType.Direct,
|
||||||
|
CoveragePercentage = 100,
|
||||||
|
CoverageAmount = approvedAmount,
|
||||||
|
PeriodFrom = request.PeriodFrom,
|
||||||
|
PeriodTo = request.PeriodTo,
|
||||||
|
Notes = $"Coverage desde remito {deliveryNote.DeliveryNoteNumber}",
|
||||||
|
Createdat = now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalAmount = details.Sum(x => x.TotalAmount);
|
||||||
|
if (totalAmount <= 0)
|
||||||
|
throw new InvalidOperationException("El total del documento debe ser mayor a cero.");
|
||||||
|
|
||||||
|
var operations = deliveryNotes.Select(x => new SalesDocumentDeliveryNoteOperationSnapshotDto
|
||||||
|
{
|
||||||
|
DeliveryNoteId = x.Id,
|
||||||
|
DeliveryNoteNumber = x.DeliveryNoteNumber,
|
||||||
|
QuoteId = x.QuoteId,
|
||||||
|
QuoteNumber = x.QuoteNumber,
|
||||||
|
CustomerId = x.CustomerId,
|
||||||
|
CustomerName = x.CustomerName,
|
||||||
|
IssueDate = x.IssueDate,
|
||||||
|
Amount = x.Items.Sum(i => i.ApprovedAmount ?? ((i.ApprovedUnitPrice ?? i.OriginalUnitPrice ?? 0) * i.Quantity)),
|
||||||
|
Coverage = x.CustomerName
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var extraInfoJson = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
source = "DeliveryNotes",
|
||||||
|
grouped = deliveryNotes.Count > 1,
|
||||||
|
coverageDefinition = "Entidad financiadora / pagadora responsable",
|
||||||
|
operations
|
||||||
|
});
|
||||||
|
|
||||||
|
var entity = new ESalesDocument
|
||||||
|
{
|
||||||
|
DocumentType = request.DocumentType,
|
||||||
|
Status = (int)SalesDocumentStatus.Draft,
|
||||||
|
QuoteId = deliveryNotes.Count == 1 ? deliveryNotes[0].QuoteId : null,
|
||||||
|
CustomerId = fiscalCustomerIds[0],
|
||||||
|
BillToCustomerId = fiscalCustomerIds[0],
|
||||||
|
IssueDate = request.IssueDate ?? now,
|
||||||
|
Currency = request.Currency.Trim(),
|
||||||
|
ExchangeRate = request.ExchangeRate <= 0 ? 1 : request.ExchangeRate,
|
||||||
|
NetAmount = totalAmount,
|
||||||
|
TaxAmount = 0,
|
||||||
|
TotalAmount = totalAmount,
|
||||||
|
Observations = request.Observations,
|
||||||
|
ExtraInfoJson = extraInfoJson,
|
||||||
|
PeriodFrom = request.PeriodFrom,
|
||||||
|
PeriodTo = request.PeriodTo,
|
||||||
|
Createdat = now,
|
||||||
|
PhSSalesDocumentDetails = details,
|
||||||
|
PhSSalesDocumentCoverages = coverages
|
||||||
|
};
|
||||||
|
|
||||||
|
var created = await _salesDocumentRepository.CreateFromDeliveryNotesAsync(entity, deliveryNoteIds);
|
||||||
|
|
||||||
|
return new SalesDocumentCreateResponse
|
||||||
|
{
|
||||||
|
Id = created.Id,
|
||||||
|
InternalDocumentNumber = created.InternalDocumentNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<SalesDocumentCreateResponse> CreateAsync(SalesDocumentCreateRequest request)
|
public async Task<SalesDocumentCreateResponse> CreateAsync(SalesDocumentCreateRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|||||||
@ -15,6 +15,10 @@ namespace Domain.Dtos.Sales
|
|||||||
public int? QuoteDetailId { get; set; }
|
public int? QuoteDetailId { get; set; }
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
public decimal Quantity { get; set; }
|
public decimal Quantity { get; set; }
|
||||||
|
public int? ProductId { get; set; }
|
||||||
|
public decimal? ApprovedUnitPrice { get; set; }
|
||||||
|
public decimal? ApprovedAmount { get; set; }
|
||||||
|
public decimal? OriginalUnitPrice { get; set; }
|
||||||
public string Notes { get; set; } = string.Empty;
|
public string Notes { get; set; } = string.Empty;
|
||||||
public DateTime Createdat { get; set; }
|
public DateTime Createdat { get; set; }
|
||||||
public DateTime? Modifiedat { get; set; }
|
public DateTime? Modifiedat { get; set; }
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
namespace Domain.Dtos.Sales
|
||||||
|
{
|
||||||
|
public class SalesDocumentCreateFromDeliveryNotesRequest
|
||||||
|
{
|
||||||
|
public List<int> DeliveryNoteIds { get; set; } = new();
|
||||||
|
public int DocumentType { get; set; } = 1;
|
||||||
|
public DateTime? IssueDate { get; set; }
|
||||||
|
public string Currency { get; set; } = "ARS";
|
||||||
|
public decimal ExchangeRate { get; set; } = 1;
|
||||||
|
public DateTime? PeriodFrom { get; set; }
|
||||||
|
public DateTime? PeriodTo { get; set; }
|
||||||
|
public string? Observations { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Domain/Dtos/Sales/SalesDocumentDeliveryNoteCandidateDto.cs
Normal file
17
Domain/Dtos/Sales/SalesDocumentDeliveryNoteCandidateDto.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace Domain.Dtos.Sales
|
||||||
|
{
|
||||||
|
public class SalesDocumentDeliveryNoteCandidateDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string DeliveryNoteNumber { get; set; } = string.Empty;
|
||||||
|
public int? QuoteId { get; set; }
|
||||||
|
public string? QuoteNumber { get; set; }
|
||||||
|
public DateTime IssueDate { get; set; }
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public int ItemCount { get; set; }
|
||||||
|
public decimal ApprovedAmount { get; set; }
|
||||||
|
public string? ExtraInfoJson { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
namespace Domain.Dtos.Sales
|
||||||
|
{
|
||||||
|
public class SalesDocumentDeliveryNoteOperationSnapshotDto
|
||||||
|
{
|
||||||
|
public int DeliveryNoteId { get; set; }
|
||||||
|
public string DeliveryNoteNumber { get; set; } = string.Empty;
|
||||||
|
public int? QuoteId { get; set; }
|
||||||
|
public string? QuoteNumber { get; set; }
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
public DateTime IssueDate { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string? Patient { get; set; }
|
||||||
|
public string? Doctor { get; set; }
|
||||||
|
public string? Hospital { get; set; }
|
||||||
|
public DateTime? SurgeryDate { get; set; }
|
||||||
|
public string? Coverage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,17 @@ namespace Models.Interfaces
|
|||||||
int pageSize = 50);
|
int pageSize = 50);
|
||||||
|
|
||||||
Task<ESalesDocument> CreateAsync(ESalesDocument entity);
|
Task<ESalesDocument> CreateAsync(ESalesDocument entity);
|
||||||
|
Task<ESalesDocument> CreateFromDeliveryNotesAsync(ESalesDocument entity, IReadOnlyCollection<int> deliveryNoteIds);
|
||||||
|
Task<List<DeliveryNoteDto>> GetDeliveryNotesForSalesDocumentAsync(IReadOnlyCollection<int> deliveryNoteIds);
|
||||||
|
Task<PagedResult<SalesDocumentDeliveryNoteCandidateDto>> SearchDeliveryNoteCandidatesAsync(
|
||||||
|
int? customerId,
|
||||||
|
string? customerText,
|
||||||
|
string? deliveryNoteNumber,
|
||||||
|
int? quoteId,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50);
|
||||||
Task<SalesDocumentDto?> GetDtoByIdAsync(int id);
|
Task<SalesDocumentDto?> GetDtoByIdAsync(int id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,6 +89,7 @@ namespace Models.Repositories
|
|||||||
.Include(x => x.Customer)
|
.Include(x => x.Customer)
|
||||||
.Include(x => x.Quote)
|
.Include(x => x.Quote)
|
||||||
.Include(x => x.PhSDeliveryNoteDetails)
|
.Include(x => x.PhSDeliveryNoteDetails)
|
||||||
|
.ThenInclude(d => d.QuoteDetail)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
@ -101,6 +102,7 @@ namespace Models.Repositories
|
|||||||
.Include(x => x.Customer)
|
.Include(x => x.Customer)
|
||||||
.Include(x => x.Quote)
|
.Include(x => x.Quote)
|
||||||
.Include(x => x.PhSDeliveryNoteDetails)
|
.Include(x => x.PhSDeliveryNoteDetails)
|
||||||
|
.ThenInclude(d => d.QuoteDetail)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(x => x.Deliverynotenumber == deliveryNoteNumber);
|
.FirstOrDefaultAsync(x => x.Deliverynotenumber == deliveryNoteNumber);
|
||||||
|
|
||||||
@ -113,6 +115,7 @@ namespace Models.Repositories
|
|||||||
.Include(x => x.Customer)
|
.Include(x => x.Customer)
|
||||||
.Include(x => x.Quote)
|
.Include(x => x.Quote)
|
||||||
.Include(x => x.PhSDeliveryNoteDetails)
|
.Include(x => x.PhSDeliveryNoteDetails)
|
||||||
|
.ThenInclude(d => d.QuoteDetail)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.QuoteId == quoteId)
|
.Where(x => x.QuoteId == quoteId)
|
||||||
.OrderByDescending(x => x.Issuedate)
|
.OrderByDescending(x => x.Issuedate)
|
||||||
@ -178,6 +181,10 @@ namespace Models.Repositories
|
|||||||
QuoteDetailId = source.QuoteDetailId,
|
QuoteDetailId = source.QuoteDetailId,
|
||||||
Description = source.Description??string.Empty,
|
Description = source.Description??string.Empty,
|
||||||
Quantity = source.Quantity,
|
Quantity = source.Quantity,
|
||||||
|
ProductId = source.QuoteDetail?.ProductId,
|
||||||
|
ApprovedUnitPrice = source.QuoteDetail?.Approvedunitprice,
|
||||||
|
ApprovedAmount = source.QuoteDetail?.Approvedamount,
|
||||||
|
OriginalUnitPrice = source.QuoteDetail?.Unitprice,
|
||||||
Notes = source.Notes,
|
Notes = source.Notes,
|
||||||
Createdat = source.Createdat,
|
Createdat = source.Createdat,
|
||||||
Modifiedat = source.Modifiedat
|
Modifiedat = source.Modifiedat
|
||||||
|
|||||||
@ -119,6 +119,166 @@ namespace Models.Repositories
|
|||||||
return EntityMapper.MapEntity<PhSSalesDocument, ESalesDocument>(mapped);
|
return EntityMapper.MapEntity<PhSSalesDocument, ESalesDocument>(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<PagedResult<SalesDocumentDeliveryNoteCandidateDto>> SearchDeliveryNoteCandidatesAsync(
|
||||||
|
int? customerId,
|
||||||
|
string? customerText,
|
||||||
|
string? deliveryNoteNumber,
|
||||||
|
int? quoteId,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50)
|
||||||
|
{
|
||||||
|
page = page <= 0 ? 1 : page;
|
||||||
|
pageSize = pageSize <= 0 ? 50 : pageSize;
|
||||||
|
|
||||||
|
var query = _context.PhSDeliveryNotes
|
||||||
|
.Include(x => x.Customer)
|
||||||
|
.Include(x => x.Quote)
|
||||||
|
.Include(x => x.PhSDeliveryNoteDetails)
|
||||||
|
.ThenInclude(d => d.QuoteDetail)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Status == "Emitido" && x.SalesinvoiceId == null)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (customerId.HasValue && customerId.Value > 0)
|
||||||
|
query = query.Where(x => x.CustomerId == customerId.Value);
|
||||||
|
else if (!string.IsNullOrWhiteSpace(customerText))
|
||||||
|
query = query.Where(x => (x.Customer.Name ?? string.Empty).Contains(customerText.Trim()));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(deliveryNoteNumber))
|
||||||
|
query = query.Where(x => x.Deliverynotenumber.Contains(deliveryNoteNumber.Trim()));
|
||||||
|
|
||||||
|
if (quoteId.HasValue && quoteId.Value > 0)
|
||||||
|
query = query.Where(x => x.QuoteId == quoteId.Value);
|
||||||
|
|
||||||
|
if (issueDateFrom.HasValue)
|
||||||
|
query = query.Where(x => x.Issuedate >= issueDateFrom.Value);
|
||||||
|
|
||||||
|
if (issueDateTo.HasValue)
|
||||||
|
query = query.Where(x => x.Issuedate <= issueDateTo.Value);
|
||||||
|
|
||||||
|
var totalItems = await query.CountAsync();
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(x => x.Issuedate)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new SalesDocumentDeliveryNoteCandidateDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
DeliveryNoteNumber = x.Deliverynotenumber,
|
||||||
|
QuoteId = x.QuoteId,
|
||||||
|
QuoteNumber = x.Quote != null ? x.Quote.Quotenumber : null,
|
||||||
|
IssueDate = x.Issuedate,
|
||||||
|
CustomerId = x.CustomerId,
|
||||||
|
CustomerName = x.Customer.Name ?? string.Empty,
|
||||||
|
Status = x.Status,
|
||||||
|
ItemCount = x.PhSDeliveryNoteDetails.Count,
|
||||||
|
ApprovedAmount = x.PhSDeliveryNoteDetails.Sum(d => d.QuoteDetail != null
|
||||||
|
? (d.QuoteDetail.Approvedamount ?? ((d.QuoteDetail.Approvedunitprice ?? d.QuoteDetail.Unitprice) * d.Quantity))
|
||||||
|
: 0),
|
||||||
|
ExtraInfoJson = x.ExtrainfoJson
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new PagedResult<SalesDocumentDeliveryNoteCandidateDto>
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
TotalItems = totalItems,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DeliveryNoteDto>> GetDeliveryNotesForSalesDocumentAsync(IReadOnlyCollection<int> deliveryNoteIds)
|
||||||
|
{
|
||||||
|
var ids = deliveryNoteIds.Distinct().ToList();
|
||||||
|
|
||||||
|
var entities = await _context.PhSDeliveryNotes
|
||||||
|
.Include(x => x.Customer)
|
||||||
|
.Include(x => x.Quote)
|
||||||
|
.Include(x => x.PhSDeliveryNoteDetails)
|
||||||
|
.ThenInclude(d => d.QuoteDetail)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => ids.Contains(x.Id))
|
||||||
|
.OrderBy(x => x.Issuedate)
|
||||||
|
.ThenBy(x => x.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return entities.Select(MapDeliveryNoteDto).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ESalesDocument> CreateFromDeliveryNotesAsync(ESalesDocument entity, IReadOnlyCollection<int> deliveryNoteIds)
|
||||||
|
{
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
var mapped = EntityMapper.MapEntity<ESalesDocument, PhSSalesDocument>(entity);
|
||||||
|
await _context.PhSSalesDocuments.AddAsync(mapped);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var ids = deliveryNoteIds.Distinct().ToList();
|
||||||
|
var deliveryNotes = await _context.PhSDeliveryNotes
|
||||||
|
.Where(x => ids.Contains(x.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var deliveryNote in deliveryNotes)
|
||||||
|
{
|
||||||
|
deliveryNote.SalesinvoiceId = mapped.Id;
|
||||||
|
deliveryNote.Modifiedat = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
return EntityMapper.MapEntity<PhSSalesDocument, ESalesDocument>(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DeliveryNoteDto MapDeliveryNoteDto(PhSDeliveryNote source)
|
||||||
|
{
|
||||||
|
return new DeliveryNoteDto
|
||||||
|
{
|
||||||
|
Id = source.Id,
|
||||||
|
DeliveryNoteNumber = source.Deliverynotenumber,
|
||||||
|
CustomerName = source.Customer?.Name ?? string.Empty,
|
||||||
|
QuoteId = source.QuoteId,
|
||||||
|
QuoteNumber = source.Quote?.Quotenumber,
|
||||||
|
SalesInvoiceId = source.SalesinvoiceId,
|
||||||
|
IssueDate = source.Issuedate,
|
||||||
|
CustomerId = source.CustomerId,
|
||||||
|
Status = source.Status,
|
||||||
|
Observations = source.Observations,
|
||||||
|
ExtraInfoJson = source.ExtrainfoJson,
|
||||||
|
PrintCount = source.Printcount,
|
||||||
|
CreatedAt = source.Createdat,
|
||||||
|
ModifiedAt = source.Modifiedat,
|
||||||
|
Items = source.PhSDeliveryNoteDetails
|
||||||
|
.OrderBy(d => d.LineNumber)
|
||||||
|
.ThenBy(d => d.Id)
|
||||||
|
.Select(d => new DeliveryNoteItemDto
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
DeliverynoteId = d.DeliverynoteId,
|
||||||
|
LineNumber = d.LineNumber,
|
||||||
|
OriginType = d.OriginType,
|
||||||
|
OriginId = d.OriginId,
|
||||||
|
QuoteDetailId = d.QuoteDetailId,
|
||||||
|
Description = d.Description ?? string.Empty,
|
||||||
|
Quantity = d.Quantity,
|
||||||
|
ProductId = d.QuoteDetail?.ProductId,
|
||||||
|
ApprovedUnitPrice = d.QuoteDetail?.Approvedunitprice,
|
||||||
|
ApprovedAmount = d.QuoteDetail?.Approvedamount,
|
||||||
|
OriginalUnitPrice = d.QuoteDetail?.Unitprice,
|
||||||
|
Notes = d.Notes,
|
||||||
|
Createdat = d.Createdat,
|
||||||
|
Modifiedat = d.Modifiedat
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<SalesDocumentDto?> GetDtoByIdAsync(int id)
|
public async Task<SalesDocumentDto?> GetDtoByIdAsync(int id)
|
||||||
{
|
{
|
||||||
var entity = await _context.PhSSalesDocuments
|
var entity = await _context.PhSSalesDocuments
|
||||||
|
|||||||
@ -69,6 +69,70 @@ namespace phronCare.API.Controllers.Sales
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("delivery-note-candidates")]
|
||||||
|
public async Task<ActionResult<PagedResult<SalesDocumentDeliveryNoteCandidateDto>>> SearchDeliveryNoteCandidates(
|
||||||
|
[FromQuery] int? customerId,
|
||||||
|
[FromQuery] string? customerText,
|
||||||
|
[FromQuery] string? deliveryNoteNumber,
|
||||||
|
[FromQuery] int? quoteId,
|
||||||
|
[FromQuery] DateTime? issueDateFrom,
|
||||||
|
[FromQuery] DateTime? issueDateTo,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 50)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _salesDocumentService.SearchDeliveryNoteCandidatesAsync(
|
||||||
|
customerId,
|
||||||
|
customerText,
|
||||||
|
deliveryNoteNumber,
|
||||||
|
quoteId,
|
||||||
|
issueDateFrom,
|
||||||
|
issueDateTo,
|
||||||
|
page,
|
||||||
|
pageSize);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
|
||||||
|
return StatusCode(500, $"{methodName} Message: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("from-delivery-notes")]
|
||||||
|
public async Task<ActionResult<SalesDocumentDto>> CreateFromDeliveryNotes([FromBody] SalesDocumentCreateFromDeliveryNotesRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
return BadRequest("El payload no puede ser nulo.");
|
||||||
|
|
||||||
|
var created = await _salesDocumentService.CreateFromDeliveryNotesAsync(request);
|
||||||
|
var salesDocument = await _salesDocumentService.GetDtoByIdAsync(created.Id);
|
||||||
|
|
||||||
|
if (salesDocument == null)
|
||||||
|
return StatusCode(500, $"No se pudo recuperar el Sales Document creado con ID {created.Id}.");
|
||||||
|
|
||||||
|
return CreatedAtAction(nameof(GetById), new { id = salesDocument.Id }, salesDocument);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
|
||||||
|
return StatusCode(500, $"{methodName} Message: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<SalesDocumentDto>> Create([FromBody] SalesDocumentCreateRequest request)
|
public async Task<ActionResult<SalesDocumentDto>> Create([FromBody] SalesDocumentCreateRequest request)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,388 +1,252 @@
|
|||||||
@page "/salesdocuments/create"
|
@page "/salesdocuments/create"
|
||||||
@using System.ComponentModel.DataAnnotations
|
|
||||||
@using Blazored.Typeahead
|
|
||||||
@using Domain.Constants
|
@using Domain.Constants
|
||||||
@using Domain.Dtos
|
|
||||||
@using Domain.Dtos.Sales
|
@using Domain.Dtos.Sales
|
||||||
@using Domain.Entities
|
@using Domain.Generics
|
||||||
@using phronCare.UIBlazor.Services.Lookups
|
|
||||||
@using phronCare.UIBlazor.Services.Sales.SalesDocuments
|
@using phronCare.UIBlazor.Services.Sales.SalesDocuments
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject ISalesDocumentService SalesDocumentService
|
@inject ISalesDocumentService SalesDocumentService
|
||||||
@inject ISalesLookupService SalesLookupService
|
|
||||||
@inject IToastService toastService
|
@inject IToastService toastService
|
||||||
|
|
||||||
<EditForm Model="Model" OnValidSubmit="HandleValidSubmit">
|
<div class="container mt-4" style="zoom:.8;">
|
||||||
<DataAnnotationsValidator />
|
<div class="card shadow-sm mb-3">
|
||||||
<ValidationSummary />
|
<div class="card-header d-flex justify-content-center align-items-center">
|
||||||
|
<h3 class="mb-0">Nuevo Sales Document desde remitos</h3>
|
||||||
<div class="container mt-4" style="zoom:.8;">
|
|
||||||
<div class="card shadow-sm mb-3">
|
|
||||||
<div class="card-header d-flex justify-content-center align-items-center">
|
|
||||||
<h3 class="mb-0">Nuevo Sales Document</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label for="issueDate" class="form-label">Fecha</label>
|
|
||||||
<InputDate id="issueDate" class="form-control" @bind-Value="Model.IssueDate" />
|
|
||||||
<ValidationMessage For="@(() => Model.IssueDate)" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label for="documentType" class="form-label">Tipo documento</label>
|
|
||||||
<InputSelect id="documentType" class="form-select" @bind-Value="Model.DocumentType">
|
|
||||||
@foreach (var item in DocumentTypeOptions)
|
|
||||||
{
|
|
||||||
<option value="@item.Value">@item.Label</option>
|
|
||||||
}
|
|
||||||
</InputSelect>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label for="currency" class="form-label">Moneda</label>
|
|
||||||
<InputText id="currency" class="form-control" @bind-Value="Model.Currency" />
|
|
||||||
<ValidationMessage For="@(() => Model.Currency)" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label for="exchangeRate" class="form-label">Cotización</label>
|
|
||||||
<InputNumber id="exchangeRate" class="form-control" @bind-Value="Model.ExchangeRate" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="quoteId" class="form-label">Presupuesto ID / coverage</label>
|
|
||||||
<InputNumber id="quoteId" class="form-control" @bind-Value="Model.QuoteId" />
|
|
||||||
<ValidationMessage For="@(() => Model.QuoteId)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="customerLookup" class="form-label">Cliente</label>
|
|
||||||
<BlazoredTypeahead id="customerLookup" TItem="ELookUpItem" TValue="ELookUpItem"
|
|
||||||
SearchMethod="SalesLookupService.SearchCustomersAsync"
|
|
||||||
Value="SelectedCustomer"
|
|
||||||
ValueChanged="OnCustomerSelected"
|
|
||||||
ValueExpression="@(() => SelectedCustomer)"
|
|
||||||
MaximumSuggestions="10"
|
|
||||||
Placeholder="Buscar cliente..."
|
|
||||||
TextProperty="Nombre">
|
|
||||||
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
|
|
||||||
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
|
|
||||||
</BlazoredTypeahead>
|
|
||||||
<ValidationMessage For="@(() => Model.CustomerId)" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="billToCustomerLookup" class="form-label">Facturar a</label>
|
|
||||||
<BlazoredTypeahead id="billToCustomerLookup" TItem="ELookUpItem" TValue="ELookUpItem"
|
|
||||||
SearchMethod="SalesLookupService.SearchCustomersAsync"
|
|
||||||
Value="SelectedBillToCustomer"
|
|
||||||
ValueChanged="OnBillToCustomerSelected"
|
|
||||||
ValueExpression="@(() => SelectedBillToCustomer)"
|
|
||||||
MaximumSuggestions="10"
|
|
||||||
Placeholder="Buscar cliente de facturación..."
|
|
||||||
TextProperty="Nombre">
|
|
||||||
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
|
|
||||||
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
|
|
||||||
</BlazoredTypeahead>
|
|
||||||
<ValidationMessage For="@(() => Model.BillToCustomerId)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="coverageType" class="form-label">Coverage type</label>
|
|
||||||
<InputSelect id="coverageType" class="form-select" @bind-Value="Model.CoverageType">
|
|
||||||
@foreach (var item in CoverageTypeOptions)
|
|
||||||
{
|
|
||||||
<option value="@item.Value">@item.Label</option>
|
|
||||||
}
|
|
||||||
</InputSelect>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="coveragePercentage" class="form-label">Coverage %</label>
|
|
||||||
<InputNumber id="coveragePercentage" class="form-control" @bind-Value="Model.CoveragePercentage" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="periodFrom" class="form-label">Desde</label>
|
|
||||||
<InputDate id="periodFrom" class="form-control" @bind-Value="Model.PeriodFrom" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="periodTo" class="form-label">Hasta</label>
|
|
||||||
<InputDate id="periodTo" class="form-control" @bind-Value="Model.PeriodTo" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label for="observations" class="form-label">Observaciones</label>
|
|
||||||
<InputTextArea id="observations" class="form-control" rows="3" @bind-Value="Model.Observations" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
Sales Document representa el documento comercial facturable. No emite comprobante fiscal ni integra ARCA/AFIP.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-3">
|
<div class="row g-2 mb-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="col-md-3">
|
||||||
<h5 class="mb-0">Detalles</h5>
|
<label class="form-label">Cliente fiscal</label>
|
||||||
<button type="button" class="btn btn-outline-success btn-sm rounded-pill" @onclick="AddItem">
|
<input class="form-control" @bind="CustomerText" placeholder="Buscar cliente..." />
|
||||||
<i class="fas fa-plus me-1"></i> Agregar ítem
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Remito</label>
|
||||||
|
<input class="form-control" @bind="DeliveryNoteNumber" placeholder="Número de remito..." />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Presupuesto ID</label>
|
||||||
|
<input type="number" class="form-control" @bind="QuoteId" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Desde</label>
|
||||||
|
<input type="date" class="form-control" @bind="IssueDateFrom" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Hasta</label>
|
||||||
|
<input type="date" class="form-control" @bind="IssueDateTo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<button type="button" class="btn btn-outline-secondary rounded-pill" @onclick="ClearFilters" disabled="@IsLoading">Limpiar</button>
|
||||||
|
<button type="button" class="btn btn-primary rounded-pill" @onclick="SearchCandidatesAsync" disabled="@IsLoading">
|
||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
}
|
||||||
|
Buscar remitos pendientes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-2">
|
|
||||||
@if (Items.Any())
|
|
||||||
{
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-bordered mb-0">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th style="width:60px;" class="text-center">#</th>
|
|
||||||
<th style="width:170px;">Origen</th>
|
|
||||||
<th style="width:110px;">Origin ID</th>
|
|
||||||
<th style="width:110px;">Quote detail</th>
|
|
||||||
<th>Descripción</th>
|
|
||||||
<th style="width:120px;">Cantidad</th>
|
|
||||||
<th style="width:130px;">Unitario</th>
|
|
||||||
<th style="width:130px;">Impuesto</th>
|
|
||||||
<th style="width:130px;">Total</th>
|
|
||||||
<th style="width:60px;"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var item in Items)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td class="text-center">@item.LineNumber</td>
|
|
||||||
<td>
|
|
||||||
<InputSelect class="form-select form-select-sm" @bind-Value="item.OriginType">
|
|
||||||
@foreach (var option in OriginTypeOptions)
|
|
||||||
{
|
|
||||||
<option value="@option.Value">@option.Label</option>
|
|
||||||
}
|
|
||||||
</InputSelect>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<InputNumber class="form-control form-control-sm text-end" @bind-Value="item.OriginId" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<InputNumber class="form-control form-control-sm text-end" @bind-Value="item.QuoteDetailId" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<InputTextArea class="form-control form-control-sm" rows="2" @bind-Value="item.Description" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<InputNumber class="form-control form-control-sm text-end" @bind-Value="item.Quantity" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<InputNumber class="form-control form-control-sm text-end" @bind-Value="item.UnitPrice" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<InputNumber class="form-control form-control-sm text-end" @bind-Value="item.TaxAmount" />
|
|
||||||
</td>
|
|
||||||
<td class="text-end align-middle">@GetItemTotal(item).ToString("N2")</td>
|
|
||||||
<td class="text-center align-middle">
|
|
||||||
<button type="button" class="btn btn-link p-0 text-danger" title="Eliminar" @onclick="() => RemoveItem(item)">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="text-center text-muted py-4">No hay ítems cargados.</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-end gap-2">
|
|
||||||
<button type="button" class="btn btn-secondary rounded-pill" @onclick="BackToList" disabled="@IsSaving">
|
|
||||||
<i class="fas fa-arrow-left me-1"></i> Volver
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary rounded-pill" disabled="@IsSaving">
|
|
||||||
@if (IsSaving)
|
|
||||||
{
|
|
||||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
||||||
}
|
|
||||||
<i class="fas fa-save me-1"></i> Crear
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EditForm>
|
|
||||||
|
<div class="card shadow-sm mb-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Remitos emitidos pendientes</h5>
|
||||||
|
<span class="badge bg-secondary">Seleccionados: @SelectedIds.Count</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
@if (Candidates.Items.Any())
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:48px;"></th>
|
||||||
|
<th>Remito</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Cliente fiscal</th>
|
||||||
|
<th>Presupuesto</th>
|
||||||
|
<th class="text-end">Ítems</th>
|
||||||
|
<th class="text-end">Importe aprobado</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in Candidates.Items)
|
||||||
|
{
|
||||||
|
<tr class="@(SelectedIds.Contains(item.Id) ? "table-primary" : string.Empty)">
|
||||||
|
<td class="text-center">
|
||||||
|
<input type="checkbox" class="form-check-input" checked="@SelectedIds.Contains(item.Id)" @onchange="args => ToggleSelection(item, args)" />
|
||||||
|
</td>
|
||||||
|
<td>@item.DeliveryNoteNumber</td>
|
||||||
|
<td>@item.IssueDate.ToString("dd/MM/yyyy")</td>
|
||||||
|
<td>@item.CustomerName</td>
|
||||||
|
<td>@(item.QuoteNumber ?? item.QuoteId?.ToString() ?? "-")</td>
|
||||||
|
<td class="text-end">@item.ItemCount</td>
|
||||||
|
<td class="text-end">@item.ApprovedAmount.ToString("N2")</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-center text-muted py-4">Buscá remitos emitidos pendientes de facturación.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm mb-3">
|
||||||
|
<div class="card-header"><h5 class="mb-0">Resumen comercial</h5></div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if (SelectedCandidates.Any())
|
||||||
|
{
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>Cliente fiscal:</strong><br />
|
||||||
|
@SelectedCustomerLabel
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<strong>Remitos:</strong><br />@SelectedCandidates.Count
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Total:</strong><br />@SelectedTotal.ToString("N2")
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Validación:</strong><br />
|
||||||
|
@if (HasSingleFiscalCustomer)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Cliente único</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-danger">Clientes fiscales distintos</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Observaciones</label>
|
||||||
|
<textarea class="form-control" rows="3" @bind="Observations"></textarea>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-muted">Seleccioná uno o más remitos para ver el resumen.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<button type="button" class="btn btn-secondary rounded-pill" @onclick="BackToList" disabled="@IsSaving">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Volver
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary rounded-pill" @onclick="CreateAsync" disabled="@(IsSaving || !CanCreate)">
|
||||||
|
@if (IsSaving)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
}
|
||||||
|
<i class="fas fa-save me-1"></i> Crear Sales Document
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private SalesDocumentCreatePageModel Model = new()
|
private string? CustomerText;
|
||||||
{
|
private string? DeliveryNoteNumber;
|
||||||
IssueDate = DateTime.Today,
|
private int? QuoteId;
|
||||||
DocumentType = (int)SalesDocumentType.Invoice,
|
private DateTime? IssueDateFrom;
|
||||||
Currency = "ARS",
|
private DateTime? IssueDateTo;
|
||||||
ExchangeRate = 1,
|
private string? Observations;
|
||||||
CoverageType = (int)SalesDocumentCoverageType.Manual,
|
private bool IsLoading;
|
||||||
CoveragePercentage = 100
|
|
||||||
};
|
|
||||||
|
|
||||||
private ELookUpItem? SelectedCustomer;
|
|
||||||
private ELookUpItem? SelectedBillToCustomer;
|
|
||||||
private List<SalesDocumentItemRow> Items = new();
|
|
||||||
private bool IsSaving;
|
private bool IsSaving;
|
||||||
|
private PagedResult<SalesDocumentDeliveryNoteCandidateDto> Candidates = new();
|
||||||
|
private readonly HashSet<int> SelectedIds = new();
|
||||||
|
private readonly Dictionary<int, SalesDocumentDeliveryNoteCandidateDto> SelectedMap = new();
|
||||||
|
|
||||||
private static readonly List<SelectOption> DocumentTypeOptions = Enum.GetValues<SalesDocumentType>()
|
private List<SalesDocumentDeliveryNoteCandidateDto> SelectedCandidates => SelectedMap.Values.OrderBy(x => x.IssueDate).ThenBy(x => x.Id).ToList();
|
||||||
.Select(x => new SelectOption((int)x, GetDocumentTypeLabel((int)x)))
|
private bool HasSingleFiscalCustomer => SelectedCandidates.Select(x => x.CustomerId).Distinct().Count() <= 1;
|
||||||
.ToList();
|
private bool CanCreate => SelectedIds.Count > 0 && HasSingleFiscalCustomer && SelectedTotal > 0;
|
||||||
|
private decimal SelectedTotal => SelectedCandidates.Sum(x => x.ApprovedAmount);
|
||||||
|
private string SelectedCustomerLabel => SelectedCandidates.FirstOrDefault()?.CustomerName ?? "-";
|
||||||
|
|
||||||
private static readonly List<SelectOption> CoverageTypeOptions = Enum.GetValues<SalesDocumentCoverageType>()
|
private async Task SearchCandidatesAsync()
|
||||||
.Select(x => new SelectOption((int)x, GetCoverageTypeLabel((int)x)))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static readonly List<SelectOption> OriginTypeOptions = Enum.GetValues<SalesDocumentOriginType>()
|
|
||||||
.Select(x => new SelectOption((int)x, GetOriginTypeLabel(x)))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
{
|
||||||
AddItem();
|
try
|
||||||
}
|
|
||||||
|
|
||||||
private void AddItem()
|
|
||||||
{
|
|
||||||
Items.Add(new SalesDocumentItemRow
|
|
||||||
{
|
{
|
||||||
LineNumber = Items.Count + 1,
|
IsLoading = true;
|
||||||
OriginType = (int)SalesDocumentOriginType.Manual,
|
Candidates = await SalesDocumentService.SearchDeliveryNoteCandidatesAsync(
|
||||||
Quantity = 1,
|
null,
|
||||||
UnitPrice = 0,
|
CustomerText,
|
||||||
TaxAmount = 0
|
DeliveryNoteNumber,
|
||||||
});
|
QuoteId,
|
||||||
}
|
IssueDateFrom,
|
||||||
|
IssueDateTo,
|
||||||
private void RemoveItem(SalesDocumentItemRow item)
|
1,
|
||||||
{
|
50);
|
||||||
if (Items.Remove(item))
|
|
||||||
ReindexItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReindexItems()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < Items.Count; i++)
|
|
||||||
Items[i].LineNumber = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnCustomerSelected(ELookUpItem? customer)
|
|
||||||
{
|
|
||||||
SelectedCustomer = customer;
|
|
||||||
Model.CustomerId = customer?.Id;
|
|
||||||
|
|
||||||
if (SelectedBillToCustomer is null && customer is not null)
|
|
||||||
{
|
|
||||||
SelectedBillToCustomer = customer;
|
|
||||||
Model.BillToCustomerId = customer.Id;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnBillToCustomerSelected(ELookUpItem? customer)
|
|
||||||
{
|
|
||||||
SelectedBillToCustomer = customer;
|
|
||||||
Model.BillToCustomerId = customer?.Id;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? ValidateBeforeSave()
|
|
||||||
{
|
|
||||||
if (Model.CustomerId is null or <= 0)
|
|
||||||
return "Debe seleccionar un cliente.";
|
|
||||||
|
|
||||||
if (Model.BillToCustomerId is null or <= 0)
|
|
||||||
return "Debe seleccionar un cliente de facturación.";
|
|
||||||
|
|
||||||
if (Model.QuoteId is null or <= 0)
|
|
||||||
return "Debe informar un Presupuesto ID para coverage.";
|
|
||||||
|
|
||||||
if (Items.Count == 0)
|
|
||||||
return "Debe incluir al menos un detail.";
|
|
||||||
|
|
||||||
if (Items.Any(x => string.IsNullOrWhiteSpace(x.Description)))
|
|
||||||
return "Todos los detalles deben tener descripción.";
|
|
||||||
|
|
||||||
if (Items.Any(x => x.Quantity <= 0))
|
|
||||||
return "Todos los detalles deben tener cantidad mayor a cero.";
|
|
||||||
|
|
||||||
if (Items.Any(x => x.UnitPrice < 0 || x.TaxAmount < 0))
|
|
||||||
return "Los importes no pueden ser negativos.";
|
|
||||||
|
|
||||||
if (Items.Any(x => x.OriginType != (int)SalesDocumentOriginType.Manual
|
|
||||||
&& (!x.OriginId.HasValue || x.OriginId.Value <= 0)
|
|
||||||
&& (!x.QuoteDetailId.HasValue || x.QuoteDetailId.Value <= 0)))
|
|
||||||
return "Los detalles no manuales deben informar Origin ID o Quote detail.";
|
|
||||||
|
|
||||||
if (Items.Sum(GetItemTotal) <= 0)
|
|
||||||
return "El total del documento debe ser mayor a cero.";
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleValidSubmit()
|
|
||||||
{
|
|
||||||
var validationError = ValidateBeforeSave();
|
|
||||||
if (!string.IsNullOrWhiteSpace(validationError))
|
|
||||||
{
|
{
|
||||||
toastService.ShowError(validationError);
|
toastService.ShowError(ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleSelection(SalesDocumentDeliveryNoteCandidateDto item, ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
var selected = args.Value is bool value && value;
|
||||||
|
if (selected)
|
||||||
|
{
|
||||||
|
SelectedIds.Add(item.Id);
|
||||||
|
SelectedMap[item.Id] = item;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedIds.Remove(item.Id);
|
||||||
|
SelectedMap.Remove(item.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearFilters()
|
||||||
|
{
|
||||||
|
CustomerText = null;
|
||||||
|
DeliveryNoteNumber = null;
|
||||||
|
QuoteId = null;
|
||||||
|
IssueDateFrom = null;
|
||||||
|
IssueDateTo = null;
|
||||||
|
Candidates = new PagedResult<SalesDocumentDeliveryNoteCandidateDto>();
|
||||||
|
SelectedIds.Clear();
|
||||||
|
SelectedMap.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateAsync()
|
||||||
|
{
|
||||||
|
if (!CanCreate)
|
||||||
|
{
|
||||||
|
toastService.ShowError("Debe seleccionar remitos pendientes de un único cliente fiscal.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsSaving = true;
|
IsSaving = true;
|
||||||
ReindexItems();
|
var created = await SalesDocumentService.CreateFromDeliveryNotesAsync(new SalesDocumentCreateFromDeliveryNotesRequest
|
||||||
|
|
||||||
var request = new SalesDocumentCreateRequest
|
|
||||||
{
|
{
|
||||||
DocumentType = Model.DocumentType,
|
DeliveryNoteIds = SelectedIds.ToList(),
|
||||||
QuoteId = Model.QuoteId,
|
DocumentType = (int)SalesDocumentType.Invoice,
|
||||||
CustomerId = Model.CustomerId!.Value,
|
IssueDate = DateTime.Today,
|
||||||
BillToCustomerId = Model.BillToCustomerId!.Value,
|
Currency = "ARS",
|
||||||
IssueDate = Model.IssueDate,
|
ExchangeRate = 1,
|
||||||
Currency = Model.Currency.Trim(),
|
Observations = Observations
|
||||||
ExchangeRate = Model.ExchangeRate <= 0 ? 1 : Model.ExchangeRate,
|
});
|
||||||
Observations = Model.Observations,
|
|
||||||
PeriodFrom = Model.PeriodFrom,
|
|
||||||
PeriodTo = Model.PeriodTo,
|
|
||||||
Details = Items.Select(x =>
|
|
||||||
{
|
|
||||||
var netAmount = GetItemNet(x);
|
|
||||||
var totalAmount = GetItemTotal(x);
|
|
||||||
return new SalesDocumentCreateDetailRequest
|
|
||||||
{
|
|
||||||
LineNumber = x.LineNumber,
|
|
||||||
OriginType = (SalesDocumentOriginType)x.OriginType,
|
|
||||||
OriginId = x.OriginId,
|
|
||||||
QuoteDetailId = x.QuoteDetailId,
|
|
||||||
Description = x.Description.Trim(),
|
|
||||||
Quantity = x.Quantity,
|
|
||||||
UnitPrice = x.UnitPrice,
|
|
||||||
NetAmount = netAmount,
|
|
||||||
TaxAmount = x.TaxAmount,
|
|
||||||
TotalAmount = totalAmount
|
|
||||||
};
|
|
||||||
}).ToList(),
|
|
||||||
Coverage = new List<SalesDocumentCreateCoverageRequest>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
QuoteId = Model.QuoteId!.Value,
|
|
||||||
CoverageType = Model.CoverageType,
|
|
||||||
CoveragePercentage = Model.CoveragePercentage,
|
|
||||||
CoverageAmount = Items.Sum(GetItemTotal),
|
|
||||||
PeriodFrom = Model.PeriodFrom,
|
|
||||||
PeriodTo = Model.PeriodTo,
|
|
||||||
Notes = "Coverage manual desde UI"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var created = await SalesDocumentService.CreateAsync(request);
|
toastService.ShowSuccess("Sales Document creado correctamente desde remitos.");
|
||||||
toastService.ShowSuccess("Sales Document creado correctamente.");
|
|
||||||
Navigation.NavigateTo($"/salesdocuments/{created.Id}");
|
Navigation.NavigateTo($"/salesdocuments/{created.Id}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -396,82 +260,4 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void BackToList() => Navigation.NavigateTo("/salesdocuments");
|
private void BackToList() => Navigation.NavigateTo("/salesdocuments");
|
||||||
|
|
||||||
private static decimal GetItemNet(SalesDocumentItemRow item) => item.Quantity * item.UnitPrice;
|
|
||||||
private static decimal GetItemTotal(SalesDocumentItemRow item) => GetItemNet(item) + item.TaxAmount;
|
|
||||||
|
|
||||||
private static string GetDocumentTypeLabel(int value) => Enum.IsDefined(typeof(SalesDocumentType), value)
|
|
||||||
? ((SalesDocumentType)value) switch
|
|
||||||
{
|
|
||||||
SalesDocumentType.Invoice => "Factura",
|
|
||||||
SalesDocumentType.DebitNote => "Nota de débito",
|
|
||||||
SalesDocumentType.CreditNote => "Nota de crédito",
|
|
||||||
SalesDocumentType.CreditInvoice => "Factura crédito",
|
|
||||||
SalesDocumentType.CreditDebitNote => "N/D crédito",
|
|
||||||
SalesDocumentType.CreditCreditNote => "N/C crédito",
|
|
||||||
_ => value.ToString()
|
|
||||||
}
|
|
||||||
: value.ToString();
|
|
||||||
|
|
||||||
private static string GetCoverageTypeLabel(int value) => Enum.IsDefined(typeof(SalesDocumentCoverageType), value)
|
|
||||||
? ((SalesDocumentCoverageType)value) switch
|
|
||||||
{
|
|
||||||
SalesDocumentCoverageType.Direct => "Directa",
|
|
||||||
SalesDocumentCoverageType.Capita => "Cápita",
|
|
||||||
SalesDocumentCoverageType.Adjustment => "Ajuste",
|
|
||||||
SalesDocumentCoverageType.Manual => "Manual",
|
|
||||||
_ => value.ToString()
|
|
||||||
}
|
|
||||||
: value.ToString();
|
|
||||||
|
|
||||||
private static string GetOriginTypeLabel(SalesDocumentOriginType value) => value switch
|
|
||||||
{
|
|
||||||
SalesDocumentOriginType.Manual => "Manual",
|
|
||||||
SalesDocumentOriginType.QuoteDetail => "Presupuesto",
|
|
||||||
SalesDocumentOriginType.Adjustment => "Ajuste",
|
|
||||||
SalesDocumentOriginType.Capita => "Cápita",
|
|
||||||
SalesDocumentOriginType.DeliveryNote => "Remito",
|
|
||||||
_ => value.ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
private sealed class SalesDocumentCreatePageModel
|
|
||||||
{
|
|
||||||
[Required(ErrorMessage = "La fecha es obligatoria.")]
|
|
||||||
public DateTime? IssueDate { get; set; }
|
|
||||||
|
|
||||||
public int DocumentType { get; set; }
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "El cliente es obligatorio.")]
|
|
||||||
public int? CustomerId { get; set; }
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "El cliente de facturación es obligatorio.")]
|
|
||||||
public int? BillToCustomerId { get; set; }
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "El presupuesto es obligatorio para coverage.")]
|
|
||||||
public int? QuoteId { get; set; }
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "La moneda es obligatoria.")]
|
|
||||||
public string Currency { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public decimal ExchangeRate { get; set; }
|
|
||||||
public int CoverageType { get; set; }
|
|
||||||
public decimal? CoveragePercentage { get; set; }
|
|
||||||
public DateTime? PeriodFrom { get; set; }
|
|
||||||
public DateTime? PeriodTo { get; set; }
|
|
||||||
public string? Observations { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class SalesDocumentItemRow
|
|
||||||
{
|
|
||||||
public int LineNumber { get; set; }
|
|
||||||
public int OriginType { get; set; }
|
|
||||||
public int? OriginId { get; set; }
|
|
||||||
public int? QuoteDetailId { get; set; }
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
public decimal UnitPrice { get; set; }
|
|
||||||
public decimal TaxAmount { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record SelectOption(int Value, string Label);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,5 +8,15 @@ namespace phronCare.UIBlazor.Services.Sales.SalesDocuments
|
|||||||
Task<PagedResult<SalesDocumentSummaryDto>> SearchAsync(SalesDocumentSearchParams searchParams);
|
Task<PagedResult<SalesDocumentSummaryDto>> SearchAsync(SalesDocumentSearchParams searchParams);
|
||||||
Task<SalesDocumentDto?> GetByIdAsync(int id);
|
Task<SalesDocumentDto?> GetByIdAsync(int id);
|
||||||
Task<SalesDocumentDto> CreateAsync(SalesDocumentCreateRequest request);
|
Task<SalesDocumentDto> CreateAsync(SalesDocumentCreateRequest request);
|
||||||
|
Task<SalesDocumentDto> CreateFromDeliveryNotesAsync(SalesDocumentCreateFromDeliveryNotesRequest request);
|
||||||
|
Task<PagedResult<SalesDocumentDeliveryNoteCandidateDto>> SearchDeliveryNoteCandidatesAsync(
|
||||||
|
int? customerId,
|
||||||
|
string? customerText,
|
||||||
|
string? deliveryNoteNumber,
|
||||||
|
int? quoteId,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,60 @@ namespace phronCare.UIBlazor.Services.Sales.SalesDocuments
|
|||||||
return result ?? new PagedResult<SalesDocumentSummaryDto>();
|
return result ?? new PagedResult<SalesDocumentSummaryDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<PagedResult<SalesDocumentDeliveryNoteCandidateDto>> SearchDeliveryNoteCandidatesAsync(
|
||||||
|
int? customerId,
|
||||||
|
string? customerText,
|
||||||
|
string? deliveryNoteNumber,
|
||||||
|
int? quoteId,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50)
|
||||||
|
{
|
||||||
|
var queryParams = new List<string>();
|
||||||
|
|
||||||
|
void AddParam(string key, string? value)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
queryParams.Add($"{key}={Uri.EscapeDataString(value)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
AddParam("customerId", customerId?.ToString());
|
||||||
|
AddParam("customerText", customerText);
|
||||||
|
AddParam("deliveryNoteNumber", deliveryNoteNumber);
|
||||||
|
AddParam("quoteId", quoteId?.ToString());
|
||||||
|
AddParam("issueDateFrom", issueDateFrom?.ToString("o"));
|
||||||
|
AddParam("issueDateTo", issueDateTo?.ToString("o"));
|
||||||
|
AddParam("page", page.ToString());
|
||||||
|
AddParam("pageSize", pageSize.ToString());
|
||||||
|
|
||||||
|
var url = "/api/SalesDocument/delivery-note-candidates";
|
||||||
|
if (queryParams.Any())
|
||||||
|
url += "?" + string.Join("&", queryParams);
|
||||||
|
|
||||||
|
var result = await _http.GetFromJsonAsync<PagedResult<SalesDocumentDeliveryNoteCandidateDto>>(url);
|
||||||
|
return result ?? new PagedResult<SalesDocumentDeliveryNoteCandidateDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SalesDocumentDto> CreateFromDeliveryNotesAsync(SalesDocumentCreateFromDeliveryNotesRequest request)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
var response = await _http.PostAsJsonAsync("/api/SalesDocument/from-delivery-notes", request);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var serverMessage = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception(string.IsNullOrWhiteSpace(serverMessage)
|
||||||
|
? "No se pudo crear el Sales Document desde remitos."
|
||||||
|
: serverMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<SalesDocumentDto>();
|
||||||
|
return result ?? throw new Exception("Respuesta vacía del servidor.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<SalesDocumentDto?> GetByIdAsync(int id)
|
public async Task<SalesDocumentDto?> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
return await _http.GetFromJsonAsync<SalesDocumentDto>($"/api/SalesDocument/{id}");
|
return await _http.GetFromJsonAsync<SalesDocumentDto>($"/api/SalesDocument/{id}");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user