Compare commits

..

2 Commits

6 changed files with 227 additions and 4 deletions

View File

@ -4,9 +4,9 @@
{ {
Quote, Quote,
Expedition, Expedition,
DeliveryNote,
Invoice, Invoice,
Order, Order,
Remito,
Certificate Certificate
} }
} }

View File

@ -2,6 +2,7 @@
using Documents.Interfaces; using Documents.Interfaces;
using Documents.Models; using Documents.Models;
using Domain.Dtos; // QuoteDto using Domain.Dtos; // QuoteDto
using Domain.Dtos.Sales; // DeliveryNoteDto
using Domain.Dtos.Stock; // ExpeditionDto using Domain.Dtos.Stock; // ExpeditionDto
using Transversal.Interfaces; using Transversal.Interfaces;
@ -57,6 +58,7 @@ public class DocumentTemplateService : IDocumentTemplateService
private static string ResolveTemplate(DocumentType type) => type switch private static string ResolveTemplate(DocumentType type) => type switch
{ {
DocumentType.Quote => "Quotes/Template_v1.cshtml", DocumentType.Quote => "Quotes/Template_v1.cshtml",
DocumentType.DeliveryNote => "DeliveryNotes/Template_v1.cshtml",
DocumentType.Expedition => "Expeditions/Template_v1.cshtml", DocumentType.Expedition => "Expeditions/Template_v1.cshtml",
_ => "Shared/Template_Generic.cshtml" _ => "Shared/Template_Generic.cshtml"
}; };
@ -72,6 +74,9 @@ public class DocumentTemplateService : IDocumentTemplateService
case ExpeditionDto e: case ExpeditionDto e:
e.LogoBase64 = base64; e.LogoBase64 = base64;
break; break;
case DeliveryNoteDto d:
d.LogoBase64 = base64;
break;
default: default:
// Si no tiene LogoBase64, no hacemos nada. // Si no tiene LogoBase64, no hacemos nada.
break; break;

View File

