mirror of
https://github.com/bitwarden/server.git
synced 2025-04-08 22:58:11 -05:00
invoice pdf generation api
This commit is contained in:
parent
c9d6a7b2c0
commit
e7b565d007
@ -13,6 +13,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="jsreport.AspNetCore" Version="1.0.0" />
|
||||||
|
<PackageReference Include="jsreport.Binary" Version="1.8.2" />
|
||||||
|
<PackageReference Include="jsreport.Local" Version="1.0.3" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.3" />
|
<PackageReference Include="System.Net.Http" Version="4.3.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
|
||||||
|
@ -12,6 +12,10 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
using jsreport.AspNetCore;
|
||||||
|
using jsreport.Types;
|
||||||
|
using Bit.Api.Models;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Api.Controllers
|
namespace Bit.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -94,6 +98,41 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/billing-invoice/{invoiceId}")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
[MiddlewareFilter(typeof(JsReportPipeline))]
|
||||||
|
public async Task<IActionResult> GetBillingInvoice(string id, string invoiceId)
|
||||||
|
{
|
||||||
|
var orgIdGuid = new Guid(id);
|
||||||
|
if(!_currentContext.OrganizationOwner(orgIdGuid))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||||
|
if(organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var invoice = await new StripeInvoiceService().GetAsync(invoiceId);
|
||||||
|
if(invoice == null || invoice.CustomerId != organization.GatewayCustomerId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new InvoiceModel(organization, invoice);
|
||||||
|
HttpContext.JsReportFeature().Recipe(Recipe.PhantomPdf);
|
||||||
|
return View("Invoice", model);
|
||||||
|
}
|
||||||
|
catch(StripeException)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/license")]
|
[HttpGet("{id}/license")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<OrganizationLicense> GetLicense(string id, [FromQuery]Guid installationId)
|
public async Task<OrganizationLicense> GetLicense(string id, [FromQuery]Guid installationId)
|
||||||
|
67
src/Api/Models/InvoiceModel.cs
Normal file
67
src/Api/Models/InvoiceModel.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Api.Models
|
||||||
|
{
|
||||||
|
public class InvoiceModel
|
||||||
|
{
|
||||||
|
public InvoiceModel(Organization organization, StripeInvoice invoice)
|
||||||
|
{
|
||||||
|
// TODO: address
|
||||||
|
OurAddress1 = "567 Green St";
|
||||||
|
OurAddress2 = "Jacksonville, FL 32256";
|
||||||
|
OurAddress3 = "United States";
|
||||||
|
|
||||||
|
CustomerName = organization.BusinessName ?? "--";
|
||||||
|
// TODO: address and vat
|
||||||
|
CustomerAddress1 = "123 Any St";
|
||||||
|
CustomerAddress2 = "New York, NY 10001";
|
||||||
|
CustomerAddress3 = "United States";
|
||||||
|
CustomerVatNumber = "PT 123456789";
|
||||||
|
|
||||||
|
InvoiceDate = invoice.Date?.ToLongDateString();
|
||||||
|
InvoiceDueDate = invoice.DueDate?.ToLongDateString();
|
||||||
|
InvoiceNumber = invoice.Id;
|
||||||
|
Items = invoice.StripeInvoiceLineItems.Select(i => new Item(i));
|
||||||
|
|
||||||
|
SubtotalAmount = (invoice.Total / 100).ToString("C");
|
||||||
|
VatTotalAmount = 0.ToString("C");
|
||||||
|
TotalAmount = SubtotalAmount;
|
||||||
|
Paid = invoice.Paid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string OurAddress1 { get; set; }
|
||||||
|
public string OurAddress2 { get; set; }
|
||||||
|
public string OurAddress3 { get; set; }
|
||||||
|
public string InvoiceDate { get; set; }
|
||||||
|
public string InvoiceDueDate { get; set; }
|
||||||
|
public string InvoiceNumber { get; set; }
|
||||||
|
public string CustomerName { get; set; }
|
||||||
|
public string CustomerVatNumber { get; set; }
|
||||||
|
public string CustomerAddress1 { get; set; }
|
||||||
|
public string CustomerAddress2 { get; set; }
|
||||||
|
public string CustomerAddress3 { get; set; }
|
||||||
|
public IEnumerable<Item> Items { get; set; }
|
||||||
|
public string SubtotalAmount { get; set; }
|
||||||
|
public string VatTotalAmount { get; set; }
|
||||||
|
public string TotalAmount { get; set; }
|
||||||
|
public bool Paid { get; set; }
|
||||||
|
public bool UsesVat => !string.IsNullOrWhiteSpace(CustomerVatNumber);
|
||||||
|
|
||||||
|
public class Item
|
||||||
|
{
|
||||||
|
public Item(StripeInvoiceLineItem item)
|
||||||
|
{
|
||||||
|
Quantity = item.Quantity?.ToString() ?? "-";
|
||||||
|
Amount = (item.Amount / 100).ToString("F");
|
||||||
|
Description = item.Description ?? "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Quantity { get; set; }
|
||||||
|
public string Amount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ using Stripe;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
using IdentityServer4.AccessTokenValidation;
|
using IdentityServer4.AccessTokenValidation;
|
||||||
|
using jsreport.AspNetCore;
|
||||||
|
|
||||||
namespace Bit.Api
|
namespace Bit.Api
|
||||||
{
|
{
|
||||||
@ -140,6 +141,16 @@ namespace Bit.Api
|
|||||||
jsonFormatter.SupportedMediaTypes.Add(textPlainMediaType);
|
jsonFormatter.SupportedMediaTypes.Add(textPlainMediaType);
|
||||||
}
|
}
|
||||||
}).AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
|
}).AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
|
||||||
|
|
||||||
|
// PDF generation
|
||||||
|
if(!globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddJsReport(new jsreport.Local.LocalReporting()
|
||||||
|
.UseBinary(jsreport.Binary.JsReportBinary.GetBinary())
|
||||||
|
.AsUtility()
|
||||||
|
.Create());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
|
145
src/Api/Views/Organizations/Invoice.cshtml
Normal file
145
src/Api/Views/Organizations/Invoice.cshtml
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
@model Bit.Api.Models.InvoiceModel
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th, table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 5px 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.items {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.items th, table.items td {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid darkgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paid {
|
||||||
|
margin-top: 50px;
|
||||||
|
font-size: 60px;
|
||||||
|
text-align: center;
|
||||||
|
color: darkgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
font-family: Courier New, Courier, monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="number">@Model.InvoiceNumber</div>
|
||||||
|
<h1>INVOICE</h1>
|
||||||
|
<p>
|
||||||
|
@if(!string.IsNullOrWhiteSpace(Model.InvoiceDate))
|
||||||
|
{
|
||||||
|
<b>Date:</b> @Model.InvoiceDate<br />
|
||||||
|
}
|
||||||
|
@if(!string.IsNullOrWhiteSpace(Model.InvoiceDueDate))
|
||||||
|
{
|
||||||
|
<b>Due Date:</b> @Model.InvoiceDueDate
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<h2>From</h2>
|
||||||
|
<p>
|
||||||
|
<b>8bit Solutions LLC</b><br />
|
||||||
|
@Model.OurAddress1<br />
|
||||||
|
@Model.OurAddress2<br />
|
||||||
|
@Model.OurAddress3
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<h2>To</h2>
|
||||||
|
<p>
|
||||||
|
<b>@Model.CustomerName</b><br />
|
||||||
|
@if(Model.UsesVat)
|
||||||
|
{
|
||||||
|
@:VAT @Model.CustomerVatNumber<br />
|
||||||
|
}
|
||||||
|
@if(!string.IsNullOrWhiteSpace(Model.CustomerAddress1))
|
||||||
|
{
|
||||||
|
@Model.CustomerAddress1<br />
|
||||||
|
}
|
||||||
|
@if(!string.IsNullOrWhiteSpace(Model.CustomerAddress2))
|
||||||
|
{
|
||||||
|
@Model.CustomerAddress2<br />
|
||||||
|
}
|
||||||
|
@if(!string.IsNullOrWhiteSpace(Model.CustomerAddress3))
|
||||||
|
{
|
||||||
|
@Model.CustomerAddress3
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2>Items</h2>
|
||||||
|
<table class="items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="right" style="width: 100px;">Qty</th>
|
||||||
|
@if(Model.UsesVat)
|
||||||
|
{
|
||||||
|
<th class="right" style="width: 100px;">VAT %</th>
|
||||||
|
<th class="right" style="width: 100px;">VAT</th>
|
||||||
|
}
|
||||||
|
<th class="right" style="width: 100px;">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach(var item in Model.Items)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@item.Description</td>
|
||||||
|
<td class="right">@item.Quantity</td>
|
||||||
|
@if(Model.UsesVat)
|
||||||
|
{
|
||||||
|
<td class="right">0</td>
|
||||||
|
<td class="right">0.00</td>
|
||||||
|
}
|
||||||
|
<td class="right">@item.Amount</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2>Totals</h2>
|
||||||
|
<b>Subtotal:</b> @Model.SubtotalAmount<br />
|
||||||
|
@if(Model.UsesVat)
|
||||||
|
{
|
||||||
|
<b>Total VAT:</b> @Model.VatTotalAmount<br />
|
||||||
|
}
|
||||||
|
<br />
|
||||||
|
<b>Total:</b> USD @Model.TotalAmount
|
||||||
|
@if(Model.Paid)
|
||||||
|
{
|
||||||
|
<div class="paid">PAID</div>
|
||||||
|
}
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user