diff --git a/Core/Interfaces/IQuoteDom.cs b/Core/Interfaces/IQuoteDom.cs index 0456b30..346d184 100644 --- a/Core/Interfaces/IQuoteDom.cs +++ b/Core/Interfaces/IQuoteDom.cs @@ -19,6 +19,7 @@ namespace Models.Interfaces #endregion #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) Task CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); + Task GetDtoByIdAsync(int id); #endregion } } \ No newline at end of file diff --git a/Core/Services/QuoteService.cs b/Core/Services/QuoteService.cs index 3188e87..c72b2e4 100644 --- a/Core/Services/QuoteService.cs +++ b/Core/Services/QuoteService.cs @@ -13,6 +13,7 @@ namespace Core.Services //private readonly IPhSQuoteRepository _quoteRepository = quoteRepository; #endregion + #region Presupuestos public async Task> 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 GetDtoByIdAsync(int id) + { + return await _quoteRepository.GetDtoByIdAsync(id); + } #endregion #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) diff --git a/Documents/Services/DocumentTemplateService.cs b/Documents/Services/DocumentTemplateService.cs index 6c67b63..f769059 100644 --- a/Documents/Services/DocumentTemplateService.cs +++ b/Documents/Services/DocumentTemplateService.cs @@ -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); } diff --git a/Documents/Templates/Quotes/Template_v1.cshtml b/Documents/Templates/Quotes/Template_v1.cshtml index b9a124e..7ccb930 100644 --- a/Documents/Templates/Quotes/Template_v1.cshtml +++ b/Documents/Templates/Quotes/Template_v1.cshtml @@ -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"); } @@ -10,171 +13,163 @@ Presupuesto @Model.Quotenumber -
- BIODEC S.A.
- CUIT: 33-70849672-9   |   Ing. Brutos: 901-070604-2
- Inicio Actividades: 02/10/2003   |   IVA: Responsable Inscripto
- Segurola 1885 – C.P. C1407AOK – CABA – Buenos Aires – Argentina
- Tel: (011) 4864-6005   |   Fax: 4864-5710
- ventas@biodec.net   |   - Web: www.biodec.net
- Urgencias: 15-2155-9380 / 15-5909-4987 / 15-5909-4892 -
-
-

phronCare - Presupuesto

-

Número: @Model.Quotenumber

-

Fecha de Emisión: @Model.IssueDate.ToString("dd/MM/yyyy")

+

Presupuesto Nº @Model.Quotenumber

+

Fecha de emisión: @Model.IssueDate.ToString("dd/MM/yyyy")

-

Datos del Cliente

- - - - - - -
Razón Social@Model.CustomerName
Paciente@Model.PatientName
Médico@Model.ProfessionalName
Institución@Model.InstitutionName
Fecha estimada cirugía@(Model.EstimatedDate?.ToString("dd/MM/yyyy") ?? "-")
- -

Productos Cotizados

- - - - - - - - - - - @foreach (var item in Model.Items) - { - - - - - - - } - -
CantidadDescripciónPrecio Unitario (@Model.Currency)Total (@Model.Currency)
@item.Quantity@item.Description@item.UnitPrice.ToString("C", culture)@item.Total.ToString("C", culture)
- -

Totales

- - - - - - @if (Model.Taxes?.Any() == true) +
+

Datos del Cliente

+

Nombre: @Model.Customer.Name

+

Domicilio: @Model.Customer.Address

+

Condición IVA: @Model.Customer.IvaCondition

+ @if (Model.Customer.Documents != null && Model.Customer.Documents.Any()) { - foreach (var tax in Model.Taxes) - { -
- - - - } +

Documentos:

+
    + @foreach (var doc in Model.Customer.Documents) + { +
  • @doc.DocumentType: @doc.Number
  • + } +
} - @if (Model.Adjustments?.Any() == true) + else { - foreach (var adj in Model.Adjustments) - { - - - - - } +

— Sin documentos —

} - - - - -
Subtotal:@Model.Items.Sum(i => i.Subtotal).ToString("C", culture)
@tax.TaxName (@tax.TaxRate%)@tax.TaxAmount.ToString("C", culture)
Ajuste: @adj.Reason@adj.Amount.ToString("C", culture)
Total Final:@Model.Total.ToString("C", culture)
+ -

Observaciones

-
- @Model.Observations +
+

Datos del Paciente y Atención

+

Paciente: @Model.PatientName

+

Médico: @Model.ProfessionalName

+

Institución: @Model.InstitutionName

+

Fecha estimada: @(Model.EstimatedDate?.ToString("dd/MM/yyyy") ?? "—")

+
+ +
+

Productos Cotizados

+ + + + + + + + + + + @foreach (var item in Model.Items) + { + + + + + + + } + +
DescripciónCantidadPrecio UnitarioSubtotal
@item.Description@item.Quantity@item.UnitPrice.ToString("C", culture)@item.Total.ToString("C", culture)
+
+ +
+

