Update GetByIdDTO
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m14s

This commit is contained in:
Leandro Hernan Rojas 2025-05-16 17:19:40 -03:00
parent 510862ed60
commit 5ec19044f2
14 changed files with 335 additions and 280 deletions

View File

@ -19,6 +19,7 @@ namespace Models.Interfaces
#endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task<QuoteDto?> GetDtoByIdAsync(int id);
#endregion
}
}

View File

@ -13,6 +13,7 @@ namespace Core.Services
//private readonly IPhSQuoteRepository _quoteRepository = quoteRepository;
#endregion
#region Presupuestos
public async Task<PagedResult<QuoteDto>> SearchAsync(int? customerId, string? customerText, string? quoteNumber, int? professionalId, string? professionalText, int? institutionId, string? institutionText, int? patientId, string? patientText, DateTime? issueDateFrom, DateTime? issueDateTo, string? status, int page, int pageSize)
{
return await _quoteRepository.SearchAsync(
@ -31,7 +32,11 @@ namespace Core.Services
page,
pageSize);
}
#region Presupuestos
public async Task<QuoteDto?> GetDtoByIdAsync(int id)
{
return await _quoteRepository.GetDtoByIdAsync(id);
}
#endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)

View File

@ -23,9 +23,6 @@ namespace Documents.Services
{
// 👉 Renderizar HTML usando RazorLight
string html = await _templateRenderer.RenderAsync("Quotes/Template_v1.cshtml", request.Model);
// 🔍 Dump HTML a archivo temporal para inspección
File.WriteAllText("/tmp/html_debug_output.html", html, Encoding.UTF8);
// 👉 Generar PDF desde el HTML
return await _pdfGeneratorService.GeneratePdfFromHtmlAsync(html);
}

View File

