From 160b8b6a1b19d3101076ed06625deb12538241cd Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Tue, 17 Aug 2021 10:47:04 +0200 Subject: [PATCH] feat: invoice, estimate and receipt printing. --- docker/docker-compose.yml | 20 +- server/.env.example | 3 +- server/package.json | 30 +- server/resources/css/modules/estimate.css | 521 +++++++++++++++++ server/resources/css/modules/invoice.css | 527 ++++++++++++++++++ server/resources/css/modules/receipt.css | 521 +++++++++++++++++ server/resources/scss/base.scss | 23 + .../resources/scss/layouts/paper-layout.scss | 57 ++ server/resources/scss/modules/estimate.scss | 141 +++++ server/resources/scss/modules/invoice.scss | 147 +++++ server/resources/scss/modules/receipt.scss | 140 +++++ server/resources/scss/normalize.scss | 379 +++++++++++++ .../resources/views/PaperTemplateLayout.pug | 7 + .../views/modules/estimate-regular.pug | 67 +++ .../views/modules/invoice-regular.pug | 68 +++ .../views/modules/payment-receipt-regular.pug | 0 .../modules/purchase-invoice-regular.pug | 0 .../views/modules/receipt-regular.pug | 61 ++ server/scripts/gulpConfig.js | 117 ++++ server/scripts/gulpfile.js | 15 + server/src/api/controllers/BaseController.ts | 12 +- .../api/controllers/Sales/PaymentReceives.ts | 56 +- .../api/controllers/Sales/SalesEstimates.ts | 45 +- .../api/controllers/Sales/SalesInvoices.ts | 34 +- .../api/controllers/Sales/SalesReceipts.ts | 37 +- server/src/api/index.ts | 2 + .../api/middleware/AsyncRenderMiddleware.ts | 23 + server/src/config/index.js | 7 + server/src/lib/Transformer/Transformer.ts | 64 +++ server/src/loaders/express.ts | 5 + server/src/locales/en.json | 29 +- server/src/models/Model.js | 7 +- server/src/services/PDF/PdfService.ts | 26 + .../BillPayments/BillPaymentTransformer.ts | 35 ++ .../Purchases/BillPayments/BillPayments.ts | 8 +- server/src/services/Purchases/Bills.ts | 8 +- .../PurchaseInvoiceTransformer.ts | 66 +++ .../Estimates/SaleEstimateTransformer.ts | 78 +++ .../Sales/Estimates/SaleEstimatesPdf.ts | 43 ++ .../PaymentReceiveTransformer.ts | 36 ++ .../Sales/PaymentReceives/PaymentsReceives.ts | 8 +- .../Sales/Receipts/SaleReceiptTransformer.ts | 44 ++ .../Sales/Receipts/SaleReceiptsPdfService.ts | 41 ++ .../src/services/Sales/Receipts/constants.ts | 10 + server/src/services/Sales/SaleInvoicePdf.ts | 35 ++ .../services/Sales/SaleInvoiceTransformer.ts | 60 ++ server/src/services/Sales/SalesEstimate.ts | 25 +- server/src/services/Sales/SalesInvoices.ts | 10 +- server/src/services/Sales/SalesReceipts.ts | 21 +- server/src/utils/index.ts | 8 + 50 files changed, 3607 insertions(+), 120 deletions(-) create mode 100644 server/resources/css/modules/estimate.css create mode 100644 server/resources/css/modules/invoice.css create mode 100644 server/resources/css/modules/receipt.css create mode 100644 server/resources/scss/base.scss create mode 100644 server/resources/scss/layouts/paper-layout.scss create mode 100644 server/resources/scss/modules/estimate.scss create mode 100644 server/resources/scss/modules/invoice.scss create mode 100644 server/resources/scss/modules/receipt.scss create mode 100644 server/resources/scss/normalize.scss create mode 100644 server/resources/views/PaperTemplateLayout.pug create mode 100644 server/resources/views/modules/estimate-regular.pug create mode 100644 server/resources/views/modules/invoice-regular.pug create mode 100644 server/resources/views/modules/payment-receipt-regular.pug create mode 100644 server/resources/views/modules/purchase-invoice-regular.pug create mode 100644 server/resources/views/modules/receipt-regular.pug create mode 100644 server/scripts/gulpConfig.js create mode 100644 server/scripts/gulpfile.js create mode 100644 server/src/api/middleware/AsyncRenderMiddleware.ts create mode 100644 server/src/lib/Transformer/Transformer.ts create mode 100644 server/src/services/PDF/PdfService.ts create mode 100644 server/src/services/Purchases/BillPayments/BillPaymentTransformer.ts create mode 100644 server/src/services/Purchases/PurchaseInvoices/PurchaseInvoiceTransformer.ts create mode 100644 server/src/services/Sales/Estimates/SaleEstimateTransformer.ts create mode 100644 server/src/services/Sales/Estimates/SaleEstimatesPdf.ts create mode 100644 server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts create mode 100644 server/src/services/Sales/Receipts/SaleReceiptTransformer.ts create mode 100644 server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts create mode 100644 server/src/services/Sales/SaleInvoicePdf.ts create mode 100644 server/src/services/Sales/SaleInvoiceTransformer.ts diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6f9085c5f..3a4ada024 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -53,4 +53,22 @@ services: - nginx volumes: - ./certbot/letsencrypt/:/var/www/letsencrypt - - ./certbot/certs/:/var/certs \ No newline at end of file + - ./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" \ No newline at end of file diff --git a/server/.env.example b/server/.env.example index fa7aed49b..b248fb7e5 100644 --- a/server/.env.example +++ b/server/.env.example @@ -36,4 +36,5 @@ LICENSES_AUTH_USER=root LICENSES_AUTH_PASSWORD=root AGENDASH_AUTH_USER=agendash -AGENDASH_AUTH_PASSWORD=123123 \ No newline at end of file +AGENDASH_AUTH_PASSWORD=123123 +BROWSER_WS_ENDPOINT=ws://localhost:3000/ \ No newline at end of file diff --git a/server/package.json b/server/package.json index 3720acbb5..af54e47cd 100644 --- a/server/package.json +++ b/server/package.json @@ -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, ", @@ -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": {} } diff --git a/server/resources/css/modules/estimate.css b/server/resources/css/modules/estimate.css new file mode 100644 index 000000000..bdd942a56 --- /dev/null +++ b/server/resources/css/modules/estimate.css @@ -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; +} \ No newline at end of file diff --git a/server/resources/css/modules/invoice.css b/server/resources/css/modules/invoice.css new file mode 100644 index 000000000..2e80bddf9 --- /dev/null +++ b/server/resources/css/modules/invoice.css @@ -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; +} \ No newline at end of file diff --git a/server/resources/css/modules/receipt.css b/server/resources/css/modules/receipt.css new file mode 100644 index 000000000..01f4d8834 --- /dev/null +++ b/server/resources/css/modules/receipt.css @@ -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; +} \ No newline at end of file diff --git a/server/resources/scss/base.scss b/server/resources/scss/base.scss new file mode 100644 index 000000000..88a246a4f --- /dev/null +++ b/server/resources/scss/base.scss @@ -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; +} diff --git a/server/resources/scss/layouts/paper-layout.scss b/server/resources/scss/layouts/paper-layout.scss new file mode 100644 index 000000000..57412dbd4 --- /dev/null +++ b/server/resources/scss/layouts/paper-layout.scss @@ -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; + } +} \ No newline at end of file diff --git a/server/resources/scss/modules/estimate.scss b/server/resources/scss/modules/estimate.scss new file mode 100644 index 000000000..2ea51ca13 --- /dev/null +++ b/server/resources/scss/modules/estimate.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/server/resources/scss/modules/invoice.scss b/server/resources/scss/modules/invoice.scss new file mode 100644 index 000000000..33662c6bc --- /dev/null +++ b/server/resources/scss/modules/invoice.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/server/resources/scss/modules/receipt.scss b/server/resources/scss/modules/receipt.scss new file mode 100644 index 000000000..2ce41fefe --- /dev/null +++ b/server/resources/scss/modules/receipt.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/server/resources/scss/normalize.scss b/server/resources/scss/normalize.scss new file mode 100644 index 000000000..5096fd90f --- /dev/null +++ b/server/resources/scss/normalize.scss @@ -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; +} \ No newline at end of file diff --git a/server/resources/views/PaperTemplateLayout.pug b/server/resources/views/PaperTemplateLayout.pug new file mode 100644 index 000000000..c51fdeb31 --- /dev/null +++ b/server/resources/views/PaperTemplateLayout.pug @@ -0,0 +1,7 @@ +html + head + title My Site - #{title} + block head + body + div.paper-template + block content \ No newline at end of file diff --git a/server/resources/views/modules/estimate-regular.pug b/server/resources/views/modules/estimate-regular.pug new file mode 100644 index 000000000..99c0bcf65 --- /dev/null +++ b/server/resources/views/modules/estimate-regular.pug @@ -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} \ No newline at end of file diff --git a/server/resources/views/modules/invoice-regular.pug b/server/resources/views/modules/invoice-regular.pug new file mode 100644 index 000000000..892fec84c --- /dev/null +++ b/server/resources/views/modules/invoice-regular.pug @@ -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} \ No newline at end of file diff --git a/server/resources/views/modules/payment-receipt-regular.pug b/server/resources/views/modules/payment-receipt-regular.pug new file mode 100644 index 000000000..e69de29bb diff --git a/server/resources/views/modules/purchase-invoice-regular.pug b/server/resources/views/modules/purchase-invoice-regular.pug new file mode 100644 index 000000000..e69de29bb diff --git a/server/resources/views/modules/receipt-regular.pug b/server/resources/views/modules/receipt-regular.pug new file mode 100644 index 000000000..30710b85b --- /dev/null +++ b/server/resources/views/modules/receipt-regular.pug @@ -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} \ No newline at end of file diff --git a/server/scripts/gulpConfig.js b/server/scripts/gulpConfig.js new file mode 100644 index 000000000..7a78a8c2a --- /dev/null +++ b/server/scripts/gulpConfig.js @@ -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/**/*'], + }, +}; diff --git a/server/scripts/gulpfile.js b/server/scripts/gulpfile.js new file mode 100644 index 000000000..40181c512 --- /dev/null +++ b/server/scripts/gulpfile.js @@ -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); +}); \ No newline at end of file diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts index ae65bffa3..d67d8c15a 100644 --- a/server/src/api/controllers/BaseController.ts +++ b/server/src/api/controllers/BaseController.ts @@ -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); + } } diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index fdb01bec5..ad7282e6e 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -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 + ), }, }, ], diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index 995d7ab6c..4de25df98 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -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); diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 18d779fcd..aa63f75ee 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -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), }); diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts index 851376d38..fabaeebdb 100644 --- a/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/server/src/api/controllers/Sales/SalesReceipts.ts @@ -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); } diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 2eeacd160..ce6540463 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -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); diff --git a/server/src/api/middleware/AsyncRenderMiddleware.ts b/server/src/api/middleware/AsyncRenderMiddleware.ts new file mode 100644 index 000000000..5bd6e736a --- /dev/null +++ b/server/src/api/middleware/AsyncRenderMiddleware.ts @@ -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; diff --git a/server/src/config/index.js b/server/src/config/index.js index 685efab74..c9e89ca64 100644 --- a/server/src/config/index.js +++ b/server/src/config/index.js @@ -162,6 +162,13 @@ export default { } }, + /** + * Puppeteer remote browserless connection. + */ + puppeteer: { + browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT, + }, + protocol: '', hostname: '', scheduleComputeItemCost: 'in 5 seconds' diff --git a/server/src/lib/Transformer/Transformer.ts b/server/src/lib/Transformer/Transformer.ts new file mode 100644 index 000000000..891c1f80a --- /dev/null +++ b/server/src/lib/Transformer/Transformer.ts @@ -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') : ''; + } +} diff --git a/server/src/loaders/express.ts b/server/src/loaders/express.ts index 528502ed0..29f354d86 100644 --- a/server/src/loaders/express.ts +++ b/server/src/loaders/express.ts @@ -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()); diff --git a/server/src/locales/en.json b/server/src/locales/en.json index 8e506540f..7d607960f 100644 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -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" } \ No newline at end of file diff --git a/server/src/models/Model.js b/server/src/models/Model.js index 8eaae93cf..084b4dde4 100644 --- a/server/src/models/Model.js +++ b/server/src/models/Model.js @@ -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; + } } diff --git a/server/src/services/PDF/PdfService.ts b/server/src/services/PDF/PdfService.ts new file mode 100644 index 000000000..04a0a316f --- /dev/null +++ b/server/src/services/PDF/PdfService.ts @@ -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; + } +} diff --git a/server/src/services/Purchases/BillPayments/BillPaymentTransformer.ts b/server/src/services/Purchases/BillPayments/BillPaymentTransformer.ts new file mode 100644 index 000000000..86964eee4 --- /dev/null +++ b/server/src/services/Purchases/BillPayments/BillPaymentTransformer.ts @@ -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, + }); + }; +} diff --git a/server/src/services/Purchases/BillPayments/BillPayments.ts b/server/src/services/Purchases/BillPayments/BillPayments.ts index 2ada489ac..ffb1fad06 100644 --- a/server/src/services/Purchases/BillPayments/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments/BillPayments.ts @@ -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(), }; diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index ed95c3c98..ad34828bd 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -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); } /** diff --git a/server/src/services/Purchases/PurchaseInvoices/PurchaseInvoiceTransformer.ts b/server/src/services/Purchases/PurchaseInvoices/PurchaseInvoiceTransformer.ts new file mode 100644 index 000000000..168f92b0d --- /dev/null +++ b/server/src/services/Purchases/PurchaseInvoices/PurchaseInvoiceTransformer.ts @@ -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 }); + }; +} diff --git a/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts b/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts new file mode 100644 index 000000000..6d91a90fa --- /dev/null +++ b/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts @@ -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, + }); + }; +} diff --git a/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts new file mode 100644 index 000000000..b31da0ad2 --- /dev/null +++ b/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts @@ -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; + } +} diff --git a/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts b/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts new file mode 100644 index 000000000..26727f97c --- /dev/null +++ b/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts @@ -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 }); + }; +} diff --git a/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts b/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts index 167bee544..68a3e4db1 100644 --- a/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts @@ -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(), }; diff --git a/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts b/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts new file mode 100644 index 000000000..fc9ff213a --- /dev/null +++ b/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts @@ -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, + }); + }; +} diff --git a/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts new file mode 100644 index 000000000..06bd52a19 --- /dev/null +++ b/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -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; + } +} diff --git a/server/src/services/Sales/Receipts/constants.ts b/server/src/services/Sales/Receipts/constants.ts index 9707bb1ec..bf0cdef18 100644 --- a/server/src/services/Sales/Receipts/constants.ts +++ b/server/src/services/Sales/Receipts/constants.ts @@ -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 = [ { diff --git a/server/src/services/Sales/SaleInvoicePdf.ts b/server/src/services/Sales/SaleInvoicePdf.ts new file mode 100644 index 000000000..6a966df63 --- /dev/null +++ b/server/src/services/Sales/SaleInvoicePdf.ts @@ -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; + } +} diff --git a/server/src/services/Sales/SaleInvoiceTransformer.ts b/server/src/services/Sales/SaleInvoiceTransformer.ts new file mode 100644 index 000000000..a2ddab709 --- /dev/null +++ b/server/src/services/Sales/SaleInvoiceTransformer.ts @@ -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, + }); + } +} diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index ca55b78e1..3303a5395 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -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(), }; diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index cf6e2b8b6..5d546c79c 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -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(), }; diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index 3c651b841..07f29060b 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -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(), }; diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts index a18bc93e2..aa2553fa3 100644 --- a/server/src/utils/index.ts +++ b/server/src/utils/index.ts @@ -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,