Skip to content

Commit a5f6a6c

Browse files
authored
#339 generate invoice to pdf (#479)
1 parent e406d6c commit a5f6a6c

File tree

12 files changed

+329
-2
lines changed

12 files changed

+329
-2
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ WORKDIR /app
4646
COPY --from=build-env /app/src/SimplCommerce.WebHost/out ./
4747
COPY --from=build-env /app/src/SimplCommerce.WebHost/dbscript.sql ./
4848

49+
RUN curl -SL "https://github.com/rdvojmoc/DinkToPdf/tree/master/v0.12.4/64%20bit/libwkhtmltox.so" --output ./libwkhtmltox.so
50+
4951
COPY --from=build-env /app/docker-entrypoint.sh /
5052
RUN chmod 755 /docker-entrypoint.sh
5153

SimplCommerce.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Storag
105105
EndProject
106106
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Pricing.Tests", "test\SimplCommerce.Module.Pricing.Tests\SimplCommerce.Module.Pricing.Tests.csproj", "{BF0C0613-B127-4580-8C3E-492BCBC2744E}"
107107
EndProject
108+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimplCommerce.Module.DinkToPdf", "src\Modules\SimplCommerce.Module.DinkToPdf\SimplCommerce.Module.DinkToPdf.csproj", "{EB2D720D-E784-4A21-A638-ACD462167FDF}"
109+
EndProject
108110
Global
109111
GlobalSection(SolutionConfigurationPlatforms) = preSolution
110112
Debug|Any CPU = Debug|Any CPU
@@ -583,6 +585,18 @@ Global
583585
{BF0C0613-B127-4580-8C3E-492BCBC2744E}.Release|x64.Build.0 = Release|Any CPU
584586
{BF0C0613-B127-4580-8C3E-492BCBC2744E}.Release|x86.ActiveCfg = Release|Any CPU
585587
{BF0C0613-B127-4580-8C3E-492BCBC2744E}.Release|x86.Build.0 = Release|Any CPU
588+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
589+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
590+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Debug|x64.ActiveCfg = Debug|Any CPU
591+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Debug|x64.Build.0 = Debug|Any CPU
592+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Debug|x86.ActiveCfg = Debug|Any CPU
593+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Debug|x86.Build.0 = Debug|Any CPU
594+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
595+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Release|Any CPU.Build.0 = Release|Any CPU
596+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Release|x64.ActiveCfg = Release|Any CPU
597+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Release|x64.Build.0 = Release|Any CPU
598+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Release|x86.ActiveCfg = Release|Any CPU
599+
{EB2D720D-E784-4A21-A638-ACD462167FDF}.Release|x86.Build.0 = Release|Any CPU
586600
EndGlobalSection
587601
GlobalSection(SolutionProperties) = preSolution
588602
HideSolutionNode = FALSE
@@ -629,6 +643,7 @@ Global
629643
{91C58341-5EE8-4FF1-9FAB-BED234C3EBE6} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
630644
{21A7421A-DF8B-4E1C-96A8-1FF7FF8EA6F6} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
631645
{BF0C0613-B127-4580-8C3E-492BCBC2744E} = {D9FD9ABA-AE5E-4427-AA6B-6285BE2E212D}
646+
{EB2D720D-E784-4A21-A638-ACD462167FDF} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
632647
EndGlobalSection
633648
GlobalSection(ExtensibilityGlobals) = postSolution
634649
SolutionGuid = {B9D0D8F0-1AB9-44DD-839F-ED8CEE7DDB10}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace SimplCommerce.Module.Core.Services
2+
{
3+
public interface IPdfConverter
4+
{
5+
byte[] Convert(string htmlContent);
6+
}
7+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using DinkToPdf;
2+
using DinkToPdf.Contracts;
3+
using SimplCommerce.Module.Core.Services;
4+
5+
namespace SimplCommerce.Module.DinkToPdf
6+
{
7+
public class DinkToPdfConverter : IPdfConverter
8+
{
9+
private IConverter _converter;
10+
11+
public DinkToPdfConverter(IConverter converter)
12+
{
13+
_converter = converter;
14+
}
15+
16+
public byte[] Convert(string htmlContent)
17+
{
18+
var doc = new HtmlToPdfDocument()
19+
{
20+
GlobalSettings = {
21+
ColorMode = ColorMode.Color,
22+
Orientation = Orientation.Portrait,
23+
PaperSize = PaperKind.A4,
24+
},
25+
Objects = {
26+
new ObjectSettings() {
27+
HtmlContent = htmlContent,
28+
WebSettings = { DefaultEncoding = "utf-8" }
29+
}
30+
}
31+
};
32+
33+
byte[] pdf = _converter.Convert(doc);
34+
return pdf;
35+
}
36+
}
37+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using DinkToPdf;
2+
using DinkToPdf.Contracts;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Hosting;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using SimplCommerce.Infrastructure;
7+
using SimplCommerce.Module.Core.Services;
8+
9+
namespace SimplCommerce.Module.DinkToPdf
10+
{
11+
public class ModuleInitializer : IModuleInitializer
12+
{
13+
public void ConfigureServices(IServiceCollection serviceCollection)
14+
{
15+
serviceCollection.AddSingleton(typeof(IConverter), new SynchronizedConverter(new PdfTools()));
16+
serviceCollection.AddTransient<IPdfConverter, DinkToPdfConverter>();
17+
}
18+
19+
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
20+
{
21+
}
22+
}
23+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp2.1</TargetFramework>
5+
<PreserveCompilationContext>false</PreserveCompilationContext>
6+
<EnableDefaultContentItems>false</EnableDefaultContentItems>
7+
<OutputType>Library</OutputType>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="DinkToPdf" Version="1.0.8" />
12+
<ProjectReference Include="..\..\SimplCommerce.Infrastructure\SimplCommerce.Infrastructure.csproj" />
13+
<ProjectReference Include="..\SimplCommerce.Module.Core\SimplCommerce.Module.Core.csproj" />
14+
</ItemGroup>
15+
16+
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "dinktopdf",
3+
"fullName": "SimplCommerce.Module.DinkToPdf",
4+
"isBundledWithHost": true,
5+
"version": "1.0.0"
6+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.EntityFrameworkCore;
6+
using SimplCommerce.Infrastructure.Data;
7+
using SimplCommerce.Infrastructure.Web;
8+
using SimplCommerce.Module.Core.Extensions;
9+
using SimplCommerce.Module.Core.Services;
10+
using SimplCommerce.Module.Orders.Models;
11+
using SimplCommerce.Module.Orders.ViewModels;
12+
13+
namespace SimplCommerce.Module.Orders.Controllers
14+
{
15+
[Authorize(Roles = "admin, vendor")]
16+
[Route("api/invoices")]
17+
public class InvoiceApiController : Controller
18+
{
19+
private readonly IRepository<Order> _orderRepository;
20+
private readonly IWorkContext _workContext;
21+
private readonly IRazorViewRenderer _viewRender;
22+
private readonly IPdfConverter _pdfConverter;
23+
24+
public InvoiceApiController(IRepository<Order> orderRepository, IWorkContext workContext, IRazorViewRenderer viewRender, IPdfConverter pdfConverter)
25+
{
26+
_orderRepository = orderRepository;
27+
_workContext = workContext;
28+
_viewRender = viewRender;
29+
_pdfConverter = pdfConverter;
30+
}
31+
32+
[HttpGet("print/{id}")]
33+
public async Task<IActionResult> Print(long id)
34+
{
35+
var order = await _orderRepository
36+
.Query()
37+
.Include(x => x.ShippingAddress).ThenInclude(x => x.District)
38+
.Include(x => x.ShippingAddress).ThenInclude(x => x.StateOrProvince)
39+
.Include(x => x.ShippingAddress).ThenInclude(x => x.Country)
40+
.Include(x => x.OrderItems).ThenInclude(x => x.Product)
41+
.Include(x => x.OrderItems).ThenInclude(x => x.Product).ThenInclude(x => x.OptionCombinations).ThenInclude(x => x.Option)
42+
.Include(x => x.CreatedBy)
43+
.FirstOrDefaultAsync(x => x.Id == id);
44+
45+
if (order == null)
46+
{
47+
return NotFound();
48+
}
49+
50+
var currentUser = await _workContext.GetCurrentUser();
51+
if (!User.IsInRole("admin") && order.VendorId != currentUser.VendorId)
52+
{
53+
return BadRequest(new { error = "You don't have permission to manage this order" });
54+
}
55+
56+
var model = new OrderDetailVm
57+
{
58+
Id = order.Id,
59+
CreatedOn = order.CreatedOn,
60+
OrderStatus = (int)order.OrderStatus,
61+
OrderStatusString = order.OrderStatus.ToString(),
62+
CustomerId = order.CreatedById,
63+
CustomerName = order.CreatedBy.FullName,
64+
CustomerEmail = order.CreatedBy.Email,
65+
ShippingMethod = order.ShippingMethod,
66+
PaymentMethod = order.PaymentMethod,
67+
PaymentFeeAmount = order.PaymentFeeAmount,
68+
Subtotal = order.SubTotal,
69+
DiscountAmount = order.DiscountAmount,
70+
SubTotalWithDiscount = order.SubTotalWithDiscount,
71+
TaxAmount = order.TaxAmount,
72+
ShippingAmount = order.ShippingFeeAmount,
73+
OrderTotal = order.OrderTotal,
74+
ShippingAddress = new ShippingAddressVm
75+
{
76+
AddressLine1 = order.ShippingAddress.AddressLine1,
77+
AddressLine2 = order.ShippingAddress.AddressLine2,
78+
ContactName = order.ShippingAddress.ContactName,
79+
DistrictName = order.ShippingAddress.District?.Name,
80+
StateOrProvinceName = order.ShippingAddress.StateOrProvince.Name,
81+
Phone = order.ShippingAddress.Phone
82+
},
83+
OrderItems = order.OrderItems.Select(x => new OrderItemVm
84+
{
85+
Id = x.Id,
86+
ProductId = x.Product.Id,
87+
ProductName = x.Product.Name,
88+
ProductPrice = x.ProductPrice,
89+
Quantity = x.Quantity,
90+
DiscountAmount = x.DiscountAmount,
91+
TaxAmount = x.TaxAmount,
92+
TaxPercent = x.TaxPercent,
93+
VariationOptions = OrderItemVm.GetVariationOption(x.Product)
94+
}).ToList()
95+
};
96+
97+
var invoiceHtml = await _viewRender.RenderViewToStringAsync("/Modules/SimplCommerce.Module.Orders/Views/Shared/InvoicePdf.cshtml", model);
98+
byte[] pdf = _pdfConverter.Convert(invoiceHtml);
99+
return new FileContentResult(pdf, "application/pdf");
100+
}
101+
}
102+
}

src/Modules/SimplCommerce.Module.Orders/Controllers/OrderApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public async Task<IActionResult> Get(long id)
149149
var currentUser = await _workContext.GetCurrentUser();
150150
if (!User.IsInRole("admin") && order.VendorId != currentUser.VendorId)
151151
{
152-
return new BadRequestObjectResult(new { error = "You don't have permission to manage this order" });
152+
return BadRequest(new { error = "You don't have permission to manage this order" });
153153
}
154154

155155
var model = new OrderDetailVm
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
@using SimplCommerce.Module.Orders.ViewModels;
2+
3+
@model OrderDetailVm
4+
5+
@{
6+
Layout = null;
7+
}
8+
9+
<!doctype html>
10+
<html>
11+
<head>
12+
<meta charset="utf-8" />
13+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
14+
<title>Invoice - SimplCommerce</title>
15+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
16+
</head>
17+
<body>
18+
<div class="container-fluid">
19+
<div class="row">
20+
<div class="col-6 text-center">
21+
<h2>SimplCommerce</h2>
22+
<p>Address: 364 Cong Hoa, Tan Binh, HCMC, Vietnam</p>
23+
<p>Phone: 000000000</p>
24+
<hr />
25+
</div>
26+
<div class="col"></div>
27+
</div>
28+
<div class="row mt-4">
29+
<div class="col text-center">
30+
<h2>INVOICE</h2>
31+
<p>Date: @Model.CreatedOn</p>
32+
</div>
33+
</div>
34+
<div class="row">
35+
<div class="col">
36+
<p>Customer: @Model.CustomerName</p>
37+
<p>
38+
Address: @Model.ShippingAddress.AddressLine1, @Model.ShippingAddress.DistrictName @Model.ShippingAddress.StateOrProvinceName
39+
</p>
40+
</div>
41+
</div>
42+
<div class="row mt-4">
43+
<div class="col">
44+
<table class="table table-striped">
45+
<thead>
46+
<tr>
47+
<th>Product</th>
48+
<th class="text-right">Quantity</th>
49+
<th class="text-right">Price</th>
50+
<th class="text-right">Amount</th>
51+
<th class="text-right">VAT rate</th>
52+
<th class="text-right">VAT Included Amount</th>
53+
</tr>
54+
</thead>
55+
<tbody>
56+
@foreach (var item in Model.OrderItems)
57+
{
58+
<tr>
59+
<td>
60+
@item.ProductName
61+
@foreach (var opt in item.VariationOptions)
62+
{
63+
<span>@opt.OptionName: @opt.Value</span>
64+
}
65+
</td>
66+
<td class="text-right">@item.Quantity</td>
67+
<td class="text-right">@item.ProductPriceString</td>
68+
<td class="text-right">@item.TotalString</td>
69+
<td class="text-right">@item.TaxPercent</td>
70+
<td class="text-right">@((item.Total + item.TaxAmount).ToString("C"))</td>
71+
</tr>
72+
}
73+
</tbody>
74+
</table>
75+
</div>
76+
</div>
77+
<div class="row justify-content-end">
78+
<div class="col-6" style="float:right">
79+
<table class="table">
80+
<tr>
81+
<td>Subtotal</td>
82+
<td class="text-right">@Model.SubtotalString</td>
83+
</tr>
84+
<tr>
85+
<td>Shipping</td>
86+
<td class="text-right">@Model.ShippingAmountString</td>
87+
</tr>
88+
<tr>
89+
<td>Tax</td>
90+
<td class="text-right">@Model.TaxAmountString</td>
91+
</tr>
92+
<tr>
93+
<td>Discount</td>
94+
<td class="text-right">@Model.DiscountAmountString</td>
95+
</tr>
96+
<tr>
97+
<td>Payment Fee</td>
98+
<td class="text-right">@Model.PaymentFeeAmountString</td>
99+
</tr>
100+
<tr>
101+
<td>Order Total</td>
102+
<td class="text-right"><strong>@Model.OrderTotalString</strong></td>
103+
</tr>
104+
</table>
105+
</div>
106+
</div>
107+
</div>
108+
</body>
109+
</html>

0 commit comments

Comments
 (0)