@ -1,6 +1,9 @@
@model Domain.Dtos.QuoteDto
@using Domain.Dtos
@using System.Globalization
@model Domain.Dtos.QuoteDto
@{
var culture = System.Globalization.CultureInfo.GetCultureInfo("es-AR");
var culture = CultureInfo.GetCultureInfo("es-AR");
}
<!DOCTYPE html>
@ -10,171 +13,163 @@
<title>Presupuesto @Model.Quotenumber</title>
<style>
body {
font-family: "Liberation Sans", "DejaVu Sans", sans-serif;
font-size: 13px;
color: #222;
margin: 40px;
line-height: 1.5;
font-family: 'Liberation Sans', Arial, sans-serif;
font-size: 12px;
color: #000;
margin: 30px;
padding: 20px;
box-sizing: border-box;
border: 1px solid #000;
}
.company-info {
font-size: 11px;
line-height: 1.4;
h1, h2, h3 {
margin: 0;
padding: 0;
}
.header {
text-align: center;
margin-bottom: 20px;
color: #444;
margin-bottom: 10px;
}
.header, .footer {
text-align: center;
}
.footer {
font-size: 10px;
margin-top: 40px;
color: #888;
}
h1 {
font-size: 22px;
margin-bottom: 0;
color: #004B8D;
}
h2 {
font-size: 16px;
margin-top: 30px;
color: #004B8D;
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
.section {
margin-top: 10px;
min-height: 60px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
margin-top: 5px;
}
th, td {
border: 1px solid #ccc;
padding: 6px;
vertical-align: top;
padding: 4px;
text-align: left;
}
th {
background-color: #f0f0f0;
text-align: left;
}
.totals-table td {
border: none;
.totales {
margin-top: 10px;
float: right;
width: 45%;
}
.right {
text-align: right;
}
.highlight {
background-color: #e6f7ff;
}
.observaciones {
white-space: pre-wrap;
border: 1px solid #ccc;
padding: 10px;
background: #f9f9f9;
.footer {
text-align: center;
font-size: 10px;
margin-top: 20px;
color: #666;
}
</style>
</head>
<body>
<div class="company-info">
<strong>BIODEC S.A.</strong><br />
CUIT: 33-70849672-9 &nbsp; | &nbsp; Ing. Brutos: 901-070604-2<br />
Inicio Actividades: 02/10/2003 &nbsp; | &nbsp; IVA: Responsable Inscripto<br />
Segurola 1885 C.P. C1407AOK CABA Buenos Aires Argentina<br />
Tel: (011) 4864-6005 &nbsp; | &nbsp; Fax: 4864-5710<br />
<a href="mailto:ventas@biodec.net">ventas@biodec.net</a> &nbsp; | &nbsp;
Web: www.biodec.net<br />
Urgencias: 15-2155-9380 / 15-5909-4987 / 15-5909-4892
</div>
<div class="header">
<h1>phronCare - Presupuesto</h1>
<p><strong>Número:</strong> @Model.Quotenumber</p>
<p><strong>Fecha de Emisión:</strong> @Model.IssueDate.ToString("dd/MM/yyyy")</p>
<h2>Presupuesto Nº @Model.Quotenumber</h2>
<p>Fecha de emisión: @Model.IssueDate.ToString("dd/MM/yyyy")</p>
</div>
<h2>Datos del Cliente</h2>
<table>
<tr><th>Razón Social</th><td>@Model.CustomerName</td></tr>
<tr><th>Paciente</th><td>@Model.PatientName</td></tr>
<tr><th>Médico</th><td>@Model.ProfessionalName</td></tr>
<tr><th>Institución</th><td>@Model.InstitutionName</td></tr>
<tr><th>Fecha estimada cirugía</th><td>@(Model.EstimatedDate?.ToString("dd/MM/yyyy") ?? "-")</td></tr>
</table>
<h2>Productos Cotizados</h2>
<table>
<thead>
<tr>
<th>Cantidad</th>
<th>Descripción</th>
<th class="right">Precio Unitario (@Model.Currency)</th>
<th class="right">Total (@Model.Currency)</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Quantity</td>
<td>@item.Description</td>
<td class="right">@item.UnitPrice.ToString("C", culture)</td>
<td class="right">@item.Total.ToString("C", culture)</td>
</tr>
}
</tbody>
</table>
<h2>Totales</h2>
<table class="totals-table">
<tr>
<td class="right"><strong>Subtotal:</strong></td>
<td class="right">@Model.Items.Sum(i => i.Subtotal).ToString("C", culture)</td>
</tr>
@if (Model.Taxes?.Any() == true)
<div class="section">
<h3>Datos del Cliente</h3>
<p><strong>Nombre:</strong> @Model.Customer.Name</p>
<p><strong>Domicilio:</strong> @Model.Customer.Address</p>
<p><strong>Condición IVA:</strong> @Model.Customer.IvaCondition</p>
@if (Model.Customer.Documents != null && Model.Customer.Documents.Any())
{
foreach (var tax in Model.Taxes)
{
<tr>
<td class="right"><strong>@tax.TaxName (@tax.TaxRate%)</strong></td>
<td class="right">@tax.TaxAmount.ToString("C", culture)</td>
</tr>
}
<p><strong>Documentos:</strong></p>
<ul>
@foreach (var doc in Model.Customer.Documents)
{
<li>@doc.DocumentType: @doc.Number</li>
}
</ul>
}
@if (Model.Adjustments?.Any() == true)
else
{
foreach (var adj in Model.Adjustments)
{
<tr>
<td class="right"><strong>Ajuste: @adj.Reason</strong></td>
<td class="right">@adj.Amount.ToString("C", culture)</td>
</tr>
}
<p>— Sin documentos —</p>
}
<tr class="highlight">
<td class="right"><strong>Total Final:</strong></td>
<td class="right">@Model.Total.ToString("C", culture)</td>
</tr>
</table>
</div>
<h2>Observaciones</h2>
<div class="observaciones">
@Model.Observations
<div class="section">
<h3>Datos del Paciente y Atención</h3>
<p><strong>Paciente:</strong> @Model.PatientName</p>
<p><strong>Médico:</strong> @Model.ProfessionalName</p>
<p><strong>Institución:</strong> @Model.InstitutionName</p>
<p><strong>Fecha estimada:</strong> @(Model.EstimatedDate?.ToString("dd/MM/yyyy") ?? "—")</p>
</div>
<div class="section">
<h3>Productos Cotizados</h3>
<table>
<thead>
<tr>
<th>Descripción</th>
<th>Cantidad</th>
<th>Precio Unitario</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Description</td>
<td>@item.Quantity</td>
<td>@item.UnitPrice.ToString("C", culture)</td>
<td>@item.Total.ToString("C", culture)</td>
</tr>
}
</tbody>
</table>
</div>
<div class="section">
<h3>Totales</h3>
<table class="totales">
<tbody>
<tr>
<td><strong>Subtotal</strong></td>
<td>@Model.Items.Sum(i => i.Subtotal).ToString("C", culture)</td>
</tr>
@if (Model.Taxes?.Any() == true)
{
@foreach (var tax in Model.Taxes)
{
<tr>
<td><strong>@tax.TaxName (@tax.TaxRate%)</strong></td>
<td>@tax.TaxAmount.ToString("C", culture)</td>
</tr>
}
}
@if (Model.Adjustments?.Any() == true)
{
@foreach (var adj in Model.Adjustments)
{
<tr>
<td><strong>Ajuste: @adj.Reason</strong></td>
<td>@adj.Amount.ToString("C", culture)</td>
</tr>
}
}
<tr>
<td><strong>Total Final</strong></td>
<td>@Model.Total.ToString("C", culture)</td>
</tr>
</tbody>
</table>
</div>
<div class="section">
<h3>Observaciones</h3>
<p>@(string.IsNullOrWhiteSpace(Model.Observations) ? "— Sin observaciones —" : Model.Observations)</p>
</div>
<div class="footer">
<p>Sistema de gestión y administración PhronCare © 2025</p>
<p>Sistema de Gestión y Administración - PhronCare © 2025</p>
</div>
</body>
</html>

View File

@ -12,4 +12,4 @@
/// </summary>
public decimal Amount { get; set; }
}
}
}