@ -0,0 +1,182 @@
@using System
@using System.Globalization
@using System.Text.Json
@using Domain.Dtos.Sales
@model DeliveryNoteDto
@{
Layout = null;
var ci = CultureInfo.GetCultureInfo("es-AR");
CultureInfo.CurrentCulture = ci;
CultureInfo.CurrentUICulture = ci;
SurgerySnapshot snap;
if (string.IsNullOrWhiteSpace(Model.ExtraInfoJson))
{
snap = new SurgerySnapshot();
}
else
{
try
{
snap = JsonSerializer.Deserialize<SurgerySnapshot>(Model.ExtraInfoJson) ?? new SurgerySnapshot();
}
catch
{
snap = new SurgerySnapshot();
}
}
var reprintText = Model.PrintCount > 0 ? (" — Reimpresión " + Model.PrintCount) : string.Empty;
}
@functions {
public class SurgerySnapshot
{
public string? Professional { get; set; }
public string? Institution { get; set; }
public string? Patient { get; set; }
public DateTime? SurgeryDate { get; set; }
}
public static string FQty(decimal q) => q.ToString("G29", CultureInfo.InvariantCulture);
public static string FDate(DateTime? d) => d.HasValue ? d.Value.ToString("dd/MM/yyyy") : string.Empty;
public static string FText(string? value) => string.IsNullOrWhiteSpace(value) ? "-" : value.Trim();
public static string FOrigin(byte originType) => originType switch
{
1 => "Presupuesto",
2 => "Manual",
_ => originType.ToString()
};
}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Remito @Model.DeliveryNoteNumber</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
@@page { size: A4; margin: 10mm 9mm 10mm 9mm; }
html, body { font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: #000; }
.sheet { width: 100%; }
.header { display: grid; grid-template-columns: 1.6fr 1fr; gap: 10px; align-items: start; }
.company { line-height: 1.2; }
.company .tagline { margin-top: 4px; font-size: 11px; }
.doc-title { text-align: right; }
.doc-title h1 { font-size: 22px; margin: 0 0 4px 0; letter-spacing: .4px; }
.doc-title .num { font-weight: bold; font-size: 14px; }
.doc-title .date { margin-top: 3px; }
.hr { border-bottom: 1px solid #000; margin: 8px 0; }
.info-block table, .snapshot table, .items table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.info-block td, .snapshot td { padding: 3px 4px; vertical-align: top; }
.info-block .lbl, .snapshot .lbl { width: 18%; font-weight: 600; }
.info-block .val, .snapshot .val { width: 32%; word-break: break-word; }
.section-title { font-size: 13px; font-weight: 700; margin: 10px 0 6px; }
.items { margin-top: 8px; }
.items thead th { border: 1px solid #000; background: #eaeaea; color: #000; padding: 4px 4px; text-align: center; font-weight: 700; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.items tbody td { border: 1px solid #000; padding: 4px 4px; vertical-align: top; }
.items thead { display: table-header-group; }
.col-line { width: 7%; text-align: center; }
.col-desc { width: 43%; text-align: left; }
.col-qty { width: 10%; text-align: center; }
.col-origin { width: 14%; text-align: center; }
.col-originid { width: 10%; text-align: center; }
.col-notes { width: 16%; text-align: left; word-break: break-word; }
.observ { margin-top: 10px; min-height: 52px; border: 1px dashed #888; padding: 8px; white-space: pre-wrap; }
.footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #000; font-size: 11px; text-align: center; }
.muted { color: #111; }
.avoid-break { page-break-inside: avoid; }
</style>
</head>
<body>
<div class="sheet">
<div class="header">
<div class="company">
@if (!string.IsNullOrWhiteSpace(Model.LogoBase64))
{
<img src="data:image/png;base64,@Model.LogoBase64" alt="Logo" style="height:48px; margin-bottom:2px;" />
}
<div class="tagline muted">Documento generado por PhronCare</div>
</div>
<div class="doc-title">
<h1>Remito</h1>
<div class="num">@Model.DeliveryNoteNumber@reprintText</div>
<div class="date">Fecha: @Model.IssueDate.ToString("dd/MM/yyyy")</div>
</div>
</div>
<div class="hr"></div>
<div class="info-block avoid-break">
<table>
<tr>
<td class="lbl">Cliente</td>
<td class="val">@FText(Model.CustomerName)</td>
<td class="lbl">Estado</td>
<td class="val">@FText(Model.Status)</td>
</tr>
<tr>
<td class="lbl">Presupuesto</td>
<td class="val">@FText(Model.QuoteNumber)</td>
<td class="lbl">ID interno</td>
<td class="val">@Model.Id</td>
</tr>
</table>
</div>
<div class="section-title">Contexto clínico</div>
<div class="snapshot avoid-break">
<table>
<tr>
<td class="lbl">Profesional</td>
<td class="val">@FText(snap.Professional)</td>
<td class="lbl">Institución</td>
<td class="val">@FText(snap.Institution)</td>
</tr>
<tr>
<td class="lbl">Paciente</td>
<td class="val">@FText(snap.Patient)</td>
<td class="lbl">Fecha cirugía</td>
<td class="val">@FDate(snap.SurgeryDate)</td>
</tr>
</table>
</div>
<div class="section-title">Detalle de ítems</div>
<div class="items">
<table>
<thead>
<tr>
<th class="col-line">#</th>
<th class="col-desc">Descripción</th>
<th class="col-qty">Cantidad</th>
<th class="col-origin">Origen</th>
<th class="col-originid">Ref.</th>
<th class="col-notes">Notas</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items.OrderBy(i => i.LineNumber))
{
<tr>
<td class="col-line">@item.LineNumber</td>
<td class="col-desc">@FText(item.Description)</td>
<td class="col-qty">@FQty(item.Quantity)</td>
<td class="col-origin">@FOrigin(item.OriginType)</td>
<td class="col-originid">@(item.OriginId?.ToString() ?? "-")</td>
<td class="col-notes">@FText(item.Notes)</td>
</tr>
}
</tbody>
</table>
</div>
<div class="section-title">Observaciones</div>
<div class="observ">@FText(Model.Observations)</div>
<div class="footer">Impreso el @DateTime.Now.ToString("dd/MM/yyyy HH:mm")</div>
</div>
</body>
</html>

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Domain.Dtos.Sales namespace Domain.Dtos.Sales
@ -11,11 +11,14 @@ namespace Domain.Dtos.Sales
{ {
public int Id { get; set; } public int Id { get; set; }
public string DeliveryNoteNumber { get; set; } = string.Empty; public string DeliveryNoteNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public int? QuoteId { get; set; } public int? QuoteId { get; set; }
public string? QuoteNumber { get; set; }
public int? SalesInvoiceId { get; set; } public int? SalesInvoiceId { get; set; }
public DateTime IssueDate { get; set; } public DateTime IssueDate { get; set; }
public int CustomerId { get; set; } public int CustomerId { get; set; }
public string Status { get; set; } = string.Empty; public string Status { get; set; } = string.Empty;
public string? LogoBase64 { get; set; }
public string? Observations { get; set; } public string? Observations { get; set; }
public string? ExtraInfoJson { get; set; } public string? ExtraInfoJson { get; set; }
public int PrintCount { get; set; } public int PrintCount { get; set; }

View File

@ -86,6 +86,8 @@ namespace Models.Repositories
public async Task<DeliveryNoteDto?> GetDtoByIdAsync(int id) public async Task<DeliveryNoteDto?> GetDtoByIdAsync(int id)
{ {
var entity = await _context.PhSDeliveryNotes var entity = await _context.PhSDeliveryNotes
.Include(x => x.Customer)
.Include(x => x.Quote)
.Include(x => x.PhSDeliveryNoteDetails) .Include(x => x.PhSDeliveryNoteDetails)
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
@ -96,6 +98,8 @@ namespace Models.Repositories
public async Task<DeliveryNoteDto?> GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber) public async Task<DeliveryNoteDto?> GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber)
{ {
var entity = await _context.PhSDeliveryNotes var entity = await _context.PhSDeliveryNotes
.Include(x => x.Customer)
.Include(x => x.Quote)
.Include(x => x.PhSDeliveryNoteDetails) .Include(x => x.PhSDeliveryNoteDetails)
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(x => x.Deliverynotenumber == deliveryNoteNumber); .FirstOrDefaultAsync(x => x.Deliverynotenumber == deliveryNoteNumber);
@ -106,6 +110,8 @@ namespace Models.Repositories
public async Task<IEnumerable<DeliveryNoteDto>> GetDtosByQuoteIdAsync(int quoteId) public async Task<IEnumerable<DeliveryNoteDto>> GetDtosByQuoteIdAsync(int quoteId)
{ {
var entities = await _context.PhSDeliveryNotes var entities = await _context.PhSDeliveryNotes
.Include(x => x.Customer)
.Include(x => x.Quote)
.Include(x => x.PhSDeliveryNoteDetails) .Include(x => x.PhSDeliveryNoteDetails)
.AsNoTracking() .AsNoTracking()
.Where(x => x.QuoteId == quoteId) .Where(x => x.QuoteId == quoteId)
@ -139,7 +145,9 @@ namespace Models.Repositories
{ {
Id = source.Id, Id = source.Id,
DeliveryNoteNumber = source.Deliverynotenumber, DeliveryNoteNumber = source.Deliverynotenumber,
CustomerName = source.Customer?.Name ?? string.Empty,
QuoteId = source.QuoteId, QuoteId = source.QuoteId,
QuoteNumber = source.Quote?.Quotenumber,
SalesInvoiceId = source.SalesinvoiceId, SalesInvoiceId = source.SalesinvoiceId,
IssueDate = source.Issuedate, IssueDate = source.Issuedate,
CustomerId = source.CustomerId, CustomerId = source.CustomerId,
@ -147,6 +155,7 @@ namespace Models.Repositories
Observations = source.Observations, Observations = source.Observations,
ExtraInfoJson = source.ExtrainfoJson, ExtraInfoJson = source.ExtrainfoJson,
PrintCount = source.Printcount, PrintCount = source.Printcount,
LogoBase64 = null,
CreatedAt = source.Createdat, CreatedAt = source.Createdat,
ModifiedAt = source.Modifiedat, ModifiedAt = source.Modifiedat,
Items = source.PhSDeliveryNoteDetails Items = source.PhSDeliveryNoteDetails

View File

@ -1,4 +1,6 @@
using Core.Interfaces; using Core.Interfaces;
using Documents.Interfaces;
using Documents.Models;
using Domain.Dtos.Sales; using Domain.Dtos.Sales;
using Domain.Generics; using Domain.Generics;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -10,10 +12,14 @@ namespace phronCare.API.Controllers.Sales
[ApiController] [ApiController]
public class DeliveryNoteController : ControllerBase public class DeliveryNoteController : ControllerBase
{ {
private readonly IDocumentTemplateService _documentTemplateService;
private readonly IDeliveryNoteDom _deliveryNoteService; private readonly IDeliveryNoteDom _deliveryNoteService;
public DeliveryNoteController(IDeliveryNoteDom deliveryNoteService) public DeliveryNoteController(
IDocumentTemplateService documentTemplateService,
IDeliveryNoteDom deliveryNoteService)
{ {
_documentTemplateService = documentTemplateService ?? throw new ArgumentNullException(nameof(documentTemplateService));
_deliveryNoteService = deliveryNoteService ?? throw new ArgumentNullException(nameof(deliveryNoteService)); _deliveryNoteService = deliveryNoteService ?? throw new ArgumentNullException(nameof(deliveryNoteService));
} }
@ -89,6 +95,24 @@ namespace phronCare.API.Controllers.Sales
} }
} }
[HttpGet("{id:int}/pdf")]
public async Task<IActionResult> GetDeliveryNotePdf(int id)
{
var deliveryNote = await _deliveryNoteService.GetDtoByIdAsync(id);
if (deliveryNote == null)
return NotFound($"Remito con ID {id} no encontrado.");
var pdfBytes = await _documentTemplateService.GenerateDocumentAsync(new DocumentGenerationRequest
{
Model = deliveryNote,
DocumentType = DocumentType.DeliveryNote
});
return File(pdfBytes, "application/pdf", $"Remito_{deliveryNote.DeliveryNoteNumber}.pdf");
}
[HttpGet("by-quote/{quoteId:int}")] [HttpGet("by-quote/{quoteId:int}")]
public async Task<ActionResult<IEnumerable<DeliveryNoteDto>>> GetByQuoteId(int quoteId) public async Task<ActionResult<IEnumerable<DeliveryNoteDto>>> GetByQuoteId(int quoteId)
{ {