mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
Compare commits
62 Commits
v0.19.15
...
upload-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37fd4a1fdb | ||
|
|
d16c57b63b | ||
|
|
5e7cff0eb7 | ||
|
|
34e781b4a2 | ||
|
|
5f40d50852 | ||
|
|
bb0d91a9cb | ||
|
|
2c790427fa | ||
|
|
4f59b27d70 | ||
|
|
94c08f0b9e | ||
|
|
ef4beaa564 | ||
|
|
8566422ce3 | ||
|
|
70551bee30 | ||
|
|
d690c6a3fe | ||
|
|
28319c2cdc | ||
|
|
df0f73f338 | ||
|
|
411ac55986 | ||
|
|
12226d469a | ||
|
|
632c4629de | ||
|
|
a7df23cebc | ||
|
|
ef74e250f1 | ||
|
|
c0769662bd | ||
|
|
5b6270a184 | ||
|
|
4541d28b68 | ||
|
|
716dec799a | ||
|
|
77a1e35ff4 | ||
|
|
317adfa0de | ||
|
|
f0dfc3d1b0 | ||
|
|
67904f52af | ||
|
|
f644ed6708 | ||
|
|
dc18bde6be | ||
|
|
132c1dfdbe | ||
|
|
9247745ab0 | ||
|
|
c5c0342c7b | ||
|
|
f5e9485a12 | ||
|
|
e6bad27771 | ||
|
|
6d24474162 | ||
|
|
5962b990c4 | ||
|
|
9f21f649f0 | ||
|
|
d3d2112b8a | ||
|
|
3795322a65 | ||
|
|
fe5cd5a8ea | ||
|
|
c032a5db16 | ||
|
|
3fcb6fefde | ||
|
|
16f5cb713d | ||
|
|
f7a7925028 | ||
|
|
66fb0c9fa3 | ||
|
|
85acc85f17 | ||
|
|
c76ce09191 | ||
|
|
e3532098b2 | ||
|
|
b6783eb22e | ||
|
|
4ef00ab122 | ||
|
|
3dd827417a | ||
|
|
cbacd02aa2 | ||
|
|
3b7e0fb78a | ||
|
|
9add716395 | ||
|
|
083ea28a1f | ||
|
|
1b51742c36 | ||
|
|
0c6f23e770 | ||
|
|
37a8ca4e97 | ||
|
|
795303c3a8 | ||
|
|
63ba3f0898 | ||
|
|
62594efa00 |
81
CHANGELOG.md
81
CHANGELOG.md
@@ -2,7 +2,77 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
## [0.19.4] - 18-08-2024
|
||||
# [0.19.17]
|
||||
|
||||
* fix: Un-categorize bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/663
|
||||
|
||||
# [0.19.16]
|
||||
|
||||
* feat: Tracking more Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/653
|
||||
* fix: Expense cannot accept credit card as payment account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/654
|
||||
* fix: Suspense the lazy loaded components in banking pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/657
|
||||
* feat: Add help dropdown menu by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/656
|
||||
* feat: Bank pages layout breaking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/658
|
||||
* feat: Datatable UI improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/655
|
||||
* fix: Array cast of recognize function rule ids by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/660
|
||||
* fix: Payment made filling the form full amount field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/661
|
||||
* feat: Tabular number of all money columns by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/659
|
||||
* refactor: The expense G/L writer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/662
|
||||
|
||||
## [0.19.15] -
|
||||
|
||||
* fix: Bank transactions infinity scrolling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/648
|
||||
* feat: Integrate multiple branches and warehouses to resource importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/645
|
||||
* fix: Integrate multiple branches with expense resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/649
|
||||
* feat: Cover more tracking events. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/650
|
||||
* feat: Track banking service events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/651
|
||||
|
||||
## [0.19.14]
|
||||
|
||||
* fix: Import bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/643
|
||||
* fix: Set default index to transaction entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/644
|
||||
* feat(server): Events tracking using Posthog by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/646
|
||||
|
||||
## [0.19.13]
|
||||
|
||||
* fix: Subscription middleware by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/624
|
||||
* fix: Getting the sheet columns in import sheet by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/641
|
||||
|
||||
## [0.19.12]
|
||||
|
||||
* fix: Typo one-click demo page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/640
|
||||
|
||||
## [0.19.11]
|
||||
|
||||
* fix: Avoid running the cost job in import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/635
|
||||
* fix: Debounce scheduling calculating items cost by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/634
|
||||
* fix: Expand the resources export page size limitation by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/636
|
||||
* feat: Optimize loading perf. by splitting big chunks and lazy loading them by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/632
|
||||
* fix: Use standard ISO 8601 format for exported data by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/638
|
||||
* fix: Add customer type to customers resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/639
|
||||
|
||||
## [0.19.10]
|
||||
|
||||
* fix: Add subscription plans offer text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/629
|
||||
|
||||
## [0.19.9]
|
||||
|
||||
* fix: Make webapp package env variables dynamic by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/628
|
||||
|
||||
## [v0.19.8]
|
||||
|
||||
* fix: Cannot import items income and cost accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/617
|
||||
* fix: Some bank account details hidden by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/618
|
||||
* feat(ee): One-click demo account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/616
|
||||
* feat: change banking service language by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/619
|
||||
* feat(banking): Filter uncategorized bank transactions by date by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/590
|
||||
* Fix: Syntax error caused error by @wolone in https://github.com/bigcapitalhq/bigcapital/pull/622
|
||||
* fix: Listen to payment webhooks by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/623
|
||||
* fix: Add prefix J-00001 to manual journals increments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/625
|
||||
* fix: Disable sms service until Twilo integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/626
|
||||
* fix: Style tweaks in onboarding page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/627
|
||||
|
||||
## [0.19.5] - 18-08-2024
|
||||
|
||||
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
|
||||
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
|
||||
@@ -23,6 +93,15 @@ All notable changes to Bigcapital server-side will be in this file.
|
||||
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
|
||||
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
|
||||
|
||||
## [v0.19.4]
|
||||
|
||||
* feat: Import and export tax rates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/591
|
||||
* feat: Un-categorize bank transactions in bulk by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/587
|
||||
* feat: Pending bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/589
|
||||
* fix: Update `dev` Script in `package.json` to Use `cross-env` by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/588
|
||||
* fix: Should not load branches on reconcile matching form if the branches not enabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/592
|
||||
* fix: Rounding the total amount the pending and matched transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/593
|
||||
|
||||
## [v0.18.0] - 10-08-2024
|
||||
|
||||
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
||||
|
||||
40
packages/server/resources/scss/base.css
Normal file
40
packages/server/resources/scss/base.css
Normal file
@@ -0,0 +1,40 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
body{
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6{
|
||||
font-family: "Noto Sans", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1,35 +1 @@
|
||||
@import "./normalize.scss";
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit; // 2
|
||||
text-align: -webkit-match-parent; // 3
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
body{
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
direction: ltr;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
379
packages/server/resources/scss/normalize.css
vendored
Normal file
379
packages/server/resources/scss/normalize.css
vendored
Normal file
@@ -0,0 +1,379 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
/* 1 */
|
||||
height: 0;
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
/* 1 */
|
||||
text-decoration: underline;
|
||||
/* 2 */
|
||||
text-decoration: underline dotted;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
line-height: 1.15;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
display: table;
|
||||
/* 1 */
|
||||
max-width: 100%;
|
||||
/* 1 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
white-space: normal;
|
||||
/* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
padding: 0;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
html(lang=locale)
|
||||
head
|
||||
title My Site - #{title}
|
||||
style
|
||||
include ../scss/normalize.css
|
||||
include ../scss/base.css
|
||||
block head
|
||||
body
|
||||
div.paper-template
|
||||
|
||||
@@ -1,81 +1,198 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/credit-rtl.css
|
||||
else
|
||||
include ../../css/modules/credit.css
|
||||
- var prefix = 'bc'
|
||||
style.
|
||||
.#{prefix}-root {
|
||||
color: #111;
|
||||
padding: 24px 30px;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.#{prefix}-terms-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-terms-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
.#{prefix}-terms-item__label {
|
||||
min-width: 120px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-terms-item__value {
|
||||
/* Styles for the term value */
|
||||
}
|
||||
.#{prefix}-address-section{
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-address-section > * {
|
||||
flex: 1 1;
|
||||
}
|
||||
.#{prefix}-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
}
|
||||
.#{prefix}-table__header {
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 2px 10px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-table__header:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__header:last-of-type{
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.#{prefix}-table__cell:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__cell:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-totals__item {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.#{prefix}-totals__item--border-gray {
|
||||
border-bottom: 1px solid #DADADA;
|
||||
}
|
||||
.#{prefix}-totals__item--border-dark {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.#{prefix}-totals__item--font-weight-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.#{prefix}-totals__item-label {
|
||||
min-width: 160px;
|
||||
}
|
||||
.#{prefix}-totals__item-amount {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-statement {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.#{prefix}-statement__label {
|
||||
color: #666;
|
||||
}
|
||||
.#{prefix}-statement__value {
|
||||
/* Styles for statement value */
|
||||
}
|
||||
|
||||
block content
|
||||
div.credit
|
||||
div.credit__header
|
||||
div.paper
|
||||
h1.title #{__('credit.paper.credit_note')}
|
||||
if creditNote.creditNoteNumber
|
||||
span.creditNoteNumber #{creditNote.creditNoteNumber}
|
||||
div(class=`${prefix}-root`)
|
||||
div(class=`${prefix}-big-title`) Credit Note
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
if showCompanyLogo
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogo alt=`Company Logo`)
|
||||
|
||||
div(class=`${prefix}-terms-list`)
|
||||
if showCreditNoteNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}:
|
||||
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
|
||||
|
||||
div.credit__full-amount
|
||||
div.label #{__('credit.paper.amount')}
|
||||
div.amount #{creditNote.formattedAmount}
|
||||
if showCreditNoteDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}:
|
||||
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
|
||||
|
||||
div.credit__meta
|
||||
div.credit__meta-item.credit__meta-item--amount
|
||||
span.label #{__('credit.paper.remaining')}
|
||||
span.value #{creditNote.formattedCreditsRemaining}
|
||||
div(class=`${prefix}-address-section`)
|
||||
if showBilledFromAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong #{companyName}
|
||||
each address in billedFromAddress
|
||||
div #{address}
|
||||
if showBilledToAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong #{billedToLabel}
|
||||
each address in billedToAddress
|
||||
div #{address}
|
||||
|
||||
div.credit__meta-item.credit__meta-item--billed-to
|
||||
span.label #{__("credit.paper.billed_to")}
|
||||
span.value #{creditNote.customer.displayName}
|
||||
|
||||
div.credit__meta-item.credit__meta-item--credit-date
|
||||
span.label #{__("credit.paper.credit_date")}
|
||||
span.value #{creditNote.formattedCreditNoteDate}
|
||||
|
||||
div.credit__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")}
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) #{'Item'}
|
||||
th(class=`${prefix}-table__header`) #{'Description'}
|
||||
th(class=`${prefix}-table__header`) #{'Rate'}
|
||||
th(class=`${prefix}-table__header`) #{'Total'}
|
||||
tbody
|
||||
each entry in creditNote.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
|
||||
each line in lines
|
||||
tr(class=`${prefix}-table__row`)
|
||||
td(class=`${prefix}-table__cell`) #{line.item}
|
||||
td(class=`${prefix}-table__cell`) #{line.description}
|
||||
td(class=`${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell--right`) #{line.total}
|
||||
|
||||
div.credit__table-after
|
||||
div.credit__table-total
|
||||
table
|
||||
tbody
|
||||
tr.total
|
||||
td #{__('credit.paper.total')}
|
||||
td #{creditNote.formattedAmount}
|
||||
tr.payment-amount
|
||||
td #{__('credit.paper.credits_used')}
|
||||
td #{creditNote.formattedCreditsUsed}
|
||||
tr.blanace-due
|
||||
td #{__('credit.paper.credits_remaining')}
|
||||
td #{creditNote.formattedCreditsRemaining}
|
||||
div(class=`${prefix}-totals`)
|
||||
if showSubtotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
|
||||
div(class=`${prefix}-totals__item-label`) #{subtotallabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||
|
||||
div.credit__footer
|
||||
if creditNote.termsConditions
|
||||
div.credit__conditions
|
||||
h3 #{__("credit.paper.terms_conditions")}
|
||||
p #{creditNote.termsConditions}
|
||||
if showTotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark`)
|
||||
div(class=`${prefix}-totals__item-amount`) #{totalLabel}:
|
||||
div(class=`${prefix}-totals__item-label`) #{total}
|
||||
|
||||
if creditNote.note
|
||||
div.credit__notes
|
||||
h3 #{__("credit.paper.notes")}
|
||||
p #{creditNote.note}
|
||||
if showCustomerNote
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{customerNoteLabel}:
|
||||
div(class=`${prefix}-statement__value`) #{customerNote}
|
||||
|
||||
if showTermsConditions
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}:
|
||||
div(class=`${prefix}-statement__value`) #{termsConditions}
|
||||
|
||||
@@ -1,82 +1,207 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/estimate-rtl.css
|
||||
else
|
||||
include ../../css/modules/estimate.css
|
||||
block head
|
||||
- var prefix = 'bc'
|
||||
style.
|
||||
.#{prefix}-root {
|
||||
color: #111;
|
||||
padding: 24px 30px;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.#{prefix}-terms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-terms-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
.#{prefix}-terms-item__label {
|
||||
min-width: 120px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-terms-item__value {
|
||||
}
|
||||
.#{prefix}-addresses{
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-addresses > * {
|
||||
flex: 1 1;
|
||||
}
|
||||
.#{prefix}-address {
|
||||
}
|
||||
.#{prefix}-address__item {
|
||||
}
|
||||
.#{prefix}-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
}
|
||||
.#{prefix}-table__header {
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 2px 10px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-table__header:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__header:last-of-type{
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.#{prefix}-table__cell--right{
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__cell:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-totals__item {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.#{prefix}-totals__item--border-gray {
|
||||
border-bottom: 1px solid #DADADA;
|
||||
}
|
||||
.#{prefix}-totals__item--border-dark {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.#{prefix}-totals__item--font-weight-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.#{prefix}-totals__item-label {
|
||||
min-width: 160px;
|
||||
}
|
||||
.#{prefix}-totals__item-amount {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-statement {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.#{prefix}-statement__label {
|
||||
color: #666;
|
||||
}
|
||||
.#{prefix}-statement__value {
|
||||
}
|
||||
|
||||
block content
|
||||
div.estimate
|
||||
div.estimate__header
|
||||
div.paper
|
||||
h1.title #{__("estimate.paper.estimate")}
|
||||
span.email #{saleEstimate.estimateNumber}
|
||||
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||
h1(class=`${prefix}-big-title`) Estimate
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
if showCompanyLogo
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(alt="", src=companyLogo)
|
||||
|
||||
div.estimate__estimate-amount
|
||||
div.label #{__('estimate.paper.estimate_amount')}
|
||||
div.amount #{saleEstimate.formattedAmount}
|
||||
//- Terms List
|
||||
div(class=`${prefix}-terms`)
|
||||
if showEstimateNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
|
||||
if showEstimateDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{estimateDateLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{estimateDate}
|
||||
if showExpirationDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{expirationDateLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{expirationDate}
|
||||
|
||||
div.estimate__meta
|
||||
if saleEstimate.estimateNumber
|
||||
div.estimate__meta-item.estimate__meta-item--estimate-number
|
||||
span.label #{__("estimate.paper.estimate_number")}
|
||||
span.value #{saleEstimate.estimateNumber}
|
||||
//- Addresses (Group section)
|
||||
div(class=`${prefix}-addresses`)
|
||||
if showBilledFromAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong #{companyName}
|
||||
each item in billedFromAddress
|
||||
div(class=`${prefix}-address__item`) #{item}
|
||||
|
||||
div.estimate__meta-item.estimate__meta-item--billed-to
|
||||
span.label #{__("estimate.paper.billed_to")}
|
||||
span.value #{saleEstimate.customer.displayName}
|
||||
if showBilledToAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong #{billedToLabel}
|
||||
each item in billedToAddress
|
||||
div(class=`${prefix}-address__item`) #{item}
|
||||
|
||||
div.estimate__meta-item.estimate__meta-item--estimate-date
|
||||
span.label #{__("estimate.paper.estimate_date")}
|
||||
span.value #{saleEstimate.formattedEstimateDate}
|
||||
|
||||
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")}
|
||||
//- Table section (Line items)
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) Item
|
||||
th(class=`${prefix}-table__header`) Description
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) 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
|
||||
each line in lines
|
||||
tr
|
||||
td(class=`${prefix}-table__cell`) #{line.item}
|
||||
td(class=`${prefix}-table__cell`) #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||
|
||||
div.estimate__table-after
|
||||
div.estimate__table-total
|
||||
table
|
||||
tbody
|
||||
tr.subtotal
|
||||
td #{__('estimate.paper.subtotal')}
|
||||
td #{saleEstimate.formattedAmount}
|
||||
tr.total
|
||||
td #{__('estimate.paper.total')}
|
||||
td #{saleEstimate.formattedAmount}
|
||||
//- Totals section
|
||||
div(class=`${prefix}-totals`)
|
||||
if showSubtotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
|
||||
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||
if showTotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||
div(class=`${prefix}-totals__item-label`) #{totalLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||
|
||||
div.estimate__footer
|
||||
if saleEstimate.termsConditions
|
||||
div.estimate__conditions
|
||||
h3 #{__("estimate.paper.conditions_title")}
|
||||
p #{saleEstimate.termsConditions}
|
||||
//- Statements section
|
||||
if showCustomerNote && customerNote
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{customerNoteLabel}
|
||||
div(class=`${prefix}-statement__value`) #{customerNote}
|
||||
|
||||
if saleEstimate.note
|
||||
div.estimate__notes
|
||||
h3 #{__("estimate.paper.notes_title")}
|
||||
p #{saleEstimate.note}
|
||||
if showTermsConditions && termsConditions
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
|
||||
div(class=`${prefix}-statement__value`) #{termsConditions}
|
||||
@@ -1,92 +0,0 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/invoice-rtl.css
|
||||
else
|
||||
include ../../css/modules/invoice.css
|
||||
|
||||
block content
|
||||
div.invoice
|
||||
div.invoice__header
|
||||
div.paper
|
||||
h1.title #{__("invoice.paper.invoice")}
|
||||
if saleInvoice.invoiceNo
|
||||
span.invoiceNo #{saleInvoice.invoiceNo}
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
|
||||
div.invoice__due-amount
|
||||
div.label #{__('invoice.paper.invoice_amount')}
|
||||
div.amount #{saleInvoice.totalFormatted}
|
||||
|
||||
div.invoice__meta
|
||||
div.invoice__meta-item.invoice__meta-item--amount
|
||||
span.label #{__('invoice.paper.due_amount')}
|
||||
span.value #{saleInvoice.dueAmountFormatted}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--billed-to
|
||||
span.label #{__("invoice.paper.billed_to")}
|
||||
span.value #{saleInvoice.customer.displayName}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--invoice-date
|
||||
span.label #{__("invoice.paper.invoice_date")}
|
||||
span.value #{saleInvoice.invoiceDateFormatted}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--due-date
|
||||
span.label #{__("invoice.paper.due_date")}
|
||||
span.value #{saleInvoice.dueDateFormatted}
|
||||
|
||||
div.invoice__table
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.item #{__("item_entry.paper.item_name")}
|
||||
th.rate #{__("item_entry.paper.rate")}
|
||||
th.quantity #{__("item_entry.paper.quantity")}
|
||||
th.total #{__("item_entry.paper.total")}
|
||||
tbody
|
||||
each entry in saleInvoice.entries
|
||||
tr
|
||||
td.item
|
||||
div.title=entry.item.name
|
||||
span.description=entry.description
|
||||
td.rate=entry.rate
|
||||
td.quantity=entry.quantity
|
||||
td.total=entry.amount
|
||||
|
||||
div.invoice__table-after
|
||||
div.invoice__table-total
|
||||
table
|
||||
tbody
|
||||
tr.subtotal
|
||||
td #{__('invoice.paper.subtotal')}
|
||||
td #{saleInvoice.subtotalFormatted}
|
||||
each tax in saleInvoice.taxes
|
||||
tr.tax_line
|
||||
td #{tax.name} [#{tax.taxRate}%]
|
||||
td #{tax.taxRateAmountFormatted}
|
||||
tr.total
|
||||
td #{__('invoice.paper.total')}
|
||||
td #{saleInvoice.totalFormatted}
|
||||
tr.payment-amount
|
||||
td #{__('invoice.paper.payment_amount')}
|
||||
td #{saleInvoice.paymentAmountFormatted}
|
||||
tr.blanace-due
|
||||
td #{__('invoice.paper.balance_due')}
|
||||
td #{saleInvoice.dueAmountFormatted}
|
||||
|
||||
div.invoice__footer
|
||||
if saleInvoice.termsConditions
|
||||
div.invoice__conditions
|
||||
h3 #{__("invoice.paper.conditions_title")}
|
||||
p #{saleInvoice.termsConditions}
|
||||
|
||||
if saleInvoice.invoiceMessage
|
||||
div.invoice__notes
|
||||
h3 #{__("invoice.paper.notes_title")}
|
||||
p #{saleInvoice.invoiceMessage}
|
||||
242
packages/server/resources/views/modules/invoice-standard.pug
Normal file
242
packages/server/resources/views/modules/invoice-standard.pug
Normal file
@@ -0,0 +1,242 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
- var prefix = 'bc'
|
||||
style.
|
||||
.#{prefix}-root {
|
||||
color: #111;
|
||||
padding: 24px 30px;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.#{prefix}-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-detail {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
.#{prefix}-detail__label {
|
||||
min-width: 120px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-detail__value {
|
||||
/* Styles for detail values */
|
||||
}
|
||||
.#{prefix}-address-root {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-address-from {
|
||||
flex: 1;
|
||||
}
|
||||
.#{prefix}-address-from__item {
|
||||
/* Styles for items in the billed-from address */
|
||||
}
|
||||
.#{prefix}-address-to {
|
||||
flex: 1;
|
||||
}
|
||||
.#{prefix}-address-to__item {
|
||||
/* Styles for items in the billed-to address */
|
||||
}
|
||||
.#{prefix}-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
}
|
||||
.#{prefix}-table__header {
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 2px 10px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-table__header:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__header:last-of-type{
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.#{prefix}-table__cell:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__cell:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-totals__item {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.#{prefix}-totals__item--border-gray {
|
||||
border-bottom: 1px solid #DADADA;
|
||||
}
|
||||
.#{prefix}-totals__item--border-dark {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.#{prefix}-totals__item--font-weight-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.#{prefix}-totals__item-label {
|
||||
min-width: 160px;
|
||||
}
|
||||
.#{prefix}-totals__item-amount {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-paragraph {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.#{prefix}-paragraph__label {
|
||||
color: #666;
|
||||
}
|
||||
.#{prefix}-paragraph__value {
|
||||
/* Styles for values within the paragraph section */
|
||||
}
|
||||
block content
|
||||
//- block head
|
||||
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||
|
||||
//- Title and company logo
|
||||
h1(class=`${prefix}-big-title`) Invoice
|
||||
|
||||
if showCompanyLogo
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(alt="", src=companyLogo)
|
||||
|
||||
//- Invoice details
|
||||
div(class=`${prefix}-details`)
|
||||
if showInvoiceNumber
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
|
||||
div(class=`${prefix}-detail__value`) #{invoiceNumber}
|
||||
|
||||
if showDateIssue
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
|
||||
div(class=`${prefix}-detail__value`) #{dateIssue}
|
||||
|
||||
if showDueDate
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{dueDateLabel}
|
||||
div(class=`${prefix}-detail__value`) #{dueDate}
|
||||
|
||||
//- Address section
|
||||
div(class=`${prefix}-address-root`)
|
||||
if showBilledFromAddress
|
||||
div(class=`${prefix}-address-from`)
|
||||
strong #{companyName}
|
||||
each item in billedFromAddres
|
||||
div(class=`${prefix}-address-from__item`) #{item}
|
||||
|
||||
if showBillingToAddress
|
||||
div(class=`${prefix}-address-to`)
|
||||
strong #{billedToLabel}
|
||||
each item in billedToAddress
|
||||
div(class=`${prefix}-address-to__item`) #{item}
|
||||
|
||||
//- Invoice table
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) #{lineItemLabel}
|
||||
th(class=`${prefix}-table__header`) #{lineDescriptionLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel}
|
||||
tbody
|
||||
each line in lines
|
||||
tr
|
||||
td(class=`${prefix}-table__cell`) #{line.item}
|
||||
td(class=`${prefix}-table__cell`) #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||
|
||||
//- Totals section
|
||||
div(class=`${prefix}-totals`)
|
||||
if showSubtotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
|
||||
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||
|
||||
if showDiscount
|
||||
div(class=`${prefix}-totals__item`)
|
||||
div(class=`${prefix}-totals__item-label`) #{discountLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{discount}
|
||||
|
||||
if showTaxes
|
||||
each tax in taxes
|
||||
div(class=`${prefix}-totals__item`)
|
||||
div(class=`${prefix}-totals__item-label`) #{tax.label}
|
||||
div(class=`${prefix}-totals__item-amount`) #{tax.amount}
|
||||
|
||||
if showTotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||
div(class=`${prefix}-totals__item-label`) #{totalLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||
|
||||
if showPaymentMade
|
||||
div(class=`${prefix}-totals__item`)
|
||||
div(class=`${prefix}-totals__item-label`) #{paymentMadeLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{paymentMade}
|
||||
|
||||
if showBalanceDue
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||
div(class=`${prefix}-totals__item-label`) #{balanceDueLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{balanceDue}
|
||||
|
||||
//- Footer section
|
||||
if showTermsConditions && termsConditions
|
||||
div(class=`${prefix}-paragraph`)
|
||||
if termsConditionsLabel
|
||||
div(class=`${prefix}-paragraph__label`) #{termsConditionsLabel}
|
||||
div(class=`${prefix}-paragraph__value`) #{termsConditions}
|
||||
|
||||
if showStatement && statement
|
||||
div(class=`${prefix}-paragraph`)
|
||||
if statementLabel
|
||||
div(class=`${prefix}-paragraph__label`) #{statementLabel}
|
||||
div(class=`${prefix}-paragraph__value`) #{statement}
|
||||
@@ -1,67 +1,178 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/payment-rtl.css
|
||||
else
|
||||
include ../../css/modules/payment.css
|
||||
- var prefix = 'bp3';
|
||||
|
||||
style.
|
||||
.#{prefix}-root{
|
||||
color: #111;
|
||||
padding: 24px 30px;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||
}
|
||||
.#{prefix}-big-title{
|
||||
font-size: 60px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap{
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.#{prefix}-terms-list{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-terms-item{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
.#{prefix}-terms-item__label{
|
||||
min-width: 120px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-addresses{
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-addresses > * {
|
||||
flex: 1 1;
|
||||
}
|
||||
.#{prefix}-address__label{
|
||||
|
||||
}
|
||||
.#{prefix}-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
}
|
||||
.#{prefix}-table__header {
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 2px 10px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-table__header:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__header:last-of-type{
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.#{prefix}-table__cell:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__cell:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-totals__item {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.#{prefix}-totals__item--gray-border {
|
||||
border-bottom: 1px solid #DADADA;
|
||||
}
|
||||
.#{prefix}-totals__item--dark-border {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.#{prefix}-totals__item--bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.#{prefix}-totals__item-label {
|
||||
min-width: 160px;
|
||||
}
|
||||
.#{prefix}-totals__item-amount {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
block content
|
||||
div.payment
|
||||
div.payment__header
|
||||
div.paper
|
||||
h1.title #{__("payment.paper.payment_receipt")}
|
||||
if paymentReceive.paymentReceiveNo
|
||||
span.paymentNumber #{paymentReceive.paymentReceiveNo}
|
||||
div(class=`${prefix}-root`)
|
||||
div(class=`${prefix}-big-title`) Payment
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
if showCompanyLogo
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogo alt="Company Logo")
|
||||
|
||||
div(class=`${prefix}-terms-list`)
|
||||
if showPaymentReceivedNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
|
||||
|
||||
div.payment__received-amount
|
||||
div.label #{__('payment.paper.amount_received')}
|
||||
div.amount #{paymentReceive.formattedAmount}
|
||||
if showPaymentReceivedDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
|
||||
|
||||
div(class=`${prefix}-addresses`)
|
||||
if showBilledFromAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong(class=`${prefix}-address__item`) #{companyName}
|
||||
each addressLine in billedFromAddress
|
||||
div(class=`${prefix}-address__item`) #{addressLine}
|
||||
|
||||
div.payment__meta
|
||||
div.payment__meta-item.payment__meta-item--billed-to
|
||||
span.label #{__("payment.paper.billed_to")}
|
||||
span.value #{paymentReceive.customer.displayName}
|
||||
if showBillingToAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong(class=`${prefix}-address__item`) #{billedToLabel}
|
||||
each addressLine in billedToAddress
|
||||
div(class=`${prefix}-address__item`) #{addressLine}
|
||||
|
||||
div.payment__meta-item.payment__meta-item--payment-date
|
||||
span.label #{__("payment.paper.payment_date")}
|
||||
span.value #{paymentReceive.formattedPaymentDate}
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) Invoice #
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Invoice Amount
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Paid Amount
|
||||
|
||||
div.payment__table
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.item #{__("payment.paper.invoice_number")}
|
||||
th.date #{__("payment.paper.invoice_date")}
|
||||
th.invoiceAmount #{__("payment.paper.invoice_amount")}
|
||||
th.paymentAmount #{__("payment.paper.payment_amount")}
|
||||
tbody
|
||||
each entry in paymentReceive.entries
|
||||
tr
|
||||
td.item=entry.invoice.invoiceNo
|
||||
td.date=entry.invoice.invoiceDateFormatted
|
||||
td.invoiceAmount=entry.invoice.totalFormatted
|
||||
td.paymentAmount=entry.invoice.paymentAmountFormatted
|
||||
tbody
|
||||
each line in lines
|
||||
tr
|
||||
td(class=`${prefix}-table__cell`) #{line.invoiceNumber}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.invoiceAmount}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.paidAmount}
|
||||
|
||||
div.payment__table-after
|
||||
div.payment__table-total
|
||||
table
|
||||
tbody
|
||||
tr.payment-amount
|
||||
td #{__('payment.paper.payment_amount')}
|
||||
td #{paymentReceive.formattedAmount}
|
||||
tr.blanace-due
|
||||
td #{__('payment.paper.balance_due')}
|
||||
td #{paymentReceive.customer.closingBalance}
|
||||
div(class=`${prefix}-totals`)
|
||||
if showSubtotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--gray-border`)
|
||||
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||
|
||||
div.payment__footer
|
||||
if paymentReceive.statement
|
||||
div.payment__notes
|
||||
h3 #{__("payment.paper.statement")}
|
||||
p #{paymentReceive.statement}
|
||||
if showTotal
|
||||
div(class=`${prefix}-totals__item ${prefix}-totals__item--dark-border`)
|
||||
div(class=`${prefix}-totals__item-label`) #{totalLabel}
|
||||
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||
|
||||
@@ -1,77 +1,198 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/receipt-rtl.css
|
||||
else
|
||||
include ../../css/modules/receipt.css
|
||||
- var prefix = 'bc'
|
||||
style.
|
||||
.#{prefix}-root {
|
||||
color: #000;
|
||||
padding: 24px 30px;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-terms-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-terms-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
.#{prefix}-terms-item__label {
|
||||
min-width: 120px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-terms-item__value {}
|
||||
.#{prefix}-address-section {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-address-section > * {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.#{prefix}-address {}
|
||||
.#{prefix}-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
}
|
||||
.#{prefix}-table__header {
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 2px 10px;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-table__header:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__header:last-of-type{
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.#{prefix}-table__cell:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
.#{prefix}-table__cell:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.#{prefix}-totals__line {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.#{prefix}-totals__line--gray-border {
|
||||
border-bottom: 1px solid #DADADA;
|
||||
}
|
||||
.#{prefix}-totals__line--dark-border {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.#{prefix}-totals__line__label {
|
||||
min-width: 160px;
|
||||
}
|
||||
.#{prefix}-totals__line__amount {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-statement {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.#{prefix}-statement__label {}
|
||||
.#{prefix}-statement__value {}
|
||||
|
||||
block content
|
||||
div.receipt
|
||||
div.receipt__header
|
||||
div.paper
|
||||
h1.title #{__("receipt.paper.receipt")}
|
||||
span.receiptNumber #{saleReceipt.receiptNumber}
|
||||
//- block head
|
||||
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||
|
||||
//- Title and company logo
|
||||
h1(class=`${prefix}-big-title`) Receipt
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if showCompanyLogo
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogo alt=`Company Logo`)
|
||||
|
||||
div.receipt__receipt-amount
|
||||
div.label #{__('receipt.paper.receipt_amount')}
|
||||
div.amount #{saleReceipt.formattedAmount}
|
||||
//- Terms List
|
||||
div(class=`${prefix}-terms-list`)
|
||||
if showReceiptNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
|
||||
span(class=`${prefix}-terms-item__value`)= receiptNumber
|
||||
if showReceiptDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
span(class=`${prefix}-terms-item__label`)= receiptDateLabel
|
||||
span(class=`${prefix}-terms-item__value`)= receiptDate
|
||||
|
||||
div.receipt__meta
|
||||
div.receipt__meta-item.receipt__meta-item--billed-to
|
||||
span.label #{__("receipt.paper.billed_to")}
|
||||
span.value #{saleReceipt.customer.displayName}
|
||||
//- Address Section
|
||||
div(class=`${prefix}-address-section`)
|
||||
if showBilledFromAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong= companyName
|
||||
each addressLine in billedFromAddress
|
||||
div= addressLine
|
||||
|
||||
div.receipt__meta-item.receipt__meta-item--invoice-date
|
||||
span.label #{__("receipt.paper.receipt_date")}
|
||||
span.value #{saleReceipt.formattedReceiptDate}
|
||||
if showBilledToAddress
|
||||
div(class=`${prefix}-address`)
|
||||
strong= billedToLabel
|
||||
each addressLine in billedToAddress
|
||||
div= addressLine
|
||||
|
||||
if saleReceipt.receiptNumber
|
||||
div.receipt__meta-item.receipt__meta-item--invoice-number
|
||||
span.label #{__("receipt.paper.receipt_number")}
|
||||
span.value #{saleReceipt.receiptNumber}
|
||||
//- Table Section
|
||||
table(class=`${prefix}-table`)
|
||||
thead(class=`${prefix}-table__header`)
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) Item
|
||||
th(class=`${prefix}-table__header`) Description
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
|
||||
tbody
|
||||
each line in lines
|
||||
tr(class=`${prefix}-table__row`)
|
||||
td(class=`${prefix}-table__cell`)= line.item
|
||||
td(class=`${prefix}-table__cell`)= line.description
|
||||
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.rate
|
||||
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.total
|
||||
|
||||
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
|
||||
|
||||
div.receipt__table-after
|
||||
div.receipt__table-total
|
||||
table
|
||||
tbody
|
||||
tr.total
|
||||
td #{__('receipt.paper.total')}
|
||||
td #{saleReceipt.formattedAmount}
|
||||
tr.payment-amount
|
||||
td #{__('receipt.paper.payment_amount')}
|
||||
td #{saleReceipt.formattedAmount}
|
||||
tr.blanace-due
|
||||
td #{__('receipt.paper.balance_due')}
|
||||
td #{'$0'}
|
||||
//- Totals Section
|
||||
div(class=`${prefix}-totals`)
|
||||
if showSubtotal
|
||||
div(class=`${prefix}-totals__line ${prefix}-totals__line--gray-border`)
|
||||
span(class=`${prefix}-totals__line__label`)= subtotalLabel
|
||||
span(class=`${prefix}-totals__line__amount`)= subtotal
|
||||
|
||||
div.receipt__footer
|
||||
if saleReceipt.statement
|
||||
div.receipt__conditions
|
||||
h3 #{__("receipt.paper.statement")}
|
||||
p #{saleReceipt.statement}
|
||||
if showTotal
|
||||
div(class=`${prefix}-totals__line ${prefix}-totals__line--dark-border`)
|
||||
span(class=`${prefix}-totals__line__label`)= totalLabel
|
||||
span(class=`${prefix}-totals__line__amount`)= total
|
||||
|
||||
if saleReceipt.receiptMessage
|
||||
div.receipt__notes
|
||||
h3 #{__("receipt.paper.notes")}
|
||||
p #{saleReceipt.receiptMessage}
|
||||
//- Customer Note Section
|
||||
if showCustomerNote
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`)= customerNoteLabel
|
||||
div(class=`${prefix}-statement__value`)= customerNote
|
||||
|
||||
//- Terms & Conditions Section
|
||||
if showTermsConditions
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`)= termsConditionsLabel
|
||||
div(class=`${prefix}-statement__value`)= termsConditions
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { PdfTemplateApplication } from '@/services/PdfTemplate/PdfTemplateApplication';
|
||||
|
||||
@Service()
|
||||
export class PdfTemplatesController extends BaseController {
|
||||
@Inject()
|
||||
public pdfTemplateApplication: PdfTemplateApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.delete(
|
||||
'/:template_id',
|
||||
[param('template_id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.deletePdfTemplate.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/:template_id',
|
||||
[
|
||||
param('template_id').exists().isInt().toInt(),
|
||||
check('template_name').exists(),
|
||||
check('attributes').exists(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.editPdfTemplate.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
[query('resource').optional()],
|
||||
this.validationResult,
|
||||
this.getPdfTemplates.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/:template_id',
|
||||
[param('template_id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.getPdfTemplate.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[
|
||||
check('template_name').exists(),
|
||||
check('resource').exists(),
|
||||
check('attributes').exists(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.createPdfInvoiceTemplate.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/:template_id/assign_default',
|
||||
[param('template_id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.assginPdfTemplateAsDefault.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
async createPdfInvoiceTemplate(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { templateName, resource, attributes } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const result = await this.pdfTemplateApplication.createPdfTemplate(
|
||||
tenantId,
|
||||
templateName,
|
||||
resource,
|
||||
attributes
|
||||
);
|
||||
return res.status(201).send({
|
||||
id: result.id,
|
||||
message: 'The PDF template has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async editPdfTemplate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { template_id: templateId } = req.params;
|
||||
const editTemplateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const result = await this.pdfTemplateApplication.editPdfTemplate(
|
||||
tenantId,
|
||||
Number(templateId),
|
||||
editTemplateDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: result.id,
|
||||
message: 'The PDF template has been updated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async deletePdfTemplate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { template_id: templateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.pdfTemplateApplication.deletePdfTemplate(
|
||||
tenantId,
|
||||
Number(templateId)
|
||||
);
|
||||
return res.status(204).send({
|
||||
id: templateId,
|
||||
message: 'The PDF template has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getPdfTemplate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { template_id: templateId } = req.params;
|
||||
|
||||
try {
|
||||
const template = await this.pdfTemplateApplication.getPdfTemplate(
|
||||
tenantId,
|
||||
Number(templateId)
|
||||
);
|
||||
return res.status(200).send(template);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getPdfTemplates(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const query = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const templates = await this.pdfTemplateApplication.getPdfTemplates(
|
||||
tenantId,
|
||||
query
|
||||
);
|
||||
return res.status(200).send(templates);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async assginPdfTemplateAsDefault(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { template_id: templateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.pdfTemplateApplication.assignPdfTemplateAsDefault(
|
||||
tenantId,
|
||||
Number(templateId)
|
||||
);
|
||||
return res.status(204).send({
|
||||
id: templateId,
|
||||
message: 'The given pdf template has been assigned as default template',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,9 @@ export default class PaymentReceivesController extends BaseController {
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Pdf template id.
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,9 @@ export default class PaymentReceivesController extends BaseController {
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Pdf template id.
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -168,9 +168,7 @@ export default class SalesEstimatesController extends BaseController {
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.quantity').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
@@ -186,6 +184,9 @@ export default class SalesEstimatesController extends BaseController {
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Pdf template id.
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -224,9 +224,7 @@ export default class SaleInvoicesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.tax_code')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
@@ -257,6 +255,9 @@ export default class SaleInvoicesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
|
||||
// Pdf template id.
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -148,17 +148,20 @@ export default class SalesReceiptsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
check('receipt_message').optional().trim(),
|
||||
|
||||
check('statement').optional().trim(),
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Pdf template id.
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ import { Webhooks } from './controllers/Webhooks/Webhooks';
|
||||
import { ExportController } from './controllers/Export/ExportController';
|
||||
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
|
||||
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
|
||||
import { PdfTemplatesController } from './controllers/PdfTemplates/PdfTemplatesController';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
@@ -81,7 +82,7 @@ export default () => {
|
||||
app.use('/jobs', Container.get(Jobs).router());
|
||||
app.use('/account', Container.get(Account).router());
|
||||
app.use('/webhooks', Container.get(Webhooks).router());
|
||||
app.use('/demo', Container.get(OneClickDemoController).router())
|
||||
app.use('/demo', Container.get(OneClickDemoController).router());
|
||||
|
||||
// - Dashboard routes.
|
||||
// ---------------------------
|
||||
@@ -147,6 +148,10 @@ export default () => {
|
||||
dashboard.use('/import', Container.get(ImportController).router());
|
||||
dashboard.use('/export', Container.get(ExportController).router());
|
||||
dashboard.use('/attachments', Container.get(AttachmentsController).router());
|
||||
dashboard.use(
|
||||
'/pdf-templates',
|
||||
Container.get(PdfTemplatesController).router()
|
||||
);
|
||||
|
||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const SALE_INVOICE_CREATED = 'Sale invoice created';
|
||||
export const SALE_INVOICE_EDITED = 'Sale invoice d';
|
||||
export const SALE_INVOICE_EDITED = 'Sale invoice edited';
|
||||
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
|
||||
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
|
||||
|
||||
@@ -34,24 +34,9 @@ export const ITEM_EVENT_DELETED = 'Item deleted';
|
||||
export const AUTH_SIGNED_UP = 'Auth Signed-up';
|
||||
export const AUTH_RESET_PASSWORD = 'Auth reset password';
|
||||
|
||||
export const BANK_TRANSACTION_MATCHED = 'Bank transaction matching deleted';
|
||||
export const BANK_TRANSACTION_EXCLUDED = 'Bank transaction excluded';
|
||||
export const BANK_TRANSACTION_CATEGORIZED = 'Bank transaction categorized';
|
||||
export const BANK_TRANSACTION_UNCATEGORIZED = 'Bank transaction uncategorized';
|
||||
export const BANK_ACCOUNT_DISCONNECTED = 'Bank account disconnected';
|
||||
|
||||
export const ACCOUNT_GROUP = 'Account';
|
||||
export const ITEM_GROUP = 'Item';
|
||||
export const AUTH_GROUP = 'Auth';
|
||||
export const SALE_GROUP = 'Sale';
|
||||
export const PAYMENT_GROUP = 'Payment';
|
||||
export const BILL_GROUP = 'Bill';
|
||||
export const EXPENSE_GROUP = 'Expense';
|
||||
|
||||
export const MANUAL_JOURNAL_CREATED = 'Manual journal created';
|
||||
export const MANUAL_JOURNAL_EDITED = 'Manual journal edited';
|
||||
export const MANUAL_JOURNAL_DELETED = 'Manual journal deleted';
|
||||
export const MANUAL_JOURNAL_PUBLISHED = 'Manual journal published';
|
||||
export const SUBSCRIPTION_CANCELLED = 'Subscription cancelled';
|
||||
export const SUBSCRIPTION_RESUMED = 'Subscription resumed';
|
||||
export const SUBSCRIPTION_PLAN_CHANGED = 'Subscription plan changed';
|
||||
|
||||
export const CUSTOMER_CREATED = 'Customer created';
|
||||
export const CUSTOMER_EDITED = 'Customer edited';
|
||||
@@ -61,7 +46,34 @@ export const VENDOR_CREATED = 'Vendor created';
|
||||
export const VENDOR_EDITED = 'Vendor edited';
|
||||
export const VENDOR_DELETED = 'Vendor deleted';
|
||||
|
||||
export const TRANSACTIONS_LOCKING_LOCKED = 'Transactions locking locked';
|
||||
export const TRANSACTIONS_LOCKING_LOCKING_CANCELLED =
|
||||
'Transactions locking cancelled';
|
||||
export const TRANSACTIONS_LOCKING_PARTIALLY_UNLOCKED =
|
||||
'Transactions locking partially unlocked';
|
||||
export const TRANSACTIONS_LOCKING_PARTIALLY_UNLOCK_CANCELLED =
|
||||
'Transactions locking partially unlock cancelled';
|
||||
|
||||
export const BANK_TRANSACTION_MATCHED = 'Bank transaction matching deleted';
|
||||
export const BANK_TRANSACTION_EXCLUDED = 'Bank transaction excluded';
|
||||
export const BANK_TRANSACTION_CATEGORIZED = 'Bank transaction categorized';
|
||||
export const BANK_TRANSACTION_UNCATEGORIZED = 'Bank transaction uncategorized';
|
||||
export const BANK_ACCOUNT_DISCONNECTED = 'Bank account disconnected';
|
||||
|
||||
export const MANUAL_JOURNAL_CREATED = 'Manual journal created';
|
||||
export const MANUAL_JOURNAL_EDITED = 'Manual journal edited';
|
||||
export const MANUAL_JOURNAL_DELETED = 'Manual journal deleted';
|
||||
export const MANUAL_JOURNAL_PUBLISHED = 'Manual journal published';
|
||||
|
||||
export const BANK_RULE_CREATED = 'Bank rule created';
|
||||
export const BANK_RULE_EDITED = 'Bank rule edited';
|
||||
export const BANK_RULE_DELETED = 'Bank rule deleted';
|
||||
|
||||
// # Event Groups
|
||||
export const ACCOUNT_GROUP = 'Account';
|
||||
export const ITEM_GROUP = 'Item';
|
||||
export const AUTH_GROUP = 'Auth';
|
||||
export const SALE_GROUP = 'Sale';
|
||||
export const PAYMENT_GROUP = 'Payment';
|
||||
export const BILL_GROUP = 'Bill';
|
||||
export const EXPENSE_GROUP = 'Expense';
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.createTable('pdf_templates', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.text('resource');
|
||||
table.text('template_name');
|
||||
table.json('attributes');
|
||||
table.boolean('predefined').defaultTo(false);
|
||||
table.boolean('default').defaultTo(false);
|
||||
table.timestamps();
|
||||
})
|
||||
.table('sales_invoices', (table) => {
|
||||
table
|
||||
.integer('pdf_template_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('pdf_templates');
|
||||
})
|
||||
.table('sales_estimates', (table) => {
|
||||
table
|
||||
.integer('pdf_template_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('pdf_templates');
|
||||
})
|
||||
.table('sales_receipts', (table) => {
|
||||
table
|
||||
.integer('pdf_template_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('pdf_templates');
|
||||
})
|
||||
.table('credit_notes', (table) => {
|
||||
table
|
||||
.integer('pdf_template_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('pdf_templates');
|
||||
})
|
||||
.table('payment_receives', (table) => {
|
||||
table
|
||||
.integer('pdf_template_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('pdf_templates');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.table('payment_receives', (table) => {
|
||||
table.dropColumn('pdf_template_id');
|
||||
})
|
||||
.table('credit_notes', (table) => {
|
||||
table.dropColumn('pdf_template_id');
|
||||
})
|
||||
.table('sales_receipts', (table) => {
|
||||
table.dropColumn('pdf_template_id');
|
||||
})
|
||||
.table('sales_estimates', (table) => {
|
||||
table.dropColumn('pdf_template_id');
|
||||
})
|
||||
.table('sales_invoices', (table) => {
|
||||
table.dropColumn('pdf_template_id');
|
||||
})
|
||||
.dropTableIfExists('pdf_templates');
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex('pdf_templates').insert([
|
||||
{
|
||||
resource: 'SaleInvoice',
|
||||
templateName: 'Standard Template',
|
||||
predefined: true,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
resource: 'SaleEstimate',
|
||||
templateName: 'Standard Template',
|
||||
predefined: true,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
resource: 'SaleReceipt',
|
||||
templateName: 'Standard Template',
|
||||
predefined: true,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
resource: 'CreditNote',
|
||||
templateName: 'Standard Template',
|
||||
predefined: true,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
resource: 'PaymentReceive',
|
||||
templateName: 'Standard Template',
|
||||
predefined: true,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {};
|
||||
@@ -146,6 +146,7 @@ export interface ICashflowTransactionUncategorizedPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
oldMainUncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ export interface ICreditNote {
|
||||
branchId?: number;
|
||||
warehouseId: number;
|
||||
createdAt?: Date;
|
||||
termsConditions: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export enum CreditNoteAction {
|
||||
@@ -258,3 +260,49 @@ export type ICreditNoteGLCommonEntry = Pick<
|
||||
| 'debit'
|
||||
| 'branchId'
|
||||
>;
|
||||
|
||||
export interface CreditNotePdfTemplateAttributes {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
companyName: string;
|
||||
|
||||
billedToAddress: string[];
|
||||
billedFromAddress: string[];
|
||||
showBilledToAddress: boolean;
|
||||
showBilledFromAddress: boolean;
|
||||
billedToLabel: string;
|
||||
|
||||
total: string;
|
||||
totalLabel: string;
|
||||
showTotal: boolean;
|
||||
|
||||
subtotal: string;
|
||||
subtotalLabel: string;
|
||||
showSubtotal: boolean;
|
||||
|
||||
showCustomerNote: boolean;
|
||||
customerNote: string;
|
||||
customerNoteLabel: string;
|
||||
|
||||
showTermsConditions: boolean;
|
||||
termsConditions: string;
|
||||
termsConditionsLabel: string;
|
||||
|
||||
lines: Array<{
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}>;
|
||||
|
||||
showCreditNoteNumber: boolean;
|
||||
creditNoteNumberLabel: string;
|
||||
creditNoteNumebr: string;
|
||||
|
||||
creditNoteDate: string;
|
||||
showCreditNoteDate: boolean;
|
||||
creditNoteDateLabel: string;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface IPaymentReceived {
|
||||
updatedAt: Date;
|
||||
localAmount?: number;
|
||||
branchId?: number;
|
||||
pdfTemplateId?: number;
|
||||
}
|
||||
export interface IPaymentReceivedCreateDTO {
|
||||
customerId: number;
|
||||
@@ -185,3 +186,52 @@ export interface PaymentReceiveMailPresendEvent {
|
||||
paymentReceiveId: number;
|
||||
messageOptions: PaymentReceiveMailOptsDTO;
|
||||
}
|
||||
|
||||
export interface PaymentReceivedPdfLineItem {
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface PaymentReceivedPdfTax {
|
||||
label: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface PaymentReceivedPdfTemplateAttributes {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
companyName: string;
|
||||
|
||||
billedToAddress: string[];
|
||||
billedFromAddress: string[];
|
||||
showBilledFromAddress: boolean;
|
||||
showBillingToAddress: boolean;
|
||||
billedToLabel: string;
|
||||
|
||||
total: string;
|
||||
totalLabel: string;
|
||||
showTotal: boolean;
|
||||
|
||||
subtotal: string;
|
||||
subtotalLabel: string;
|
||||
showSubtotal: boolean;
|
||||
|
||||
lines: Array<{
|
||||
invoiceNumber: string;
|
||||
invoiceAmount: string;
|
||||
paidAmount: string;
|
||||
}>;
|
||||
|
||||
showPaymentReceivedNumber: boolean;
|
||||
paymentReceivedNumberLabel: string;
|
||||
paymentReceivedNumebr: string;
|
||||
|
||||
paymentReceivedDate: string;
|
||||
showPaymentReceivedDate: boolean;
|
||||
paymentReceivedDateLabel: string;
|
||||
}
|
||||
|
||||
@@ -143,3 +143,4 @@ export interface ISaleEstimateMailPresendEvent {
|
||||
saleEstimateId: number;
|
||||
messageOptions: SaleEstimateMailOptionsDTO;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ export interface ISaleInvoice {
|
||||
subtotal: number;
|
||||
subtotalLocal: number;
|
||||
subtotalExludingTax: number;
|
||||
|
||||
termsConditions: string;
|
||||
invoiceMessage: string;
|
||||
|
||||
pdfTemplateId?: number;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDTO {
|
||||
@@ -217,3 +222,83 @@ export interface ISaleInvoiceMailSent {
|
||||
saleInvoiceId: number;
|
||||
messageOptions: SendInvoiceMailDTO;
|
||||
}
|
||||
|
||||
|
||||
// Invoice Pdf Document
|
||||
export interface InvoicePdfLine {
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface InvoicePdfTax {
|
||||
label: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface InvoicePdfTemplateAttributes {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
|
||||
companyName: string;
|
||||
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
|
||||
dueDate: string;
|
||||
dueDateLabel: string;
|
||||
showDueDate: boolean;
|
||||
|
||||
dateIssue: string;
|
||||
dateIssueLabel: string;
|
||||
showDateIssue: boolean;
|
||||
|
||||
invoiceNumberLabel: string;
|
||||
invoiceNumber: string;
|
||||
showInvoiceNumber: boolean;
|
||||
|
||||
showBillingToAddress: boolean;
|
||||
showBilledFromAddress: boolean;
|
||||
billedToLabel: string;
|
||||
|
||||
lineItemLabel: string;
|
||||
lineDescriptionLabel: string;
|
||||
lineRateLabel: string;
|
||||
lineTotalLabel: string;
|
||||
|
||||
totalLabel: string;
|
||||
subtotalLabel: string;
|
||||
discountLabel: string;
|
||||
paymentMadeLabel: string;
|
||||
balanceDueLabel: string;
|
||||
|
||||
showTotal: boolean;
|
||||
showSubtotal: boolean;
|
||||
showDiscount: boolean;
|
||||
showTaxes: boolean;
|
||||
showPaymentMade: boolean;
|
||||
showDueAmount: boolean;
|
||||
showBalanceDue: boolean;
|
||||
|
||||
total: string;
|
||||
subtotal: string;
|
||||
discount: string;
|
||||
paymentMade: string;
|
||||
balanceDue: string;
|
||||
|
||||
termsConditionsLabel: string;
|
||||
showTermsConditions: boolean;
|
||||
termsConditions: string;
|
||||
|
||||
lines: InvoicePdfLine[];
|
||||
taxes: InvoicePdfTax[];
|
||||
|
||||
statementLabel: string;
|
||||
showStatement: boolean;
|
||||
statement: string;
|
||||
|
||||
billedToAddress: string[];
|
||||
billedFromAddres: string[];
|
||||
}
|
||||
@@ -155,3 +155,57 @@ export interface ISaleReceiptMailPresend {
|
||||
saleReceiptId: number;
|
||||
messageOptions: SaleReceiptMailOptsDTO;
|
||||
}
|
||||
|
||||
export interface ISaleReceiptBrandingTemplateAttributes {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
companyName: string;
|
||||
|
||||
// Address
|
||||
billedToAddress: string[];
|
||||
billedFromAddress: string[];
|
||||
showBilledFromAddress: boolean;
|
||||
showBilledToAddress: boolean;
|
||||
billedToLabel: string;
|
||||
|
||||
// Total
|
||||
total: string;
|
||||
totalLabel: string;
|
||||
showTotal: boolean;
|
||||
|
||||
// Subtotal
|
||||
subtotal: string;
|
||||
subtotalLabel: string;
|
||||
showSubtotal: boolean;
|
||||
|
||||
// Customer Note
|
||||
showCustomerNote: boolean;
|
||||
customerNote: string;
|
||||
customerNoteLabel: string;
|
||||
|
||||
// Terms & Conditions
|
||||
showTermsConditions: boolean;
|
||||
termsConditions: string;
|
||||
termsConditionsLabel: string;
|
||||
|
||||
// Lines
|
||||
lines: Array<{
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}>;
|
||||
|
||||
// Receipt Number
|
||||
showReceiptNumber: boolean;
|
||||
receiptNumberLabel: string;
|
||||
receiptNumebr: string;
|
||||
|
||||
// Receipt Date
|
||||
receiptDate: string;
|
||||
showReceiptDate: boolean;
|
||||
receiptDateLabel: string;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import { BankRule } from '@/models/BankRule';
|
||||
import { BankRuleCondition } from '@/models/BankRuleCondition';
|
||||
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
|
||||
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
|
||||
import { PdfTemplate } from '@/models/PdfTemplate';
|
||||
|
||||
export default (knex) => {
|
||||
const models = {
|
||||
@@ -139,6 +140,7 @@ export default (knex) => {
|
||||
BankRuleCondition,
|
||||
RecognizedBankTransaction,
|
||||
MatchedBankTransaction,
|
||||
PdfTemplate
|
||||
};
|
||||
return mapValues(models, (model) => model.bindKnex(knex));
|
||||
};
|
||||
|
||||
45
packages/server/src/models/PdfTemplate.ts
Normal file
45
packages/server/src/models/PdfTemplate.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import TenantModel from 'models/TenantModel';
|
||||
|
||||
export class PdfTemplate extends TenantModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'pdf_templates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Json schema.
|
||||
*/
|
||||
static get jsonSchema() {
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer' },
|
||||
templateName: { type: 'string' },
|
||||
attributes: { type: 'object' }, // JSON field definition
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -78,9 +78,9 @@ export class RecognizeTranasctionsService {
|
||||
});
|
||||
|
||||
const bankRules = await BankRule.query(trx).onBuild((q) => {
|
||||
const rulesIds = castArray(ruleId);
|
||||
const rulesIds = !isEmpty(ruleId) ? castArray(ruleId) : [];
|
||||
|
||||
if (!isEmpty(rulesIds)) {
|
||||
if (rulesIds?.length > 0) {
|
||||
q.whereIn('id', rulesIds);
|
||||
}
|
||||
q.withGraphFetched('conditions');
|
||||
|
||||
@@ -33,22 +33,25 @@ export class UncategorizeCashflowTransaction {
|
||||
): Promise<Array<number>> {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldUncategorizedTransaction =
|
||||
const oldMainUncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query()
|
||||
.findById(uncategorizedTransactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
validateTransactionShouldBeCategorized(oldUncategorizedTransaction);
|
||||
validateTransactionShouldBeCategorized(oldMainUncategorizedTransaction);
|
||||
|
||||
const associatedUncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query()
|
||||
.where('categorizeRefId', oldUncategorizedTransaction.categorizeRefId)
|
||||
.where('categorizeRefId', oldMainUncategorizedTransaction.categorizeRefId)
|
||||
.where(
|
||||
'categorizeRefType',
|
||||
oldUncategorizedTransaction.categorizeRefType
|
||||
);
|
||||
oldMainUncategorizedTransaction.categorizeRefType
|
||||
)
|
||||
// Exclude the main transaction.
|
||||
.whereNot('id', uncategorizedTransactionId);
|
||||
|
||||
const oldUncategorizedTransactions = [
|
||||
oldUncategorizedTransaction,
|
||||
oldMainUncategorizedTransaction,
|
||||
...associatedUncategorizedTransactions,
|
||||
];
|
||||
const oldUncategoirzedTransactionsIds = oldUncategorizedTransactions.map(
|
||||
@@ -85,6 +88,7 @@ export class UncategorizeCashflowTransaction {
|
||||
{
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
oldMainUncategorizedTransaction,
|
||||
uncategorizedTransactions,
|
||||
oldUncategorizedTransactions,
|
||||
trx,
|
||||
|
||||
@@ -22,32 +22,25 @@ export class DeleteCashflowTransactionOnUncategorize {
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the cashflow transaction on uncategorize transaction.
|
||||
* Deletes the cashflow transaction once uncategorize the bank transaction.
|
||||
* @param {ICashflowTransactionUncategorizedPayload} payload
|
||||
*/
|
||||
public async deleteCashflowTransactionOnUncategorize({
|
||||
tenantId,
|
||||
oldUncategorizedTransactions,
|
||||
oldMainUncategorizedTransaction,
|
||||
trx,
|
||||
}: ICashflowTransactionUncategorizedPayload) {
|
||||
const _oldUncategorizedTransactions = oldUncategorizedTransactions.filter(
|
||||
(transaction) => transaction.categorizeRefType === 'CashflowTransaction'
|
||||
);
|
||||
|
||||
// Deletes the cashflow transaction.
|
||||
if (_oldUncategorizedTransactions.length > 0) {
|
||||
const result = await PromisePool.withConcurrency(1)
|
||||
.for(_oldUncategorizedTransactions)
|
||||
.process(async (oldUncategorizedTransaction) => {
|
||||
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||
tenantId,
|
||||
oldUncategorizedTransaction.categorizeRefId,
|
||||
trx
|
||||
);
|
||||
});
|
||||
if (result.errors.length > 0) {
|
||||
throw new ServiceError('SOMETHING_WRONG');
|
||||
}
|
||||
// Cannot continue if the main transaction does not reference to cashflow type.
|
||||
if (
|
||||
oldMainUncategorizedTransaction.categorizeRefType !==
|
||||
'CashflowTransaction'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||
tenantId,
|
||||
oldMainUncategorizedTransaction.categorizeRefId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ export class ChromiumlyTenancy {
|
||||
properties?: PageProperties,
|
||||
pdfFormat?: PdfFormat
|
||||
) {
|
||||
return this.htmlConvert.convert(tenantId, content, properties, pdfFormat);
|
||||
const parsedProperties = {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
...properties,
|
||||
}
|
||||
return this.htmlConvert.convert(tenantId, content, parsedProperties, pdfFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class CreateCreditNote extends BaseCreditNotes {
|
||||
creditNoteDTO.entries
|
||||
);
|
||||
// Transformes the given DTO to storage layer data.
|
||||
const creditNoteModel = this.transformCreateEditDTOToModel(
|
||||
const creditNoteModel = await this.transformCreateEditDTOToModel(
|
||||
tenantId,
|
||||
creditNoteDTO,
|
||||
customer.currencyCode
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Inject } from "typedi";
|
||||
import { GetPdfTemplate } from "../PdfTemplate/GetPdfTemplate";
|
||||
import { defaultCreditNoteBrandingAttributes } from "./constants";
|
||||
import { mergePdfTemplateWithDefaultAttributes } from "../Sales/Invoices/utils";
|
||||
|
||||
export class CreditNoteBrandingTemplate {
|
||||
@Inject()
|
||||
private getPdfTemplateService: GetPdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieves the credit note branding template.
|
||||
* @param {number} tenantId
|
||||
* @param {number} templateId
|
||||
* @returns {}
|
||||
*/
|
||||
public async getCreditNoteBrandingTemplate(tenantId: number, templateId: number) {
|
||||
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
template.attributes,
|
||||
defaultCreditNoteBrandingAttributes
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { omit } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import composeAsync from 'async/compose';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
@@ -16,6 +17,7 @@ import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersServ
|
||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { assocItemEntriesDefaultIndex } from '../Items/utils';
|
||||
import { BrandingTemplateDTOTransformer } from '../PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
|
||||
@Service()
|
||||
export default class BaseCreditNotes {
|
||||
@@ -34,17 +36,20 @@ export default class BaseCreditNotes {
|
||||
@Inject()
|
||||
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||
|
||||
/**
|
||||
* Transformes the credit/edit DTO to model.
|
||||
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
|
||||
* @param {string} customerCurrencyCode -
|
||||
*/
|
||||
protected transformCreateEditDTOToModel = (
|
||||
protected transformCreateEditDTOToModel = async (
|
||||
tenantId: number,
|
||||
creditNoteDTO: ICreditNoteNewDTO | ICreditNoteEditDTO,
|
||||
customerCurrencyCode: string,
|
||||
oldCreditNote?: ICreditNote
|
||||
): ICreditNote => {
|
||||
): Promise<ICreditNote> => {
|
||||
// Retrieve the total amount of the given items entries.
|
||||
const amount = this.itemsEntriesService.getTotalItemsEntries(
|
||||
creditNoteDTO.entries
|
||||
@@ -83,10 +88,18 @@ export default class BaseCreditNotes {
|
||||
refundedAmount: 0,
|
||||
invoicesAmount: 0,
|
||||
};
|
||||
const initialAsyncDTO = await composeAsync(
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
tenantId,
|
||||
'CreditNote'
|
||||
)
|
||||
)(initialDTO);
|
||||
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<ICreditNote>(tenantId),
|
||||
this.warehouseDTOTransform.transformDTO<ICreditNote>(tenantId)
|
||||
)(initialDTO);
|
||||
)(initialAsyncDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,7 +63,7 @@ export default class EditCreditNote extends BaseCreditNotes {
|
||||
creditNoteEditDTO.entries
|
||||
);
|
||||
// Transformes the given DTO to storage layer data.
|
||||
const creditNoteModel = this.transformCreateEditDTOToModel(
|
||||
const creditNoteModel = await this.transformCreateEditDTOToModel(
|
||||
tenantId,
|
||||
creditNoteEditDTO,
|
||||
customer.currencyCode,
|
||||
|
||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
||||
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
|
||||
import GetCreditNote from './GetCreditNote';
|
||||
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
|
||||
import { CreditNotePdfTemplateAttributes } from '@/interfaces';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { transformCreditNoteToPdfTemplate } from './utils';
|
||||
|
||||
@Service()
|
||||
export default class GetCreditNotePdf {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@@ -14,25 +21,62 @@ export default class GetCreditNotePdf {
|
||||
@Inject()
|
||||
private getCreditNoteService: GetCreditNote;
|
||||
|
||||
@Inject()
|
||||
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* Retrieves sale invoice pdf content.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
*/
|
||||
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
|
||||
const brandingAttributes = await this.getCreditNoteBrandingAttributes(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
console.log(brandingAttributes, 'brandingAttributes');
|
||||
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/credit-note-standard',
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves credit note branding attributes.
|
||||
* @param {number} tenantId - The ID of the tenant.
|
||||
* @param {number} creditNoteId - The ID of the credit note.
|
||||
* @returns {Promise<CreditNotePdfTemplateAttributes>} The credit note branding attributes.
|
||||
*/
|
||||
public async getCreditNoteBrandingAttributes(
|
||||
tenantId: number,
|
||||
creditNoteId: number
|
||||
): Promise<CreditNotePdfTemplateAttributes> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
const creditNote = await this.getCreditNoteService.getCreditNote(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/credit-note-standard',
|
||||
{
|
||||
creditNote,
|
||||
}
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
// Retrieve the invoice template id of not found get the default template id.
|
||||
const templateId =
|
||||
creditNote.pdfTemplateId ??
|
||||
(
|
||||
await PdfTemplate.query().findOne({
|
||||
resource: 'CreditNote',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
// Retrieves the credit note branding template.
|
||||
const brandingTemplate =
|
||||
await this.creditNoteBrandingTemplate.getCreditNoteBrandingTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformCreditNoteToPdfTemplate(creditNote),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const ERRORS = {
|
||||
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
|
||||
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
|
||||
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
|
||||
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES'
|
||||
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
@@ -66,3 +66,72 @@ export const DEFAULT_VIEWS = [
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultCreditNoteBrandingAttributes = {
|
||||
primaryColor: '',
|
||||
secondaryColor: '',
|
||||
showCompanyLogo: true,
|
||||
companyLogo: '',
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
// Address
|
||||
billedToAddress: [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddress: [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
showBilledToAddress: true,
|
||||
showBilledFromAddress: true,
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Total
|
||||
total: '$1000.00',
|
||||
totalLabel: 'Total',
|
||||
showTotal: true,
|
||||
|
||||
// Subtotal
|
||||
subtotal: '1000/00',
|
||||
subtotalLabel: 'Subtotal',
|
||||
showSubtotal: true,
|
||||
|
||||
// Customer note
|
||||
showCustomerNote: true,
|
||||
customerNote:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
customerNoteLabel: 'Customer Note',
|
||||
|
||||
// Terms & conditions
|
||||
showTermsConditions: true,
|
||||
termsConditions:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
|
||||
lines: [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
// Credit note number.
|
||||
showCreditNoteNumber: true,
|
||||
creditNoteNumberLabel: 'Credit Note Number',
|
||||
creditNoteNumebr: '346D3D40-0001',
|
||||
|
||||
// Credit note date.
|
||||
creditNoteDate: 'September 3, 2024',
|
||||
showCreditNoteDate: true,
|
||||
creditNoteDateLabel: 'Credit Note Date',
|
||||
};
|
||||
|
||||
23
packages/server/src/services/CreditNotes/utils.ts
Normal file
23
packages/server/src/services/CreditNotes/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { CreditNotePdfTemplateAttributes, ICreditNote } from '@/interfaces';
|
||||
|
||||
export const transformCreditNoteToPdfTemplate = (
|
||||
creditNote: ICreditNote
|
||||
): Partial<CreditNotePdfTemplateAttributes> => {
|
||||
return {
|
||||
creditNoteDate: creditNote.formattedCreditNoteDate,
|
||||
creditNoteNumebr: creditNote.creditNoteNumber,
|
||||
|
||||
total: creditNote.formattedAmount,
|
||||
subtotal: creditNote.formattedSubtotal,
|
||||
|
||||
lines: creditNote.entries?.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
customerNote: creditNote.note,
|
||||
termsConditions: creditNote.termsConditions,
|
||||
};
|
||||
};
|
||||
@@ -18,9 +18,6 @@ export class CustomerEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.customers.onCreated,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { ITransactionsLockingPartialUnlocked } from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import {
|
||||
TRANSACTIONS_LOCKING_LOCKED,
|
||||
TRANSACTIONS_LOCKING_LOCKING_CANCELLED,
|
||||
TRANSACTIONS_LOCKING_PARTIALLY_UNLOCK_CANCELLED,
|
||||
TRANSACTIONS_LOCKING_PARTIALLY_UNLOCKED,
|
||||
} from '@/constants/event-tracker';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class TransactionsLockingEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.transactionsLocking.locked,
|
||||
this.handleTransactionsLockingLockedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.transactionsLocking.lockCanceled,
|
||||
this.handleTransactionsLockingCancelledEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.transactionsLocking.partialUnlocked,
|
||||
this.handleTransactionsLockingPartiallyUnlockedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.transactionsLocking.partialUnlockCanceled,
|
||||
this.handleTransactionsLockingPartiallyUnlockCancelledEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTransactionsLockingLockedEvent = ({
|
||||
tenantId,
|
||||
}: ITransactionsLockingPartialUnlocked) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: TRANSACTIONS_LOCKING_LOCKED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTransactionsLockingCancelledEvent = ({
|
||||
tenantId,
|
||||
}: ITransactionsLockingPartialUnlocked) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: TRANSACTIONS_LOCKING_LOCKING_CANCELLED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTransactionsLockingPartiallyUnlockedEvent = ({
|
||||
tenantId,
|
||||
}: ITransactionsLockingPartialUnlocked) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: TRANSACTIONS_LOCKING_PARTIALLY_UNLOCKED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTransactionsLockingPartiallyUnlockCancelledEvent = ({
|
||||
tenantId,
|
||||
}: ITransactionsLockingPartialUnlocked) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: TRANSACTIONS_LOCKING_PARTIALLY_UNLOCK_CANCELLED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { ITransactionsLockingPartialUnlocked } from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import {
|
||||
SUBSCRIPTION_CANCELLED,
|
||||
SUBSCRIPTION_PLAN_CHANGED,
|
||||
SUBSCRIPTION_RESUMED,
|
||||
} from '@/constants/event-tracker';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class TransactionsLockingEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.subscription.onSubscriptionResumed,
|
||||
this.handleSubscriptionResumedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.subscription.onSubscriptionCancelled,
|
||||
this.handleSubscriptionCancelledEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.subscription.onSubscriptionPlanChanged,
|
||||
this.handleSubscriptionPlanChangedEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleSubscriptionResumedEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SUBSCRIPTION_RESUMED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleSubscriptionCancelledEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SUBSCRIPTION_CANCELLED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleSubscriptionPlanChangedEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SUBSCRIPTION_PLAN_CHANGED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -18,9 +18,6 @@ export class VendorEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.vendors.onCreated, this.handleTrackVendorCreatedEvent);
|
||||
bus.subscribe(events.vendors.onEdited, this.handleTrackEditedVendorEvent);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { sumBy, difference } from 'lodash';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from '../constants';
|
||||
import { ERRORS, SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES } from '../constants';
|
||||
import {
|
||||
IAccount,
|
||||
IExpense,
|
||||
@@ -79,7 +79,9 @@ export class CommandExpenseValidator {
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public validatePaymentAccountType = (paymentAccount: number[]) => {
|
||||
if (!paymentAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) {
|
||||
if (
|
||||
!paymentAccount.isAccountType(SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES)
|
||||
) {
|
||||
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE);
|
||||
}
|
||||
};
|
||||
|
||||
113
packages/server/src/services/Expenses/ExpenseGL.ts
Normal file
113
packages/server/src/services/Expenses/ExpenseGL.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
AccountNormal,
|
||||
IExpenseCategory,
|
||||
ILedger,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import Ledger from '../Accounting/Ledger';
|
||||
|
||||
export class ExpenseGL {
|
||||
private expense: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(expense: any) {
|
||||
this.expense = expense;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL common entry.
|
||||
* @param {IExpense} expense
|
||||
* @returns {Partial<ILedgerEntry>}
|
||||
*/
|
||||
private getExpenseGLCommonEntry = (): Partial<ILedgerEntry> => {
|
||||
return {
|
||||
currencyCode: this.expense.currencyCode,
|
||||
exchangeRate: this.expense.exchangeRate,
|
||||
|
||||
transactionType: 'Expense',
|
||||
transactionId: this.expense.id,
|
||||
|
||||
date: this.expense.paymentDate,
|
||||
userId: this.expense.userId,
|
||||
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
branchId: this.expense.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL payment entry.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getExpenseGLPaymentEntry = (): ILedgerEntry => {
|
||||
const commonEntry = this.getExpenseGLCommonEntry();
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: this.expense.localAmount,
|
||||
accountId: this.expense.paymentAccountId,
|
||||
accountNormal:
|
||||
this.expense?.paymentAccount?.accountNormal === 'debit'
|
||||
? AccountNormal.DEBIT
|
||||
: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL category entry.
|
||||
* @param {IExpense} expense -
|
||||
* @param {IExpenseCategory} expenseCategory -
|
||||
* @param {number} index
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getExpenseGLCategoryEntry = R.curry(
|
||||
(category: IExpenseCategory, index: number): ILedgerEntry => {
|
||||
const commonEntry = this.getExpenseGLCommonEntry();
|
||||
const localAmount = category.amount * this.expense.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
accountId: category.expenseAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
debit: localAmount,
|
||||
note: category.description,
|
||||
index: index + 2,
|
||||
projectId: category.projectId,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL entries.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getExpenseGLEntries = (): ILedgerEntry[] => {
|
||||
const getCategoryEntry = this.getExpenseGLCategoryEntry();
|
||||
|
||||
const paymentEntry = this.getExpenseGLPaymentEntry();
|
||||
const categoryEntries = this.expense.categories.map(getCategoryEntry);
|
||||
|
||||
return [paymentEntry, ...categoryEntries];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given expense ledger.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getExpenseLedger = (): ILedger => {
|
||||
const entries = this.getExpenseGLEntries();
|
||||
|
||||
console.log(entries, 'entries');
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import * as R from 'ramda';
|
||||
import { Service } from 'typedi';
|
||||
import {
|
||||
AccountNormal,
|
||||
IExpense,
|
||||
IExpenseCategory,
|
||||
ILedger,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
|
||||
@Service()
|
||||
export class ExpenseGLEntries {
|
||||
/**
|
||||
* Retrieves the expense GL common entry.
|
||||
* @param {IExpense} expense
|
||||
* @returns
|
||||
*/
|
||||
private getExpenseGLCommonEntry = (expense: IExpense) => {
|
||||
return {
|
||||
currencyCode: expense.currencyCode,
|
||||
exchangeRate: expense.exchangeRate,
|
||||
|
||||
transactionType: 'Expense',
|
||||
transactionId: expense.id,
|
||||
|
||||
date: expense.paymentDate,
|
||||
userId: expense.userId,
|
||||
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
branchId: expense.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL payment entry.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getExpenseGLPaymentEntry = (expense: IExpense): ILedgerEntry => {
|
||||
const commonEntry = this.getExpenseGLCommonEntry(expense);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: expense.localAmount,
|
||||
accountId: expense.paymentAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL category entry.
|
||||
* @param {IExpense} expense -
|
||||
* @param {IExpenseCategory} expenseCategory -
|
||||
* @param {number} index
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getExpenseGLCategoryEntry = R.curry(
|
||||
(
|
||||
expense: IExpense,
|
||||
category: IExpenseCategory,
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getExpenseGLCommonEntry(expense);
|
||||
const localAmount = category.amount * expense.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
accountId: category.expenseAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
debit: localAmount,
|
||||
note: category.description,
|
||||
index: index + 2,
|
||||
projectId: category.projectId,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL entries.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getExpenseGLEntries = (expense: IExpense): ILedgerEntry[] => {
|
||||
const getCategoryEntry = this.getExpenseGLCategoryEntry(expense);
|
||||
|
||||
const paymentEntry = this.getExpenseGLPaymentEntry(expense);
|
||||
const categoryEntries = expense.categories.map(getCategoryEntry);
|
||||
|
||||
return [paymentEntry, ...categoryEntries];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given expense ledger.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getExpenseLedger = (expense: IExpense): ILedger => {
|
||||
const entries = this.getExpenseGLEntries(expense);
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IExpense, ILedger } from '@/interfaces';
|
||||
import { ExpenseGL } from './ExpenseGL';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class ExpenseGLEntries {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieves the expense G/L of the given id.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<ILedger>}
|
||||
*/
|
||||
public getExpenseLedgerById = async (
|
||||
tenantId: number,
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<ILedger> => {
|
||||
const { Expense } = await this.tenancy.models(tenantId);
|
||||
|
||||
const expense = await Expense.query(trx)
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories')
|
||||
.withGraphFetched('paymentAccount')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.getExpenseLedger(expense);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given expense ledger.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getExpenseLedger = (expense: IExpense): ILedger => {
|
||||
const expenseGL = new ExpenseGL(expense);
|
||||
|
||||
return expenseGL.getExpenseLedger();
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Knex } from 'knex';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ExpenseGLEntries } from './ExpenseGLEntries';
|
||||
import { ExpenseGLEntries } from './ExpenseGLEntriesService';
|
||||
|
||||
@Service()
|
||||
export class ExpenseGLEntriesStorage {
|
||||
@@ -12,9 +12,6 @@ export class ExpenseGLEntriesStorage {
|
||||
@Inject()
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Writes the expense GL entries.
|
||||
* @param {number} tenantId
|
||||
@@ -26,15 +23,12 @@ export class ExpenseGLEntriesStorage {
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { Expense } = await this.tenancy.models(tenantId);
|
||||
|
||||
const expense = await Expense.query(trx)
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories');
|
||||
|
||||
// Retrieves the given expense ledger.
|
||||
const expenseLedger = this.expenseGLEntries.getExpenseLedger(expense);
|
||||
|
||||
const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById(
|
||||
tenantId,
|
||||
expenseId,
|
||||
trx
|
||||
);
|
||||
// Commits the expense ledger entries.
|
||||
await this.ledgerStorage.commit(tenantId, expenseLedger, trx);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
@@ -76,3 +78,12 @@ export const ExpensesSampleData = [
|
||||
Publish: 'T',
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES = [
|
||||
ACCOUNT_TYPE.CASH,
|
||||
ACCOUNT_TYPE.BANK,
|
||||
ACCOUNT_TYPE.CREDIT_CARD,
|
||||
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
|
||||
ACCOUNT_TYPE.NON_CURRENT_ASSET,
|
||||
ACCOUNT_TYPE.FIXED_ASSET,
|
||||
];
|
||||
|
||||
@@ -238,7 +238,7 @@ export default class ItemsEntriesService {
|
||||
* Sets the cost/sell accounts to the invoice entries.
|
||||
*/
|
||||
public setItemsEntriesDefaultAccounts(tenantId: number) {
|
||||
return async (entries: IItemEntry[]) => {
|
||||
return async (entries: IItemEntry[]) => {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class AssignPdfTemplateDefault {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Assigns a default PDF template for a specific tenant.
|
||||
* @param {number} tenantId - The ID of the tenant for whom the default template is being assigned.
|
||||
* @param {number} templateId - The ID of the template to be set as the default.
|
||||
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
||||
* @throws {Error} Throws ddan error if the specified template is not found.
|
||||
*/
|
||||
public async assignDefaultTemplate(tenantId: number, templateId: number) {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldPdfTempalte = await PdfTemplate.query()
|
||||
.findById(templateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx?: Knex.Transaction) => {
|
||||
// Triggers `onPdfTemplateAssigningDefault` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.pdfTemplate.onAssigningDefault,
|
||||
{
|
||||
tenantId,
|
||||
templateId,
|
||||
}
|
||||
);
|
||||
await PdfTemplate.query(trx)
|
||||
.where('resource', oldPdfTempalte.resource)
|
||||
.patch({ default: false });
|
||||
|
||||
await PdfTemplate.query(trx)
|
||||
.findById(templateId)
|
||||
.patch({ default: true });
|
||||
|
||||
// Triggers `onPdfTemplateAssignedDefault` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.pdfTemplate.onAssignedDefault,
|
||||
{
|
||||
tenantId,
|
||||
templateId,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as R from 'ramda';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export class BrandingTemplateDTOTransformer {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Associates the default branding template id.
|
||||
* @param {number} tenantId
|
||||
* @param {string} resource
|
||||
* @param {Record<string, any>} object
|
||||
* @param {string} attributeName
|
||||
* @returns
|
||||
*/
|
||||
public assocDefaultBrandingTemplate =
|
||||
(tenantId: number, resource: string) =>
|
||||
async (object: Record<string, any>) => {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
const attributeName = 'pdfTemplateId';
|
||||
|
||||
const defaultTemplate = await PdfTemplate.query().findOne({
|
||||
resource,
|
||||
default: true,
|
||||
});
|
||||
if (!defaultTemplate || !isEmpty(object[attributeName])) {
|
||||
return object;
|
||||
}
|
||||
return {
|
||||
...object,
|
||||
[attributeName]: defaultTemplate.id,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ICreateInvoicePdfTemplateDTO } from './types';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class CreatePdfTemplate {
|
||||
@Inject()
|
||||
private tennacy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Creates a new pdf template.
|
||||
* @param {number} tenantId
|
||||
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO
|
||||
*/
|
||||
public createPdfTemplate(
|
||||
tenantId: number,
|
||||
templateName: string,
|
||||
resource: string,
|
||||
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
|
||||
) {
|
||||
const { PdfTemplate } = this.tennacy.models(tenantId);
|
||||
const attributes = invoiceTemplateDTO;
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
// Triggers `onPdfTemplateCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.pdfTemplate.onCreating, {
|
||||
tenantId,
|
||||
});
|
||||
|
||||
const pdfTemplate = await PdfTemplate.query(trx).insert({
|
||||
templateName,
|
||||
resource,
|
||||
attributes,
|
||||
});
|
||||
// Triggers `onPdfTemplateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.pdfTemplate.onCreated, {
|
||||
tenantId,
|
||||
});
|
||||
return pdfTemplate;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from './types';
|
||||
|
||||
@Service()
|
||||
export class DeletePdfTemplate {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Deletes a pdf template.
|
||||
* @param {number} tenantId
|
||||
* @param {number} templateId - Pdf template id.
|
||||
*/
|
||||
public async deletePdfTemplate(tenantId: number, templateId: number) {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldPdfTemplate = await PdfTemplate.query()
|
||||
.findById(templateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Cannot delete the predefined pdf templates.
|
||||
if (oldPdfTemplate.predefined) {
|
||||
throw new ServiceError(ERRORS.CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE);
|
||||
}
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
// Triggers `onPdfTemplateDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleting, {
|
||||
tenantId,
|
||||
templateId,
|
||||
oldPdfTemplate,
|
||||
trx,
|
||||
});
|
||||
await PdfTemplate.query(trx).deleteById(templateId);
|
||||
|
||||
// Triggers `onPdfTemplateDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleted, {
|
||||
tenantId,
|
||||
templateId,
|
||||
oldPdfTemplate,
|
||||
trx,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
58
packages/server/src/services/PdfTemplate/EditPdfTemplate.ts
Normal file
58
packages/server/src/services/PdfTemplate/EditPdfTemplate.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { IEditPdfTemplateDTO } from './types';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class EditPdfTemplate {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Edits an existing pdf template.
|
||||
* @param {number} tenantId
|
||||
* @param {number} templateId - Template id.
|
||||
* @param {IEditPdfTemplateDTO} editTemplateDTO
|
||||
*/
|
||||
public async editPdfTemplate(
|
||||
tenantId: number,
|
||||
templateId: number,
|
||||
editTemplateDTO: IEditPdfTemplateDTO
|
||||
) {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldPdfTemplate = await PdfTemplate.query()
|
||||
.findById(templateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onPdfTemplateEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.pdfTemplate.onEditing, {
|
||||
tenantId,
|
||||
templateId,
|
||||
});
|
||||
const pdfTemplate = await PdfTemplate.query(trx)
|
||||
.where('id', templateId)
|
||||
.update({
|
||||
templateName: editTemplateDTO.templateName,
|
||||
attributes: editTemplateDTO.attributes,
|
||||
});
|
||||
|
||||
// Triggers `onPdfTemplatedEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.pdfTemplate.onEdited, {
|
||||
tenantId,
|
||||
templateId,
|
||||
});
|
||||
return pdfTemplate;
|
||||
});
|
||||
}
|
||||
}
|
||||
38
packages/server/src/services/PdfTemplate/GetPdfTemplate.ts
Normal file
38
packages/server/src/services/PdfTemplate/GetPdfTemplate.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { GetPdfTemplateTransformer } from './GetPdfTemplateTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export class GetPdfTemplate {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable
|
||||
|
||||
/**
|
||||
* Retrieves a pdf template by its ID.
|
||||
* @param {number} tenantId - The ID of the tenant.
|
||||
* @param {number} templateId - The ID of the pdf template to retrieve.
|
||||
* @return {Promise<any>} - The retrieved pdf template.
|
||||
*/
|
||||
async getPdfTemplate(
|
||||
tenantId: number,
|
||||
templateId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<any> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const template = await PdfTemplate.query(trx)
|
||||
.findById(templateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
template,
|
||||
new GetPdfTemplateTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
|
||||
export class GetPdfTemplateTransformer extends Transformer {
|
||||
/**
|
||||
* Includeded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['createdAtFormatted', 'resourceFormatted', 'attributes'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the creation date of the PDF template.
|
||||
* @param {Object} template
|
||||
* @returns {string} A formatted string representing the creation date of the template.
|
||||
*/
|
||||
protected createdAtFormatted = (template) => {
|
||||
return this.formatDate(template.createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the creation date of the PDF template.
|
||||
* @param {Object} template -
|
||||
* @returns {string} A formatted string representing the creation date of the template.
|
||||
*/
|
||||
protected resourceFormatted = (template) => {
|
||||
return getTransactionTypeLabel(template.resource);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves transformed brand attributes.
|
||||
* @param {} template
|
||||
* @returns
|
||||
*/
|
||||
protected attributes = (template) => {
|
||||
return this.item(
|
||||
template.attributes,
|
||||
new GetPdfTemplateAttributesTransformer()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
class GetPdfTemplateAttributesTransformer extends Transformer {
|
||||
/**
|
||||
* Includeded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['companyLogoUri'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company logo uri.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected companyLogoUri(template) {
|
||||
return template.companyLogoKey
|
||||
? `https://bigcapital.sfo3.digitaloceanspaces.com/${template.companyLogoKey}`
|
||||
: '';
|
||||
}
|
||||
}
|
||||
37
packages/server/src/services/PdfTemplate/GetPdfTemplates.ts
Normal file
37
packages/server/src/services/PdfTemplate/GetPdfTemplates.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { GetPdfTemplatesTransformer } from './GetPdfTemplatesTransformer';
|
||||
import { Inject, Service } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export class GetPdfTemplates {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformInjectable: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves a list of PDF templates for a specified tenant.
|
||||
* @param {number} tenantId - The ID of the tenant for which to retrieve templates.
|
||||
* @param {Object} [query] - Optional query parameters to filter the templates.
|
||||
* @param {string} [query.resource] - The resource type to filter the templates by.
|
||||
* @returns {Promise<any>} - A promise that resolves to the transformed list of PDF templates.
|
||||
*/
|
||||
async getPdfTemplates(tenantId: number, query?: { resource?: string }) {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const templates = await PdfTemplate.query().onBuild((q) => {
|
||||
if (query?.resource) {
|
||||
q.where('resource', query?.resource);
|
||||
}
|
||||
q.orderBy('createdAt', 'ASC');
|
||||
});
|
||||
|
||||
return this.transformInjectable.transform(
|
||||
tenantId,
|
||||
templates,
|
||||
new GetPdfTemplatesTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
|
||||
export class GetPdfTemplatesTransformer extends Transformer {
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['attributes'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Includeded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['createdAtFormatted', 'resourceFormatted'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the creation date of the PDF template.
|
||||
* @param {Object} template
|
||||
* @returns {string} A formatted string representing the creation date of the template.
|
||||
*/
|
||||
protected createdAtFormatted = (template) => {
|
||||
return this.formatDate(template.createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the creation date of the PDF template.
|
||||
* @param {Object} template -
|
||||
* @returns {string} A formatted string representing the creation date of the template.
|
||||
*/
|
||||
protected resourceFormatted = (template) => {
|
||||
return getTransactionTypeLabel(template.resource);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ICreateInvoicePdfTemplateDTO, IEditPdfTemplateDTO } from './types';
|
||||
import { CreatePdfTemplate } from './CreatePdfTemplate';
|
||||
import { DeletePdfTemplate } from './DeletePdfTemplate';
|
||||
import { GetPdfTemplate } from './GetPdfTemplate';
|
||||
import { GetPdfTemplates } from './GetPdfTemplates';
|
||||
import { EditPdfTemplate } from './EditPdfTemplate';
|
||||
import { AssignPdfTemplateDefault } from './AssignPdfTemplateDefault';
|
||||
|
||||
@Service()
|
||||
export class PdfTemplateApplication {
|
||||
@Inject()
|
||||
private createPdfTemplateService: CreatePdfTemplate;
|
||||
|
||||
@Inject()
|
||||
private deletePdfTemplateService: DeletePdfTemplate;
|
||||
|
||||
@Inject()
|
||||
private getPdfTemplateService: GetPdfTemplate;
|
||||
|
||||
@Inject()
|
||||
private getPdfTemplatesService: GetPdfTemplates;
|
||||
|
||||
@Inject()
|
||||
private editPdfTemplateService: EditPdfTemplate;
|
||||
|
||||
@Inject()
|
||||
private assignPdfTemplateDefaultService: AssignPdfTemplateDefault;
|
||||
|
||||
/**
|
||||
* Creates a new PDF template.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} templateName - The name of the PDF template to create.
|
||||
* @param {string} resource - The resource type associated with the PDF template.
|
||||
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO - The data transfer object containing the details for the new PDF template.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
public async createPdfTemplate(
|
||||
tenantId: number,
|
||||
templateName: string,
|
||||
resource: string,
|
||||
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
|
||||
) {
|
||||
return this.createPdfTemplateService.createPdfTemplate(
|
||||
tenantId,
|
||||
templateName,
|
||||
resource,
|
||||
invoiceTemplateDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an existing PDF template.
|
||||
* @param {number} tenantId - The ID of the tenant.
|
||||
* @param {number} templateId - The ID of the PDF template to edit.
|
||||
* @param {IEditPdfTemplateDTO} editTemplateDTO - The data transfer object containing the updated details for the PDF template.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
public async editPdfTemplate(
|
||||
tenantId: number,
|
||||
templateId: number,
|
||||
editTemplateDTO: IEditPdfTemplateDTO
|
||||
) {
|
||||
return this.editPdfTemplateService.editPdfTemplate(
|
||||
tenantId,
|
||||
templateId,
|
||||
editTemplateDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a PDF template.
|
||||
* @param {number} tenantId - The ID of the tenant.
|
||||
* @param {number} templateId - The ID of the PDF template to delete.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
|
||||
public async deletePdfTemplate(tenantId: number, templateId: number) {
|
||||
return this.deletePdfTemplateService.deletePdfTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a PDF template by its ID for a specified tenant.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} templateId - The ID of the PDF template to retrieve.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
public async getPdfTemplate(tenantId: number, templateId: number) {
|
||||
return this.getPdfTemplateService.getPdfTemplate(tenantId, templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of PDF templates.
|
||||
* @param {number} tenantId - The ID of the tenant for which to retrieve templates.
|
||||
* @param {Object} query
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
public async getPdfTemplates(
|
||||
tenantId: number,
|
||||
query?: { resource?: string }
|
||||
) {
|
||||
return this.getPdfTemplatesService.getPdfTemplates(tenantId, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a PDF template as the default template.
|
||||
* @param {number} tenantId
|
||||
* @param {number} templateId - The ID of the PDF template to assign as default.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
public async assignPdfTemplateAsDefault(
|
||||
tenantId: number,
|
||||
templateId: number
|
||||
) {
|
||||
return this.assignPdfTemplateDefaultService.assignDefaultTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
}
|
||||
}
|
||||
67
packages/server/src/services/PdfTemplate/types.ts
Normal file
67
packages/server/src/services/PdfTemplate/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export enum ERRORS {
|
||||
CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE = 'CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE',
|
||||
}
|
||||
|
||||
export interface IEditPdfTemplateDTO {
|
||||
templateName: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ICreateInvoicePdfTemplateDTO {
|
||||
// Colors
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
|
||||
// Company Logo
|
||||
showCompanyLogo?: boolean;
|
||||
companyLogo?: string;
|
||||
|
||||
// Top details.
|
||||
showInvoiceNumber?: boolean;
|
||||
invoiceNumberLabel?: string;
|
||||
|
||||
showDateIssue?: boolean;
|
||||
dateIssueLabel?: string;
|
||||
|
||||
showDueDate?: boolean;
|
||||
dueDateLabel?: string;
|
||||
|
||||
// Company name
|
||||
companyName?: string;
|
||||
|
||||
// Addresses
|
||||
showBilledFromAddress?: boolean;
|
||||
showBillingToAddress?: boolean;
|
||||
billedToLabel?: string;
|
||||
|
||||
// Entries
|
||||
itemNameLabel?: string;
|
||||
itemDescriptionLabel?: string;
|
||||
itemRateLabel?: string;
|
||||
itemTotalLabel?: string;
|
||||
|
||||
// Totals
|
||||
showSubtotal?: boolean;
|
||||
subtotalLabel?: string;
|
||||
|
||||
showDiscount?: boolean;
|
||||
discountLabel?: string;
|
||||
|
||||
showTaxes?: boolean;
|
||||
|
||||
showTotal?: boolean;
|
||||
totalLabel?: string;
|
||||
|
||||
paymentMadeLabel?: string;
|
||||
showPaymentMade?: boolean;
|
||||
|
||||
dueAmountLabel?: string;
|
||||
showDueAmount?: boolean;
|
||||
|
||||
// Footer paragraphs.
|
||||
termsConditionsLabel?: string;
|
||||
showTermsConditions?: boolean;
|
||||
|
||||
statementLabel?: string;
|
||||
showStatement?: boolean;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import composeAsync from 'async/compose';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces';
|
||||
import { SaleEstimateValidators } from './SaleEstimateValidators';
|
||||
@@ -10,6 +11,7 @@ import { formatDateFields } from '@/utils';
|
||||
import moment from 'moment';
|
||||
import { SaleEstimateIncrement } from './SaleEstimateIncrement';
|
||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimateDTOTransformer {
|
||||
@@ -28,6 +30,9 @@ export class SaleEstimateDTOTransformer {
|
||||
@Inject()
|
||||
private estimateIncrement: SaleEstimateIncrement;
|
||||
|
||||
@Inject()
|
||||
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||
|
||||
/**
|
||||
* Transform create DTO object ot model object.
|
||||
* @param {number} tenantId
|
||||
@@ -81,10 +86,18 @@ export class SaleEstimateDTOTransformer {
|
||||
deliveredAt: moment().toMySqlDateTime(),
|
||||
}),
|
||||
};
|
||||
const initialAsyncDTO = await composeAsync(
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
tenantId,
|
||||
'SaleEstimate'
|
||||
)
|
||||
)(initialDTO);
|
||||
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<ISaleEstimate>(tenantId),
|
||||
this.warehouseDTOTransform.transformDTO<ISaleEstimate>(tenantId)
|
||||
)(initialDTO);
|
||||
)(initialAsyncDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { GetSaleEstimate } from './GetSaleEstimate';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
|
||||
import { transformEstimateToPdfTemplate } from './utils';
|
||||
import { EstimatePdfBrandingAttributes } from './constants';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@@ -14,25 +21,59 @@ export class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
private getSaleEstimate: GetSaleEstimate;
|
||||
|
||||
@Inject()
|
||||
private estimatePdfTemplate: SaleEstimatePdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
*/
|
||||
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
|
||||
const saleEstimate = await this.getSaleEstimate.getEstimate(
|
||||
const brandingAttributes = await this.getEstimateBrandingAttributes(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/estimate-regular',
|
||||
{
|
||||
saleEstimate,
|
||||
}
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given estimate branding attributes.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} estimateId - Estimate id.
|
||||
* @returns {Promise<EstimatePdfBrandingAttributes>}
|
||||
*/
|
||||
async getEstimateBrandingAttributes(
|
||||
tenantId: number,
|
||||
estimateId: number
|
||||
): Promise<EstimatePdfBrandingAttributes> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
const saleEstimate = await this.getSaleEstimate.getEstimate(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
// Retrieve the invoice template id of not found get the default template id.
|
||||
const templateId =
|
||||
saleEstimate.pdfTemplateId ??
|
||||
(
|
||||
await PdfTemplate.query().findOne({
|
||||
resource: 'SaleEstimate',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
const brandingTemplate =
|
||||
await this.estimatePdfTemplate.getEstimatePdfTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformEstimateToPdfTemplate(saleEstimate),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,3 +173,122 @@ export const SaleEstimatesSampleData = [
|
||||
'Line Description': 'Qui suscipit ducimus qui qui.',
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultEstimatePdfBrandingAttributes = {
|
||||
primaryColor: '#000',
|
||||
secondaryColor: '#000',
|
||||
showCompanyLogo: true,
|
||||
companyLogo: '',
|
||||
companyName: '',
|
||||
|
||||
billedToAddress: [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddress: [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
showBilledFromAddress: true,
|
||||
showBilledToAddress: true,
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
total: '$1000.00',
|
||||
totalLabel: 'Total',
|
||||
showTotal: true,
|
||||
|
||||
subtotal: '1000/00',
|
||||
subtotalLabel: 'Subtotal',
|
||||
showSubtotal: true,
|
||||
|
||||
showCustomerNote: true,
|
||||
customerNote:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
customerNoteLabel: 'Customer Note',
|
||||
|
||||
showTermsConditions: true,
|
||||
termsConditions:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
|
||||
lines: [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
showEstimateNumber: true,
|
||||
estimateNumberLabel: 'Estimate Number',
|
||||
estimateNumebr: '346D3D40-0001',
|
||||
|
||||
estimateDate: 'September 3, 2024',
|
||||
showEstimateDate: true,
|
||||
estimateDateLabel: 'Estimate Date',
|
||||
|
||||
expirationDateLabel: 'Expiration Date',
|
||||
showExpirationDate: true,
|
||||
expirationDate: 'September 3, 2024',
|
||||
};
|
||||
|
||||
|
||||
interface EstimatePdfBrandingLineItem {
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface EstimatePdfBrandingAttributes {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
companyName: string;
|
||||
|
||||
billedToAddress: string[];
|
||||
billedFromAddress: string[];
|
||||
showBilledFromAddress: boolean;
|
||||
showBilledToAddress: boolean;
|
||||
billedToLabel: string;
|
||||
|
||||
total: string;
|
||||
totalLabel: string;
|
||||
showTotal: boolean;
|
||||
|
||||
subtotal: string;
|
||||
subtotalLabel: string;
|
||||
showSubtotal: boolean;
|
||||
|
||||
showCustomerNote: boolean;
|
||||
customerNote: string;
|
||||
customerNoteLabel: string;
|
||||
|
||||
showTermsConditions: boolean;
|
||||
termsConditions: string;
|
||||
termsConditionsLabel: string;
|
||||
|
||||
lines: EstimatePdfBrandingLineItem[];
|
||||
|
||||
showEstimateNumber: boolean;
|
||||
estimateNumberLabel: string;
|
||||
estimateNumebr: string;
|
||||
|
||||
estimateDate: string;
|
||||
showEstimateDate: boolean;
|
||||
estimateDateLabel: string;
|
||||
|
||||
expirationDateLabel: string;
|
||||
showExpirationDate: boolean;
|
||||
expirationDate: string;
|
||||
}
|
||||
22
packages/server/src/services/Sales/Estimates/utils.ts
Normal file
22
packages/server/src/services/Sales/Estimates/utils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { EstimatePdfBrandingAttributes } from './constants';
|
||||
|
||||
export const transformEstimateToPdfTemplate = (
|
||||
estimate
|
||||
): Partial<EstimatePdfBrandingAttributes> => {
|
||||
return {
|
||||
expirationDate: estimate.formattedExpirationDate,
|
||||
estimateNumebr: estimate.estimateNumber,
|
||||
estimateDate: estimate.formattedEstimateDate,
|
||||
lines: estimate.entries.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
total: estimate.formattedSubtotal,
|
||||
subtotal: estimate.formattedSubtotal,
|
||||
customerNote: estimate.customerNote,
|
||||
termsConditions: estimate.termsConditions,
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import { formatDateFields } from 'utils';
|
||||
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||
import { ItemEntry } from '@/models';
|
||||
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
|
||||
@Service()
|
||||
export class CommandSaleInvoiceDTOTransformer {
|
||||
@@ -40,6 +41,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
@Inject()
|
||||
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
||||
|
||||
@Inject()
|
||||
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||
|
||||
/**
|
||||
* Transformes the create DTO to invoice object model.
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
||||
@@ -113,11 +117,19 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
userId: authorizedUser.id,
|
||||
} as ISaleInvoice;
|
||||
|
||||
const initialAsyncDTO = await composeAsync(
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
tenantId,
|
||||
'SaleInvoice'
|
||||
)
|
||||
)(initialDTO);
|
||||
|
||||
return R.compose(
|
||||
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
||||
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
||||
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
||||
)(initialDTO);
|
||||
)(initialAsyncDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { mergePdfTemplateWithDefaultAttributes } from './utils';
|
||||
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||
import { defaultEstimatePdfBrandingAttributes } from '../Estimates/constants';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatePdfTemplate {
|
||||
@Inject()
|
||||
private getPdfTemplateService: GetPdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieves the estimate pdf template.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceTemplateId
|
||||
* @returns
|
||||
*/
|
||||
async getEstimatePdfTemplate(tenantId: number, estimateTemplateId: number) {
|
||||
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||
tenantId,
|
||||
estimateTemplateId
|
||||
);
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
template.attributes,
|
||||
defaultEstimatePdfBrandingAttributes
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { transformInvoiceToPdfTemplate } from './utils';
|
||||
import { InvoicePdfTemplateAttributes } from '@/interfaces';
|
||||
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@@ -14,6 +21,9 @@ export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
private getInvoiceService: GetSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId - Tenant Id.
|
||||
@@ -24,19 +34,54 @@ export class SaleInvoicePdf {
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<Buffer> {
|
||||
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
|
||||
const brandingAttributes = await this.getInvoiceBrandingAttributes(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/invoice-regular',
|
||||
{
|
||||
saleInvoice,
|
||||
}
|
||||
'modules/invoice-standard',
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
// Converts the given html content to pdf document.
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the branding attributes of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceId
|
||||
* @returns {Promise<InvoicePdfTemplateAttributes>}
|
||||
*/
|
||||
async getInvoiceBrandingAttributes(
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<InvoicePdfTemplateAttributes> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const invoice = await this.getInvoiceService.getSaleInvoice(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
// Retrieve the invoice template id of not found get the default template id.
|
||||
const templateId =
|
||||
invoice.pdfTemplateId ??
|
||||
(
|
||||
await PdfTemplate.query().findOne({
|
||||
resource: 'SaleInvoice',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
// Getting the branding template attributes.
|
||||
const brandingTemplate =
|
||||
await this.invoiceBrandingTemplateService.getInvoicePdfTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
// Merge the branding template attributes with the invoice.
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformInvoiceToPdfTemplate(invoice),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { mergePdfTemplateWithDefaultAttributes } from './utils';
|
||||
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||
import { defaultInvoicePdfTemplateAttributes } from './constants';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdfTemplate {
|
||||
@Inject()
|
||||
private getPdfTemplateService: GetPdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieves the invoice pdf template.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceTemplateId
|
||||
* @returns
|
||||
*/
|
||||
async getInvoicePdfTemplate(tenantId: number, invoiceTemplateId: number){
|
||||
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||
tenantId,
|
||||
invoiceTemplateId
|
||||
);
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
template.attributes,
|
||||
defaultInvoicePdfTemplateAttributes
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -158,3 +158,88 @@ export const SaleInvoicesSampleData = [
|
||||
Description: 'Description',
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultInvoicePdfTemplateAttributes = {
|
||||
primaryColor: 'red',
|
||||
secondaryColor: 'red',
|
||||
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
showCompanyLogo: true,
|
||||
companyLogo: '',
|
||||
|
||||
dueDateLabel: 'Date due',
|
||||
showDueDate: true,
|
||||
|
||||
dateIssueLabel: 'Date of issue',
|
||||
showDateIssue: true,
|
||||
|
||||
// dateIssue,
|
||||
invoiceNumberLabel: 'Invoice number',
|
||||
showInvoiceNumber: true,
|
||||
|
||||
// Address
|
||||
showBillingToAddress: true,
|
||||
showBilledFromAddress: true,
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Entries
|
||||
lineItemLabel: 'Item',
|
||||
lineDescriptionLabel: 'Description',
|
||||
lineRateLabel: 'Rate',
|
||||
lineTotalLabel: 'Total',
|
||||
|
||||
totalLabel: 'Total',
|
||||
subtotalLabel: 'Subtotal',
|
||||
discountLabel: 'Discount',
|
||||
paymentMadeLabel: 'Payment Made',
|
||||
balanceDueLabel: 'Balance Due',
|
||||
|
||||
// Totals
|
||||
showTotal: true,
|
||||
showSubtotal: true,
|
||||
showDiscount: true,
|
||||
showTaxes: true,
|
||||
showPaymentMade: true,
|
||||
showDueAmount: true,
|
||||
showBalanceDue: true,
|
||||
|
||||
discount: '0.00',
|
||||
|
||||
// Footer paragraphs.
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
showTermsConditions: true,
|
||||
|
||||
lines: [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
taxes: [
|
||||
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
|
||||
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
|
||||
],
|
||||
|
||||
statementLabel: 'Statement',
|
||||
showStatement: true,
|
||||
billedToAddress: [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddres: [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
46
packages/server/src/services/Sales/Invoices/utils.ts
Normal file
46
packages/server/src/services/Sales/Invoices/utils.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { pickBy } from 'lodash';
|
||||
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
|
||||
|
||||
export const mergePdfTemplateWithDefaultAttributes = (
|
||||
brandingTemplate?: Record<string, any>,
|
||||
defaultAttributes: Record<string, any> = {}
|
||||
) => {
|
||||
const brandingAttributes = pickBy(
|
||||
brandingTemplate,
|
||||
(val, key) => val !== null && Object.keys(defaultAttributes).includes(key)
|
||||
);
|
||||
return {
|
||||
...defaultAttributes,
|
||||
...brandingAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformInvoiceToPdfTemplate = (
|
||||
invoice: ISaleInvoice
|
||||
): Partial<InvoicePdfTemplateAttributes> => {
|
||||
return {
|
||||
dueDate: invoice.dueDateFormatted,
|
||||
dateIssue: invoice.invoiceDateFormatted,
|
||||
invoiceNumber: invoice.invoiceNo,
|
||||
|
||||
total: invoice.totalFormatted,
|
||||
subtotal: invoice.subtotalFormatted,
|
||||
paymentMade: invoice.paymentAmountFormatted,
|
||||
balanceDue: invoice.balanceAmountFormatted,
|
||||
|
||||
termsConditions: invoice.termsConditions,
|
||||
statement: invoice.invoiceMessage,
|
||||
|
||||
lines: invoice.entries.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
taxes: invoice.taxes.map((tax) => ({
|
||||
label: tax.name,
|
||||
amount: tax.taxRateAmountFormatted,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { GetPaymentReceived } from './GetPaymentReceived';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
|
||||
import { transformPaymentReceivedToPdfTemplate } from './utils';
|
||||
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class GetPaymentReceivedPdf {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@@ -14,6 +21,9 @@ export default class GetPaymentReceivedPdf {
|
||||
@Inject()
|
||||
private getPaymentService: GetPaymentReceived;
|
||||
|
||||
@Inject()
|
||||
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
@@ -24,19 +34,52 @@ export default class GetPaymentReceivedPdf {
|
||||
tenantId: number,
|
||||
paymentReceiveId: number
|
||||
): Promise<Buffer> {
|
||||
const paymentReceive = await this.getPaymentService.getPaymentReceive(
|
||||
const brandingAttributes = await this.getPaymentBrandingAttributes(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/payment-receive-standard',
|
||||
{
|
||||
paymentReceive,
|
||||
}
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
// Converts the given html content to pdf document.
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given payment received branding attributes.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceivedId
|
||||
* @returns {Promise<PaymentReceivedPdfTemplateAttributes>}
|
||||
*/
|
||||
async getPaymentBrandingAttributes(
|
||||
tenantId: number,
|
||||
paymentReceivedId: number
|
||||
): Promise<PaymentReceivedPdfTemplateAttributes> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentReceived = await this.getPaymentService.getPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceivedId
|
||||
);
|
||||
const templateId =
|
||||
paymentReceived?.pdfTemplateId ??
|
||||
(
|
||||
await PdfTemplate.query().findOne({
|
||||
resource: 'PaymentReceive',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
|
||||
const brandingTemplate =
|
||||
await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformPaymentReceivedToPdfTemplate(paymentReceived),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils';
|
||||
import { defaultPaymentReceivedPdfTemplateAttributes } from './constants';
|
||||
import { PdfTemplate } from '@/models/PdfTemplate';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceivedBrandingTemplate {
|
||||
@Inject()
|
||||
private getPdfTemplateService: GetPdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieves the payment received pdf template.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentTemplateId
|
||||
* @returns
|
||||
*/
|
||||
public async getPaymentReceivedPdfTemplate(
|
||||
tenantId: number,
|
||||
paymentTemplateId: number
|
||||
) {
|
||||
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||
tenantId,
|
||||
paymentTemplateId
|
||||
);
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
template.attributes,
|
||||
defaultPaymentReceivedPdfTemplateAttributes
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import composeAsync from 'async/compose';
|
||||
import {
|
||||
ICustomer,
|
||||
IPaymentReceived,
|
||||
@@ -12,6 +13,7 @@ import { PaymentReceivedIncrement } from './PaymentReceivedIncrement';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceiveDTOTransformer {
|
||||
@@ -24,6 +26,9 @@ export class PaymentReceiveDTOTransformer {
|
||||
@Inject()
|
||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||
|
||||
/**
|
||||
* Transformes the create payment receive DTO to model object.
|
||||
* @param {number} tenantId
|
||||
@@ -68,8 +73,16 @@ export class PaymentReceiveDTOTransformer {
|
||||
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
||||
entries,
|
||||
};
|
||||
const initialAsyncDTO = await composeAsync(
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
tenantId,
|
||||
'SaleInvoice'
|
||||
)
|
||||
)(initialDTO);
|
||||
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<IPaymentReceived>(tenantId)
|
||||
)(initialDTO);
|
||||
)(initialAsyncDTO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,3 +45,53 @@ export const PaymentsReceiveSampleData = [
|
||||
'Payment Amount': 850,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultPaymentReceivedPdfTemplateAttributes = {
|
||||
primaryColor: '#000',
|
||||
secondaryColor: '#000',
|
||||
showCompanyLogo: true,
|
||||
companyLogo: '',
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
billedToAddress: [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddress: [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
showBilledFromAddress: true,
|
||||
showBillingToAddress: true,
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
total: '$1000.00',
|
||||
totalLabel: 'Total',
|
||||
showTotal: true,
|
||||
|
||||
subtotal: '1000/00',
|
||||
subtotalLabel: 'Subtotal',
|
||||
showSubtotal: true,
|
||||
|
||||
lines: [
|
||||
{
|
||||
invoiceNumber: 'INV-00001',
|
||||
invoiceAmount: '$1000.00',
|
||||
paidAmount: '$1000.00',
|
||||
},
|
||||
],
|
||||
showPaymentReceivedNumber: true,
|
||||
paymentReceivedNumberLabel: 'Payment Number',
|
||||
paymentReceivedNumebr: '346D3D40-0001',
|
||||
|
||||
paymentReceivedDate: 'September 3, 2024',
|
||||
showPaymentReceivedDate: true,
|
||||
paymentReceivedDateLabel: 'Payment Date',
|
||||
};
|
||||
|
||||
21
packages/server/src/services/Sales/PaymentReceived/utils.ts
Normal file
21
packages/server/src/services/Sales/PaymentReceived/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
IPaymentReceived,
|
||||
PaymentReceivedPdfTemplateAttributes,
|
||||
} from '@/interfaces';
|
||||
|
||||
export const transformPaymentReceivedToPdfTemplate = (
|
||||
payment: IPaymentReceived
|
||||
): Partial<PaymentReceivedPdfTemplateAttributes> => {
|
||||
return {
|
||||
total: payment.formattedAmount,
|
||||
subtotal: payment.subtotalFormatted,
|
||||
paymentReceivedNumebr: payment.paymentReceiveNo,
|
||||
paymentReceivedDate: payment.formattedPaymentDate,
|
||||
customerName: payment.customer.displayName,
|
||||
lines: payment.entries.map((entry) => ({
|
||||
invoiceNumber: entry.invoice.invoiceNo,
|
||||
invoiceAmount: entry.invoice.totalFormatted,
|
||||
paidAmount: entry.paymentAmountFormatted,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { defaultSaleReceiptBrandingAttributes } from './constants';
|
||||
import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptBrandingTemplate {
|
||||
@Inject()
|
||||
private getPdfTemplateService: GetPdfTemplate;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the sale receipt branding template.
|
||||
* @param {number} tenantId - The ID of the tenant.
|
||||
* @param {number} templateId - The ID of the PDF template.
|
||||
* @returns {Promise<Object>} The sale receipt branding template with merged attributes.
|
||||
*/
|
||||
public async getSaleReceiptBrandingTemplate(
|
||||
tenantId: number,
|
||||
templateId: number
|
||||
) {
|
||||
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
template.attributes,
|
||||
defaultSaleReceiptBrandingAttributes
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { formatDateFields } from '@/utils';
|
||||
import { SaleReceiptIncrement } from './SaleReceiptIncrement';
|
||||
import { ItemEntry } from '@/models';
|
||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptDTOTransformer {
|
||||
@@ -30,6 +31,9 @@ export class SaleReceiptDTOTransformer {
|
||||
@Inject()
|
||||
private receiptIncrement: SaleReceiptIncrement;
|
||||
|
||||
@Inject()
|
||||
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||
|
||||
/**
|
||||
* Transform create DTO object to model object.
|
||||
* @param {ISaleReceiptDTO} saleReceiptDTO -
|
||||
@@ -88,9 +92,17 @@ export class SaleReceiptDTOTransformer {
|
||||
}),
|
||||
entries,
|
||||
};
|
||||
const initialAsyncDTO = await composeAsync(
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
tenantId,
|
||||
'SaleReceipt'
|
||||
)
|
||||
)(initialDTO);
|
||||
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<ISaleReceipt>(tenantId),
|
||||
this.warehouseDTOTransform.transformDTO<ISaleReceipt>(tenantId)
|
||||
)(initialDTO);
|
||||
)(initialAsyncDTO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
|
||||
import { transformReceiptToBrandingTemplateAttributes } from './utils';
|
||||
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptsPdf {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@@ -14,26 +21,64 @@ export class SaleReceiptsPdf {
|
||||
@Inject()
|
||||
private getSaleReceiptService: GetSaleReceipt;
|
||||
|
||||
@Inject()
|
||||
private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate;
|
||||
|
||||
/**
|
||||
* Retrieves sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} tenantId -
|
||||
* @param {number} saleInvoiceId -
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
|
||||
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||
const brandingAttributes = await this.getReceiptBrandingAttributes(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
// Converts the receipt template to html content.
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/receipt-regular',
|
||||
{
|
||||
saleReceipt,
|
||||
}
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
// Renders the html content to pdf document.
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves receipt branding attributes.
|
||||
* @param {number} tenantId
|
||||
* @param {number} receiptId
|
||||
* @returns {Promise<ISaleReceiptBrandingTemplateAttributes>}
|
||||
*/
|
||||
public async getReceiptBrandingAttributes(
|
||||
tenantId: number,
|
||||
receiptId: number
|
||||
): Promise<ISaleReceiptBrandingTemplateAttributes> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
// Retrieve the invoice template id of not found get the default template id.
|
||||
const templateId =
|
||||
saleReceipt.pdfTemplateId ??
|
||||
(
|
||||
await PdfTemplate.query().findOne({
|
||||
resource: 'SaleReceipt',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
// Retrieves the receipt branding template.
|
||||
const brandingTemplate =
|
||||
await this.saleReceiptBrandingTemplate.getSaleReceiptBrandingTemplate(
|
||||
tenantId,
|
||||
templateId
|
||||
);
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformReceiptToBrandingTemplateAttributes(saleReceipt),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ERRORS = {
|
||||
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',
|
||||
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR'
|
||||
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
@@ -47,22 +47,84 @@ export const DEFAULT_VIEWS = [
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export const SaleReceiptsSampleData = [
|
||||
{
|
||||
"Receipt Date": "2023-01-01",
|
||||
"Customer": "Randall Kohler",
|
||||
"Deposit Account": "Petty Cash",
|
||||
"Exchange Rate": "",
|
||||
"Receipt Number": "REC-00001",
|
||||
"Reference No.": "REF-0001",
|
||||
"Statement": "Delectus unde aut soluta et accusamus placeat.",
|
||||
"Receipt Message": "Vitae asperiores dicta.",
|
||||
"Closed": "T",
|
||||
"Item": "Schmitt Group",
|
||||
"Quantity": 100,
|
||||
"Rate": 200,
|
||||
"Line Description": "Distinctio distinctio sit veritatis consequatur iste quod veritatis."
|
||||
}
|
||||
|
||||
]
|
||||
'Receipt Date': '2023-01-01',
|
||||
Customer: 'Randall Kohler',
|
||||
'Deposit Account': 'Petty Cash',
|
||||
'Exchange Rate': '',
|
||||
'Receipt Number': 'REC-00001',
|
||||
'Reference No.': 'REF-0001',
|
||||
Statement: 'Delectus unde aut soluta et accusamus placeat.',
|
||||
'Receipt Message': 'Vitae asperiores dicta.',
|
||||
Closed: 'T',
|
||||
Item: 'Schmitt Group',
|
||||
Quantity: 100,
|
||||
Rate: 200,
|
||||
'Line Description':
|
||||
'Distinctio distinctio sit veritatis consequatur iste quod veritatis.',
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultSaleReceiptBrandingAttributes = {
|
||||
primaryColor: '',
|
||||
secondaryColor: '',
|
||||
showCompanyLogo: true,
|
||||
companyLogo: '',
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
// # Address
|
||||
billedToAddress: [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddress: [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
showBilledFromAddress: true,
|
||||
showBilledToAddress: true,
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
total: '$1000.00',
|
||||
totalLabel: 'Total',
|
||||
showTotal: true,
|
||||
|
||||
subtotal: '1000/00',
|
||||
subtotalLabel: 'Subtotal',
|
||||
showSubtotal: true,
|
||||
|
||||
showCustomerNote: true,
|
||||
customerNote:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
customerNoteLabel: 'Customer Note',
|
||||
|
||||
showTermsConditions: true,
|
||||
termsConditions:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
|
||||
lines: [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
showReceiptNumber: true,
|
||||
receiptNumberLabel: 'Receipt Number',
|
||||
receiptNumebr: '346D3D40-0001',
|
||||
|
||||
receiptDate: 'September 3, 2024',
|
||||
showReceiptDate: true,
|
||||
receiptDateLabel: 'Receipt Date',
|
||||
};
|
||||
|
||||
20
packages/server/src/services/Sales/Receipts/utils.ts
Normal file
20
packages/server/src/services/Sales/Receipts/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ISaleReceipt, ISaleReceiptBrandingTemplateAttributes } from "@/interfaces";
|
||||
|
||||
|
||||
|
||||
export const transformReceiptToBrandingTemplateAttributes = (saleReceipt: ISaleReceipt): Partial<ISaleReceiptBrandingTemplateAttributes> => {
|
||||
return {
|
||||
total: saleReceipt.formattedAmount,
|
||||
subtotal: saleReceipt.formattedSubtotal,
|
||||
lines: saleReceipt.entries?.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
|
||||
receiptNumber: saleReceipt.receiptNumber,
|
||||
receiptDate: saleReceipt.formattedReceiptDate,
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export class TemplateInjectable {
|
||||
public async render(
|
||||
tenantId: number,
|
||||
filename: string,
|
||||
options: Record<string, string | number | boolean>
|
||||
options: Record<string, any>
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
|
||||
|
||||
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
|
||||
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed'
|
||||
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed',
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -684,4 +684,19 @@ export default {
|
||||
import: {
|
||||
onImportCommitted: 'onImportFileCommitted',
|
||||
},
|
||||
|
||||
// Branding templates
|
||||
pdfTemplate: {
|
||||
onCreating: 'onPdfTemplateCreating',
|
||||
onCreated: 'onPdfTemplateCreated',
|
||||
|
||||
onEditing: 'onPdfTemplateEditing',
|
||||
onEdited: 'onPdfTemplatedEdited',
|
||||
|
||||
onDeleting: 'onPdfTemplateDeleting',
|
||||
onDeleted: 'onPdfTemplateDeleted',
|
||||
|
||||
onAssignedDefault: 'onPdfTemplateAssignedDefault',
|
||||
onAssigningDefault: 'onPdfTemplateAssigningDefault',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^1.0.6",
|
||||
"react-body-classname": "^1.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-content-loader": "^6.0.1",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export function Card({ className, children }) {
|
||||
return <CardRoot className={className}>{children}</CardRoot>;
|
||||
export function Card({ className, style, children }) {
|
||||
return (
|
||||
<CardRoot className={className} style={style}>
|
||||
{children}
|
||||
</CardRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const CardRoot = styled.div`
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
Classes,
|
||||
Tooltip,
|
||||
Position,
|
||||
MenuItem,
|
||||
Menu,
|
||||
MenuDivider,
|
||||
} from '@blueprintjs/core';
|
||||
import { Popover2 } from '@blueprintjs/popover2';
|
||||
|
||||
import { FormattedMessage as T, Icon, Hint, If } from '@/components';
|
||||
|
||||
import DashboardTopbarUser from '@/components/Dashboard/TopbarUser';
|
||||
@@ -19,9 +24,20 @@ import DashboardBackLink from '@/components/Dashboard/DashboardBackLink';
|
||||
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown';
|
||||
import { DashboardHamburgerButton, DashboardQuickSearchButton } from './_components';
|
||||
import {
|
||||
DashboardHamburgerButton,
|
||||
DashboardQuickSearchButton,
|
||||
} from './_components';
|
||||
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import {
|
||||
COMMUNITY_BIGCAPITAL_LINK,
|
||||
DOCS_BIGCAPITAL_LINK,
|
||||
} from '@/constants/routes';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
@@ -41,6 +57,9 @@ function DashboardTopbar({
|
||||
|
||||
// #withGlobalSearch
|
||||
openGlobalSearch,
|
||||
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
@@ -112,11 +131,34 @@ function DashboardTopbar({
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'help-24'} iconSize={20} />}
|
||||
text={<T id={'help'} />}
|
||||
/>
|
||||
<Popover2
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={'Documents'}
|
||||
onClick={() => window.open(DOCS_BIGCAPITAL_LINK)}
|
||||
labelElement={<Icon icon={'share'} iconSize={16} />}
|
||||
/>
|
||||
<MenuItem
|
||||
text={'Community support'}
|
||||
onClick={() => window.open(COMMUNITY_BIGCAPITAL_LINK)}
|
||||
labelElement={<Icon icon={'share'} iconSize={16} />}
|
||||
/>
|
||||
<MenuItem
|
||||
text={'Keyboard shortcuts'}
|
||||
onClick={() => openDialog(DialogsName.KeyboardShortcutForm)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem text={'Share feedback'} />
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'help-24'} iconSize={20} />}
|
||||
text={<T id={'help'} />}
|
||||
/>
|
||||
</Popover2>
|
||||
<NavbarDivider />
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
@@ -138,4 +180,5 @@ export default compose(
|
||||
pageHint,
|
||||
})),
|
||||
withDashboardActions,
|
||||
withDialogActions,
|
||||
)(DashboardTopbar);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { camelCase} from 'lodash';
|
||||
import { camelCase } from 'lodash';
|
||||
|
||||
import { If, Skeleton } from '@/components';
|
||||
import { useAppIntlContext } from '@/components/AppIntlProvider';
|
||||
import TableContext from './TableContext';
|
||||
import { saveInvoke, ignoreEventFromSelectors } from '@/utils';
|
||||
import { isCellLoading } from './utils';
|
||||
import { MoneyDisplay } from '../Money/MoneyDisplay';
|
||||
|
||||
const ROW_CLICK_SELECTORS_INGORED = ['.expand-toggle', '.selection-checkbox'];
|
||||
|
||||
@@ -58,7 +59,7 @@ export default function TableCell({ cell, row, index }) {
|
||||
return;
|
||||
}
|
||||
saveInvoke(onCellClick, cell, event);
|
||||
};
|
||||
};
|
||||
const cellType = camelCase(cell.column.Cell.cellType) || 'text';
|
||||
|
||||
return (
|
||||
@@ -109,7 +110,11 @@ export default function TableCell({ cell, row, index }) {
|
||||
</span>
|
||||
</If>
|
||||
|
||||
{cell.render('Cell')}
|
||||
{cell.column?.money ? (
|
||||
<MoneyDisplay>{cell.render('Cell')}</MoneyDisplay>
|
||||
) : (
|
||||
<>{cell.render('Cell')}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
const DrawerContext = createContext();
|
||||
interface DrawerContextValue {
|
||||
name: string;
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
|
||||
const DrawerContext = createContext<DrawerContextValue>(
|
||||
{} as DrawerContextValue,
|
||||
);
|
||||
|
||||
/**
|
||||
* Account form provider.
|
||||
|
||||
@@ -23,6 +23,12 @@ import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransfe
|
||||
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
||||
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
||||
import ChangeSubscriptionPlanDrawer from '@/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer';
|
||||
import { InvoiceCustomizeDrawer } from '@/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeDrawer';
|
||||
import { EstimateCustomizeDrawer } from '@/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer';
|
||||
import { ReceiptCustomizeDrawer } from '@/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeDrawer';
|
||||
import { CreditNoteCustomizeDrawer } from '@/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer';
|
||||
import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeDrawer';
|
||||
import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer';
|
||||
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
@@ -65,6 +71,14 @@ export default function DrawersContainer() {
|
||||
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
||||
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
||||
<ChangeSubscriptionPlanDrawer name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN} />
|
||||
<InvoiceCustomizeDrawer name={DRAWERS.INVOICE_CUSTOMIZE} />
|
||||
<EstimateCustomizeDrawer name={DRAWERS.ESTIMATE_CUSTOMIZE} />
|
||||
<ReceiptCustomizeDrawer name={DRAWERS.RECEIPT_CUSTOMIZE} />
|
||||
<CreditNoteCustomizeDrawer name={DRAWERS.CREDIT_NOTE_CUSTOMIZE} />
|
||||
<PaymentReceivedCustomizeDrawer
|
||||
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
|
||||
/>
|
||||
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
15
packages/webapp/src/components/Forms/ColorInput.module.scss
Normal file
15
packages/webapp/src/components/Forms/ColorInput.module.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
.field{
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.colorPicker{
|
||||
background-color: rgb(103, 114, 229);
|
||||
border-radius: 3px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
85
packages/webapp/src/components/Forms/ColorInput.tsx
Normal file
85
packages/webapp/src/components/Forms/ColorInput.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useState } from 'react';
|
||||
import clsx from 'classnames';
|
||||
import {
|
||||
IInputGroupProps,
|
||||
InputGroup,
|
||||
IPopoverProps,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||
import { Box, BoxProps } from '@/components';
|
||||
import { sanitizeToHexColor } from '@/utils/sanitize-hex-color';
|
||||
import styles from './ColorInput.module.scss';
|
||||
|
||||
export interface ColorInputProps {
|
||||
value?: string;
|
||||
initialValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
popoverProps?: Partial<IPopoverProps>;
|
||||
inputProps?: Partial<IInputGroupProps>;
|
||||
pickerProps?: Partial<BoxProps>;
|
||||
pickerWrapProps?: Partial<BoxProps>;
|
||||
}
|
||||
|
||||
export function ColorInput({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
popoverProps,
|
||||
inputProps,
|
||||
pickerWrapProps,
|
||||
pickerProps,
|
||||
}: ColorInputProps) {
|
||||
const [_value, handleChange] = useUncontrolled({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
finalValue: '',
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={<HexColorPicker color={_value} onChange={handleChange} />}
|
||||
position={Position.BOTTOM}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
minimal
|
||||
{...popoverProps}
|
||||
>
|
||||
<InputGroup
|
||||
value={_value}
|
||||
leftElement={
|
||||
<Box
|
||||
{...pickerWrapProps}
|
||||
style={{ padding: 8, ...pickerWrapProps?.style }}
|
||||
>
|
||||
<Box
|
||||
onClick={() => setIsOpen((oldValue) => !oldValue)}
|
||||
style={{ backgroundColor: _value }}
|
||||
className={clsx(styles.colorPicker, pickerProps?.className)}
|
||||
{...pickerProps}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = sanitizeToHexColor(e.currentTarget.value);
|
||||
handleChange(value);
|
||||
}}
|
||||
{...inputProps}
|
||||
className={clsx(styles.field, inputProps?.className)}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
64
packages/webapp/src/components/Forms/FColorInput.tsx
Normal file
64
packages/webapp/src/components/Forms/FColorInput.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { getIn, FieldConfig, FieldProps } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Field } from '@blueprintjs-formik/core';
|
||||
import { ColorInput, ColorInputProps } from './ColorInput';
|
||||
|
||||
interface ColorInputInputGroupProps
|
||||
extends Omit<FieldConfig, 'children' | 'component' | 'as' | 'value'>,
|
||||
ColorInputProps {}
|
||||
|
||||
export interface ColorInputToInputProps
|
||||
extends Omit<FieldProps, 'onChange'>,
|
||||
ColorInputProps {}
|
||||
|
||||
/**
|
||||
* Transforms field props to input group props for ColorInput.
|
||||
* @param {ColorInputToInputProps}
|
||||
* @returns {ColorInputProps}
|
||||
*/
|
||||
function fieldToColorInputInputGroup({
|
||||
field: { onBlur: onFieldBlur, onChange: onFieldChange, value, ...field },
|
||||
form: { touched, errors, setFieldValue },
|
||||
onChange,
|
||||
...props
|
||||
}: ColorInputToInputProps): ColorInputProps {
|
||||
const fieldError = getIn(errors, field.name);
|
||||
const showError = getIn(touched, field.name) && !!fieldError;
|
||||
|
||||
return {
|
||||
inputProps: {
|
||||
intent: showError ? Intent.DANGER : Intent.NONE,
|
||||
},
|
||||
value,
|
||||
onChange:
|
||||
onChange ??
|
||||
function (value: string) {
|
||||
setFieldValue(field.name, value);
|
||||
},
|
||||
...field,
|
||||
...props,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms field props to input group props for ColorInput.
|
||||
* @param {ColorInputToInputProps} props -
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function ColorInputToInputGroup({
|
||||
...props
|
||||
}: ColorInputToInputProps): JSX.Element {
|
||||
return <ColorInput {...fieldToColorInputInputGroup(props)} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input group Blueprint component binded with Formik for ColorInput.
|
||||
* @param {ColorInputInputGroupProps}
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function FColorInput({
|
||||
...props
|
||||
}: ColorInputInputGroupProps): JSX.Element {
|
||||
return <Field {...props} component={ColorInputToInputGroup} />;
|
||||
}
|
||||
@@ -6,16 +6,14 @@ import styled from 'styled-components';
|
||||
import clsx from 'classnames';
|
||||
|
||||
export function FSelect({ ...props }) {
|
||||
const input = ({ activeItem, text, label, value }) => {
|
||||
return (
|
||||
<SelectButton
|
||||
text={text || props.placeholder || 'Select an item ...'}
|
||||
disabled={props.disabled || false}
|
||||
{...props.buttonProps}
|
||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const input = ({ activeItem, text, label, value }) => (
|
||||
<SelectButton
|
||||
text={text || props.placeholder || 'Select an item ...'}
|
||||
disabled={props.disabled || false}
|
||||
{...props.buttonProps}
|
||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||
/>
|
||||
);
|
||||
return <Select input={input} fill={true} {...props} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
.root {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
9
packages/webapp/src/components/Money/MoneyDisplay.tsx
Normal file
9
packages/webapp/src/components/Money/MoneyDisplay.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import styles from './MoneyDisplay.module.scss';
|
||||
|
||||
interface MoneyDisplayProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MoneyDisplay({ children }: MoneyDisplayProps) {
|
||||
return <span className={styles.root}>{children}</span>;
|
||||
}
|
||||
@@ -24,5 +24,12 @@ export enum DRAWERS {
|
||||
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
||||
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
||||
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
||||
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan'
|
||||
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan',
|
||||
INVOICE_CUSTOMIZE = 'INVOICE_CUSTOMIZE',
|
||||
ESTIMATE_CUSTOMIZE = 'ESTIMATE_CUSTOMIZE',
|
||||
PAYMENT_RECEIPT_CUSTOMIZE = 'PAYMENT_RECEIPT_CUSTOMIZE',
|
||||
RECEIPT_CUSTOMIZE = 'RECEIPT_CUSTOMIZE',
|
||||
CREDIT_NOTE_CUSTOMIZE = 'CREDIT_NOTE_CUSTOMIZE',
|
||||
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
|
||||
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES'
|
||||
}
|
||||
|
||||
2
packages/webapp/src/constants/routes.ts
Normal file
2
packages/webapp/src/constants/routes.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const DOCS_BIGCAPITAL_LINK = 'https://docs.bigcapital.app';
|
||||
export const COMMUNITY_BIGCAPITAL_LINK = 'https://community.bigcapital.app';
|
||||
@@ -27,6 +27,7 @@ export const useManualJournalsColumns = () => {
|
||||
accessor: 'formatted_amount',
|
||||
width: 115,
|
||||
clickable: true,
|
||||
money: true,
|
||||
align: 'right',
|
||||
className: clsx(CLASSES.FONT_BOLD),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Intent, Tag } from '@blueprintjs/core';
|
||||
import { Intent, Tag, Classes } from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
|
||||
import { If, AppToaster } from '@/components';
|
||||
import { NormalCell, BalanceCell, BankBalanceCell } from './components';
|
||||
@@ -73,7 +74,7 @@ export const useAccountsTableColumns = () => {
|
||||
id: 'type',
|
||||
Header: intl.get('type'),
|
||||
accessor: 'account_type_label',
|
||||
className: 'type',
|
||||
className: clsx('type', Classes.TEXT_MUTED),
|
||||
width: 140,
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
@@ -91,6 +92,7 @@ export const useAccountsTableColumns = () => {
|
||||
id: 'currency',
|
||||
Header: intl.get('currency'),
|
||||
accessor: 'currency_code',
|
||||
className: clsx(Classes.TEXT_MUTED),
|
||||
width: 75,
|
||||
clickable: true,
|
||||
},
|
||||
@@ -102,6 +104,7 @@ export const useAccountsTableColumns = () => {
|
||||
width: 150,
|
||||
clickable: true,
|
||||
align: 'right',
|
||||
money: true,
|
||||
},
|
||||
{
|
||||
id: 'balance',
|
||||
@@ -110,6 +113,7 @@ export const useAccountsTableColumns = () => {
|
||||
Cell: BalanceCell,
|
||||
width: 150,
|
||||
clickable: true,
|
||||
money: true,
|
||||
align: 'right',
|
||||
},
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user