feat: invoice, estimate and receipt printing.

This commit is contained in:
a.bouhuolia
2021-08-17 10:47:04 +02:00
parent 70939c5741
commit 160b8b6a1b
50 changed files with 3607 additions and 120 deletions

View File

@@ -53,4 +53,22 @@ services:
- nginx
volumes:
- ./certbot/letsencrypt/:/var/www/letsencrypt
- ./certbot/certs/:/var/certs
- ./certbot/certs/:/var/certs
browserless:
image: browserless/chrome:latest
environment:
- DEBUG=browserless:*
- MAX_CONCURRENT_SESSIONS=1
- MAX_QUEUE_LENGTH=20
- PREBOOT_CHROME=true
- HOST=0.0.0.0
- ENABLE_DEBUGGER=false
- PORT=3000
- WORKSPACE_DELETE_EXPIRED=true
container_name: "browserless_bigcapital"
restart: always
ports:
- "3000:3000"
expose:
- "3000"

View File

@@ -36,4 +36,5 @@ LICENSES_AUTH_USER=root
LICENSES_AUTH_PASSWORD=root
AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123
AGENDASH_AUTH_PASSWORD=123123
BROWSER_WS_ENDPOINT=ws://localhost:3000/

View File

@@ -9,6 +9,7 @@
"copy-18n": "cpy --cwd=src/locales --parents '**/*.json' ../../build/locales",
"clear": "rimraf build",
"build:ts": "tsc -p tsconfig.json",
"build:resources": "gulp --gulpfile=scripts/gulpfile.js styles",
"build": "npm-run-all clear build:ts copy-18n"
},
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",
@@ -47,6 +48,8 @@
"express-fileupload": "^1.1.7-alpha.3",
"express-oauth-server": "^2.0.0",
"express-validator": "^6.8.0",
"gulp": "^4.0.2",
"gulp-sass": "^5.0.0",
"helmet": "^3.21.0",
"i18n": "^0.8.5",
"is-my-json-valid": "^2.20.5",
@@ -73,6 +76,8 @@
"objection-filter": "^4.0.1",
"objection-soft-delete": "^1.0.7",
"pluralize": "^8.0.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
"ramda": "^0.27.1",
"rate-limiter-flexible": "^2.1.14",
"reflect-metadata": "^0.1.13",
@@ -103,32 +108,13 @@
"npm-run-all": "^4.1.5",
"nyc": "^14.1.1",
"regenerator-runtime": "^0.13.7",
"rimraf": "^3.0.2",
"sass": "^1.37.5",
"sinon": "^7.4.2",
"ts-node": "^9.0.0",
"typedi": "^0.8.0",
"typescript": "^3.9.7",
"webpack-cli": "^4.6.0",
"rimraf": "^3.0.2"
},
"_moduleAliases": {
"loaders": "build/loaders",
"collection": "build/collection",
"config": "build/config",
"api": "build/api",
"data": "build/data",
"database": "build/database",
"decorators": "build/decorators",
"exceptions": "build/exceptions",
"interfaces": "build/interfaces",
"jobs": "build/jobs",
"lib": "build/lib",
"utils": "build/utils",
"locales": "build/locales",
"models": "build/models",
"repositories": "build/repositories",
"services": "build/services",
"subscribers": "build/subscribers",
"system": "build/system"
"webpack-cli": "^4.6.0"
},
"_moduleAliases": {}
}

View File

