mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +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";
|
||||
|
||||
*,
|
||||
*::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)
|
||||
head
|
||||
title My Site - #{title}
|
||||
style
|
||||
include ../scss/normalize.css
|
||||
include ../scss/base.css
|
||||
block head
|
||||
body
|
||||
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;
|
||||
subtotalLocal: number;
|
||||
subtotalExludingTax: number;
|
||||
|
||||
termsConditions: string;
|
||||
invoiceMessage: string;
|
||||
|
||||
pdfTemplateId?: number;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDTO {
|
||||
@@ -217,3 +222,83 @@ export interface ISaleInvoiceMailSent {
|
||||
saleInvoiceId: number;
|
||||
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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Json schema.
|
||||
*/
|
||||
static get jsonSchema() {
|
||||
return {
|
||||
type: 'object',
|
||||
|
||||
@@ -20,6 +20,10 @@ export class ChromiumlyTenancy {
|
||||
properties?: PageProperties,
|
||||
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.
|
||||
*/
|
||||
public setItemsEntriesDefaultAccounts(tenantId: number) {
|
||||
return async (entries: IItemEntry[]) => {
|
||||
return async (entries: IItemEntry[]) => {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||
|
||||
@@ -33,6 +33,14 @@ export class AssignPdfTemplateDefault {
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx?: Knex.Transaction) => {
|
||||
// Triggers `onPdfTemplateAssigningDefault` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.pdfTemplate.onAssigningDefault,
|
||||
{
|
||||
tenantId,
|
||||
templateId,
|
||||
}
|
||||
);
|
||||
await PdfTemplate.query(trx)
|
||||
.where('resource', oldPdfTempalte.resource)
|
||||
.patch({ default: false });
|
||||
@@ -41,6 +49,7 @@ export class AssignPdfTemplateDefault {
|
||||
.findById(templateId)
|
||||
.patch({ default: true });
|
||||
|
||||
// Triggers `onPdfTemplateAssignedDefault` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
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 { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||
import { ItemEntry } from '@/models';
|
||||
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
|
||||
@Service()
|
||||
export class CommandSaleInvoiceDTOTransformer {
|
||||
@@ -40,6 +41,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
@Inject()
|
||||
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
||||
|
||||
@Inject()
|
||||
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||
|
||||
/**
|
||||
* Transformes the create DTO to invoice object model.
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
||||
@@ -113,11 +117,19 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
userId: authorizedUser.id,
|
||||
} 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(
|
||||
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
||||
this.branchDTOTransform.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 { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { transformInvoiceToPdfTemplate } from './utils';
|
||||
import { InvoicePdfTemplateAttributes } from '@/interfaces';
|
||||
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@@ -14,6 +21,9 @@ export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
private getInvoiceService: GetSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId - Tenant Id.
|
||||
@@ -24,19 +34,54 @@ export class SaleInvoicePdf {
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<Buffer> {
|
||||
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
|
||||
const brandingAttributes = await this.getInvoiceBrandingAttributes(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/invoice-regular',
|
||||
{
|
||||
saleInvoice,
|
||||
}
|
||||
'modules/invoice-standard',
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
// Converts the given html content to pdf document.
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
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(
|
||||
tenantId: number,
|
||||
filename: string,
|
||||
options: Record<string, string | number | boolean>
|
||||
options: Record<string, any>
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
|
||||
@@ -6,16 +6,14 @@ import styled from 'styled-components';
|
||||
import clsx from 'classnames';
|
||||
|
||||
export function FSelect({ ...props }) {
|
||||
const input = ({ activeItem, text, label, value }) => {
|
||||
return (
|
||||
<SelectButton
|
||||
text={text || props.placeholder || 'Select an item ...'}
|
||||
disabled={props.disabled || false}
|
||||
{...props.buttonProps}
|
||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const input = ({ activeItem, text, label, value }) => (
|
||||
<SelectButton
|
||||
text={text || props.placeholder || 'Select an item ...'}
|
||||
disabled={props.disabled || false}
|
||||
{...props.buttonProps}
|
||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||
/>
|
||||
);
|
||||
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
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Intent,
|
||||
Button,
|
||||
@@ -10,12 +10,17 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
} 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 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 { useInvoiceFormBrandingTemplatesOptions } from './utils';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
/**
|
||||
* Invoice floating actions bar.
|
||||
@@ -75,6 +80,8 @@ export default function InvoiceFloatingActions() {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const brandingTemplatesOptions = useInvoiceFormBrandingTemplatesOptions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
spacing={10}
|
||||
@@ -192,6 +199,26 @@ export default function InvoiceFloatingActions() {
|
||||
onClick={handleCancelBtnClick}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@/hooks/query';
|
||||
import { useProjects } from '@/containers/Projects/hooks';
|
||||
import { useTaxRates } from '@/hooks/query/taxRates';
|
||||
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||
|
||||
const InvoiceFormContext = createContext();
|
||||
|
||||
@@ -55,6 +56,10 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
{ enabled: !!estimateId },
|
||||
);
|
||||
|
||||
// Fetches branding templates of invoice.
|
||||
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
|
||||
useGetPdfTemplates({ resource: 'SaleInvoice' });
|
||||
|
||||
const newInvoice = !isEmpty(estimate)
|
||||
? transformToEditForm({
|
||||
...pick(estimate, ['customer_id', 'currency_code', 'entries']),
|
||||
@@ -105,7 +110,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
|
||||
// Determines whether the warehouse and branches are loading.
|
||||
const isFeatureLoading =
|
||||
isWarehouesLoading || isBranchesLoading || isProjectsLoading;
|
||||
isWarehouesLoading || isBranchesLoading || isProjectsLoading || isBrandingTemplatesLoading;
|
||||
|
||||
const provider = {
|
||||
invoice,
|
||||
@@ -119,6 +124,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
warehouses,
|
||||
projects,
|
||||
taxRates,
|
||||
brandingTemplates,
|
||||
|
||||
isInvoiceLoading,
|
||||
isItemsLoading,
|
||||
@@ -130,6 +136,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
isBranchesSuccess,
|
||||
isWarehousesSuccess,
|
||||
isTaxRatesLoading,
|
||||
isBrandingTemplatesLoading,
|
||||
|
||||
createInvoiceMutate,
|
||||
editInvoiceMutate,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -66,6 +67,7 @@ export const defaultInvoice = {
|
||||
branch_id: '',
|
||||
warehouse_id: '',
|
||||
project_id: '',
|
||||
pdf_template_id: '',
|
||||
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
@@ -406,3 +408,12 @@ export const useInvoiceCurrencyCode = () => {
|
||||
|
||||
return values.currency_code;
|
||||
};
|
||||
|
||||
export const useInvoiceFormBrandingTemplatesOptions = () => {
|
||||
const { brandingTemplates } = useInvoiceFormContext();
|
||||
|
||||
return React.useMemo(
|
||||
() => convertBrandingTemplatesToOptions(brandingTemplates),
|
||||
[brandingTemplates],
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user