mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: hook up branding templates to invoices
This commit is contained in:
42
packages/server/resources/scss/base.css
Normal file
42
packages/server/resources/scss/base.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
font-family: "Noto Sans", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
/* font-weight: <weight>; */
|
||||||
|
font-style: normal;
|
||||||
|
/* font-variation-settings:
|
||||||
|
"wdth" 100; */
|
||||||
|
}
|
||||||
@@ -1,35 +1 @@
|
|||||||
@import "./normalize.scss";
|
@import "./normalize.scss";
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: inherit; // 2
|
|
||||||
text-align: -webkit-match-parent; // 3
|
|
||||||
}
|
|
||||||
|
|
||||||
thead,
|
|
||||||
tbody,
|
|
||||||
tfoot,
|
|
||||||
tr,
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-color: inherit;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #212529;
|
|
||||||
background-color: #fff;
|
|
||||||
direction: ltr;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|||||||
379
packages/server/resources/scss/normalize.css
vendored
Normal file
379
packages/server/resources/scss/normalize.css
vendored
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
/* 1 */
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none;
|
||||||
|
/* 1 */
|
||||||
|
text-decoration: underline;
|
||||||
|
/* 2 */
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
display: table;
|
||||||
|
/* 1 */
|
||||||
|
max-width: 100%;
|
||||||
|
/* 1 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
white-space: normal;
|
||||||
|
/* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
padding: 0;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
html(lang=locale)
|
html(lang=locale)
|
||||||
head
|
head
|
||||||
title My Site - #{title}
|
title My Site - #{title}
|
||||||
|
style
|
||||||
|
include ../scss/normalize.css
|
||||||
|
include ../scss/base.css
|
||||||
block head
|
block head
|
||||||
body
|
body
|
||||||
div.paper-template
|
div.paper-template
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
extends ../PaperTemplateLayout.pug
|
|
||||||
|
|
||||||
block head
|
|
||||||
style
|
|
||||||
if (isRtl)
|
|
||||||
include ../../css/modules/invoice-rtl.css
|
|
||||||
else
|
|
||||||
include ../../css/modules/invoice.css
|
|
||||||
|
|
||||||
block content
|
|
||||||
div.invoice
|
|
||||||
div.invoice__header
|
|
||||||
div.paper
|
|
||||||
h1.title #{__("invoice.paper.invoice")}
|
|
||||||
if saleInvoice.invoiceNo
|
|
||||||
span.invoiceNo #{saleInvoice.invoiceNo}
|
|
||||||
|
|
||||||
div.organization
|
|
||||||
h3.title #{organizationName}
|
|
||||||
if organizationEmail
|
|
||||||
span.email #{organizationEmail}
|
|
||||||
|
|
||||||
div.invoice__due-amount
|
|
||||||
div.label #{__('invoice.paper.invoice_amount')}
|
|
||||||
div.amount #{saleInvoice.totalFormatted}
|
|
||||||
|
|
||||||
div.invoice__meta
|
|
||||||
div.invoice__meta-item.invoice__meta-item--amount
|
|
||||||
span.label #{__('invoice.paper.due_amount')}
|
|
||||||
span.value #{saleInvoice.dueAmountFormatted}
|
|
||||||
|
|
||||||
div.invoice__meta-item.invoice__meta-item--billed-to
|
|
||||||
span.label #{__("invoice.paper.billed_to")}
|
|
||||||
span.value #{saleInvoice.customer.displayName}
|
|
||||||
|
|
||||||
div.invoice__meta-item.invoice__meta-item--invoice-date
|
|
||||||
span.label #{__("invoice.paper.invoice_date")}
|
|
||||||
span.value #{saleInvoice.invoiceDateFormatted}
|
|
||||||
|
|
||||||
div.invoice__meta-item.invoice__meta-item--due-date
|
|
||||||
span.label #{__("invoice.paper.due_date")}
|
|
||||||
span.value #{saleInvoice.dueDateFormatted}
|
|
||||||
|
|
||||||
div.invoice__table
|
|
||||||
table
|
|
||||||
thead
|
|
||||||
tr
|
|
||||||
th.item #{__("item_entry.paper.item_name")}
|
|
||||||
th.rate #{__("item_entry.paper.rate")}
|
|
||||||
th.quantity #{__("item_entry.paper.quantity")}
|
|
||||||
th.total #{__("item_entry.paper.total")}
|
|
||||||
tbody
|
|
||||||
each entry in saleInvoice.entries
|
|
||||||
tr
|
|
||||||
td.item
|
|
||||||
div.title=entry.item.name
|
|
||||||
span.description=entry.description
|
|
||||||
td.rate=entry.rate
|
|
||||||
td.quantity=entry.quantity
|
|
||||||
td.total=entry.amount
|
|
||||||
|
|
||||||
div.invoice__table-after
|
|
||||||
div.invoice__table-total
|
|
||||||
table
|
|
||||||
tbody
|
|
||||||
tr.subtotal
|
|
||||||
td #{__('invoice.paper.subtotal')}
|
|
||||||
td #{saleInvoice.subtotalFormatted}
|
|
||||||
each tax in saleInvoice.taxes
|
|
||||||
tr.tax_line
|
|
||||||
td #{tax.name} [#{tax.taxRate}%]
|
|
||||||
td #{tax.taxRateAmountFormatted}
|
|
||||||
tr.total
|
|
||||||
td #{__('invoice.paper.total')}
|
|
||||||
td #{saleInvoice.totalFormatted}
|
|
||||||
tr.payment-amount
|
|
||||||
td #{__('invoice.paper.payment_amount')}
|
|
||||||
td #{saleInvoice.paymentAmountFormatted}
|
|
||||||
tr.blanace-due
|
|
||||||
td #{__('invoice.paper.balance_due')}
|
|
||||||
td #{saleInvoice.dueAmountFormatted}
|
|
||||||
|
|
||||||
div.invoice__footer
|
|
||||||
if saleInvoice.termsConditions
|
|
||||||
div.invoice__conditions
|
|
||||||
h3 #{__("invoice.paper.conditions_title")}
|
|
||||||
p #{saleInvoice.termsConditions}
|
|
||||||
|
|
||||||
if saleInvoice.invoiceMessage
|
|
||||||
div.invoice__notes
|
|
||||||
h3 #{__("invoice.paper.notes_title")}
|
|
||||||
p #{saleInvoice.invoiceMessage}
|
|
||||||
244
packages/server/resources/views/modules/invoice-standard.pug
Normal file
244
packages/server/resources/views/modules/invoice-standard.pug
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
extends ../PaperTemplateLayout.pug
|
||||||
|
|
||||||
|
block head
|
||||||
|
- var prefix = 'bc'
|
||||||
|
style.
|
||||||
|
.#{prefix}-root {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #111;
|
||||||
|
padding: 24px 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||||
|
}
|
||||||
|
.#{prefix}-big-title {
|
||||||
|
font-size: 60px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-logo-wrap {
|
||||||
|
height: 120px;
|
||||||
|
width: 120px;
|
||||||
|
position: absolute;
|
||||||
|
right: 26px;
|
||||||
|
top: 26px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.#{prefix}-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.#{prefix}-detail__label {
|
||||||
|
min-width: 120px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-detail__value {
|
||||||
|
/* Styles for detail values */
|
||||||
|
}
|
||||||
|
.#{prefix}-address-root {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-from {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-from__item {
|
||||||
|
/* Styles for items in the billed-from address */
|
||||||
|
}
|
||||||
|
.#{prefix}-address-to {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-to__item {
|
||||||
|
/* Styles for items in the billed-to address */
|
||||||
|
}
|
||||||
|
.#{prefix}-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header {
|
||||||
|
font-weight: 400;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
padding: 2px 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:last-of-type{
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell {
|
||||||
|
border-bottom: 1px solid #F6F6F6;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-gray {
|
||||||
|
border-bottom: 1px solid #DADADA;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-dark {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--font-weight-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
/* Additional styles for total items with bold font weight */
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-label {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-amount {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-paragraph {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.#{prefix}-paragraph__label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.#{prefix}-paragraph__value {
|
||||||
|
/* Styles for values within the paragraph section */
|
||||||
|
}
|
||||||
|
block content
|
||||||
|
//- block head
|
||||||
|
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||||
|
//- Title and company logo
|
||||||
|
h1(class=`${prefix}-big-title`) Invoice
|
||||||
|
|
||||||
|
if showCompanyLogo
|
||||||
|
div(class=`${prefix}-logo-wrap`)
|
||||||
|
img(alt="", src=companyLogo)
|
||||||
|
|
||||||
|
//- Invoice details
|
||||||
|
div(class=`${prefix}-details`)
|
||||||
|
if showInvoiceNumber
|
||||||
|
div(class=`${prefix}-detail`)
|
||||||
|
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
|
||||||
|
div(class=`${prefix}-detail__value`) #{invoiceNumber}
|
||||||
|
|
||||||
|
if showDateIssue
|
||||||
|
div(class=`${prefix}-detail`)
|
||||||
|
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
|
||||||
|
div(class=`${prefix}-detail__value`) #{dateIssue}
|
||||||
|
|
||||||
|
if showDueDate
|
||||||
|
div(class=`${prefix}-detail`)
|
||||||
|
div(class=`${prefix}-detail__label`) #{dueDateLabel}
|
||||||
|
div(class=`${prefix}-detail__value`) #{dueDate}
|
||||||
|
|
||||||
|
//- Address section
|
||||||
|
div(class=`${prefix}-address-root`)
|
||||||
|
if showBilledFromAddress
|
||||||
|
div(class=`${prefix}-address-from`)
|
||||||
|
strong #{companyName}
|
||||||
|
each item in billedFromAddres
|
||||||
|
div(class=`${prefix}-address-from__item`) #{item}
|
||||||
|
|
||||||
|
if showBillingToAddress
|
||||||
|
div(class=`${prefix}-address-to`)
|
||||||
|
strong #{billedToLabel}
|
||||||
|
each item in billedToAddress
|
||||||
|
div(class=`${prefix}-address-to__item`) #{item}
|
||||||
|
|
||||||
|
//- Invoice table
|
||||||
|
table(class=`${prefix}-table`)
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th(class=`${prefix}-table__header`) #{lineItemLabel}
|
||||||
|
th(class=`${prefix}-table__header`) #{lineDescriptionLabel}
|
||||||
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel}
|
||||||
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel}
|
||||||
|
tbody
|
||||||
|
each line in lines
|
||||||
|
tr
|
||||||
|
td(class=`${prefix}-table__cell`) #{line.item}
|
||||||
|
td(class=`${prefix}-table__cell`) #{line.description}
|
||||||
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||||
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||||
|
|
||||||
|
//- Totals section
|
||||||
|
div(class=`${prefix}-totals`)
|
||||||
|
if showSubtotal
|
||||||
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||||
|
|
||||||
|
if showDiscount
|
||||||
|
div(class=`${prefix}-totals__item`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{discountLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{discount}
|
||||||
|
|
||||||
|
if showTaxes
|
||||||
|
each tax in taxes
|
||||||
|
div(class=`${prefix}-totals__item`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{tax.label}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{tax.amount}
|
||||||
|
|
||||||
|
if showTotal
|
||||||
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{totalLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||||
|
|
||||||
|
if showPaymentMade
|
||||||
|
div(class=`${prefix}-totals__item`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{paymentMadeLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{paymentMade}
|
||||||
|
|
||||||
|
if showBalanceDue
|
||||||
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{balanceDueLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{balanceDue}
|
||||||
|
|
||||||
|
//- Footer section
|
||||||
|
if showTermsConditions
|
||||||
|
div(class=`${prefix}-paragraph`)
|
||||||
|
if termsConditionsLabel
|
||||||
|
div(class=`${prefix}-paragraph__label`) #{termsConditionsLabel}
|
||||||
|
div(class=`${prefix}-paragraph__value`) #{termsConditions}
|
||||||
|
|
||||||
|
if showStatement
|
||||||
|
div(class=`${prefix}-paragraph`)
|
||||||
|
if statementLabel
|
||||||
|
div(class=`${prefix}-paragraph__label`) #{statementLabel}
|
||||||
|
div(class=`${prefix}-paragraph__value`) #{statement}
|
||||||
@@ -45,6 +45,11 @@ export interface ISaleInvoice {
|
|||||||
subtotal: number;
|
subtotal: number;
|
||||||
subtotalLocal: number;
|
subtotalLocal: number;
|
||||||
subtotalExludingTax: number;
|
subtotalExludingTax: number;
|
||||||
|
|
||||||
|
termsConditions: string;
|
||||||
|
invoiceMessage: string;
|
||||||
|
|
||||||
|
pdfTemplateId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISaleInvoiceDTO {
|
export interface ISaleInvoiceDTO {
|
||||||
@@ -217,3 +222,83 @@ export interface ISaleInvoiceMailSent {
|
|||||||
saleInvoiceId: number;
|
saleInvoiceId: number;
|
||||||
messageOptions: SendInvoiceMailDTO;
|
messageOptions: SendInvoiceMailDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Invoice Pdf Document
|
||||||
|
export interface InvoicePdfLine {
|
||||||
|
item: string;
|
||||||
|
description: string;
|
||||||
|
rate: string;
|
||||||
|
quantity: string;
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoicePdfTax {
|
||||||
|
label: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoicePdfTemplateAttributes {
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
showCompanyLogo: boolean;
|
||||||
|
companyLogo: string;
|
||||||
|
|
||||||
|
dueDate: string;
|
||||||
|
dueDateLabel: string;
|
||||||
|
showDueDate: boolean;
|
||||||
|
|
||||||
|
dateIssue: string;
|
||||||
|
dateIssueLabel: string;
|
||||||
|
showDateIssue: boolean;
|
||||||
|
|
||||||
|
invoiceNumberLabel: string;
|
||||||
|
invoiceNumber: string;
|
||||||
|
showInvoiceNumber: boolean;
|
||||||
|
|
||||||
|
showBillingToAddress: boolean;
|
||||||
|
showBilledFromAddress: boolean;
|
||||||
|
billedToLabel: string;
|
||||||
|
|
||||||
|
lineItemLabel: string;
|
||||||
|
lineDescriptionLabel: string;
|
||||||
|
lineRateLabel: string;
|
||||||
|
lineTotalLabel: string;
|
||||||
|
|
||||||
|
totalLabel: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
discountLabel: string;
|
||||||
|
paymentMadeLabel: string;
|
||||||
|
balanceDueLabel: string;
|
||||||
|
|
||||||
|
showTotal: boolean;
|
||||||
|
showSubtotal: boolean;
|
||||||
|
showDiscount: boolean;
|
||||||
|
showTaxes: boolean;
|
||||||
|
showPaymentMade: boolean;
|
||||||
|
showDueAmount: boolean;
|
||||||
|
showBalanceDue: boolean;
|
||||||
|
|
||||||
|
total: string;
|
||||||
|
subtotal: string;
|
||||||
|
discount: string;
|
||||||
|
paymentMade: string;
|
||||||
|
balanceDue: string;
|
||||||
|
|
||||||
|
termsConditionsLabel: string;
|
||||||
|
showTermsConditions: boolean;
|
||||||
|
termsConditions: string;
|
||||||
|
|
||||||
|
lines: InvoicePdfLine[];
|
||||||
|
taxes: InvoicePdfTax[];
|
||||||
|
|
||||||
|
statementLabel: string;
|
||||||
|
showStatement: boolean;
|
||||||
|
statement: string;
|
||||||
|
|
||||||
|
billedToAddress: string[];
|
||||||
|
billedFromAddres: string[];
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ export class PdfTemplate extends TenantModel {
|
|||||||
return ['createdAt', 'updatedAt'];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json schema.
|
||||||
|
*/
|
||||||
static get jsonSchema() {
|
static get jsonSchema() {
|
||||||
return {
|
return {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export class ChromiumlyTenancy {
|
|||||||
properties?: PageProperties,
|
properties?: PageProperties,
|
||||||
pdfFormat?: PdfFormat
|
pdfFormat?: PdfFormat
|
||||||
) {
|
) {
|
||||||
return this.htmlConvert.convert(tenantId, content, properties, pdfFormat);
|
const parsedProperties = {
|
||||||
|
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||||
|
...properties,
|
||||||
|
}
|
||||||
|
return this.htmlConvert.convert(tenantId, content, parsedProperties, pdfFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export default class ItemsEntriesService {
|
|||||||
* Sets the cost/sell accounts to the invoice entries.
|
* Sets the cost/sell accounts to the invoice entries.
|
||||||
*/
|
*/
|
||||||
public setItemsEntriesDefaultAccounts(tenantId: number) {
|
public setItemsEntriesDefaultAccounts(tenantId: number) {
|
||||||
return async (entries: IItemEntry[]) => {
|
return async (entries: IItemEntry[]) => {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
const { Item } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ export class AssignPdfTemplateDefault {
|
|||||||
return this.uow.withTransaction(
|
return this.uow.withTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
async (trx?: Knex.Transaction) => {
|
async (trx?: Knex.Transaction) => {
|
||||||
|
// Triggers `onPdfTemplateAssigningDefault` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.pdfTemplate.onAssigningDefault,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
}
|
||||||
|
);
|
||||||
await PdfTemplate.query(trx)
|
await PdfTemplate.query(trx)
|
||||||
.where('resource', oldPdfTempalte.resource)
|
.where('resource', oldPdfTempalte.resource)
|
||||||
.patch({ default: false });
|
.patch({ default: false });
|
||||||
@@ -41,6 +49,7 @@ export class AssignPdfTemplateDefault {
|
|||||||
.findById(templateId)
|
.findById(templateId)
|
||||||
.patch({ default: true });
|
.patch({ default: true });
|
||||||
|
|
||||||
|
// Triggers `onPdfTemplateAssignedDefault` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.pdfTemplate.onAssignedDefault,
|
events.pdfTemplate.onAssignedDefault,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class BrandingTemplateDTOTransformer {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates the default branding template id.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} resource
|
||||||
|
* @param {Record<string, any>} object
|
||||||
|
* @param {string} attributeName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public assocDefaultBrandingTemplate = (
|
||||||
|
tenantId: number,
|
||||||
|
resource: string,
|
||||||
|
) => async (object: Record<string, any>) => {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
const attributeName = 'pdfTemplateId';
|
||||||
|
|
||||||
|
const defaultTemplate = await PdfTemplate.query().findOne({
|
||||||
|
resource,
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
console.log(defaultTemplate);
|
||||||
|
|
||||||
|
if (!defaultTemplate) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...object,
|
||||||
|
[attributeName]: defaultTemplate.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { formatDateFields } from 'utils';
|
|||||||
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
||||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||||
import { ItemEntry } from '@/models';
|
import { ItemEntry } from '@/models';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommandSaleInvoiceDTOTransformer {
|
export class CommandSaleInvoiceDTOTransformer {
|
||||||
@@ -40,6 +41,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the create DTO to invoice object model.
|
* Transformes the create DTO to invoice object model.
|
||||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
||||||
@@ -113,11 +117,19 @@ export class CommandSaleInvoiceDTOTransformer {
|
|||||||
userId: authorizedUser.id,
|
userId: authorizedUser.id,
|
||||||
} as ISaleInvoice;
|
} as ISaleInvoice;
|
||||||
|
|
||||||
|
const initialAsyncDTO = await composeAsync(
|
||||||
|
// Assigns the default branding template id to the invoice DTO.
|
||||||
|
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||||
|
tenantId,
|
||||||
|
'SaleInvoice'
|
||||||
|
)
|
||||||
|
)(initialDTO);
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
||||||
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
||||||
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
||||||
)(initialDTO);
|
)(initialAsyncDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { GetSaleInvoice } from './GetSaleInvoice';
|
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { transformInvoiceToPdfTemplate } from './utils';
|
||||||
|
import { InvoicePdfTemplateAttributes } from '@/interfaces';
|
||||||
|
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleInvoicePdf {
|
export class SaleInvoicePdf {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||||
|
|
||||||
@@ -14,6 +21,9 @@ export class SaleInvoicePdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getInvoiceService: GetSaleInvoice;
|
private getInvoiceService: GetSaleInvoice;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {number} tenantId - Tenant Id.
|
* @param {number} tenantId - Tenant Id.
|
||||||
@@ -24,19 +34,54 @@ export class SaleInvoicePdf {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
invoiceId: number
|
invoiceId: number
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
|
const brandingAttributes = await this.getInvoiceBrandingAttributes(
|
||||||
tenantId,
|
tenantId,
|
||||||
invoiceId
|
invoiceId
|
||||||
);
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/invoice-regular',
|
'modules/invoice-standard',
|
||||||
{
|
brandingAttributes
|
||||||
saleInvoice,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
// Converts the given html content to pdf document.
|
||||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the branding attributes of the given sale invoice.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} invoiceId
|
||||||
|
* @returns {Promise<InvoicePdfTemplateAttributes>}
|
||||||
|
*/
|
||||||
|
async getInvoiceBrandingAttributes(
|
||||||
|
tenantId: number,
|
||||||
|
invoiceId: number
|
||||||
|
): Promise<InvoicePdfTemplateAttributes> {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const invoice = await this.getInvoiceService.getSaleInvoice(
|
||||||
|
tenantId,
|
||||||
|
invoiceId
|
||||||
|
);
|
||||||
|
// Retrieve the invoice template id of not found get the default template id.
|
||||||
|
const templateId =
|
||||||
|
invoice.pdfTemplateId ??
|
||||||
|
(
|
||||||
|
await PdfTemplate.query().findOne({
|
||||||
|
resource: 'SaleInvoice',
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
)?.id;
|
||||||
|
// Getting the branding template attributes.
|
||||||
|
const brandingTemplate =
|
||||||
|
await this.invoiceBrandingTemplateService.getInvoicePdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
// Merge the branding template attributes with the invoice.
|
||||||
|
return {
|
||||||
|
...brandingTemplate.attributes,
|
||||||
|
...transformInvoiceToPdfTemplate(invoice),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { mergePdfTemplateWithDefaultAttributes } from './utils';
|
||||||
|
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||||
|
import { defaultInvoicePdfTemplateAttributes } from './constants';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleInvoicePdfTemplate {
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplateService: GetPdfTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the invoice pdf template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} invoiceTemplateId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getInvoicePdfTemplate(tenantId: number, invoiceTemplateId: number){
|
||||||
|
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
invoiceTemplateId
|
||||||
|
);
|
||||||
|
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||||
|
template.attributes,
|
||||||
|
defaultInvoicePdfTemplateAttributes
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,3 +158,88 @@ export const SaleInvoicesSampleData = [
|
|||||||
Description: 'Description',
|
Description: 'Description',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const defaultInvoicePdfTemplateAttributes = {
|
||||||
|
primaryColor: 'red',
|
||||||
|
secondaryColor: 'red',
|
||||||
|
|
||||||
|
companyName: 'Bigcapital Technology, Inc.',
|
||||||
|
|
||||||
|
showCompanyLogo: true,
|
||||||
|
companyLogo: '',
|
||||||
|
|
||||||
|
dueDateLabel: 'Date due',
|
||||||
|
showDueDate: true,
|
||||||
|
|
||||||
|
dateIssueLabel: 'Date of issue',
|
||||||
|
showDateIssue: true,
|
||||||
|
|
||||||
|
// dateIssue,
|
||||||
|
invoiceNumberLabel: 'Invoice number',
|
||||||
|
showInvoiceNumber: true,
|
||||||
|
|
||||||
|
// Address
|
||||||
|
showBillingToAddress: true,
|
||||||
|
showBilledFromAddress: true,
|
||||||
|
billedToLabel: 'Billed To',
|
||||||
|
|
||||||
|
// Entries
|
||||||
|
lineItemLabel: 'Item',
|
||||||
|
lineDescriptionLabel: 'Description',
|
||||||
|
lineRateLabel: 'Rate',
|
||||||
|
lineTotalLabel: 'Total',
|
||||||
|
|
||||||
|
totalLabel: 'Total',
|
||||||
|
subtotalLabel: 'Subtotal',
|
||||||
|
discountLabel: 'Discount',
|
||||||
|
paymentMadeLabel: 'Payment Made',
|
||||||
|
balanceDueLabel: 'Balance Due',
|
||||||
|
|
||||||
|
// Totals
|
||||||
|
showTotal: true,
|
||||||
|
showSubtotal: true,
|
||||||
|
showDiscount: true,
|
||||||
|
showTaxes: true,
|
||||||
|
showPaymentMade: true,
|
||||||
|
showDueAmount: true,
|
||||||
|
showBalanceDue: true,
|
||||||
|
|
||||||
|
discount: '0.00',
|
||||||
|
|
||||||
|
// Footer paragraphs.
|
||||||
|
termsConditionsLabel: 'Terms & Conditions',
|
||||||
|
showTermsConditions: true,
|
||||||
|
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
item: 'Simply dummy text',
|
||||||
|
description: 'Simply dummy text of the printing and typesetting',
|
||||||
|
rate: '1',
|
||||||
|
quantity: '1000',
|
||||||
|
total: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
taxes: [
|
||||||
|
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
|
||||||
|
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
|
||||||
|
],
|
||||||
|
|
||||||
|
statementLabel: 'Statement',
|
||||||
|
showStatement: true,
|
||||||
|
billedToAddress: [
|
||||||
|
'Bigcapital Technology, Inc.',
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
billedFromAddres: [
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
43
packages/server/src/services/Sales/Invoices/utils.ts
Normal file
43
packages/server/src/services/Sales/Invoices/utils.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { pickBy } from 'lodash';
|
||||||
|
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
|
||||||
|
|
||||||
|
export const mergePdfTemplateWithDefaultAttributes = (
|
||||||
|
brandingTemplate?: Record<string, any>,
|
||||||
|
defaultAttributes: Record<string, any> = {}
|
||||||
|
) => {
|
||||||
|
const brandingAttributes = pickBy(
|
||||||
|
brandingTemplate,
|
||||||
|
(val, key) => val !== null && Object.keys(defaultAttributes).includes(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultAttributes,
|
||||||
|
...brandingAttributes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformInvoiceToPdfTemplate = (
|
||||||
|
invoice: ISaleInvoice
|
||||||
|
): Partial<InvoicePdfTemplateAttributes> => {
|
||||||
|
return {
|
||||||
|
dueDate: invoice.dueDateFormatted,
|
||||||
|
dateIssue: invoice.invoiceDateFormatted,
|
||||||
|
invoiceNumber: invoice.invoiceNo,
|
||||||
|
|
||||||
|
total: invoice.totalFormatted,
|
||||||
|
subtotal: invoice.subtotalFormatted,
|
||||||
|
paymentMade: invoice.paymentAmountFormatted,
|
||||||
|
balanceDue: invoice.balanceAmountFormatted,
|
||||||
|
|
||||||
|
termsConditions: invoice.termsConditions,
|
||||||
|
statement: invoice.invoiceMessage,
|
||||||
|
|
||||||
|
lines: invoice.entries.map((entry) => ({
|
||||||
|
item: entry.item.name,
|
||||||
|
description: entry.description,
|
||||||
|
rate: entry.rateFormatted,
|
||||||
|
quantity: entry.quantityFormatted,
|
||||||
|
total: entry.totalFormatted,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -17,7 +17,7 @@ export class TemplateInjectable {
|
|||||||
public async render(
|
public async render(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
filename: string,
|
filename: string,
|
||||||
options: Record<string, string | number | boolean>
|
options: Record<string, any>
|
||||||
) {
|
) {
|
||||||
const i18n = this.tenancy.i18n(tenantId);
|
const i18n = this.tenancy.i18n(tenantId);
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,14 @@ import styled from 'styled-components';
|
|||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
|
|
||||||
export function FSelect({ ...props }) {
|
export function FSelect({ ...props }) {
|
||||||
const input = ({ activeItem, text, label, value }) => {
|
const input = ({ activeItem, text, label, value }) => (
|
||||||
return (
|
<SelectButton
|
||||||
<SelectButton
|
text={text || props.placeholder || 'Select an item ...'}
|
||||||
text={text || props.placeholder || 'Select an item ...'}
|
disabled={props.disabled || false}
|
||||||
disabled={props.disabled || false}
|
{...props.buttonProps}
|
||||||
{...props.buttonProps}
|
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
return <Select input={input} fill={true} {...props} />;
|
return <Select input={input} fill={true} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Button } from '@blueprintjs/core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { FFormGroup } from '@/components';
|
||||||
|
|
||||||
|
export const BrandingThemeFormGroup = styled(FFormGroup)`
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.bp4-label {
|
||||||
|
color: #7a8492;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bp4-inline label.bp4-label {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BrandingThemeSelectButton = styled(Button)`
|
||||||
|
position: relative;
|
||||||
|
padding-right: 26px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-top: 5px solid #98a1ae;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
export const convertBrandingTemplatesToOptions = (brandingTemplates: Array<any>) => {
|
||||||
|
return brandingTemplates?.map(
|
||||||
|
(template) =>
|
||||||
|
({ text: template.template_name, value: template.id } || []),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Intent,
|
Intent,
|
||||||
Button,
|
Button,
|
||||||
@@ -10,12 +10,17 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
|
||||||
import { useFormikContext } from 'formik';
|
|
||||||
import { If, Icon, FormattedMessage as T, Group } from '@/components';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { CLASSES } from '@/constants/classes';
|
||||||
|
import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components';
|
||||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||||
|
import { useInvoiceFormBrandingTemplatesOptions } from './utils';
|
||||||
|
import {
|
||||||
|
BrandingThemeFormGroup,
|
||||||
|
BrandingThemeSelectButton,
|
||||||
|
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice floating actions bar.
|
* Invoice floating actions bar.
|
||||||
@@ -75,6 +80,8 @@ export default function InvoiceFloatingActions() {
|
|||||||
resetForm();
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const brandingTemplatesOptions = useInvoiceFormBrandingTemplatesOptions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
spacing={10}
|
spacing={10}
|
||||||
@@ -192,6 +199,26 @@ export default function InvoiceFloatingActions() {
|
|||||||
onClick={handleCancelBtnClick}
|
onClick={handleCancelBtnClick}
|
||||||
text={<T id={'cancel'} />}
|
text={<T id={'cancel'} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* ----------- Branding Template Select ----------- */}
|
||||||
|
<BrandingThemeFormGroup
|
||||||
|
name={'pdf_template_id'}
|
||||||
|
label={'Branding'}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
style={{ marginLeft: 20 }}
|
||||||
|
>
|
||||||
|
<FSelect
|
||||||
|
name={'pdf_template_id'}
|
||||||
|
items={brandingTemplatesOptions}
|
||||||
|
input={({ activeItem, text, label, value }) => (
|
||||||
|
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||||
|
)}
|
||||||
|
filterable={false}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
/>
|
||||||
|
</BrandingThemeFormGroup>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '@/hooks/query';
|
} from '@/hooks/query';
|
||||||
import { useProjects } from '@/containers/Projects/hooks';
|
import { useProjects } from '@/containers/Projects/hooks';
|
||||||
import { useTaxRates } from '@/hooks/query/taxRates';
|
import { useTaxRates } from '@/hooks/query/taxRates';
|
||||||
|
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||||
|
|
||||||
const InvoiceFormContext = createContext();
|
const InvoiceFormContext = createContext();
|
||||||
|
|
||||||
@@ -55,6 +56,10 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
{ enabled: !!estimateId },
|
{ enabled: !!estimateId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fetches branding templates of invoice.
|
||||||
|
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
|
||||||
|
useGetPdfTemplates({ resource: 'SaleInvoice' });
|
||||||
|
|
||||||
const newInvoice = !isEmpty(estimate)
|
const newInvoice = !isEmpty(estimate)
|
||||||
? transformToEditForm({
|
? transformToEditForm({
|
||||||
...pick(estimate, ['customer_id', 'currency_code', 'entries']),
|
...pick(estimate, ['customer_id', 'currency_code', 'entries']),
|
||||||
@@ -105,7 +110,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
|
|
||||||
// Determines whether the warehouse and branches are loading.
|
// Determines whether the warehouse and branches are loading.
|
||||||
const isFeatureLoading =
|
const isFeatureLoading =
|
||||||
isWarehouesLoading || isBranchesLoading || isProjectsLoading;
|
isWarehouesLoading || isBranchesLoading || isProjectsLoading || isBrandingTemplatesLoading;
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
invoice,
|
invoice,
|
||||||
@@ -119,6 +124,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
warehouses,
|
warehouses,
|
||||||
projects,
|
projects,
|
||||||
taxRates,
|
taxRates,
|
||||||
|
brandingTemplates,
|
||||||
|
|
||||||
isInvoiceLoading,
|
isInvoiceLoading,
|
||||||
isItemsLoading,
|
isItemsLoading,
|
||||||
@@ -130,6 +136,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
isBranchesSuccess,
|
isBranchesSuccess,
|
||||||
isWarehousesSuccess,
|
isWarehousesSuccess,
|
||||||
isTaxRatesLoading,
|
isTaxRatesLoading,
|
||||||
|
isBrandingTemplatesLoading,
|
||||||
|
|
||||||
createInvoiceMutate,
|
createInvoiceMutate,
|
||||||
editInvoiceMutate,
|
editInvoiceMutate,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
transformAttachmentsToForm,
|
transformAttachmentsToForm,
|
||||||
transformAttachmentsToRequest,
|
transformAttachmentsToRequest,
|
||||||
} from '@/containers/Attachments/utils';
|
} from '@/containers/Attachments/utils';
|
||||||
|
import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||||
|
|
||||||
export const MIN_LINES_NUMBER = 1;
|
export const MIN_LINES_NUMBER = 1;
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ export const defaultInvoice = {
|
|||||||
branch_id: '',
|
branch_id: '',
|
||||||
warehouse_id: '',
|
warehouse_id: '',
|
||||||
project_id: '',
|
project_id: '',
|
||||||
|
pdf_template_id: '',
|
||||||
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
|
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
};
|
};
|
||||||
@@ -406,3 +408,12 @@ export const useInvoiceCurrencyCode = () => {
|
|||||||
|
|
||||||
return values.currency_code;
|
return values.currency_code;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useInvoiceFormBrandingTemplatesOptions = () => {
|
||||||
|
const { brandingTemplates } = useInvoiceFormContext();
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
() => convertBrandingTemplatesToOptions(brandingTemplates),
|
||||||
|
[brandingTemplates],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user