Totales

+ + + + + + + @if (Model.Taxes?.Any() == true) + { + @foreach (var tax in Model.Taxes) + { + + + + + } + } + @if (Model.Adjustments?.Any() == true) + { + @foreach (var adj in Model.Adjustments) + { + + + + + } + } + + + + + +
Subtotal@Model.Items.Sum(i => i.Subtotal).ToString("C", culture)
@tax.TaxName (@tax.TaxRate%)@tax.TaxAmount.ToString("C", culture)
Ajuste: @adj.Reason@adj.Amount.ToString("C", culture)
Total Final@Model.Total.ToString("C", culture)
+
+ +
+

Observaciones

+

@(string.IsNullOrWhiteSpace(Model.Observations) ? "— Sin observaciones —" : Model.Observations)

diff --git a/Domain/Dtos/QuoteAdjustmentDto.cs b/Domain/Dtos/QuoteAdjustmentDto.cs index 58aef71..8a6c9ce 100644 --- a/Domain/Dtos/QuoteAdjustmentDto.cs +++ b/Domain/Dtos/QuoteAdjustmentDto.cs @@ -12,4 +12,4 @@ /// public decimal Amount { get; set; } } -} +} \ No newline at end of file diff --git a/Domain/Dtos/QuoteCustomer.cs b/Domain/Dtos/QuoteCustomer.cs index 468381c..2e7ba95 100644 --- a/Domain/Dtos/QuoteCustomer.cs +++ b/Domain/Dtos/QuoteCustomer.cs @@ -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 Documents { get; set; } = new(); } - -} +} \ No newline at end of file diff --git a/Domain/Dtos/QuoteCustomerDocumentDto.cs b/Domain/Dtos/QuoteCustomerDocumentDto.cs new file mode 100644 index 0000000..65a1a52 --- /dev/null +++ b/Domain/Dtos/QuoteCustomerDocumentDto.cs @@ -0,0 +1,8 @@ +namespace Domain.Dtos +{ + public class QuoteCustomerDocumentDto + { + public string DocumentType { get; set; } = ""; + public string Number { get; set; } = ""; + } +} \ No newline at end of file diff --git a/Domain/Dtos/QuoteDto.cs b/Domain/Dtos/QuoteDto.cs index d005c00..43d846c 100644 --- a/Domain/Dtos/QuoteDto.cs +++ b/Domain/Dtos/QuoteDto.cs @@ -91,4 +91,4 @@ /// public QuoteCustomerDto Customer { get; set; } = new(); } -} +} \ No newline at end of file diff --git a/Domain/Dtos/QuoteTaxDto.cs b/Domain/Dtos/QuoteTaxDto.cs index 79c11a4..96991da 100644 --- a/Domain/Dtos/QuoteTaxDto.cs +++ b/Domain/Dtos/QuoteTaxDto.cs @@ -32,4 +32,4 @@ /// public bool IsIncludedInPrice { get; set; } } -} +} \ No newline at end of file diff --git a/Models/Interfaces/IQuoteRepository.cs b/Models/Interfaces/IQuoteRepository.cs index 57380db..b1f38da 100644 --- a/Models/Interfaces/IQuoteRepository.cs +++ b/Models/Interfaces/IQuoteRepository.cs @@ -11,6 +11,7 @@ namespace Models.Interfaces #endregion #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) Task CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); + Task GetDtoByIdAsync(int id); #endregion } } diff --git a/Models/Repositories/PhSQuoteRepository.cs b/Models/Repositories/PhSQuoteRepository.cs index e364180..32dc858 100644 --- a/Models/Repositories/PhSQuoteRepository.cs +++ b/Models/Repositories/PhSQuoteRepository.cs @@ -213,6 +213,129 @@ namespace Models.Repositories PageSize = pagedEntities.PageSize }; } + public async Task 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) diff --git a/phronCare.API/Controllers/Documents/DocumentsController.cs b/phronCare.API/Controllers/Documents/DocumentsController.cs new file mode 100644 index 0000000..c0198e7 --- /dev/null +++ b/phronCare.API/Controllers/Documents/DocumentsController.cs @@ -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; + } + + + } +} diff --git a/phronCare.API/Controllers/Sales/QuoteController.cs b/phronCare.API/Controllers/Sales/QuoteController.cs index fcb2799..6e7569a 100644 --- a/phronCare.API/Controllers/Sales/QuoteController.cs +++ b/phronCare.API/Controllers/Sales/QuoteController.cs @@ -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 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 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 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> 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 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 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 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 CreateFullQuote([FromBody] CreateFullQuoteRequest request) @@ -243,5 +130,7 @@ namespace phronCare.API.Controllers.Sales } #endregion + + } } \ No newline at end of file diff --git a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json index f947915..92fddc1 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -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",