View File

@ -3,10 +3,8 @@
public class QuoteCustomerDto
{
public string Name { get; set; } = "";
public string Cuit { get; set; } = "";
public string IngresosBrutos { get; set; } = "";
public string Address { get; set; } = "";
public string IvaCondition { get; set; } = "";
public List<QuoteCustomerDocumentDto> Documents { get; set; } = new();
}
}
}

View File

@ -0,0 +1,8 @@
namespace Domain.Dtos
{
public class QuoteCustomerDocumentDto
{
public string DocumentType { get; set; } = "";
public string Number { get; set; } = "";
}
}

View File

@ -91,4 +91,4 @@
/// </summary>
public QuoteCustomerDto Customer { get; set; } = new();
}
}
}

View File

@ -32,4 +32,4 @@
/// </summary>
public bool IsIncludedInPrice { get; set; }
}
}
}

View File

@ -11,6 +11,7 @@ namespace Models.Interfaces
#endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task<QuoteDto?> GetDtoByIdAsync(int id);
#endregion
}
}

View File

@ -213,6 +213,129 @@ namespace Models.Repositories
PageSize = pagedEntities.PageSize
};
}
public async Task<QuoteDto?> GetDtoByIdAsync(int id)
{
var header = await _context.PhSQuoteHeaders
.Include(q => q.PhSQuoteDetails)
.Include(q => q.PhSQuoteRoles)
.Include(q => q.PhSQuoteAdjustments)
.Include(q => q.PhSQuoteTaxes)
.FirstOrDefaultAsync(q => q.Id == id);
if (header == null)
return null;
// Cargar Customer completo con documentos y tipos
var customer = await _context.PhSCustomers
.Include(c => c.PhSCustomerDocuments)
.ThenInclude(d => d.Documenttypes) // ✅ CORRECTO
.Include(c => c.PhSCustomerAddresses)
.Include(c => c.TaxCondition)
.FirstOrDefaultAsync(c => c.Id == header.CustomerId);
var totalTaxAmount = header.PhSQuoteTaxes.Sum(t => t.Taxamount);
var netBase = header.Netamount.GetValueOrDefault() != 0m
? header.Netamount.Value
: 1m;
var dto = new QuoteDto
{
Id = header.Id,
Quotenumber = header.Quotenumber,
IssueDate = header.Issuedate,
EstimatedDate = header.Estimateddate,
Currency = header.Currency,
Total = header.Total.GetValueOrDefault(0m),
Status = header.Status.Trim(),
Observations = header.Observations ?? "",
CustomerName = customer?.Name ?? "",
BusinessUnitName = _context.PhSBusinessUnits
.Where(b => b.Id == header.BusinessunitId)
.Select(b => b.Code)
.FirstOrDefault() ?? "",
ProfessionalName = header.PhSQuoteRoles
.Where(r => r.Entitytype == PhSEntityTypes.Professional)
.Select(r => _context.PhSProfessionals
.Where(p => p.Id == r.EntityId)
.Select(p => p.Fullname)
.FirstOrDefault())
.FirstOrDefault() ?? "",
InstitutionName = header.PhSQuoteRoles
.Where(r => r.Entitytype == PhSEntityTypes.Institution)
.Select(r => _context.PhSInstitutions
.Where(i => i.Id == r.EntityId)
.Select(i => i.Name)
.FirstOrDefault())
.FirstOrDefault() ?? "",
PatientName = header.PhSQuoteRoles
.Where(r => r.Entitytype == PhSEntityTypes.Patient)
.Select(r => _context.PhSPatients
.Where(pt => pt.Id == r.EntityId)
.Select(pt => (pt.Firstname + " " + pt.Lastname).Trim())
.FirstOrDefault())
.FirstOrDefault() ?? "",
SalespersonName = _context.PhSPeople
.Where(u => u.Id == header.PeopleId)
.Select(u => u.Name)
.FirstOrDefault() ?? "",
Items = header.PhSQuoteDetails.Select(d =>
{
var itemBase = d.Quantity * d.Unitprice;
var itemTax = totalTaxAmount * itemBase / netBase;
return new QuoteItemDto
{
Description = d.ProductDescription,
Quantity = d.Quantity,
UnitPrice = d.Unitprice,
Subtotal = itemBase,
TaxAmount = itemTax,
Total = itemBase + itemTax
};
}).ToList(),
Taxes = header.PhSQuoteTaxes.Select(t => new QuoteTaxDto
{
TaxName = t.Taxname,
TaxCode = t.Taxcode,
TaxableAmount = t.TaxableAmount,
TaxRate = t.Taxrate,
TaxAmount = t.Taxamount,
IsIncludedInPrice = t.IsIncludedInPrice
}).ToList(),
Adjustments = header.PhSQuoteAdjustments.Select(a => new QuoteAdjustmentDto
{
Reason = _context.PhSAdjustmentReasons
.Where(r => r.Code == a.ReasonCode)
.Select(r => r.Description)
.FirstOrDefault() ?? "",
Amount = a.Amount.GetValueOrDefault(0m)
}).ToList(),
Customer = new QuoteCustomerDto
{
Name = customer?.Name ?? "",
Address = customer?.PhSCustomerAddresses
.Select(a => a.Streetaddress1)
.FirstOrDefault() ?? "",
IvaCondition = customer?.TaxCondition?.Description ?? "",
Documents = customer?.PhSCustomerDocuments
.Select(d => new QuoteCustomerDocumentDto
{
DocumentType = d.Documenttypes.Name, // ✅ correcto
Number = d.DocumentNumber
}).ToList() ?? new()
}
};
return dto;
}
#endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)