@@ -0,0 +1,521 @@
/*! 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;
}
*,
*::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 {
background: #f8f9fa;
font-family: "Noto Sans", sans-serif;
text-align: left;
}
@media print {
body {
background: #fff;
}
}
.page {
background: white;
display: block;
margin: 0.5cm auto;
box-shadow: rgba(122, 136, 146, 0.15) 0px 1px 3px 1px;
width: 21cm;
height: 29.7cm;
}
@media print {
.page {
margin: 0;
box-shadow: 0 0 0;
width: 100%;
height: auto;
}
}
.page[size=A4] {
width: 21cm;
height: 29.7cm;
}
.page[size=A4][layout=landscape] {
width: 29.7cm;
height: 21cm;
}
.page[size=A3] {
width: 29.7cm;
height: 42cm;
}
.page[size=A3][layout=landscape] {
width: 42cm;
height: 29.7cm;
}
.page[size=A5] {
width: 14.8cm;
height: 21cm;
}
.page[size=A5][layout=landscape] {
width: 21cm;
height: 14.8cm;
}
.estimate {
text-align: left;
padding: 45px;
}
.estimate__header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 60px;
}
.estimate__header .organization .title {
margin: 0 0 10px;
}
.estimate__header .paper .title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 6px;
font-size: 26px;
}
.estimate__meta {
display: flex;
flex-wrap: wrap;
margin-bottom: 40px;
}
.estimate__meta-item {
flex: 0 1 25%;
padding-right: 10px;
font-size: 16px;
font-weight: 400;
line-height: 1.6rem;
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.estimate__meta-item .value {
color: #000;
}
.estimate__meta-item .label {
color: #555;
margin-bottom: 2px;
}
.estimate__meta-item--amount {
flex: 0 1 50%;
}
.estimate__meta-item--amount .value {
font-weight: bold;
font-size: 20px;
}
.estimate__meta-item--billed-to {
flex: 0 1 50%;
}
.estimate__table {
display: flex;
flex-direction: column;
margin-bottom: 60px;
}
.estimate__table table {
font-size: 15px;
color: #000;
border-top: 2px solid #000;
text-align: left;
}
.estimate__table table thead th,
.estimate__table table tbody tr td {
margin-bottom: 15px;
background: transparent;
}
.estimate__table table thead tr {
color: #000;
}
.estimate__table table thead th {
font-size: 16px;
font-weight: 400;
border-bottom: none;
padding: 10px;
}
.estimate__table table thead th:first-child {
padding-left: 0;
}
.estimate__table table thead th:last-child {
padding-right: 0;
}
.estimate__table table tbody tr td {
font-size: 15px;
padding: 10px;
border-bottom: 1px solid #cecbcb;
}
.estimate__table table tbody tr td:first-child {
padding-left: 0;
}
.estimate__table table tbody tr td::last-child {
padding-right: 0;
}
.estimate__table table thead tr th.item {
width: 45%;
}
.estimate__table table thead tr th.rate {
width: 18%;
}
.estimate__table table thead tr th.quantity {
width: 16%;
}
.estimate__table table thead tr th.total {
width: 21%;
}
.estimate__conditions__title {
color: #666;
}

View File

@@ -0,0 +1,527 @@
/*! 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;
}
*,
*::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 {
background: #f8f9fa;
font-family: "Noto Sans", sans-serif;
text-align: left;
}
@media print {
body {
background: #fff;
}
}
.page {
background: white;
display: block;
margin: 0.5cm auto;
box-shadow: rgba(122, 136, 146, 0.15) 0px 1px 3px 1px;
width: 21cm;
height: 29.7cm;
}
@media print {
.page {
margin: 0;
box-shadow: 0 0 0;
width: 100%;
height: auto;
}
}
.page[size=A4] {
width: 21cm;
height: 29.7cm;
}
.page[size=A4][layout=landscape] {
width: 29.7cm;
height: 21cm;
}
.page[size=A3] {
width: 29.7cm;
height: 42cm;
}
.page[size=A3][layout=landscape] {
width: 42cm;
height: 29.7cm;
}
.page[size=A5] {
width: 14.8cm;
height: 21cm;
}
.page[size=A5][layout=landscape] {
width: 21cm;
height: 14.8cm;
}
.invoice {
text-align: left;
padding: 45px;
}
.invoice__header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 60px;
}
.invoice__header .organization .title {
margin: 0 0 10px;
}
.invoice__header .paper .title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 6px;
font-size: 26px;
}
.invoice__meta {
display: flex;
flex-wrap: wrap;
margin-bottom: 40px;
}
.invoice__meta-item {
flex: 0 1 25%;
padding-right: 10px;
font-size: 16px;
font-weight: 400;
line-height: 1.6rem;
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.invoice__meta-item .value {
color: #000;
}
.invoice__meta-item .label {
color: #555;
margin-bottom: 2px;
}
.invoice__meta-item--amount {
flex: 0 1 50%;
}
.invoice__meta-item--amount .value {
font-weight: bold;
font-size: 20px;
}
.invoice__meta-item--billed-to {
flex: 0 1 50%;
}
.invoice__table {
display: flex;
flex-direction: column;
margin-bottom: 60px;
}
.invoice__table table {
font-size: 15px;
color: #000;
text-align: left;
}
.invoice__table table thead th,
.invoice__table table tbody tr td {
margin-bottom: 15px;
background: transparent;
}
.invoice__table table thead tr {
color: #000;
}
.invoice__table table thead tr th {
border-top: 2px solid #000;
}
.invoice__table table thead th {
font-size: 16px;
font-weight: 400;
border-bottom: none;
padding: 10px;
}
.invoice__table table thead th:first-child {
padding-left: 0;
}
.invoice__table table thead th:last-child {
padding-right: 0;
}
.invoice__table table tbody tr td {
font-size: 15px;
padding: 10px;
border-bottom: 1px solid #cecbcb;
}
.invoice__table table tbody tr td:first-child {
padding-left: 0;
}
.invoice__table table tbody tr td::last-child {
padding-right: 0;
}
.invoice__table table thead tr th.item {
width: 45%;
}
.invoice__table table thead tr th.rate {
width: 18%;
}
.invoice__table table thead tr th.quantity {
width: 16%;
}
.invoice__table table thead tr th.total {
width: 21%;
}
.invoice__table table .description {
font-size: 14px;
color: #666;
}
.invoice__conditions__title {
color: #666;
}

View File

@@ -0,0 +1,521 @@
/*! 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;
}
*,
*::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 {
background: #f8f9fa;
font-family: "Noto Sans", sans-serif;
text-align: left;
}
@media print {
body {
background: #fff;
}
}
.page {
background: white;
display: block;
margin: 0.5cm auto;
box-shadow: rgba(122, 136, 146, 0.15) 0px 1px 3px 1px;
width: 21cm;
height: 29.7cm;
}
@media print {
.page {
margin: 0;
box-shadow: 0 0 0;
width: 100%;
height: auto;
}
}
.page[size=A4] {
width: 21cm;
height: 29.7cm;
}
.page[size=A4][layout=landscape] {
width: 29.7cm;
height: 21cm;
}
.page[size=A3] {
width: 29.7cm;
height: 42cm;
}
.page[size=A3][layout=landscape] {
width: 42cm;
height: 29.7cm;
}
.page[size=A5] {
width: 14.8cm;
height: 21cm;
}
.page[size=A5][layout=landscape] {
width: 21cm;
height: 14.8cm;
}
.receipt {
text-align: left;
padding: 45px;
}
.receipt__header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 60px;
}
.receipt__header .organization .title {
margin: 0 0 10px;
}
.receipt__header .paper .title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 6px;
font-size: 26px;
}
.receipt__meta {
display: flex;
flex-wrap: wrap;
margin-bottom: 40px;
}
.receipt__meta-item {
flex: 0 1 25%;
padding-right: 10px;
font-size: 16px;
font-weight: 400;
line-height: 1.6rem;
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.receipt__meta-item .value {
color: #000;
}
.receipt__meta-item .label {
color: #555;
margin-bottom: 2px;
}
.receipt__meta-item--amount {
flex: 0 1 50%;
}
.receipt__meta-item--amount .value {
font-weight: bold;
font-size: 20px;
}
.receipt__meta-item--billed-to {
flex: 0 1 50%;
}
.receipt__table {
display: flex;
flex-direction: column;
margin-bottom: 60px;
}
.receipt__table table {
font-size: 15px;
color: #000;
border-top: 2px solid #000;
text-align: left;
}
.receipt__table table thead th,
.receipt__table table tbody tr td {
margin-bottom: 15px;
background: transparent;
}
.receipt__table table thead tr {
color: #000;
}
.receipt__table table thead th {
font-size: 16px;
font-weight: 400;
border-bottom: none;
padding: 10px;
}
.receipt__table table thead th:first-child {
padding-left: 0;
}
.receipt__table table thead th:last-child {
padding-right: 0;
}
.receipt__table table tbody tr td {
font-size: 15px;
padding: 10px;
border-bottom: 1px solid #cecbcb;
}
.receipt__table table tbody tr td:first-child {
padding-left: 0;
}
.receipt__table table tbody tr td::last-child {
padding-right: 0;
}
.receipt__table table thead tr th.item {
width: 45%;
}
.receipt__table table thead tr th.rate {
width: 18%;
}
.receipt__table table thead tr th.quantity {
width: 16%;
}
.receipt__table table thead tr th.total {
width: 21%;
}
.receipt__conditions__title {
color: #666;
}

View File

@@ -0,0 +1,23 @@
@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;
}

View File

@@ -0,0 +1,57 @@
@import "../base.scss";
body {
background: #f8f9fa;
font-family: 'Noto Sans', sans-serif;
text-align: left;
@media print {
background: #fff;
}
}
.page {
background: white;
display: block;
margin: 0.5cm auto;
box-shadow: rgba(122, 136, 146, 0.15) 0px 1px 3px 1px;
width: 21cm;
height: 29.7cm;
@media print {
margin: 0;
box-shadow: 0 0 0;
width: 100%;
height: auto;
}
&[size="A4"] {
width: 21cm;
height: 29.7cm;
}
&[size="A4"][layout="landscape"] {
width: 29.7cm;
height: 21cm;
}
&[size="A3"] {
width: 29.7cm;
height: 42cm;
}
&[size="A3"][layout="landscape"] {
width: 42cm;
height: 29.7cm;
}
&[size="A5"] {
width: 14.8cm;
height: 21cm;
}
&[size="A5"][layout="landscape"] {
width: 21cm;
height: 14.8cm;
}
}

View File

@@ -0,0 +1,141 @@
@import "../layouts/paper-layout.scss";
.estimate{
text-align: left;
padding: 45px;
&__header{
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 60px;
.organization{
.title{
margin: 0 0 10px;
}
}
.paper{
.title{
font-weight: 400;
text-transform: uppercase;
margin: 0 0 6px;
font-size: 26px;
}
}
}
&__meta{
display: flex;
flex-wrap: wrap;
margin-bottom: 40px;
&-item{
flex: 0 1 25%;
padding-right: 10px;
font-size: 16px;
font-weight: 400;
line-height: 1.6rem;
margin-bottom: 20px;
display: flex;
flex-direction: column;
.value{
color: #000;
}
.label{
color: #555;
margin-bottom: 2px;
}
&--amount{
flex: 0 1 50%;
.value{
font-weight: bold;
font-size: 20px;
}
}
&--billed-to{
flex: 0 1 50%;
}
}
}
&__table {
display: flex;
flex-direction: column;
margin-bottom: 60px;
table {
font-size: 15px;
color: #000;
border-top: 2px solid #000;
text-align: left;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead{
tr {
color: #000;
}
}
thead th {
font-size: 16px;
font-weight: 400;
border-bottom: none;
padding: 10px;
&:first-child{
padding-left: 0;
}
&:last-child{
padding-right: 0;
}
}
tbody tr td {
font-size: 15px;
padding: 10px;
border-bottom: 1px solid #cecbcb;
&:first-child{
padding-left: 0;
}
&::last-child{
padding-right: 0;
}
}
thead tr th{
&.item{
width: 45%;
}
&.rate{
width: 18%;
}
&.quantity{
width: 16%;
}
&.total{
width: 21%;
}
}
}
}
&__conditions{
&__title{
color: #666;
}
}
}

View File

@@ -0,0 +1,147 @@
@import "../layouts/paper-layout.scss";
.invoice{
text-align: left;
padding: 45px;
&__header{
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 60px;
.organization{
.title{
margin: 0 0 10px;
}
}
.paper{
.title{
font-weight: 400;
text-transform: uppercase;
margin: 0 0 6px;
font-size: 26px;
}
}
}
&__meta{
display: flex;
flex-wrap: wrap;
margin-bottom: 40px;
&-item{
flex: 0 1 25%;
padding-right: 10px;
font-size: 16px;
font-weight: 400;
line-height: 1.6rem;
margin-bottom: 20px;
display: flex;
flex-direction: column;
.value{
color: #000;
}
.label{
color: #555;
margin-bottom: 2px;
}
&--amount{
flex: 0 1 50%;
.value{
font-weight: bold;
font-size: 20px;
}
}
&--billed-to{
flex: 0 1 50%;
}
}
}
&__table {
display: flex;
flex-direction: column;
margin-bottom: 60px;
table {
font-size: 15px;
color: #000;
text-align: left;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead{
tr {
color: #000;
th{
border-top: 2px solid #000;
}
}
}
thead th {
font-size: 16px;
font-weight: 400;
border-bottom: none;
padding: 10px;
&:first-child{
padding-left: 0;
}
&:last-child{
padding-right: 0;
}
}
tbody tr td {
font-size: 15px;
padding: 10px;
border-bottom: 1px solid #cecbcb;
&:first-child{
padding-left: 0;
}
&::last-child{
padding-right: 0;
}
}
thead tr th{
&.item{
width: 45%;
}
&.rate{
width: 18%;
}
&.quantity{
width: 16%;
}
&.total{
width: 21%;
}
}
.description{
font-size: 14px;
color: #666;
}
}
}
&__conditions{
&__title{
color: #666;
}
}
}

View File

@@ -0,0 +1,140 @@
@import "../layouts/paper-layout.scss";
.receipt{
text-align: left;
padding: 45px;
&__header{
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 60px;
.organization{
.title{
margin: 0 0 10px;
}
}
.paper{
.title{
font-weight: 400;
text-transform: uppercase;
margin: 0 0 6px;
font-size: 26px;
}
}
}
&__meta{
display: flex;
flex-wrap: wrap;
margin-bottom: 40px;
&-item{
flex: 0 1 25%;
padding-right: 10px;
font-size: 16px;
font-weight: 400;
line-height: 1.6rem;
margin-bottom: 20px;
display: flex;
flex-direction: column;
.value{
color: #000;
}
.label{
color: #555;
margin-bottom: 2px;
}
&--amount{
flex: 0 1 50%;
.value{
font-weight: bold;
font-size: 20px;
}
}
&--billed-to{
flex: 0 1 50%;
}
}
}
&__table {
display: flex;
flex-direction: column;
margin-bottom: 60px;
table {
font-size: 15px;
color: #000;
border-top: 2px solid #000;
text-align: left;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead{
tr {
color: #000;
}
}
thead th {
font-size: 16px;
font-weight: 400;
border-bottom: none;
padding: 10px;
&:first-child{
padding-left: 0;
}
&:last-child{
padding-right: 0;
}
}
tbody tr td {
font-size: 15px;
padding: 10px;
border-bottom: 1px solid #cecbcb;
&:first-child{
padding-left: 0;
}
&::last-child{
padding-right: 0;
}
}
thead tr th{
&.item{
width: 45%;
}
&.rate{
width: 18%;
}
&.quantity{
width: 16%;
}
&.total{
width: 21%;
}
}
}
}
&__conditions{
&__title{
color: #666;
}
}
}

379
server/resources/scss/normalize.scss vendored Normal file
View 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;
}

View File

@@ -0,0 +1,7 @@
html
head
title My Site - #{title}
block head
body
div.paper-template
block content

View File

@@ -0,0 +1,67 @@
extends ../PaperTemplateLayout.pug
block head
style
include ../../css/modules/estimate.css
block content
div.estimate
div.estimate__header
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.paper
h1.title #{__("estimate.paper.estimate")}
span.email #{saleEstimate.estimateNumber}
div.estimate__meta
div.estimate__meta-item.estimate__meta-item--amount
span.label #{__('estimate.paper.due_amount')}
span.value #{saleEstimate.formattedAmount}
div.estimate__meta-item.estimate__meta-item--billed-to
span.label #{__("estimate.paper.billed_to")}
span.value #{saleEstimate.customer.displayName}
div.estimate__meta-item.estimate__meta-item--estimate-date
span.label #{__("estimate.paper.estimate_date")}
span.value #{saleEstimate.formattedEstimateDate}
if saleEstimate.estimateNumber
div.estimate__meta-item.estimate__meta-item--estimate-number
span.label #{__("estimate.paper.estimate_number")}
span.value #{saleEstimate.estimateNumber}
div.estimate__meta-item.estimate__meta-item--due-date
span.label #{__("estimate.paper.expiration_date")}
span.value #{saleEstimate.formattedExpirationDate}
div.estimate__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 saleEstimate.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
if saleEstimate.termsConditions
div.estimate__conditions
h3 #{__("estimate.paper.conditions_title")}
p #{saleEstimate.termsConditions}
if saleEstimate.note
div.estimate__notes
h3 #{__("estimate.paper.notes_title")}
p #{saleEstimate.note}

View File

@@ -0,0 +1,68 @@
extends ../PaperTemplateLayout.pug
block head
style
include ../../css/modules/invoice.css
block content
div.invoice
div.invoice__header
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.paper
h1.title #{__("invoice.paper.invoice")}
if saleInvoice.invoiceNo
span.email #{saleInvoice.invoiceNo}
div.invoice__meta
div.invoice__meta-item.invoice__meta-item--amount
span.label #{__('estimate.paper.due_amount')}
span.value #{saleInvoice.formattedAmount}
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.formattedInvoiceDate}
if saleInvoice.invoiceNo
div.invoice__meta-item.invoice__meta-item--invoice-number
span.label #{__("invoice.paper.invoice_number")}
span.value #{saleInvoice.invoiceNo}
div.invoice__meta-item.invoice__meta-item--due-date
span.label #{__("invoice.paper.due_date")}
span.value #{saleInvoice.formattedDueDate}
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
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}

View File

@@ -0,0 +1,61 @@
extends ../PaperTemplateLayout.pug
block head
style
include ../../css/modules/receipt.css
block content
div.receipt
div.receipt__header
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.paper
h1.title #{__("receipt.paper.receipt")}
span.email #{saleReceipt.receiptNumber}
div.receipt__meta
div.receipt__meta-item.receipt__meta-item--amount
span.label #{__('receipt.paper.receipt_amount')}
span.value #{saleReceipt.formattedAmount}
div.receipt__meta-item.receipt__meta-item--billed-to
span.label #{__("receipt.paper.billed_to")}
span.value #{saleReceipt.customer.displayName}
div.receipt__meta-item.receipt__meta-item--invoice-date
span.label #{__("receipt.paper.receipt_date")}
span.value #{saleReceipt.formattedReceiptDate}
if saleReceipt.receiptNumber
div.receipt__meta-item.receipt__meta-item--invoice-number
span.label #{__("receipt.paper.receipt_number")}
span.value #{saleReceipt.receiptNumber}
div.receipt__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 saleReceipt.entries
tr
td.item=entry.item.name
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
if saleReceipt.statement
div.receipt__conditions
h3 #{__("receipt.paper.statement_title")}
p #{saleReceipt.statement}
if saleReceipt.receiptMessage
div.receipt__notes
h3 #{__("receipt.paper.notes_title")}
p #{saleReceipt.receiptMessage}

View File

@@ -0,0 +1,117 @@
/**
* # Gulp Configuration.
* ------------------------------------------------------------------
*/
const RESOURCES_PATH = '../resources/';
module.exports = {
banner: [
'/**',
' * <%= pkg.name %> - <%= pkg.description %>',
' * @version v<%= pkg.version %>',
' * @link <%= pkg.homepage %>',
' * @author <%= pkg.author %>',
' * @license <%= pkg.license %>',
'**/',
'',
].join('\n'),
// Browser Sync
browsersync: {
files: ['**/*', '!**.map', '!**.css'], // Exclude map files.
notify: false, //
open: true, // Set it to false if you don't like the broser window opening automatically.
port: 8080, //
proxy: 'localhost/customatic', //
watchOptions: {
debounceDelay: 2000, // This introduces a small delay when watching for file change events to avoid triggering too many reloads
},
snippetOptions: {
whitelist: ['/wp-admin/admin-ajax.php'],
blacklist: ['/wp-admin/**'],
},
},
// Style Related.
style: {
clean: ['style.css', 'style.min.css', 'style-rtl.css', 'style-rtl.min.css'],
build: [
{
src: `${RESOURCES_PATH}/scss/modules/invoice.scss`, // Path to main .scss file.
dest: `${RESOURCES_PATH}/css/modules`, // Path to place the compiled CSS file.
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
{
src: `${RESOURCES_PATH}/scss/modules/estimate.scss`, // Path to main .scss file.
dest: `${RESOURCES_PATH}/css/modules`, // Path to place the compiled CSS file.
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
{
src: `${RESOURCES_PATH}/scss/modules/receipt.scss`, // Path to main .scss file.
dest: `${RESOURCES_PATH}/css/modules`, // Path to place the compiled CSS file.
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
// {
// src: './assets/sass/editor-style.scss',
// dest: './assets/css',
// sourcemaps: true,
// minify: true,
// },
],
rtl: [
// RTL builds.
{
src: './style.css',
dest: './', // The source files will be converted and suffixed to `-rtl` in this destination.
},
],
// Browsers you care about for auto-prefixing.
autoprefixer: {
browsers: [
'Android 2.3',
'Android >= 4',
'Chrome >= 20',
'Firefox >= 24',
'Explorer >= 9',
'iOS >= 6',
'Opera >= 12',
'Safari >= 6',
],
},
// SASS Configuration for all builds.
sass: {
errLogToConsole: true,
// outputStyle: 'compact',
},
// CSS MQ Packer configuration for all builds and style tasks.
cssMqpacker: {},
// CSS nano configuration for all builds.
cssnano: {},
// rtlcss configuration for all builds.
rtlcss: {},
},
// Clean specific files.
clean: [
'**/.DS_Store',
'./assets/js/**/*.min.js',
'**/*.map',
'**/*.min.css',
'assets/js/hypernews.js',
],
// Watch related.
watch: {
css: ['./assets/sass/**/*'],
js: ['assets/js/**/*.js', '!assets/js/**/*.min.js'],
images: ['./assets/images/**/*'],
},
};

View File

@@ -0,0 +1,15 @@
const gulp = require('gulp');
const sass = require('sass');
const gulpSass = require('gulp-sass')(sass); // Gulp pluign for Sass compilation.
const mergeStream = require('merge-stream');
const config = require('./gulpConfig');
gulp.task('styles', () => {
const builds = config.style.build.map((build) => {
return gulp
.src(build.src)
.pipe(gulpSass(config.style.sass))
.pipe(gulp.dest(build.dest));
});
return mergeStream(builds);
});

View File

@@ -111,8 +111,6 @@ export default class BaseController {
return response;
}
/**
* Async middleware.
* @param {function} callback
@@ -129,4 +127,14 @@ export default class BaseController {
protected accepts(req) {
return accepts(req);
}
/**
*
* @param {Request} req
* @param {string[]} types
* @returns {string}
*/
protected acceptTypes(req: Request, types: string[]) {
return this.accepts(req).types(types);
}
}

View File

@@ -165,11 +165,12 @@ export default class PaymentReceivesController extends BaseController {
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
try {
const storedPaymentReceive = await this.paymentReceiveService.createPaymentReceive(
tenantId,
paymentReceive,
user
);
const storedPaymentReceive =
await this.paymentReceiveService.createPaymentReceive(
tenantId,
paymentReceive,
user
);
return res.status(200).send({
id: storedPaymentReceive.id,
message: 'The payment receive has been created successfully.',
@@ -247,11 +248,13 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params;
try {
const invoices = await this.paymentReceiveService.getPaymentReceiveInvoices(
tenantId,
paymentReceiveId
);
return res.status(200).send({ sale_invoices: invoices });
const saleInvoices =
await this.paymentReceiveService.getPaymentReceiveInvoices(
tenantId,
paymentReceiveId
);
return res.status(200).send(this.transfromToResponse({ saleInvoices }));
} catch (error) {
next(error);
}
@@ -274,17 +277,11 @@ export default class PaymentReceivesController extends BaseController {
};
try {
const {
paymentReceives,
pagination,
filterMeta,
} = await this.paymentReceiveService.listPaymentReceives(
tenantId,
filter
);
const { paymentReceives, pagination, filterMeta } =
await this.paymentReceiveService.listPaymentReceives(tenantId, filter);
return res.status(200).send({
payment_receives: paymentReceives,
payment_receives: this.transfromToResponse(paymentReceives),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
@@ -334,14 +331,12 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params;
try {
const {
paymentReceive,
entries,
} = await this.PaymentReceivesPages.getPaymentReceiveEditPage(
tenantId,
paymentReceiveId,
user
);
const { paymentReceive, entries } =
await this.PaymentReceivesPages.getPaymentReceiveEditPage(
tenantId,
paymentReceiveId,
user
);
return res.status(200).send({
payment_receive: this.transfromToResponse({ ...paymentReceive }),
@@ -442,9 +437,10 @@ export default class PaymentReceivesController extends BaseController {
type: 'INVOICES_NOT_DELIVERED_YET',
code: 200,
data: {
not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map(
(invoice) => invoice.id
),
not_delivered_invoices_ids:
error.payload.notDeliveredInvoices.map(
(invoice) => invoice.id
),
},
},
],

View File

@@ -7,7 +7,12 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions';
import SaleEstimatesPdfService from 'services/Sales/Estimates/SaleEstimatesPdf';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SalesEstimatesController extends BaseController {
@Inject()
@@ -16,6 +21,9 @@ export default class SalesEstimatesController extends BaseController {
@Inject()
dynamicListService: DynamicListingService;
@Inject()
saleEstimatesPdf: SaleEstimatesPdfService;
/**
* Router constructor.
*/
@@ -135,7 +143,7 @@ export default class SalesEstimatesController extends BaseController {
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('search_keyword').optional({ nullable: true }).isString().trim()
query('search_keyword').optional({ nullable: true }).isString().trim(),
];
}
@@ -292,8 +300,25 @@ export default class SalesEstimatesController extends BaseController {
tenantId,
estimateId
);
return res.status(200).send({ estimate });
// Response formatter.
res.format({
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleEstimatesPdf.saleEstimatePdf(
tenantId,
estimate
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
// JSON content type.
default: () => {
return res.status(200).send(this.transfromToResponse({ estimate }));
},
});
} catch (error) {
next(error);
}
@@ -318,10 +343,16 @@ export default class SalesEstimatesController extends BaseController {
const { salesEstimates, pagination, filterMeta } =
await this.saleEstimateService.estimatesList(tenantId, filter);
return res.status(200).send({
sales_estimates: this.transfromToResponse(salesEstimates),
pagination,
filter_meta: this.transfromToResponse(filterMeta),
res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(
this.transfromToResponse({
salesEstimates,
pagination,
filterMeta,
})
);
},
});
} catch (error) {
next(error);

View File

@@ -8,7 +8,12 @@ import ItemsService from 'services/Items/ItemsService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions';
import { ISaleInvoiceDTO, ISaleInvoiceCreateDTO } from 'interfaces';
import SaleInvoicePdf from 'services/Sales/SaleInvoicePdf';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SaleInvoicesController extends BaseController {
@Inject()
@@ -20,6 +25,9 @@ export default class SaleInvoicesController extends BaseController {
@Inject()
dynamicListService: DynamicListingService;
@Inject()
saleInvoicePdf: SaleInvoicePdf;
/**
* Router constructor.
*/
@@ -254,8 +262,8 @@ export default class SaleInvoicesController extends BaseController {
/**
* Retrieve the sale invoice with associated entries.
* @param {Request} req
* @param {Response} res
* @param {Request} req - Request object.
* @param {Response} res - Response object.
*/
async getSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { id: saleInvoiceId } = req.params;
@@ -267,7 +275,25 @@ export default class SaleInvoicesController extends BaseController {
saleInvoiceId,
user
);
return res.status(200).send({ sale_invoice: saleInvoice });
// Response formatter.
res.format({
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoicePdf.saleInvoicePdf(
tenantId,
saleInvoice
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ saleInvoice }));
},
});
} catch (error) {
next(error);
}
@@ -296,7 +322,7 @@ export default class SaleInvoicesController extends BaseController {
await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
return res.status(200).send({
sales_invoices: salesInvoices,
sales_invoices: this.transfromToResponse(salesInvoices),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});

View File

@@ -3,6 +3,7 @@ import { check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleReceiptService from 'services/Sales/SalesReceipts';
import SaleReceiptsPdfService from 'services/Sales/Receipts/SaleReceiptsPdfService';
import BaseController from '../BaseController';
import { ISaleReceiptDTO } from 'interfaces/SaleReceipt';
import { ServiceError } from 'exceptions';
@@ -13,6 +14,9 @@ export default class SalesReceiptsController extends BaseController {
@Inject()
saleReceiptService: SaleReceiptService;
@Inject()
saleReceiptsPdf: SaleReceiptsPdfService;
@Inject()
dynamicListService: DynamicListingService;
@@ -239,17 +243,13 @@ export default class SalesReceiptsController extends BaseController {
};
try {
const {
salesReceipts,
pagination,
filterMeta,
} = await this.saleReceiptService.salesReceiptsList(tenantId, filter);
const { salesReceipts, pagination, filterMeta } =
await this.saleReceiptService.salesReceiptsList(tenantId, filter);
return res.status(200).send({
sale_receipts: salesReceipts,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
const response = this.transfromToResponse({
salesReceipts, pagination, filterMeta
});
return res.status(200).send(response);
} catch (error) {
next(error);
}
@@ -271,9 +271,22 @@ export default class SalesReceiptsController extends BaseController {
saleReceiptId
);
return res.status(200).send({
sale_receipt: saleReceipt,
});
res.format({
'application/pdf': async () => {
const pdfContent = await this.saleReceiptsPdf.saleReceiptPdf(
tenantId,
saleReceipt
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
'application/json': () => {
return res.status(200).send(this.transfromToResponse({ saleReceipt }));
}
})
} catch (error) {
next(error);
}

View File

@@ -42,12 +42,14 @@ import Subscription from 'api/controllers/Subscription';
import Licenses from 'api/controllers/Subscription/Licenses';
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
import Setup from 'api/controllers/Setup';
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
export default () => {
const app = Router();
// - Global routes.
// ---------------------------
app.use(asyncRenderMiddleware);
app.use(i18n.init);
app.use(I18nMiddleware);

View File

@@ -0,0 +1,23 @@
import { Request, Response } from 'express';
const asyncRender = (app) => (path: string, attributes = {}) =>
new Promise((resolve, reject) => {
app.render(path, attributes, (error, data) => {
if (error) { reject(error); }
resolve(data);
});
});
/**
* Injects `asyncRender` method to response object.
* @param {Request} req Express req Object
* @param {Response} res Express res Object
* @param {NextFunction} next Express next Function
*/
const asyncRenderMiddleware = (req: Request, res: Response, next: Function) => {
res.asyncRender = asyncRender(req.app);
next();
};
export default asyncRenderMiddleware;

View File

@@ -162,6 +162,13 @@ export default {
}
},
/**
* Puppeteer remote browserless connection.
*/
puppeteer: {
browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
},
protocol: '',
hostname: '',
scheduleComputeItemCost: 'in 5 seconds'

View File

@@ -0,0 +1,64 @@
import moment from "moment";
import { isEmpty, isObject, isUndefined } from 'lodash';
export class Transformer {
/**
*
* @returns
*/
protected includeAttributes = (): string[] => {
return [];
}
/**
*
*/
public transform = (object: any) => {
if (Array.isArray(object)) {
return object.map(this.getTransformation);
} else if (isObject(object)) {
return this.getTransformation(object);
}
return object;
};
/**
*
* @param item
* @returns
*/
protected getTransformation = (item) => {
const attributes = this.getIncludeAttributesTransformed(item);
return {
...!isUndefined(item.toJSON) ? item.toObject() : item,
...attributes
};
};
/**
*
* @param item
* @returns
*/
protected getIncludeAttributesTransformed = (item) => {
const attributes = this.includeAttributes();
return attributes
.filter((attribute) => !isUndefined(this[attribute]))
.reduce((acc, attribute: string) => {
acc[attribute] = this[attribute](item);
return acc;
}, {});
}
/**
*
* @param date
* @returns
*/
protected formatDate(date) {
return date ? moment(date).format('YYYY/MM/DD') : '';
}
}

View File

@@ -10,11 +10,16 @@ import AgendashController from 'api/controllers/Agendash';
import ConvertEmptyStringsToNull from 'api/middleware/ConvertEmptyStringsToNull';
import RateLimiterMiddleware from 'api/middleware/RateLimiterMiddleware'
import config from 'config';
import path from 'path';
export default ({ app }) => {
// Express configuration.
app.set('port', 3000);
// Template engine configuration.
app.set('views', path.join(__dirname, '../resources/views'));
app.set('view engine', 'pug');
// Helmet helps you secure your Express apps by setting various HTTP headers.
app.use(helmet());

View File

@@ -159,5 +159,32 @@
"Liabilities and Equity": "Liabilities and Equity",
"Closing balance": "Closing balance",
"Opening Balance": "Opening balance",
"Total {{accountName}}": "Total {{accountName}}"
"Total {{accountName}}": "Total {{accountName}}",
"invoice.paper.invoice": "Invoice",
"invoice.paper.billed_to": "Billed to",
"invoice.paper.invoice_date": "Invoice date",
"invoice.paper.invoice_number": "Invoice No.",
"invoice.paper.due_date": "Due date",
"invoice.paper.conditions_title": "Conditions & terms",
"invoice.paper.notes_title": "Notes",
"item_entry.paper.item_name": "Item name",
"item_entry.paper.rate": "Rate",
"item_entry.paper.quantity": "Quantity",
"item_entry.paper.total": "Total",
"estimate.paper.estimate": "Estimate",
"estimate.paper.billed_to": "Billed to",
"estimate.paper.estimate_date": "Estimate date",
"estimate.paper.estimate_number": "Estimate number",
"estimate.paper.expiration_date": "Expiration date",
"estimate.paper.conditions_title": "Conditions & terms",
"estimate.paper.notes_title": "Notes",
"estimate.paper.due_amount": "Due amount",
"receipt.paper.receipt": "Receipt",
"receipt.paper.billed_to": "Billed to",
"receipt.paper.receipt_date": "Receipt date",
"receipt.paper.receipt_number": "Receipt number",
"receipt.paper.expiration_date": "Expiration date",
"receipt.paper.conditions_title": "Conditions & terms",
"receipt.paper.notes_title": "Notes",
"receipt.paper.receipt_amount": "Receipt amount"
}

View File

@@ -1,5 +1,5 @@
import { Model, mixin } from 'objection';
import { snakeCase } from 'lodash';
import { snakeCase, transform } from 'lodash';
import { mapKeysDeep } from 'utils';
import PaginationQueryBuilder from 'models/Pagination';
import DateSession from 'models/DateSession';
@@ -47,4 +47,9 @@ export default class ModelBase extends mixin(Model, [DateSession]) {
static relationBindKnex(model) {
return this.knexBinded ? model.bindKnex(this.knexBinded) : model;
}
toObject(opt) {
const parsedJson = super.$formatJson(this, opt);
return parsedJson;
}
}

View File

@@ -0,0 +1,26 @@
import { Service } from 'typedi';
import puppeteer from 'puppeteer';
import config from 'config';
@Service()
export default class PdfService {
/**
* Pdf document.
* @param content
* @returns
*/
async pdfDocument(content: string) {
const browser = await puppeteer.connect({
browserWSEndpoint: config.puppeteer.browserWSEndpoint,
});
const page = await browser.newPage();
await page.setContent(content);
const pdf = await page.pdf({ format: 'a4' });
await browser.close();
return pdf;
}
}

View File

@@ -0,0 +1,35 @@
import { Service } from 'typedi';
import { IBill, IBillPayment } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class BillPaymentTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return ['formattedPaymentDate', 'formattedAmount'];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedPaymentDate = (billPayment: IBillPayment): string => {
return this.formatDate(billPayment.paymentDate);
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedAmount = (billPayment: IBillPayment): string => {
return formatNumber(billPayment.amount, {
currencyCode: billPayment.currencyCode,
});
};
}

View File

@@ -28,6 +28,7 @@ import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from 'exceptions';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import VendorsService from 'services/Contacts/VendorsService';
import BillPaymentTransformer from './BillPaymentTransformer';
import { ERRORS } from './constants';
/**
@@ -57,6 +58,9 @@ export default class BillPaymentsService implements IBillPaymentsService {
@Inject('logger')
logger: any;
@Inject()
billPaymentTransformer: BillPaymentTransformer;
/**
* Validate whether the bill payment vendor exists on the storage.
* @param {Request} req
@@ -546,7 +550,7 @@ export default class BillPaymentsService implements IBillPaymentsService {
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return billPayment;
return this.billPaymentTransformer.transform(billPayment);
}
/**
@@ -680,7 +684,7 @@ export default class BillPaymentsService implements IBillPaymentsService {
.pagination(filter.page - 1, filter.pageSize);
return {
billPayments: results,
billPayments: this.billPaymentTransformer.transform(results),
pagination,
filterMeta: dynamicList.getResponseMeta(),
};

View File

@@ -35,6 +35,7 @@ import JournalPosterService from 'services/Sales/JournalPosterService';
import VendorsService from 'services/Contacts/VendorsService';
import { ERRORS } from './constants';
import EntriesService from 'services/Entries';
import PurchaseInvoiceTransfromer from './PurchaseInvoices/PurchaseInvoiceTransformer';
/**
* Vendor bills services.
@@ -78,6 +79,9 @@ export default class BillsService
@Inject()
entriesService: EntriesService;
@Inject()
purchaseInvoiceTransformer: PurchaseInvoiceTransfromer;
/**
* Validates whether the vendor is exist.
* @async
@@ -568,7 +572,7 @@ export default class BillsService
.pagination(filter.page - 1, filter.pageSize);
return {
bills: results,
bills: this.purchaseInvoiceTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
@@ -616,7 +620,7 @@ export default class BillsService
if (!bill) {
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
return bill;
return this.purchaseInvoiceTransformer.transform(bill);
}
/**

View File

@@ -0,0 +1,66 @@
import { Service } from 'typedi';
import { IBill } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class PurchaseInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount',
'formattedDueAmount',
];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedBillDate = (bill: IBill): string => {
return this.formatDate(bill.billDate);
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedDueDate = (bill: IBill): string => {
return this.formatDate(bill.dueDate);
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedAmount = (bill): string => {
return formatNumber(bill.amount, { currencyCode: bill.currencyCode });
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedPaymentAmount = (bill): string => {
return formatNumber(bill.paymentAmount, { currencyCode: bill.currencyCode});
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedDueAmount = (bill): string => {
return formatNumber(bill.dueAmount, { currencyCode: bill.currencyCode });
};
}

View File

@@ -0,0 +1,78 @@
import { Service } from 'typedi';
import { ISaleEstimate } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class SaleEstimateTransfromer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedEstimateDate',
'formattedExpirationDate',
'formattedDeliveredAtDate',
'formattedApprovedAtDate',
'formattedRejectedAtDate',
];
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedEstimateDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.estimateDate);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedExpirationDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.expirationDate);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedDeliveredAtDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.deliveredAt);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedApprovedAtDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.approvedAt);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedRejectedAtDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.rejectedAt);
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleEstimate} estimate
* @returns {string}
*/
protected formattedAmount = (estimate: ISaleEstimate): string => {
return formatNumber(estimate.amount, {
currencyCode: estimate.currencyCode,
});
};
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import PdfService from 'services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class SaleEstimatesPdf {
@Inject()
pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleEstimatePdf(tenantId: number, saleEstimate) {
const i18n = this.tenancy.i18n(tenantId);
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const organizationEmail = settings.get({
group: 'organization',
key: 'email',
});
const htmlContent = templateRender('modules/estimate-regular', {
saleEstimate,
organizationName,
organizationEmail,
...i18n,
});
console.log(htmlContent, 'XXX');
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -0,0 +1,36 @@
import { Service } from 'typedi';
import { IPaymentReceive } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class PaymentReceiveTransfromer extends Transformer {
/**
* Include these attributes to payment receive object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedPaymentDate',
'formattedAmount',
];
};
/**
* Retrieve formatted payment receive date.
* @param {ISaleInvoice} invoice
* @returns {String}
*/
protected formattedPaymentDate = (payment: IPaymentReceive): string => {
return this.formatDate(payment.paymentDate);
};
/**
* Retrieve formatted payment amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedAmount = (payment: IPaymentReceive): string => {
return formatNumber(payment.amount, { currencyCode: payment.currencyCode });
};
}

View File

@@ -34,6 +34,7 @@ import JournalCommands from 'services/Accounting/JournalCommands';
import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from 'data/AccountTypes';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
import { ERRORS } from './constants';
import PaymentReceiveTransfromer from './PaymentReceiveTransformer';
/**
* Payment receive service.
@@ -68,6 +69,9 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject()
paymentReceiveTransformer: PaymentReceiveTransfromer;
/**
* Validates the payment receive number existance.
* @param {number} tenantId -
@@ -584,7 +588,7 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return paymentReceive;
return this.paymentReceiveTransformer.transform(paymentReceive);
}
/**
@@ -661,7 +665,7 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
);
return {
paymentReceives: results,
paymentReceives: this.paymentReceiveTransformer.transform(results),
pagination,
filterMeta: dynamicList.getResponseMeta(),
};

View File

@@ -0,0 +1,44 @@
import { Service } from 'typedi';
import { ISaleReceipt } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class SaleReceiptTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return ['formattedAmount', 'formattedReceiptDate', 'formattedClosedAtDate'];
};
/**
* Retrieve formatted receipt date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected formattedReceiptDate = (receipt: ISaleReceipt): string => {
return this.formatDate(receipt.receiptDate);
};
/**
* Retrieve formatted estimate closed at date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected formattedClosedAtDate = (receipt: ISaleReceipt): string => {
return this.formatDate(receipt.closedAt);
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleReceipt} estimate
* @returns {string}
*/
protected formattedAmount = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, {
currencyCode: receipt.currencyCode,
});
};
}

View File

@@ -0,0 +1,41 @@
import { Inject, Service } from 'typedi';
import PdfService from 'services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class SaleReceiptsPdf {
@Inject()
pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleReceiptPdf(tenantId: number, saleReceipt) {
const i18n = this.tenancy.i18n(tenantId);
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const organizationEmail = settings.get({
group: 'organization',
key: 'email',
});
const htmlContent = templateRender('modules/receipt-regular', {
saleReceipt,
organizationEmail,
organizationName,
...i18n,
});
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -1,3 +1,13 @@
export const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import PdfService from 'services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class SaleInvoicePdf {
@Inject()
pdfService: PdfService
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleInvoicePdf(tenantId: number, saleInvoice) {
const i18n = this.tenancy.i18n(tenantId);
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({ group: 'organization', key: 'name' });
const organizationEmail = settings.get({ group: 'organization', key: 'email' });
const htmlContent = templateRender('modules/invoice-regular', {
organizationName,
organizationEmail,
saleInvoice,
...i18n
});
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -0,0 +1,60 @@
import { Service } from 'typedi';
import { ISaleInvoice } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class SaleInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedInvoiceDate',
'formattedDueDate',
'formattedAmount',
'formattedDueAmount',
];
};
/**
* Retrieve formatted invoice date.
* @param {ISaleInvoice} invoice
* @returns {String}
*/
protected formattedInvoiceDate = (invoice): string => {
return this.formatDate(invoice.invoiceDate);
};
/**
* Retrieve formatted due date.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedDueDate = (invoice): string => {
return this.formatDate(invoice.dueDate);
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedAmount = (invoice): string => {
return formatNumber(invoice.balance, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve formatted invoice due amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedDueAmount(invoice) {
return formatNumber(invoice.dueAmount, {
currencyCode: invoice.currencyCode,
});
}
}

View File

@@ -22,20 +22,8 @@ import { ServiceError } from 'exceptions';
import CustomersService from 'services/Contacts/CustomersService';
import moment from 'moment';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
const ERRORS = {
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
CUSTOMER_NOT_FOUND: 'CUSTOMER_NOT_FOUND',
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
ITEMS_IDS_NOT_EXISTS: 'ITEMS_IDS_NOT_EXISTS',
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE',
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
};
import { ERRORS } from './constants';
import SaleEstimateTransfromer from './Estimates/SaleEstimateTransformer';
/**
* Sale estimate service.
@@ -64,6 +52,9 @@ export default class SaleEstimateService implements ISalesEstimatesService{
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
saleEstimateTransformer: SaleEstimateTransfromer;
/**
* Retrieve sale estimate or throw service error.
* @param {number} tenantId
@@ -404,13 +395,13 @@ export default class SaleEstimateService implements ISalesEstimatesService{
const { SaleEstimate } = this.tenancy.models(tenantId);
const estimate = await SaleEstimate.query()
.findById(estimateId)
.withGraphFetched('entries')
.withGraphFetched('entries.item')
.withGraphFetched('customer');
if (!estimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
}
return estimate;
return this.saleEstimateTransformer.transform(estimate);
}
/**
@@ -457,7 +448,7 @@ export default class SaleEstimateService implements ISalesEstimatesService{
.pagination(filter.page - 1, filter.pageSize);
return {
salesEstimates: results,
salesEstimates: this.saleEstimateTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};

View File

@@ -33,6 +33,7 @@ import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import JournalPosterService from './JournalPosterService';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import SaleInvoiceTransfromer from './SaleInvoiceTransformer';
import { ERRORS } from './constants';
/**
@@ -74,6 +75,9 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
saleInvoiceTransformer: SaleInvoiceTransfromer;
/**
* Validate whether sale invoice number unqiue on the storage.
*/
@@ -639,13 +643,13 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries')
.withGraphFetched('entries.item')
.withGraphFetched('customer');
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
return saleInvoice;
return this.saleInvoiceTransformer.transform(saleInvoice);
}
/**
@@ -698,7 +702,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
.pagination(filter.page - 1, filter.pageSize);
return {
salesInvoices: results,
salesInvoices: this.saleInvoiceTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};

View File

@@ -29,16 +29,8 @@ import InventoryService from 'services/Inventory/Inventory';
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import CustomersService from 'services/Contacts/CustomersService';
const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
};
import { ERRORS } from './Receipts/constants';
import SaleReceiptTransfromer from './Receipts/SaleReceiptTransformer';
@Service('SalesReceipts')
export default class SalesReceiptService implements ISalesReceiptsService {
@@ -69,6 +61,9 @@ export default class SalesReceiptService implements ISalesReceiptsService {
@Inject()
customersService: CustomersService;
@Inject()
saleReceiptTransformer: SaleReceiptTransfromer;
/**
* Validate whether sale receipt exists on the storage.
* @param {number} tenantId -
@@ -397,14 +392,14 @@ export default class SalesReceiptService implements ISalesReceiptsService {
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries')
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('depositAccount');
if (!saleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
}
return saleReceipt;
return this.saleReceiptTransformer.transform(saleReceipt);
}
/**
@@ -456,7 +451,7 @@ export default class SalesReceiptService implements ISalesReceiptsService {
.pagination(filter.page - 1, filter.pageSize);
return {
salesReceipts: results,
salesReceipts: this.saleReceiptTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};

View File

@@ -1,7 +1,9 @@
import bcrypt from 'bcryptjs';
import moment from 'moment';
import _ from 'lodash';
import path from 'path';
import accounting from 'accounting';
import pug from 'pug';
import Currencies from 'js-money/lib/currency';
import definedOptions from 'data/options';
@@ -378,7 +380,13 @@ const mergeObjectsBykey = (object1, object2, key) => {
return _.values(merged);
}
function templateRender(filePath, options) {
const basePath = path.join(__dirname, '../../resources/views');
return pug.renderFile(`${basePath}/${filePath}.pug`, options);
}
export {
templateRender,
accumSum,
increment,
hashPassword,