View File

@ -0,0 +1,22 @@
using Documents.Interfaces;
using Documents.Models;
using Domain.Dtos;
using Microsoft.AspNetCore.Mvc;
using Models.Interfaces;
namespace phronCare.API.Controllers.Documents
{
public class DocumentController : ControllerBase
{
private readonly IDocumentTemplateService _documentTemplateService;
private readonly IQuoteDom _quoteService;
public DocumentController(IDocumentTemplateService documentTemplateService, IQuoteDom quoteService)
{
_documentTemplateService = documentTemplateService;
_quoteService = quoteService;
}
}
}

View File

@ -1,9 +1,11 @@
using Domain.Dtos;
using Microsoft.AspNetCore.Mvc;
using Documents.Models;
using Domain.Dtos;
using Domain.Entities;
using Domain.Generics;
using Microsoft.AspNetCore.Mvc;
using Models.Interfaces;
using System.Reflection;
using Documents.Interfaces;
namespace phronCare.API.Controllers.Sales
{
@ -11,10 +13,14 @@ namespace phronCare.API.Controllers.Sales
[ApiController]
public class QuoteController : ControllerBase
{
private readonly IDocumentTemplateService _documentTemplateService;
private readonly IQuoteDom _quoteService;
public QuoteController(IQuoteDom quoteService)
public QuoteController(IDocumentTemplateService documentTemplateService, IQuoteDom quoteService)
{
_quoteService = quoteService ?? throw new ArgumentNullException(nameof(quoteService));
_documentTemplateService = documentTemplateService ??
throw new ArgumentNullException(nameof(documentTemplateService)); ;
_quoteService = quoteService ??
throw new ArgumentNullException(nameof(quoteService));
}
#region Obtener Presupuestos
@ -65,143 +71,24 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpGet("quote/{id}/pdf")]
public async Task<IActionResult> GetQuotePdf(int id)
{
var quote = await _quoteService.GetDtoByIdAsync(id);
if (quote == null)
return NotFound($"Presupuesto con ID {id} no encontrado.");
var pdfBytes = await _documentTemplateService.GenerateDocumentAsync(new DocumentGenerationRequest
{
Model = quote
});
return File(pdfBytes, "application/pdf", $"Presupuesto_{quote.Quotenumber}.pdf");
}
#endregion
//[HttpGet("all")]
//public async Task<IActionResult> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50)
//{
// try
// {
// var result = await _quoteService.GetAllQuotesAsync(page, pageSize);
// return Ok(result);
// }
// catch (Exception ex)
// {
// var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
// return StatusCode(500, $"{methodName} Message: {ex.Message}");
// }
//}
//[HttpGet("search")]
//public async Task<IActionResult> Search(
// [FromQuery] int? customerId,
// [FromQuery] string? customerText,
// [FromQuery] string? quoteNumber,
// [FromQuery] int? professionalId,
// [FromQuery] string? professionalText,
// [FromQuery] int? institutionId,
// [FromQuery] string? institutionText,
// [FromQuery] int? patientId,
// [FromQuery] string? patientText,
// [FromQuery] DateTime? issueDateFrom,
// [FromQuery] DateTime? issueDateTo,
// [FromQuery] string? status,
// [FromQuery] int page = 1,
// [FromQuery] int pageSize = 50)
//{
// try
// {
// var result = await _quoteService.SearchAsync(
// customerId,
// customerText,
// quoteNumber,
// professionalId,
// professionalText,
// institutionId,
// institutionText,
// patientId,
// patientText,
// issueDateFrom,
// issueDateTo,
// status,
// page,
// pageSize);
// return Ok(result);
// }
// catch (Exception ex)
// {
// var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
// return StatusCode(500, $"{methodName} Message: {ex.Message}");
// }
//}
//[HttpGet("{id:int}")]
//public async Task<ActionResult<EQuoteHeader>> GetById(int id)
//{
// try
// {
// var quote = await _quoteService.GetQuoteByIdAsync(id);
// if (quote == null)
// return NotFound();
// return Ok(quote);
// }
// catch (Exception ex)
// {
// var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
// return StatusCode(500, $"{methodName} Message: {ex.Message}");
// }
//}
//#endregion
//#region Crear / Actualizar / Eliminar
//[HttpPut("update")]
//public async Task<IActionResult> Update([FromBody] EQuoteHeader quote)
//{
// try
// {
// if (quote == null || quote.Id <= 0)
// return BadRequest("El presupuesto es inválido o no tiene un ID válido.");
// await _quoteService.UpdateQuoteAsync(quote);
// return Ok("Presupuesto actualizado correctamente.");
// }
// catch (Exception ex)
// {
// var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
// return StatusCode(500, $"{methodName} Message: {ex.Message}");
// }
//}
//[HttpDelete("delete/{id:int}")]
//public async Task<IActionResult> Delete(int id)
//{
// try
// {
// await _quoteService.DeleteQuoteAsync(id);
// return Ok("Presupuesto eliminado correctamente.");
// }
// catch (Exception ex)
// {
// var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
// return StatusCode(500, $"{methodName} Message: {ex.Message}");
// }
//}
//#region Exportación
//[HttpPost("exportfiltered")]
//public async Task<IActionResult> ExportFiltered([FromBody] QuoteSearchParams searchParams)
//{
// try
// {
// var file = await _quoteService.ExportFilteredQuotesToExcelAsync(searchParams);
// return File(file,
// "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
// "Presupuestos.xlsx");
// }
// catch (Exception ex)
// {
// return BadRequest(ex.Message);
// }
//}
//#endregion
#region Endpoint de emision de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
[HttpPost("createfull")]
public async Task<IActionResult> CreateFullQuote([FromBody] CreateFullQuoteRequest request)
@ -243,5 +130,7 @@ namespace phronCare.API.Controllers.Sales
}
#endregion
}
}

View File

@ -1602,6 +1602,22 @@
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetQuotePdf",
"RelativePath": "api/Quote/quote/{id}/pdf",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "id",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "Search",