Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
415673656c | ||
|
|
1906d9f3f5 | ||
|
|
d640dc1f40 | ||
|
|
8cd1b36a02 | ||
|
|
5a8d9cc7e8 | ||
|
|
6323e2ffec | ||
|
|
7af2e7ccbc | ||
|
|
baf4c691d6 | ||
|
|
c633fa8522 | ||
|
|
1d54947764 | ||
|
|
477da0e7c0 | ||
|
|
b9963aa241 | ||
|
|
994c441bb8 | ||
|
|
0a5115fc20 | ||
|
|
11d7a40326 | ||
|
|
46719ef361 | ||
|
|
14ae978bde | ||
|
|
beec09788e | ||
|
|
391dc77071 | ||
|
|
38f2004e56 | ||
|
|
5a5eac246b | ||
|
|
a7bafd4f62 | ||
|
|
a25ab39647 | ||
|
|
7dd09e2903 | ||
|
|
6ab461a212 | ||
|
|
fabc88c81a | ||
|
|
3a19518440 | ||
|
|
56b5e3469e | ||
|
|
542763ddf5 | ||
|
|
1010d97a92 | ||
|
|
d5dacaa988 | ||
|
|
154ade9647 | ||
|
|
5b75fa9286 | ||
|
|
05cf94940e | ||
|
|
03b0d2519b | ||
|
|
000c3e40e1 | ||
|
|
ffb06f5194 | ||
|
|
73ab92e693 | ||
|
|
dd1392cdc8 | ||
|
|
17b3bbe1d8 | ||
|
|
e02ad1e795 | ||
|
|
df8391201f | ||
|
|
aa4aaeb612 | ||
|
|
09b98664c5 | ||
|
|
be46147d9a | ||
|
|
1fe7d58c8c | ||
|
|
4f57782be4 | ||
|
|
7b5f0d3930 | ||
|
|
831fb9180c | ||
|
|
2594e37dc7 | ||
|
|
ca44d6346d | ||
|
|
df41de7239 | ||
|
|
459bf4cd55 | ||
|
|
3537a05ea2 | ||
|
|
da47418f17 | ||
|
|
d5b0546301 | ||
|
|
b6f3c0145f | ||
|
|
c5c85bdfbe | ||
|
|
63a95df534 | ||
|
|
6103f1e4c7 | ||
|
|
3e591beb03 | ||
|
|
c6db54175f | ||
|
|
f7bf24acb3 | ||
|
|
ddffe630ff | ||
|
|
2c54092591 | ||
|
|
7e65f3f642 | ||
|
|
7df6aa4110 | ||
|
|
7df316aa56 | ||
|
|
53ab40a075 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -2,6 +2,66 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
# [0.22.0]
|
||||
|
||||
* feat: estimate, receipt, credit note mail preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/757
|
||||
* feat: Add discount to transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/758
|
||||
* fix: update financial forms to use new formatted amount utilities and… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/760
|
||||
* fix: total lines style by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/761
|
||||
* fix: discount & adjustment sale transactions bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/762
|
||||
* fix: discount transactions GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/763
|
||||
|
||||
# [0.21.2]
|
||||
|
||||
* hotbug: upload attachments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/755
|
||||
|
||||
# [0.21.1]
|
||||
|
||||
* fix: download invoice document on payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/750
|
||||
* fix: attach branding template attrs to payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/751
|
||||
* fix: make manual entries adjust decimal credit/debit amounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/754
|
||||
* feat: allow quantity of entries accept decimal value by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/753
|
||||
|
||||
# [0.21.0]
|
||||
|
||||
* fix: Credit and debit totals not balancing when decimal values are used by @nklmantey in https://github.com/bigcapitalhq/bigcapital/pull/722
|
||||
* docs: add nklmantey as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/725
|
||||
* feat: track more services events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/721
|
||||
* feat: Invoice mail receipt preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/723
|
||||
* fix: change the send mail button on invoice drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/730
|
||||
* refactor: notification mail services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/731
|
||||
* fix: attach payment link in sending invoice mail receipt by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/732
|
||||
* fix: send invoice drawer layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/733
|
||||
* fix: hook up cc and bcc fields to mail sender by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/734
|
||||
* fix: company logo does not show up in mail receipt preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/736
|
||||
* fix: change default invoice mail message by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/737
|
||||
* fix: typing invoice send mail fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/738
|
||||
* fix: clean up ivnoice mail receipt preview component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/739
|
||||
* feat: add shared package to pdf templates to render in the server and… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/735
|
||||
* feat: getting invoice preview on send mail view by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/740
|
||||
* fix: style SSR invoice paper template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/741
|
||||
* fix: send invoice receipt addresses by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/742
|
||||
* fix: due invoice server invoice by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/744
|
||||
* fix: `BIG-265` forgot password text by @ibutiti in https://github.com/bigcapitalhq/bigcapital/pull/745
|
||||
* Crims on sv translation by @Crims-on in https://github.com/bigcapitalhq/bigcapital/pull/671
|
||||
* feat: Added Spanish language to the App 🇪🇸 by @angelosorno in https://github.com/bigcapitalhq/bigcapital/pull/530
|
||||
* fix: mail services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/746
|
||||
* fix: company logo of the template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/747
|
||||
* fix: monorepo dependencies scope by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/748
|
||||
|
||||
# [0.20.6]
|
||||
|
||||
* fix: Import category column of item resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/710
|
||||
* fix: Parse the uppercase values in importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/711
|
||||
* chore: Move i18nApply localization to the account transformer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/713
|
||||
* fix: Sync Plaid credit card account type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/714
|
||||
* fix: Sync account normal of cashflow GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/715
|
||||
* feat: Add quantity column to pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/716
|
||||
* feat: Pre-line invoice statements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/717
|
||||
* feat: Invoice number in downloaded pdf document by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/718
|
||||
* feat: Track events of pdf documents views by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/719
|
||||
* fix: Customer note does not appear in pdf document by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/720
|
||||
|
||||
# [0.20.5]
|
||||
|
||||
* fix: Disable tabs of the pdf customization if the first field not filed up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/701
|
||||
|
||||
4
packages/server/.gitignore
vendored
4
packages/server/.gitignore
vendored
@@ -4,5 +4,5 @@ stdout.log
|
||||
/dist
|
||||
/build
|
||||
/public/imports
|
||||
|
||||
dist
|
||||
dist
|
||||
newrelic_agent.log
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
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}-header {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
flex: 0 0 auto;
|
||||
-webkit-box-align: start;
|
||||
align-items: start;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
.#{prefix}-header-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{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;
|
||||
align-items: flex-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__header--item{
|
||||
width: 50%;
|
||||
}
|
||||
.#{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}-table__cell--item .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item .item__description{
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.#{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 {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
block content
|
||||
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||
|
||||
//- Header (invluces big title, details and logo)
|
||||
div(class=`${prefix}-header`)
|
||||
|
||||
//- Header details (includes big title and details )
|
||||
div(class=`${prefix}-header-details`)
|
||||
h1(class=`${prefix}-big-title`) Estimate
|
||||
|
||||
//- 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}
|
||||
|
||||
//- Company logo
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(alt="Company logo", src=companyLogoUri)
|
||||
|
||||
//- Addresses (Group section)
|
||||
div(class=`${prefix}-addresses`)
|
||||
if showCompanyAddress
|
||||
div(class=`${prefix}-address-from`)
|
||||
div !{companyAddress}
|
||||
|
||||
if showCustomerAddress
|
||||
div(class=`${prefix}-address-to`)
|
||||
strong #{billedToLabel}
|
||||
div !{customerAddress}
|
||||
|
||||
//- Table section (Line items)
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
|
||||
tbody
|
||||
each line in lines
|
||||
tr
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
|
||||
div.item
|
||||
div.item__label #{line.item}
|
||||
div.item__description #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${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 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}
|
||||
|
||||
//- Statements section
|
||||
if showTermsConditions && termsConditions
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
|
||||
div(class=`${prefix}-statement__value`) #{termsConditions}
|
||||
|
||||
if showCustomerNote && customerNote
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{customerNoteLabel}
|
||||
div(class=`${prefix}-statement__value`) #{customerNote}
|
||||
@@ -1,272 +0,0 @@
|
||||
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}-header{
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
flex: 0 0 auto;
|
||||
-webkit-box-align: start;
|
||||
align-items: start;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
.#{prefix}-header-details{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{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;
|
||||
align-items: flex-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__header--item {
|
||||
width: 50%;
|
||||
}
|
||||
.#{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}-table__cell--item .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item__description {
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.#{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 {
|
||||
white-space: pre-line;
|
||||
}
|
||||
block content
|
||||
//- block head
|
||||
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||
|
||||
//- Header (includes big title, details and logo )
|
||||
div(class=`${prefix}-header`)
|
||||
//- Header details (includes big title and details )
|
||||
div(class=`${prefix}-header-details`)
|
||||
//- Title and company logo
|
||||
h1(class=`${prefix}-big-title`) Invoice
|
||||
|
||||
//- 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}
|
||||
|
||||
//- Company logo
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(alt="Company logo", src=companyLogoUri)
|
||||
|
||||
//- Address section
|
||||
div(class=`${prefix}-address-root`)
|
||||
if showCompanyAddress
|
||||
div(class=`${prefix}-address-from`)
|
||||
div !{companyAddress}
|
||||
|
||||
if showCustomerAddress
|
||||
div(class=`${prefix}-address-to`)
|
||||
strong #{billedToLabel}
|
||||
div !{customerAddress}
|
||||
|
||||
//- Invoice table
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{lineItemLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{lineQuantityLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{lineRateLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{lineTotalLabel}
|
||||
tbody
|
||||
each line in lines
|
||||
tr
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
|
||||
div.item
|
||||
div.item__label #{line.item}
|
||||
div.item__description #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
|
||||
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,192 +0,0 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
- 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}-header{
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
flex: 0 0 auto;
|
||||
-webkit-box-align: start;
|
||||
align-items: start;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
.#{prefix}-header-details{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-big-title{
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{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;
|
||||
align-items: flex-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(class=`${prefix}-root`)
|
||||
//- Header (includes big title, details and logo )
|
||||
div(class=`${prefix}-header`)
|
||||
//- Header details (includes big title and details )
|
||||
div(class=`${prefix}-header-details`)
|
||||
div(class=`${prefix}-big-title`) Payment
|
||||
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}
|
||||
|
||||
if showPaymentReceivedDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
|
||||
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogoUri alt="Company Logo")
|
||||
|
||||
div(class=`${prefix}-addresses`)
|
||||
if showCompanyAddress
|
||||
div(class=`${prefix}-address-from`)
|
||||
div !{companyAddress}
|
||||
|
||||
if showCustomerAddress
|
||||
div(class=`${prefix}-address-to`)
|
||||
strong #{billedToLabel}
|
||||
div !{customerAddress}
|
||||
|
||||
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
|
||||
|
||||
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(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}
|
||||
|
||||
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,231 +0,0 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
- 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}-header{
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
flex: 0 0 auto;
|
||||
align-items: start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
.#{prefix}-header-details{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
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: flex-start;
|
||||
align-items: flex-start;
|
||||
-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__header--item{
|
||||
width: 50%;
|
||||
}
|
||||
.#{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}-table__cell--item .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item .item__description{
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.#{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 {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
block content
|
||||
//- block head
|
||||
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||
|
||||
//- Header (includes big title, details and logo )
|
||||
div(class=`${prefix}-header`)
|
||||
//- Header details (includes big title and details )
|
||||
div(class=`${prefix}-header-details`)
|
||||
//- Title and company logo
|
||||
h1(class=`${prefix}-big-title`) Receipt
|
||||
|
||||
//- 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
|
||||
|
||||
//- Company logo
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogoUri alt=`Company Logo`)
|
||||
|
||||
//- Address Section
|
||||
div(class=`${prefix}-address-section`)
|
||||
if showCompanyAddress
|
||||
div(class=`${prefix}-address-from`)
|
||||
div !{companyAddress}
|
||||
|
||||
if showCustomerAddress
|
||||
div(class=`${prefix}-address-to`)
|
||||
strong #{billedToLabel}
|
||||
div !{customerAddress}
|
||||
|
||||
//- Table Section
|
||||
table(class=`${prefix}-table`)
|
||||
thead(class=`${prefix}-table__header`)
|
||||
tr
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
|
||||
tbody
|
||||
each line in lines
|
||||
tr(class=`${prefix}-table__row`)
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
|
||||
div.item
|
||||
div.item__label #{line.item}
|
||||
div.item__description #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
|
||||
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__line ${prefix}-totals__line--gray-border`)
|
||||
span(class=`${prefix}-totals__line__label`)= subtotalLabel
|
||||
span(class=`${prefix}-totals__line__amount`)= subtotal
|
||||
|
||||
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
|
||||
|
||||
//- Customer Note Section
|
||||
if showCustomerNote && customerNote
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`)= customerNoteLabel
|
||||
div(class=`${prefix}-statement__value`)= customerNote
|
||||
|
||||
//- Terms & Conditions Section
|
||||
if showTermsConditions && termsConditions
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`)= termsConditionsLabel
|
||||
div(class=`${prefix}-statement__value`)= termsConditions
|
||||
@@ -4,6 +4,7 @@ import { check, param, query } from 'express-validator';
|
||||
import {
|
||||
AbilitySubject,
|
||||
BillAction,
|
||||
DiscountType,
|
||||
IBillDTO,
|
||||
IBillEditDTO,
|
||||
} from '@/interfaces';
|
||||
@@ -126,6 +127,11 @@ export default class BillsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.discount_type')
|
||||
.default(DiscountType.Percentage)
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.landed_cost')
|
||||
.optional({ nullable: true })
|
||||
@@ -144,8 +150,18 @@ export default class BillsController extends BaseController {
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
// Attachments
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// # Discount
|
||||
check('discount_type')
|
||||
.default(DiscountType.Amount)
|
||||
.isIn([DiscountType.Amount, DiscountType.Percentage]),
|
||||
check('discount').optional({ nullable: true }).isDecimal().toFloat(),
|
||||
|
||||
// # Adjustment
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -188,6 +204,15 @@ export default class BillsController extends BaseController {
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// # Discount
|
||||
check('discount_type')
|
||||
.default(DiscountType.Amount)
|
||||
.isIn([DiscountType.Amount, DiscountType.Percentage]),
|
||||
check('discount').optional({ nullable: true }).isDecimal().toFloat(),
|
||||
|
||||
// # Adjustment
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
DiscountType,
|
||||
IVendorCreditCreateDTO,
|
||||
IVendorCreditEditDTO,
|
||||
VendorCreditAction,
|
||||
@@ -175,6 +176,10 @@ export default class VendorCreditController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.discount_type')
|
||||
.default(DiscountType.Percentage)
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
@@ -183,6 +188,16 @@ export default class VendorCreditController extends BaseController {
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Discount.
|
||||
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('discount_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
// Adjustment.
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -214,6 +229,10 @@ export default class VendorCreditController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.discount_type')
|
||||
.default(DiscountType.Percentage)
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
@@ -222,6 +241,16 @@ export default class VendorCreditController extends BaseController {
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Discount.
|
||||
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('discount_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
// Adjustment.
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
CreditNoteAction,
|
||||
DiscountType,
|
||||
ICreditNoteEditDTO,
|
||||
ICreditNoteNewDTO,
|
||||
} from '@/interfaces';
|
||||
@@ -238,17 +239,32 @@ export default class PaymentReceivesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.discount_type')
|
||||
.default(DiscountType.Percentage)
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
// Attachments.
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Pdf template id.
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
// Discount.
|
||||
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('discount_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
// Adjustment.
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -755,8 +771,9 @@ export default class PaymentReceivesController extends BaseController {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.getCreditNoteStateService.getCreditNoteState(tenantId);
|
||||
const data = await this.getCreditNoteStateService.getCreditNoteState(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -130,9 +130,18 @@ export default class PaymentReceivesController extends BaseController {
|
||||
[
|
||||
...this.paymentReceiveValidation,
|
||||
body('subject').isString().optional(),
|
||||
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
body('body').isString().optional(),
|
||||
|
||||
body('to').isArray().exists(),
|
||||
body('to.*').isString().isEmail().optional(),
|
||||
|
||||
body('cc').isArray().optional({ nullable: true }),
|
||||
body('cc.*').isString().isEmail().optional(),
|
||||
|
||||
body('bcc').isArray().optional({ nullable: true }),
|
||||
body('bcc.*').isString().isEmail().optional(),
|
||||
|
||||
body('attach_invoice').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
this.sendPaymentReceiveByMail.bind(this),
|
||||
@@ -470,8 +479,9 @@ export default class PaymentReceivesController extends BaseController {
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
|
||||
]);
|
||||
// Response in pdf format.
|
||||
// Responds pdf format.
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const [pdfContent, filename] =
|
||||
await this.paymentReceiveApplication.getPaymentReceivePdf(
|
||||
@@ -484,7 +494,15 @@ export default class PaymentReceivesController extends BaseController {
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Response in json format.
|
||||
// Responds html format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
|
||||
const htmlContent =
|
||||
await this.paymentReceiveApplication.getPaymentReceivedHtml(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
return res.status(200).send({ htmlContent });
|
||||
// Responds json format.
|
||||
} else {
|
||||
const paymentReceive =
|
||||
await this.paymentReceiveApplication.getPaymentReceive(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { body, check, param, query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
DiscountType,
|
||||
ISaleEstimateDTO,
|
||||
SaleEstimateAction,
|
||||
SaleEstimateMailOptionsDTO,
|
||||
@@ -13,11 +14,8 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { SaleEstimatesApplication } from '@/services/Sales/Estimates/SaleEstimatesApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
const ACCEPT_TYPE = {
|
||||
APPLICATION_PDF: 'application/pdf',
|
||||
APPLICATION_JSON: 'application/json',
|
||||
};
|
||||
@Service()
|
||||
export default class SalesEstimatesController extends BaseController {
|
||||
@Inject()
|
||||
@@ -133,8 +131,18 @@ export default class SalesEstimatesController extends BaseController {
|
||||
[
|
||||
...this.validateSpecificEstimateSchema,
|
||||
body('subject').isString().optional(),
|
||||
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
|
||||
body('to').isArray().exists(),
|
||||
body('to.*').isString().isEmail().optional(),
|
||||
|
||||
body('cc').isArray().optional({ nullable: true }),
|
||||
body('cc.*').isString().isEmail().optional(),
|
||||
|
||||
body('bcc').isArray().optional({ nullable: true }),
|
||||
body('bcc.*').isString().isEmail().optional(),
|
||||
|
||||
body('body').isString().optional(),
|
||||
body('attach_invoice').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
@@ -143,10 +151,10 @@ export default class SalesEstimatesController extends BaseController {
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/mail',
|
||||
'/:id/mail/state',
|
||||
[...this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSaleEstimateMail.bind(this)),
|
||||
asyncMiddleware(this.getSaleEstimateMailState.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
@@ -179,6 +187,11 @@ export default class SalesEstimatesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.discount_type')
|
||||
.default(DiscountType.Percentage)
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
@@ -188,11 +201,21 @@ export default class SalesEstimatesController extends BaseController {
|
||||
check('terms_conditions').optional().trim(),
|
||||
check('send_to_email').optional().trim(),
|
||||
|
||||
// # Attachments
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Pdf template id.
|
||||
// # Pdf template id.
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
// # Discount
|
||||
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('discount_type')
|
||||
.default(DiscountType.Amount)
|
||||
.isIn([DiscountType.Amount, DiscountType.Percentage]),
|
||||
|
||||
// # Adjustment
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -395,6 +418,7 @@ export default class SalesEstimatesController extends BaseController {
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
|
||||
]);
|
||||
// Retrieves estimate in pdf format.
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||
@@ -410,7 +434,14 @@ export default class SalesEstimatesController extends BaseController {
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Retrieves estimates in json format.
|
||||
} else {
|
||||
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
|
||||
const htmlContent =
|
||||
await this.saleEstimatesApplication.getSaleEstimateHtml(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return res.status(200).send({ htmlContent });
|
||||
} else if (ACCEPT_TYPE.APPLICATION_JSON) {
|
||||
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
|
||||
tenantId,
|
||||
estimateId
|
||||
@@ -535,18 +566,18 @@ export default class SalesEstimatesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getSaleEstimateMail = async (
|
||||
req: Request,
|
||||
private getSaleEstimateMailState = async (
|
||||
req: Request<{ id: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
const { id: estimateId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.saleEstimatesApplication.getSaleEstimateMail(
|
||||
const data = await this.saleEstimatesApplication.getSaleEstimateMailState(
|
||||
tenantId,
|
||||
invoiceId
|
||||
estimateId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
@@ -562,8 +593,9 @@ export default class SalesEstimatesController extends BaseController {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.saleEstimatesApplication.getSaleEstimateState(tenantId);
|
||||
const data = await this.saleEstimatesApplication.getSaleEstimateState(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SaleInvoiceAction,
|
||||
AbilitySubject,
|
||||
SendInvoiceMailDTO,
|
||||
DiscountType,
|
||||
} from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
|
||||
@@ -242,6 +243,10 @@ export default class SaleInvoicesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.discount_type')
|
||||
.default(DiscountType.Percentage)
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.tax_code')
|
||||
.optional({ nullable: true })
|
||||
@@ -281,6 +286,16 @@ export default class SaleInvoicesController extends BaseController {
|
||||
check('payment_methods').optional({ nullable: true }).isArray(),
|
||||
check('payment_methods.*.payment_integration_id').exists().toInt(),
|
||||
check('payment_methods.*.enable').exists().isBoolean(),
|
||||
|
||||
// Discount
|
||||
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('discount_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
// Adjustments
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
|
||||
import { AbilitySubject, DiscountType, SaleReceiptAction } from '@/interfaces';
|
||||
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
@@ -56,8 +56,18 @@ export default class SalesReceiptsController extends BaseController {
|
||||
[
|
||||
...this.specificReceiptValidationSchema,
|
||||
body('subject').isString().optional(),
|
||||
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
|
||||
body('to').isArray().exists(),
|
||||
body('to.*').isString().isEmail().optional(),
|
||||
|
||||
body('cc').isArray().optional({ nullable: true }),
|
||||
body('cc.*').isString().isEmail().optional(),
|
||||
|
||||
body('bcc').isArray().optional({ nullable: true }),
|
||||
body('bcc.*').isString().isEmail().optional(),
|
||||
|
||||
body('body').isString().optional(),
|
||||
body('attach_receipt').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
@@ -154,6 +164,11 @@ export default class SalesReceiptsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.discount_type')
|
||||
.default(DiscountType.Percentage)
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
@@ -166,8 +181,18 @@ export default class SalesReceiptsController extends BaseController {
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
// Pdf template id.
|
||||
// # Pdf template
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
// # Discount
|
||||
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('discount_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isIn([DiscountType.Percentage, DiscountType.Amount]),
|
||||
|
||||
// # Adjustment
|
||||
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -353,6 +378,7 @@ export default class SalesReceiptsController extends BaseController {
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
|
||||
]);
|
||||
// Retrieves receipt in pdf format.
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||
@@ -368,6 +394,12 @@ export default class SalesReceiptsController extends BaseController {
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Retrieves receipt in json format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
|
||||
const htmlContent = await this.saleReceiptsApplication.getSaleReceiptHtml(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
res.send({ htmlContent });
|
||||
} else {
|
||||
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
|
||||
tenantId,
|
||||
@@ -392,8 +424,9 @@ export default class SalesReceiptsController extends BaseController {
|
||||
|
||||
// Retrieves receipt in pdf format.
|
||||
try {
|
||||
const data =
|
||||
await this.saleReceiptsApplication.getSaleReceiptState(tenantId);
|
||||
const data = await this.saleReceiptsApplication.getSaleReceiptState(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -506,7 +539,7 @@ export default class SalesReceiptsController extends BaseController {
|
||||
const { id: receiptId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.saleReceiptsApplication.getSaleReceiptMail(
|
||||
const data = await this.saleReceiptsApplication.getSaleReceiptMailState(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.alterTable('sales_invoices', (table) => {
|
||||
table.decimal('discount', 10, 2).nullable().after('credited_amount');
|
||||
table.string('discount_type').nullable().after('discount');
|
||||
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable('sale_invoices', (table) => {
|
||||
table.dropColumn('discount');
|
||||
table.dropColumn('discount_type');
|
||||
table.dropColumn('adjustment');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('sales_estimates', (table) => {
|
||||
table.decimal('discount', 10, 2).nullable().after('amount');
|
||||
table.string('discount_type').nullable().after('discount');
|
||||
|
||||
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('sales_estimates', (table) => {
|
||||
table.dropColumn('discount');
|
||||
table.dropColumn('discount_type');
|
||||
table.dropColumn('adjustment');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('sales_receipts', (table) => {
|
||||
table.decimal('discount', 10, 2).nullable().after('amount');
|
||||
table.string('discount_type').nullable().after('discount');
|
||||
|
||||
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('sales_receipts', (table) => {
|
||||
table.dropColumn('discount');
|
||||
table.dropColumn('discount_type');
|
||||
table.dropColumn('adjustment');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('bills', (table) => {
|
||||
// Discount.
|
||||
table.decimal('discount', 10, 2).nullable().after('amount');
|
||||
table.string('discount_type').nullable().after('discount');
|
||||
|
||||
// Adjustment.
|
||||
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('bills', (table) => {
|
||||
table.dropColumn('discount');
|
||||
table.dropColumn('discount_type');
|
||||
table.dropColumn('adjustment');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('credit_notes', (table) => {
|
||||
table.decimal('discount', 10, 2).nullable().after('exchange_rate');
|
||||
table.string('discount_type').nullable().after('discount');
|
||||
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('credit_notes', (table) => {
|
||||
table.dropColumn('discount');
|
||||
table.dropColumn('discount_type');
|
||||
table.dropColumn('adjustment');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('vendor_credits', (table) => {
|
||||
table.decimal('discount', 10, 2).nullable().after('amount');
|
||||
table.string('discount_type').nullable().after('discount');
|
||||
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('vendor_credits', (table) => {
|
||||
table.dropColumn('discount');
|
||||
table.dropColumn('discount_type');
|
||||
table.dropColumn('adjustment');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.alterTable('items_entries', (table) => {
|
||||
table.string('discount_type').defaultTo('percentage').after('discount');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable('items_entries', (table) => {
|
||||
table.dropColumn('discount_type');
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,14 @@
|
||||
export const OtherExpensesAccount = {
|
||||
name: 'Other Expenses',
|
||||
slug: 'other-expenses',
|
||||
account_type: 'other-expense',
|
||||
code: '40011',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
};
|
||||
|
||||
export const TaxPayableAccount = {
|
||||
name: 'Tax Payable',
|
||||
slug: 'tax-payable',
|
||||
@@ -39,8 +50,38 @@ export const StripeClearingAccount = {
|
||||
code: '100020',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
}
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const DiscountExpenseAccount = {
|
||||
name: 'Discount',
|
||||
slug: 'discount',
|
||||
account_type: 'other-income',
|
||||
code: '40008',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const PurchaseDiscountAccount = {
|
||||
name: 'Purchase Discount',
|
||||
slug: 'purchase-discount',
|
||||
account_type: 'other-expense',
|
||||
code: '40009',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const OtherChargesAccount = {
|
||||
name: 'Other Charges',
|
||||
slug: 'other-charges',
|
||||
account_type: 'other-income',
|
||||
code: '40010',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -231,17 +272,7 @@ export default [
|
||||
},
|
||||
|
||||
// Expenses
|
||||
{
|
||||
name: 'Other Expenses',
|
||||
slug: 'other-expenses',
|
||||
account_type: 'other-expense',
|
||||
parent_account_id: null,
|
||||
code: '40001',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
OtherExpensesAccount,
|
||||
{
|
||||
name: 'Cost of Goods Sold',
|
||||
slug: 'cost-of-goods-sold',
|
||||
@@ -358,4 +389,7 @@ export default [
|
||||
},
|
||||
UnearnedRevenueAccount,
|
||||
PrepardExpenses,
|
||||
DiscountExpenseAccount,
|
||||
PurchaseDiscountAccount,
|
||||
OtherChargesAccount,
|
||||
];
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IDynamicListFilterDTO } from './DynamicFilter';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
import { IBillLandedCost } from './LandedCost';
|
||||
import { AttachmentLinkDTO } from './Attachments';
|
||||
import { DiscountType } from './SaleInvoice';
|
||||
|
||||
export interface IBillDTO {
|
||||
vendorId: number;
|
||||
@@ -22,6 +23,13 @@ export interface IBillDTO {
|
||||
projectId?: number;
|
||||
isInclusiveTax?: boolean;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
|
||||
// # Discount
|
||||
discount?: number;
|
||||
discountType?: DiscountType;
|
||||
|
||||
// # Adjustment
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
export interface IBillEditDTO {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IDynamicListFilter, IItemEntry } from '@/interfaces';
|
||||
import { DiscountType, IDynamicListFilter, IItemEntry } from '@/interfaces';
|
||||
import { ILedgerEntry } from './Ledger';
|
||||
import { AttachmentLinkDTO } from './Attachments';
|
||||
|
||||
@@ -23,6 +23,9 @@ export interface ICreditNoteNewDTO {
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
discount?: number;
|
||||
discountType?: DiscountType;
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
export interface ICreditNoteEditDTO {
|
||||
|
||||
@@ -13,14 +13,17 @@ export interface IItemEntry {
|
||||
|
||||
itemId: number;
|
||||
description: string;
|
||||
discountType?: string;
|
||||
discount: number;
|
||||
quantity: number;
|
||||
rate: number;
|
||||
amount: number;
|
||||
|
||||
total: number;
|
||||
amountInclusingTax: number;
|
||||
amountExludingTax: number;
|
||||
totalExcludingTax?: number;
|
||||
|
||||
subtotalInclusingTax: number;
|
||||
subtotalExcludingTax: number;
|
||||
discountAmount: number;
|
||||
|
||||
landedCost: number;
|
||||
|
||||
@@ -177,7 +177,9 @@ export type IPaymentReceiveGLCommonEntry = Pick<
|
||||
| 'branchId'
|
||||
>;
|
||||
|
||||
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
|
||||
export interface PaymentReceiveMailOpts extends CommonMailOptions {
|
||||
attachPdf?: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
|
||||
|
||||
@@ -239,7 +241,6 @@ export interface PaymentReceivedPdfTemplateAttributes {
|
||||
paymentReceivedDateLabel: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IPaymentReceivedState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||
import { AttachmentLinkDTO } from './Attachments';
|
||||
import { DiscountType } from './SaleInvoice';
|
||||
|
||||
export interface ISaleEstimate {
|
||||
id?: number;
|
||||
@@ -24,6 +25,14 @@ export interface ISaleEstimate {
|
||||
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
|
||||
total?: number;
|
||||
totalLocal?: number;
|
||||
|
||||
discountAmount?: number;
|
||||
discountPercentage?: number | null;
|
||||
|
||||
adjustment?: number;
|
||||
}
|
||||
export interface ISaleEstimateDTO {
|
||||
customerId: number;
|
||||
@@ -40,6 +49,13 @@ export interface ISaleEstimateDTO {
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
|
||||
// # Discount
|
||||
discount?: number;
|
||||
discountType?: DiscountType;
|
||||
|
||||
// # Adjustment
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {
|
||||
|
||||
@@ -80,6 +80,18 @@ export interface ISaleInvoice {
|
||||
pdfTemplateId?: number;
|
||||
|
||||
paymentMethods?: Array<PaymentIntegrationTransactionLink>;
|
||||
|
||||
adjustment?: number;
|
||||
adjustmentLocal?: number | null;
|
||||
|
||||
discount?: number;
|
||||
discountAmount?: number;
|
||||
discountAmountLocal?: number | null;
|
||||
}
|
||||
|
||||
export enum DiscountType {
|
||||
Percentage = 'percentage',
|
||||
Amount = 'amount',
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDTO {
|
||||
@@ -102,6 +114,13 @@ export interface ISaleInvoiceDTO {
|
||||
isInclusiveTax?: boolean;
|
||||
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
|
||||
// # Discount
|
||||
discount?: number;
|
||||
discountType?: DiscountType;
|
||||
|
||||
// # Adjustments
|
||||
adjustments?: string;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Knex } from 'knex';
|
||||
import { IItemEntry } from './ItemEntry';
|
||||
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||
import { AttachmentLinkDTO } from './Attachments';
|
||||
import { DiscountType } from './SaleInvoice';
|
||||
|
||||
export interface ISaleReceipt {
|
||||
id?: number;
|
||||
@@ -27,6 +28,20 @@ export interface ISaleReceipt {
|
||||
|
||||
localAmount?: number;
|
||||
entries?: IItemEntry[];
|
||||
|
||||
subtotal?: number;
|
||||
subtotalLocal?: number;
|
||||
|
||||
total?: number;
|
||||
totalLocal?: number;
|
||||
|
||||
discountAmount: number;
|
||||
discountPercentage?: number | null;
|
||||
|
||||
adjustment?: number;
|
||||
adjustmentLocal?: number | null;
|
||||
|
||||
discountAmountLocal?: number | null;
|
||||
}
|
||||
|
||||
export interface ISalesReceiptsFilter {
|
||||
@@ -47,6 +62,11 @@ export interface ISaleReceiptDTO {
|
||||
entries: any[];
|
||||
branchId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
|
||||
discount?: number;
|
||||
discountType?: DiscountType;
|
||||
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
export interface ISalesReceiptsService {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IDynamicListFilter, IItemEntry, IItemEntryDTO } from '@/interfaces';
|
||||
import { DiscountType, IDynamicListFilter, IItemEntry, IItemEntryDTO } from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import { AttachmentLinkDTO } from './Attachments';
|
||||
|
||||
@@ -63,6 +63,11 @@ export interface IVendorCreditDTO {
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
|
||||
discount?: number;
|
||||
discountType?: DiscountType;
|
||||
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
export interface IVendorCreditCreateDTO extends IVendorCreditDTO {}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Model, raw, mixin } from 'objection';
|
||||
import { castArray, difference } from 'lodash';
|
||||
import { castArray, defaultTo, difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import BillSettings from './Bill.Settings';
|
||||
import ModelSetting from './ModelSetting';
|
||||
import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
|
||||
export default class Bill extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
@@ -21,6 +23,11 @@ export default class Bill extends mixin(TenantModel, [
|
||||
public taxAmountWithheld: number;
|
||||
public exchangeRate: number;
|
||||
|
||||
public discount: number;
|
||||
public discountType: DiscountType;
|
||||
|
||||
public adjustment: number;
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
@@ -47,6 +54,13 @@ export default class Bill extends mixin(TenantModel, [
|
||||
'localAllocatedCostAmount',
|
||||
'billableAmount',
|
||||
'amountLocal',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountLocal',
|
||||
'discountPercentage',
|
||||
|
||||
'adjustmentLocal',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
'subtotalExludingTax',
|
||||
@@ -98,14 +112,53 @@ export default class Bill extends mixin(TenantModel, [
|
||||
return this.taxAmountWithheld * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount in local currency.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountAmountLocal() {
|
||||
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage ? this.discount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjustment amount in local currency.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get adjustmentLocal() {
|
||||
return this.adjustment ? this.adjustment * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.isInclusiveTax
|
||||
? this.subtotal
|
||||
: this.subtotal + this.taxAmountWithheld;
|
||||
const adjustmentAmount = defaultTo(this.adjustment, 0);
|
||||
|
||||
return R.compose(
|
||||
R.add(adjustmentAmount),
|
||||
R.subtract(R.__, this.discountAmount),
|
||||
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld))
|
||||
)(this.subtotal);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,12 +5,20 @@ import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/CreditNotes/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import CreditNoteMeta from './CreditNote.Meta';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
|
||||
export default class CreditNote extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
public amount: number;
|
||||
public exchangeRate: number;
|
||||
public openedAt: Date;
|
||||
public discount: number;
|
||||
public discountType: DiscountType;
|
||||
public adjustment: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -35,8 +43,21 @@ export default class CreditNote extends mixin(TenantModel, [
|
||||
'isPublished',
|
||||
'isOpen',
|
||||
'isClosed',
|
||||
|
||||
'creditsRemaining',
|
||||
'creditsUsed',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountLocal',
|
||||
'discountPercentage',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
'adjustmentLocal',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -48,6 +69,72 @@ export default class CreditNote extends mixin(TenantModel, [
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note subtotal.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note subtotal in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.subtotal * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmountLocal() {
|
||||
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage ? this.discount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjustment amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get adjustmentLocal() {
|
||||
return this.adjustment ? this.adjustment * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note total.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.subtotal - this.discountAmount + this.adjustment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note total in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the credit note is draft.
|
||||
* @returns {boolean}
|
||||
@@ -176,6 +263,7 @@ export default class CreditNote extends mixin(TenantModel, [
|
||||
const Branch = require('models/Branch');
|
||||
const Document = require('models/Document');
|
||||
const Warehouse = require('models/Warehouse');
|
||||
const { PdfTemplate } = require('models/PdfTemplate');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -266,6 +354,18 @@ export default class CreditNote extends mixin(TenantModel, [
|
||||
query.where('model_ref', 'CreditNote');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Credit note may belongs to pdf branding template.
|
||||
*/
|
||||
pdfTemplate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: PdfTemplate,
|
||||
join: {
|
||||
from: 'credit_notes.pdfTemplateId',
|
||||
to: 'pdf_templates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
|
||||
// Subtotal (qty * rate) (tax inclusive)
|
||||
// Subtotal Tax Exclusive (Subtotal - Tax Amount)
|
||||
// Discount (Is percentage ? amount * discount : discount)
|
||||
// Total (Subtotal - Discount)
|
||||
|
||||
export default class ItemEntry extends TenantModel {
|
||||
public taxRate: number;
|
||||
@@ -8,7 +14,7 @@ export default class ItemEntry extends TenantModel {
|
||||
public quantity: number;
|
||||
public rate: number;
|
||||
public isInclusiveTax: number;
|
||||
|
||||
public discountType: DiscountType;
|
||||
/**
|
||||
* Table name.
|
||||
* @returns {string}
|
||||
@@ -31,10 +37,24 @@ export default class ItemEntry extends TenantModel {
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
// Amount (qty * rate)
|
||||
'amount',
|
||||
|
||||
'taxAmount',
|
||||
'amountExludingTax',
|
||||
'amountInclusingTax',
|
||||
|
||||
// Subtotal (qty * rate) + (tax inclusive)
|
||||
'subtotalInclusingTax',
|
||||
|
||||
// Subtotal Tax Exclusive (Subtotal - Tax Amount)
|
||||
'subtotalExcludingTax',
|
||||
|
||||
// Subtotal (qty * rate) + (tax inclusive)
|
||||
'subtotal',
|
||||
|
||||
// Discount (Is percentage ? amount * discount : discount)
|
||||
'discountAmount',
|
||||
|
||||
// Total (Subtotal - Discount)
|
||||
'total',
|
||||
];
|
||||
}
|
||||
@@ -45,7 +65,15 @@ export default class ItemEntry extends TenantModel {
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.amountInclusingTax;
|
||||
return this.subtotal - this.discountAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total (excluding tax).
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalExcludingTax() {
|
||||
return this.subtotalExcludingTax - this.discountAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,19 +85,27 @@ export default class ItemEntry extends TenantModel {
|
||||
return this.quantity * this.rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal amount (tax inclusive).
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.subtotalInclusingTax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry amount including tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amountInclusingTax() {
|
||||
get subtotalInclusingTax() {
|
||||
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry amount excluding tax.
|
||||
* Subtotal amount (tax exclusive).
|
||||
* @returns {number}
|
||||
*/
|
||||
get amountExludingTax() {
|
||||
get subtotalExcludingTax() {
|
||||
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
|
||||
}
|
||||
|
||||
@@ -78,7 +114,9 @@ export default class ItemEntry extends TenantModel {
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.amount * (this.discount / 100);
|
||||
return this.discountType === DiscountType.Percentage
|
||||
? this.amount * (this.discount / 100)
|
||||
: this.discount;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,10 @@ export default class PaymentReceive extends mixin(TenantModel, [
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
amount!: number;
|
||||
paymentAmount!: number;
|
||||
exchangeRate!: number;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -29,7 +33,7 @@ export default class PaymentReceive extends mixin(TenantModel, [
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['localAmount'];
|
||||
return ['localAmount', 'total'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +44,14 @@ export default class PaymentReceive extends mixin(TenantModel, [
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive total.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.paymentAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resourcable model.
|
||||
*/
|
||||
@@ -57,6 +69,7 @@ export default class PaymentReceive extends mixin(TenantModel, [
|
||||
const Account = require('models/Account');
|
||||
const Branch = require('models/Branch');
|
||||
const Document = require('models/Document');
|
||||
const { PdfTemplate } = require('models/PdfTemplate');
|
||||
|
||||
return {
|
||||
customer: {
|
||||
@@ -131,6 +144,18 @@ export default class PaymentReceive extends mixin(TenantModel, [
|
||||
query.where('model_ref', 'PaymentReceive');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Payment received may belongs to pdf branding template.
|
||||
*/
|
||||
pdfTemplate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: PdfTemplate,
|
||||
join: {
|
||||
from: 'payment_receives.pdfTemplateId',
|
||||
to: 'pdf_templates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,30 @@ import ModelSetting from './ModelSetting';
|
||||
import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/Sales/Estimates/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
import { defaultTo } from 'lodash';
|
||||
|
||||
export default class SaleEstimate extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
public amount: number;
|
||||
public exchangeRate: number;
|
||||
|
||||
public discount: number;
|
||||
public discountType: DiscountType;
|
||||
|
||||
public adjustment: number;
|
||||
|
||||
public expirationDate!: string;
|
||||
public deliveredAt!: string | null;
|
||||
public approvedAt!: string | null;
|
||||
public rejectedAt!: string | null;
|
||||
|
||||
public convertedToInvoiceId!: number | null;
|
||||
public convertedToInvoiceAt!: string | null;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -33,6 +51,12 @@ export default class SaleEstimate extends mixin(TenantModel, [
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'localAmount',
|
||||
'discountAmount',
|
||||
'discountPercentage',
|
||||
'total',
|
||||
'totalLocal',
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
'isDelivered',
|
||||
'isExpired',
|
||||
'isConvertedToInvoice',
|
||||
@@ -49,6 +73,60 @@ export default class SaleEstimate extends mixin(TenantModel, [
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate subtotal.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate subtotal in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.localAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage
|
||||
? this.discount
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate total.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
const adjustmentAmount = defaultTo(this.adjustment, 0);
|
||||
|
||||
return this.subtotal - this.discountAmount + adjustmentAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate total in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the sale estimate converted to sale invoice.
|
||||
* @return {boolean}
|
||||
@@ -184,6 +262,7 @@ export default class SaleEstimate extends mixin(TenantModel, [
|
||||
const Branch = require('models/Branch');
|
||||
const Warehouse = require('models/Warehouse');
|
||||
const Document = require('models/Document');
|
||||
const { PdfTemplate } = require('models/PdfTemplate');
|
||||
|
||||
return {
|
||||
customer: {
|
||||
@@ -252,6 +331,18 @@ export default class SaleEstimate extends mixin(TenantModel, [
|
||||
query.where('model_ref', 'SaleEstimate');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale estimate may belongs to pdf branding template.
|
||||
*/
|
||||
pdfTemplate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: PdfTemplate,
|
||||
join: {
|
||||
from: 'sales_estimates.pdfTemplateId',
|
||||
to: 'pdf_templates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
import { castArray, takeWhile } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { castArray, defaultTo, takeWhile } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSetting from './ModelSetting';
|
||||
@@ -7,6 +8,7 @@ import SaleInvoiceMeta from './SaleInvoice.Settings';
|
||||
import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
|
||||
export default class SaleInvoice extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
@@ -23,6 +25,9 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
public writtenoffAt: Date;
|
||||
public dueDate: Date;
|
||||
public deliveredAt: Date;
|
||||
public discount: number;
|
||||
public discountType: DiscountType;
|
||||
public adjustment: number | null;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
@@ -67,10 +72,15 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
'subtotalExludingTax',
|
||||
|
||||
'taxAmountWithheldLocal',
|
||||
'discountAmount',
|
||||
'discountAmountLocal',
|
||||
'discountPercentage',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
'writtenoffAmountLocal',
|
||||
'adjustmentLocal',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -125,14 +135,52 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
return this.taxAmountWithheld * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Local discount amount.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountAmountLocal() {
|
||||
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage ? this.discount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjustment amount in local currency.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get adjustmentLocal(): number | null {
|
||||
return this.adjustment ? this.adjustment * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.isInclusiveTax
|
||||
? this.subtotal
|
||||
: this.subtotal + this.taxAmountWithheld;
|
||||
const adjustmentAmount = defaultTo(this.adjustment, 0);
|
||||
|
||||
return R.compose(
|
||||
R.add(adjustmentAmount),
|
||||
R.subtract(R.__, this.discountAmount),
|
||||
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld))
|
||||
)(this.subtotal);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -604,7 +652,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
join: {
|
||||
from: 'sales_invoices.pdfTemplateId',
|
||||
to: 'pdf_templates.id',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,23 @@ import SaleReceiptSettings from './SaleReceipt.Settings';
|
||||
import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
import { defaultTo } from 'lodash';
|
||||
|
||||
export default class SaleReceipt extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
public amount: number;
|
||||
public exchangeRate: number;
|
||||
public closedAt: Date;
|
||||
|
||||
public discount: number;
|
||||
public discountType: DiscountType;
|
||||
|
||||
public adjustment: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -29,7 +40,28 @@ export default class SaleReceipt extends mixin(TenantModel, [
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['localAmount', 'isClosed', 'isDraft'];
|
||||
return [
|
||||
'localAmount',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
'adjustment',
|
||||
'adjustmentLocal',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountLocal',
|
||||
'discountPercentage',
|
||||
|
||||
'paid',
|
||||
'paidLocal',
|
||||
|
||||
'isClosed',
|
||||
'isDraft',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +72,90 @@ export default class SaleReceipt extends mixin(TenantModel, [
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt subtotal.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt subtotal in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.localAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount in local currency.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountAmountLocal() {
|
||||
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage ? this.discount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt total.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
const adjustmentAmount = defaultTo(this.adjustment, 0);
|
||||
|
||||
return this.subtotal - this.discountAmount + adjustmentAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt total in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjustment amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get adjustmentLocal() {
|
||||
return this.adjustment * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt paid amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get paid() {
|
||||
return this.total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt paid amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get paidLocal() {
|
||||
return this.paid * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmine whether the sale receipt closed.
|
||||
* @return {boolean}
|
||||
|
||||
@@ -6,26 +6,26 @@ import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/Purchases/VendorCredits/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import VendorCreditMeta from './VendorCredit.Meta';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
|
||||
export default class VendorCredit extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
public amount: number;
|
||||
public exchangeRate: number;
|
||||
public openedAt: Date;
|
||||
public discount: number;
|
||||
public discountType: DiscountType;
|
||||
public adjustment: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'vendor_credits';
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['localAmount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor credit amount in local currency.
|
||||
* @returns {number}
|
||||
@@ -34,6 +34,72 @@ export default class VendorCredit extends mixin(TenantModel, [
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor credit subtotal.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor credit subtotal in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.subtotal * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount in local currency.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountAmountLocal() {
|
||||
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage ? this.discount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjustment amount in local currency.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get adjustmentLocal() {
|
||||
return this.adjustment ? this.adjustment * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor credit total.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.subtotal - this.discountAmount + this.adjustment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor credit total in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
@@ -120,7 +186,24 @@ export default class VendorCredit extends mixin(TenantModel, [
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isDraft', 'isPublished', 'isOpen', 'isClosed', 'creditsRemaining'];
|
||||
return [
|
||||
'isDraft',
|
||||
'isPublished',
|
||||
'isOpen',
|
||||
'isClosed',
|
||||
|
||||
'creditsRemaining',
|
||||
'localAmount',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountLocal',
|
||||
'discountPercentage',
|
||||
|
||||
'adjustmentLocal',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,11 @@ import TenantRepository from '@/repositories/TenantRepository';
|
||||
import { IAccount } from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
DiscountExpenseAccount,
|
||||
OtherChargesAccount,
|
||||
OtherExpensesAccount,
|
||||
PrepardExpenses,
|
||||
PurchaseDiscountAccount,
|
||||
StripeClearingAccount,
|
||||
TaxPayableAccount,
|
||||
UnearnedRevenueAccount,
|
||||
@@ -188,9 +192,9 @@ export default class AccountRepository extends TenantRepository {
|
||||
|
||||
/**
|
||||
* Finds or creates the unearned revenue.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreateUnearnedRevenue(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
@@ -219,9 +223,9 @@ export default class AccountRepository extends TenantRepository {
|
||||
|
||||
/**
|
||||
* Finds or creates the prepard expenses account.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreatePrepardExpenses(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
@@ -249,12 +253,11 @@ export default class AccountRepository extends TenantRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds or creates the stripe clearing account.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreateStripeClearing(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
@@ -281,4 +284,114 @@ export default class AccountRepository extends TenantRepository {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds or creates the discount expense account.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreateDiscountAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: DiscountExpenseAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...DiscountExpenseAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findOrCreatePurchaseDiscountAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: PurchaseDiscountAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...PurchaseDiscountAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findOrCreateOtherChargesAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: OtherChargesAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...OtherChargesAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findOrCreateOtherExpensesAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: OtherExpensesAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...OtherExpensesAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ export default class Ledger implements ILedger {
|
||||
return {
|
||||
credit: defaultTo(entry.credit, 0),
|
||||
debit: defaultTo(entry.debit, 0),
|
||||
|
||||
exchangeRate: entry.exchangeRate,
|
||||
currencyCode: entry.currencyCode,
|
||||
|
||||
|
||||
@@ -9,15 +9,18 @@ import {
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { transformLedgerEntryToTransaction } from './utils';
|
||||
|
||||
// Filter the blank entries.
|
||||
const filterBlankEntry = (entry: ILedgerEntry) => Boolean(entry.credit || entry.debit);
|
||||
|
||||
@Service()
|
||||
export class LedgerEntriesStorage {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Saves entries of the given ledger.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {Knex.Transaction} knex
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {Knex.Transaction} knex
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public saveEntries = async (
|
||||
@@ -26,7 +29,7 @@ export class LedgerEntriesStorage {
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const saveEntryQueue = async.queue(this.saveEntryTask, 10);
|
||||
const entries = ledger.getEntries();
|
||||
const entries = ledger.filter(filterBlankEntry).getEntries();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
saveEntryQueue.push({ tenantId, entry, trx });
|
||||
@@ -57,8 +60,8 @@ export class LedgerEntriesStorage {
|
||||
|
||||
/**
|
||||
* Saves the ledger entry to the account transactions repository.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedgerEntry} entry
|
||||
* @param {number} tenantId
|
||||
* @param {ILedgerEntry} entry
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveEntry = async (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import { SaleReceipt } from '@/models';
|
||||
|
||||
@Service()
|
||||
export default class CreditNoteGLEntries {
|
||||
@@ -29,11 +30,15 @@ export default class CreditNoteGLEntries {
|
||||
*/
|
||||
private getCreditNoteGLedger = (
|
||||
creditNote: ICreditNote,
|
||||
receivableAccount: number
|
||||
receivableAccount: number,
|
||||
discountAccount: number,
|
||||
adjustmentAccount: number
|
||||
): Ledger => {
|
||||
const ledgerEntries = this.getCreditNoteGLEntries(
|
||||
creditNote,
|
||||
receivableAccount
|
||||
receivableAccount,
|
||||
discountAccount,
|
||||
adjustmentAccount
|
||||
);
|
||||
return new Ledger(ledgerEntries);
|
||||
};
|
||||
@@ -49,9 +54,16 @@ export default class CreditNoteGLEntries {
|
||||
tenantId: number,
|
||||
creditNote: ICreditNote,
|
||||
payableAccount: number,
|
||||
discountAccount: number,
|
||||
adjustmentAccount: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const ledger = this.getCreditNoteGLedger(creditNote, payableAccount);
|
||||
const ledger = this.getCreditNoteGLedger(
|
||||
creditNote,
|
||||
payableAccount,
|
||||
discountAccount,
|
||||
adjustmentAccount
|
||||
);
|
||||
|
||||
await this.ledgerStorage.commit(tenantId, ledger, trx);
|
||||
};
|
||||
@@ -98,11 +110,18 @@ export default class CreditNoteGLEntries {
|
||||
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
|
||||
creditNoteWithItems.currencyCode
|
||||
);
|
||||
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
|
||||
{}
|
||||
);
|
||||
const adjustmentAccount =
|
||||
await accountRepository.findOrCreateOtherChargesAccount({});
|
||||
// Saves the credit note GL entries.
|
||||
await this.saveCreditNoteGLEntries(
|
||||
tenantId,
|
||||
creditNoteWithItems,
|
||||
ARAccount.id,
|
||||
discountAccount.id,
|
||||
adjustmentAccount.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
@@ -169,7 +188,7 @@ export default class CreditNoteGLEntries {
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: creditNote.localAmount,
|
||||
credit: creditNote.totalLocal,
|
||||
accountId: ARAccountId,
|
||||
contactId: creditNote.customerId,
|
||||
index: 1,
|
||||
@@ -191,11 +210,11 @@ export default class CreditNoteGLEntries {
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
|
||||
const localAmount = entry.amount * creditNote.exchangeRate;
|
||||
const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: localAmount,
|
||||
debit: totalLocal,
|
||||
accountId: entry.sellAccountId || entry.item.sellAccountId,
|
||||
note: entry.description,
|
||||
index: index + 2,
|
||||
@@ -206,6 +225,50 @@ export default class CreditNoteGLEntries {
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the credit note discount entry.
|
||||
* @param {ICreditNote} creditNote
|
||||
* @param {number} discountAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getDiscountEntry = (
|
||||
creditNote: ICreditNote,
|
||||
discountAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: creditNote.discountAmountLocal,
|
||||
accountId: discountAccountId,
|
||||
index: 1,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the credit note adjustment entry.
|
||||
* @param {ICreditNote} creditNote
|
||||
* @param {number} adjustmentAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAdjustmentEntry = (
|
||||
creditNote: ICreditNote,
|
||||
adjustmentAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
|
||||
const adjustmentAmount = Math.abs(creditNote.adjustmentLocal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: creditNote.adjustmentLocal < 0 ? adjustmentAmount : 0,
|
||||
debit: creditNote.adjustmentLocal > 0 ? adjustmentAmount : 0,
|
||||
accountId: adjustmentAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the credit note GL entries.
|
||||
* @param {ICreditNote} creditNote - Credit note.
|
||||
@@ -214,13 +277,21 @@ export default class CreditNoteGLEntries {
|
||||
*/
|
||||
public getCreditNoteGLEntries = (
|
||||
creditNote: ICreditNote,
|
||||
ARAccountId: number
|
||||
ARAccountId: number,
|
||||
discountAccountId: number,
|
||||
adjustmentAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId);
|
||||
|
||||
const getItemEntry = this.getCreditNoteItemEntry(creditNote);
|
||||
const itemsEntries = creditNote.entries.map(getItemEntry);
|
||||
|
||||
return [AREntry, ...itemsEntries];
|
||||
const discountEntry = this.getDiscountEntry(creditNote, discountAccountId);
|
||||
const adjustmentEntry = this.getAdjustmentEntry(
|
||||
creditNote,
|
||||
adjustmentAccountId
|
||||
);
|
||||
|
||||
return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,18 @@ export class CreditNoteTransformer extends Transformer {
|
||||
'formattedAmount',
|
||||
'formattedCreditsUsed',
|
||||
'formattedSubtotal',
|
||||
|
||||
'discountAmountFormatted',
|
||||
'discountAmountLocalFormatted',
|
||||
|
||||
'discountPercentageFormatted',
|
||||
|
||||
'adjustmentFormatted',
|
||||
'adjustmentLocalFormatted',
|
||||
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
@@ -34,7 +46,7 @@ export class CreditNoteTransformer extends Transformer {
|
||||
|
||||
/**
|
||||
* Retrieve formatted created at date.
|
||||
* @param credit
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreatedAt = (credit): string => {
|
||||
@@ -83,6 +95,85 @@ export class CreditNoteTransformer extends Transformer {
|
||||
return formatNumber(credit.amount, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount amount.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (credit): string => {
|
||||
return formatNumber(credit.discountAmount, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount amount in local currency.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountLocalFormatted = (credit): string => {
|
||||
return formatNumber(credit.discountAmountLocal, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount percentage.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountPercentageFormatted = (credit): string => {
|
||||
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted adjustment amount.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (credit): string => {
|
||||
return this.formatMoney(credit.adjustment, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted adjustment amount in local currency.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentLocalFormatted = (credit): string => {
|
||||
return formatNumber(credit.adjustmentLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (credit): string => {
|
||||
return formatNumber(credit.total, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total in local currency.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalLocalFormatted = (credit): string => {
|
||||
return formatNumber(credit.totalLocal, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the credit note.
|
||||
* @param {ICreditNote} credit
|
||||
|
||||
@@ -24,7 +24,7 @@ export class CreateItem {
|
||||
|
||||
/**
|
||||
* Authorize the creating item.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId
|
||||
* @param {IItemDTO} itemDTO
|
||||
*/
|
||||
async authorize(tenantId: number, itemDTO: IItemDTO) {
|
||||
|
||||
@@ -52,10 +52,18 @@ export class BillGLEntries {
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Find or create other expenses account.
|
||||
const otherExpensesAccount =
|
||||
await accountRepository.findOrCreateOtherExpensesAccount({}, trx);
|
||||
// Find or create purchase discount account.
|
||||
const purchaseDiscountAccount =
|
||||
await accountRepository.findOrCreatePurchaseDiscountAccount({}, trx);
|
||||
const billLedger = this.getBillLedger(
|
||||
bill,
|
||||
APAccount.id,
|
||||
taxPayableAccount.id
|
||||
taxPayableAccount.id,
|
||||
purchaseDiscountAccount.id,
|
||||
otherExpensesAccount.id
|
||||
);
|
||||
// Commit the GL enties on the storage.
|
||||
await this.ledgerStorage.commit(tenantId, billLedger, trx);
|
||||
@@ -102,6 +110,7 @@ export class BillGLEntries {
|
||||
return {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
currencyCode: bill.currencyCode,
|
||||
exchangeRate: bill.exchangeRate || 1,
|
||||
|
||||
@@ -130,13 +139,12 @@ export class BillGLEntries {
|
||||
private getBillItemEntry = R.curry(
|
||||
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
|
||||
const commonJournalMeta = this.getBillCommonEntry(bill);
|
||||
|
||||
const localAmount = bill.exchangeRate * entry.amountExludingTax;
|
||||
const totalLocal = bill.exchangeRate * entry.totalExcludingTax;
|
||||
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
|
||||
|
||||
return {
|
||||
...commonJournalMeta,
|
||||
debit: localAmount + landedCostAmount,
|
||||
debit: totalLocal + landedCostAmount,
|
||||
accountId:
|
||||
['inventory'].indexOf(entry.item.type) !== -1
|
||||
? entry.item.inventoryAccountId
|
||||
@@ -240,6 +248,52 @@ export class BillGLEntries {
|
||||
return nonZeroTaxEntries.map(transformTaxEntry);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the purchase discount GL entry.
|
||||
* @param {IBill} bill
|
||||
* @param {number} purchaseDiscountAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getPurchaseDiscountEntry = (
|
||||
bill: IBill,
|
||||
purchaseDiscountAccountId: number
|
||||
) => {
|
||||
const commonEntry = this.getBillCommonEntry(bill);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: bill.discountAmountLocal,
|
||||
accountId: purchaseDiscountAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
indexGroup: 40,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the purchase other charges GL entry.
|
||||
* @param {IBill} bill
|
||||
* @param {number} otherChargesAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAdjustmentEntry = (
|
||||
bill: IBill,
|
||||
otherExpensesAccountId: number
|
||||
) => {
|
||||
const commonEntry = this.getBillCommonEntry(bill);
|
||||
const adjustmentAmount = Math.abs(bill.adjustmentLocal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: bill.adjustmentLocal > 0 ? adjustmentAmount : 0,
|
||||
credit: bill.adjustmentLocal < 0 ? adjustmentAmount : 0,
|
||||
accountId: otherExpensesAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
indexGroup: 40,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given bill GL entries.
|
||||
* @param {IBill} bill
|
||||
@@ -249,7 +303,9 @@ export class BillGLEntries {
|
||||
private getBillGLEntries = (
|
||||
bill: IBill,
|
||||
payableAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
taxPayableAccountId: number,
|
||||
purchaseDiscountAccountId: number,
|
||||
otherExpensesAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
|
||||
|
||||
@@ -262,8 +318,24 @@ export class BillGLEntries {
|
||||
);
|
||||
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
|
||||
|
||||
const purchaseDiscountEntry = this.getPurchaseDiscountEntry(
|
||||
bill,
|
||||
purchaseDiscountAccountId
|
||||
);
|
||||
const adjustmentEntry = this.getAdjustmentEntry(
|
||||
bill,
|
||||
otherExpensesAccountId
|
||||
);
|
||||
|
||||
// Allocate cost entries journal entries.
|
||||
return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
|
||||
return [
|
||||
payableEntry,
|
||||
...itemsEntries,
|
||||
...landedCostEntries,
|
||||
...taxEntries,
|
||||
purchaseDiscountEntry,
|
||||
adjustmentEntry,
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -275,14 +347,17 @@ export class BillGLEntries {
|
||||
private getBillLedger = (
|
||||
bill: IBill,
|
||||
payableAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
taxPayableAccountId: number,
|
||||
purchaseDiscountAccountId: number,
|
||||
otherExpensesAccountId: number
|
||||
) => {
|
||||
const entries = this.getBillGLEntries(
|
||||
bill,
|
||||
payableAccountId,
|
||||
taxPayableAccountId
|
||||
taxPayableAccountId,
|
||||
purchaseDiscountAccountId,
|
||||
otherExpensesAccountId
|
||||
);
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,10 +20,21 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
||||
'formattedBalance',
|
||||
'formattedDueAmount',
|
||||
'formattedExchangeRate',
|
||||
|
||||
'subtotalFormatted',
|
||||
'subtotalLocalFormatted',
|
||||
|
||||
'subtotalExcludingTaxFormatted',
|
||||
'taxAmountWithheldLocalFormatted',
|
||||
|
||||
'discountAmountFormatted',
|
||||
'discountAmountLocalFormatted',
|
||||
|
||||
'discountPercentageFormatted',
|
||||
|
||||
'adjustmentFormatted',
|
||||
'adjustmentLocalFormatted',
|
||||
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
'taxes',
|
||||
@@ -160,6 +171,63 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount amount.
|
||||
* @param {IBill} bill
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (bill): string => {
|
||||
return formatNumber(bill.discountAmount, {
|
||||
currencyCode: bill.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount amount in local currency.
|
||||
* @param {IBill} bill
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountLocalFormatted = (bill): string => {
|
||||
return formatNumber(bill.discountAmountLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount percentage.
|
||||
* @param {IBill} bill
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountPercentageFormatted = (bill): string => {
|
||||
return bill.discountPercentage ? `${bill.discountPercentage}%` : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted adjustment amount.
|
||||
* @param {IBill} bill
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (bill): string => {
|
||||
return formatNumber(bill.adjustment, {
|
||||
currencyCode: bill.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted adjustment amount in local currency.
|
||||
* @param {IBill} bill
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentLocalFormatted = (bill): string => {
|
||||
return formatNumber(bill.adjustmentLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total formatted.
|
||||
* @param {IBill} bill
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class VendorCreditGLEntries {
|
||||
|
||||
return {
|
||||
...commonEntity,
|
||||
debit: vendorCredit.localAmount,
|
||||
debit: vendorCredit.totalLocal,
|
||||
accountId: APAccountId,
|
||||
contactId: vendorCredit.vendorId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
@@ -77,11 +77,11 @@ export default class VendorCreditGLEntries {
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit);
|
||||
const localAmount = entry.amount * vendorCredit.exchangeRate;
|
||||
const totalLocal = entry.totalExcludingTax * vendorCredit.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntity,
|
||||
credit: localAmount,
|
||||
credit: totalLocal,
|
||||
index: index + 2,
|
||||
itemId: entry.itemId,
|
||||
itemQuantity: entry.quantity,
|
||||
@@ -94,6 +94,52 @@ export default class VendorCreditGLEntries {
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the vendor credit discount GL entry.
|
||||
* @param {IVendorCredit} vendorCredit
|
||||
* @param {number} discountAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
public getDiscountEntry = (
|
||||
vendorCredit: IVendorCredit,
|
||||
purchaseDiscountAccountId: number
|
||||
) => {
|
||||
const commonEntry = this.getVendorCreditGLCommonEntry(vendorCredit);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: vendorCredit.discountAmountLocal,
|
||||
accountId: purchaseDiscountAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
indexGroup: 40,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the vendor credit adjustment GL entry.
|
||||
* @param {IVendorCredit} vendorCredit
|
||||
* @param {number} adjustmentAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
public getAdjustmentEntry = (
|
||||
vendorCredit: IVendorCredit,
|
||||
otherExpensesAccountId: number
|
||||
) => {
|
||||
const commonEntry = this.getVendorCreditGLCommonEntry(vendorCredit);
|
||||
const adjustmentAmount = Math.abs(vendorCredit.adjustmentLocal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: vendorCredit.adjustmentLocal > 0 ? adjustmentAmount : 0,
|
||||
debit: vendorCredit.adjustmentLocal < 0 ? adjustmentAmount : 0,
|
||||
accountId: otherExpensesAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
indexGroup: 40,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the vendor credit GL entries.
|
||||
* @param {IVendorCredit} vendorCredit -
|
||||
@@ -102,7 +148,9 @@ export default class VendorCreditGLEntries {
|
||||
*/
|
||||
public getVendorCreditGLEntries = (
|
||||
vendorCredit: IVendorCredit,
|
||||
payableAccountId: number
|
||||
payableAccountId: number,
|
||||
purchaseDiscountAccountId: number,
|
||||
otherExpensesAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const payableEntry = this.getVendorCreditPayableGLEntry(
|
||||
vendorCredit,
|
||||
@@ -111,7 +159,15 @@ export default class VendorCreditGLEntries {
|
||||
const getItemEntry = this.getVendorCreditGLItemEntry(vendorCredit);
|
||||
const itemsEntries = vendorCredit.entries.map(getItemEntry);
|
||||
|
||||
return [payableEntry, ...itemsEntries];
|
||||
const discountEntry = this.getDiscountEntry(
|
||||
vendorCredit,
|
||||
purchaseDiscountAccountId
|
||||
);
|
||||
const adjustmentEntry = this.getAdjustmentEntry(
|
||||
vendorCredit,
|
||||
otherExpensesAccountId
|
||||
);
|
||||
return [payableEntry, discountEntry, adjustmentEntry, ...itemsEntries];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -158,10 +214,17 @@ export default class VendorCreditGLEntries {
|
||||
{},
|
||||
trx
|
||||
);
|
||||
const purchaseDiscountAccount =
|
||||
await accountRepository.findOrCreatePurchaseDiscountAccount({}, trx);
|
||||
|
||||
const otherExpensesAccount =
|
||||
await accountRepository.findOrCreateOtherExpensesAccount({}, trx);
|
||||
// Saves the vendor credit GL entries.
|
||||
const ledgerEntries = this.getVendorCreditGLEntries(
|
||||
vendorCredit,
|
||||
APAccount.id
|
||||
APAccount.id,
|
||||
purchaseDiscountAccount.id,
|
||||
otherExpensesAccount.id
|
||||
);
|
||||
const ledger = new Ledger(ledgerEntries);
|
||||
|
||||
|
||||
@@ -17,6 +17,15 @@ export class VendorCreditTransformer extends Transformer {
|
||||
'formattedCreatedAt',
|
||||
'formattedCreditsRemaining',
|
||||
'formattedInvoicedAmount',
|
||||
|
||||
'discountAmountFormatted',
|
||||
'discountPercentageFormatted',
|
||||
'discountAmountLocalFormatted',
|
||||
|
||||
'adjustmentFormatted',
|
||||
'adjustmentLocalFormatted',
|
||||
|
||||
'totalFormatted',
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
@@ -33,7 +42,7 @@ export class VendorCreditTransformer extends Transformer {
|
||||
|
||||
/**
|
||||
* Retireve formatted created at date.
|
||||
* @param vendorCredit
|
||||
* @param vendorCredit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreatedAt = (vendorCredit): string => {
|
||||
@@ -71,6 +80,63 @@ export class VendorCreditTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount amount.
|
||||
* @param {IVendorCredit} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (credit): string => {
|
||||
return formatNumber(credit.discountAmount, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount amount in local currency.
|
||||
* @param {IVendorCredit} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountLocalFormatted = (credit): string => {
|
||||
return formatNumber(credit.discountAmountLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount percentage.
|
||||
* @param {IVendorCredit} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountPercentageFormatted = (credit): string => {
|
||||
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted adjustment amount.
|
||||
* @param {IVendorCredit} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (credit): string => {
|
||||
return formatNumber(credit.adjustment, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted adjustment amount in local currency.
|
||||
* @param {IVendorCredit} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentLocalFormatted = (credit): string => {
|
||||
return formatNumber(credit.adjustmentLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted invoiced amount.
|
||||
* @param credit
|
||||
@@ -82,6 +148,15 @@ export class VendorCreditTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total.
|
||||
* @param {IVendorCredit} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (credit) => {
|
||||
return formatNumber(credit.total, { currencyCode: credit.currencyCode });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the bill.
|
||||
* @param {IVendorCredit} vendorCredit
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
renderEstimateEmailTemplate,
|
||||
EstimatePaymentEmailProps,
|
||||
} from '@bigcapital/email-components';
|
||||
import { GetSaleEstimate } from './GetSaleEstimate';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetEstimateMailTemplateAttributesTransformer } from './GetEstimateMailTemplateAttributesTransformer';
|
||||
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||
|
||||
@Service()
|
||||
export class GetEstimateMailTemplate {
|
||||
@Inject()
|
||||
private getEstimateService: GetSaleEstimate;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private getBrandingTemplate: GetPdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieves the mail template attributes of the given estimate.
|
||||
* Estimate template attributes are composed of the estimate and branding template attributes.
|
||||
* @param {number} tenantId
|
||||
* @param {number} estimateId - Estimate id.
|
||||
* @returns {Promise<EstimatePaymentEmailProps>}
|
||||
*/
|
||||
public async getMailTemplateAttributes(
|
||||
tenantId: number,
|
||||
estimateId: number
|
||||
): Promise<EstimatePaymentEmailProps> {
|
||||
const estimate = await this.getEstimateService.getEstimate(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
|
||||
tenantId,
|
||||
estimate.pdfTemplateId
|
||||
);
|
||||
const mailTemplateAttributes = await this.transformer.transform(
|
||||
tenantId,
|
||||
estimate,
|
||||
new GetEstimateMailTemplateAttributesTransformer(),
|
||||
{
|
||||
estimate,
|
||||
brandingTemplate,
|
||||
}
|
||||
);
|
||||
return mailTemplateAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rertieves the mail template html content.
|
||||
* @param {number} tenantId
|
||||
* @param {number} estimateId
|
||||
* @param overrideAttributes
|
||||
* @returns
|
||||
*/
|
||||
public async getMailTemplate(
|
||||
tenantId: number,
|
||||
estimateId: number,
|
||||
overrideAttributes?: Partial<any>
|
||||
): Promise<string> {
|
||||
const attributes = await this.getMailTemplateAttributes(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
const mergedAttributes = {
|
||||
...attributes,
|
||||
...overrideAttributes,
|
||||
};
|
||||
return renderEstimateEmailTemplate(mergedAttributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class GetEstimateMailTemplateAttributesTransformer extends Transformer {
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'companyLogoUri',
|
||||
'companyName',
|
||||
|
||||
'estimateAmount',
|
||||
|
||||
'primaryColor',
|
||||
|
||||
'estimateAmount',
|
||||
'estimateMessage',
|
||||
|
||||
'dueDate',
|
||||
'dueDateLabel',
|
||||
|
||||
'estimateNumber',
|
||||
'estimateNumberLabel',
|
||||
|
||||
'total',
|
||||
'totalLabel',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLabel',
|
||||
|
||||
'discount',
|
||||
'discountLabel',
|
||||
|
||||
'adjustment',
|
||||
'adjustmentLabel',
|
||||
|
||||
'dueAmount',
|
||||
'dueAmountLabel',
|
||||
|
||||
'viewEstimateButtonLabel',
|
||||
'viewEstimateButtonUrl',
|
||||
|
||||
'items',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Company logo uri.
|
||||
* @returns {string}
|
||||
*/
|
||||
public companyLogoUri(): string {
|
||||
return this.options.brandingTemplate?.companyLogoUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Company name.
|
||||
* @returns {string}
|
||||
*/
|
||||
public companyName(): string {
|
||||
return this.context.organization.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary color.
|
||||
* @returns {string}
|
||||
*/
|
||||
public primaryColor(): string {
|
||||
return this.options?.brandingTemplate?.attributes?.primaryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate number.
|
||||
* @returns {string}
|
||||
*/
|
||||
public estimateNumber(): string {
|
||||
return this.options.estimate.estimateNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate number label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public estimateNumberLabel(): string {
|
||||
return 'Estimate No: {estimateNumber}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Expiration date.
|
||||
* @returns {string}
|
||||
*/
|
||||
public expirationDate(): string {
|
||||
return this.options.estimate.formattedExpirationDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expiration date label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public expirationDateLabel(): string {
|
||||
return 'Expiration Date: {expirationDate}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate total.
|
||||
*/
|
||||
public total(): string {
|
||||
return this.options.estimate.totalFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate total label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public totalLabel(): string {
|
||||
return 'Total';
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate discount.
|
||||
* @returns {string}
|
||||
*/
|
||||
public discount(): string {
|
||||
return this.options.estimate?.discountAmountFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate discount label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public discountLabel(): string {
|
||||
return 'Discount';
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate adjustment.
|
||||
* @returns {string}
|
||||
*/
|
||||
public adjustment(): string {
|
||||
return this.options.estimate?.adjustmentFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate adjustment label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public adjustmentLabel(): string {
|
||||
return 'Adjustment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate subtotal.
|
||||
*/
|
||||
public subtotal(): string {
|
||||
return this.options.estimate.formattedSubtotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate subtotal label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public subtotalLabel(): string {
|
||||
return 'Subtotal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate mail items attributes.
|
||||
*/
|
||||
public items(): any[] {
|
||||
return this.item(
|
||||
this.options.estimate.entries,
|
||||
new GetEstimateMailTemplateEntryAttributesTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetEstimateMailTemplateEntryAttributesTransformer extends Transformer {
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['label', 'quantity', 'rate', 'total'];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
public label(entry): string {
|
||||
return entry?.item?.name;
|
||||
}
|
||||
|
||||
public quantity(entry): string {
|
||||
return entry?.quantity;
|
||||
}
|
||||
|
||||
public rate(entry): string {
|
||||
return entry?.rateFormatted;
|
||||
}
|
||||
|
||||
public total(entry): string {
|
||||
return entry?.totalFormatted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { GetSaleEstimateMailStateTransformer } from './GetSaleEstimateMailStateTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export class GetSaleEstimateMailState {
|
||||
@Inject()
|
||||
private estimateMail: SendSaleEstimateMail;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the estimate mail state of the given sale estimate.
|
||||
* Estimate mail state includes the mail options, branding attributes and the estimate details.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<SaleEstimateMailState>}
|
||||
*/
|
||||
async getEstimateMailState(
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<SaleEstimateMailState> {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleEstimate = await SaleEstimate.query()
|
||||
.findById(saleEstimateId)
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('pdfTemplate')
|
||||
.throwIfNotFound();
|
||||
|
||||
const mailOptions = await this.estimateMail.getMailOptions(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
saleEstimate,
|
||||
new GetSaleEstimateMailStateTransformer(),
|
||||
{
|
||||
mailOptions,
|
||||
}
|
||||
);
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
|
||||
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
|
||||
|
||||
export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer {
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'estimateDate',
|
||||
'estimateDateFormatted',
|
||||
|
||||
'expirationDate',
|
||||
'expirationDateFormatted',
|
||||
|
||||
'total',
|
||||
'totalFormatted',
|
||||
|
||||
'subtotal',
|
||||
'subtotalFormatted',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountFormatted',
|
||||
'discountPercentage',
|
||||
'discountPercentageFormatted',
|
||||
'discountLabel',
|
||||
|
||||
'adjustment',
|
||||
'adjustmentFormatted',
|
||||
'adjustmentLabel',
|
||||
|
||||
'estimateNumber',
|
||||
'entries',
|
||||
|
||||
'companyName',
|
||||
'companyLogoUri',
|
||||
|
||||
'primaryColor',
|
||||
'customerName',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the customer name of the invoice.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected customerName = (invoice) => {
|
||||
return invoice.customer.displayName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company name.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected companyName = () => {
|
||||
return this.context.organization.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company logo uri.
|
||||
* @returns {string | null}
|
||||
*/
|
||||
protected companyLogoUri = (invoice) => {
|
||||
return invoice.pdfTemplate?.companyLogoUri || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the primary color.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected primaryColor = (invoice) => {
|
||||
return invoice.pdfTemplate?.attributes?.primaryColor || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the estimate number.
|
||||
*/
|
||||
protected estimateDateFormatted = (estimate) => {
|
||||
return this.formattedEstimateDate(estimate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expiration date of the estimate.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected expirationDateFormatted = (estimate) => {
|
||||
return this.formattedExpirationDate(estimate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total amount of the estimate.
|
||||
* @param estimate
|
||||
* @returns
|
||||
*/
|
||||
protected total(estimate) {
|
||||
return estimate.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the subtotal amount of the estimate.
|
||||
* @param estimate
|
||||
* @returns {number}
|
||||
*/
|
||||
protected subtotal(estimate) {
|
||||
return estimate.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the discount label of the estimate.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountLabel(estimate) {
|
||||
return estimate.discountType === 'percentage'
|
||||
? `Discount [${estimate.discountPercentageFormatted}]`
|
||||
: 'Discount';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted subtotal of the estimate.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalFormatted = (estimate) => {
|
||||
return this.formattedSubtotal(estimate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the estimate entries.
|
||||
* @param invoice
|
||||
* @returns {Array}
|
||||
*/
|
||||
protected entries = (invoice) => {
|
||||
return this.item(
|
||||
invoice.entries,
|
||||
new GetSaleEstimateMailStateEntryTransformer(),
|
||||
{
|
||||
currencyCode: invoice.currencyCode,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges the mail options with the invoice object.
|
||||
*/
|
||||
public transform = (object: any) => {
|
||||
return {
|
||||
...this.options.mailOptions,
|
||||
...object,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class GetSaleEstimateMailStateEntryTransformer extends ItemEntryTransformer {
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Item name.
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public name = (entry) => {
|
||||
return entry.item.name;
|
||||
};
|
||||
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'name',
|
||||
'quantity',
|
||||
'unitPrice',
|
||||
'unitPriceFormatted',
|
||||
'total',
|
||||
'totalFormatted',
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,11 @@ export class SaleEstimateTransfromer extends Transformer {
|
||||
'formattedDeliveredAtDate',
|
||||
'formattedApprovedAtDate',
|
||||
'formattedRejectedAtDate',
|
||||
'discountAmountFormatted',
|
||||
'discountPercentageFormatted',
|
||||
'adjustmentFormatted',
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
'formattedCreatedAt',
|
||||
'entries',
|
||||
'attachments',
|
||||
@@ -98,6 +103,62 @@ export class SaleEstimateTransfromer extends Transformer {
|
||||
return formatNumber(estimate.amount, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount amount.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (estimate: ISaleEstimate): string => {
|
||||
return formatNumber(estimate.discountAmount, {
|
||||
currencyCode: estimate.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount percentage.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountPercentageFormatted = (estimate: ISaleEstimate): string => {
|
||||
return estimate.discountPercentage
|
||||
? `${estimate.discountPercentage}%`
|
||||
: '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted adjustment amount.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (estimate: ISaleEstimate): string => {
|
||||
return this.formatMoney(estimate.adjustment, {
|
||||
currencyCode: estimate.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted estimate total.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (estimate: ISaleEstimate): string => {
|
||||
return this.formatMoney(estimate.total, {
|
||||
currencyCode: estimate.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted estimate total in local currency.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalLocalFormatted = (estimate: ISaleEstimate): string => {
|
||||
return this.formatMoney(estimate.totalLocal, {
|
||||
currencyCode: estimate.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the sale estimate.
|
||||
* @param {ISaleEstimate} estimate
|
||||
|
||||
@@ -21,6 +21,7 @@ import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
|
||||
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
|
||||
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||
import { GetSaleEstimateState } from './GetSaleEstimateState';
|
||||
import { GetSaleEstimateMailState } from './GetSaleEstimateMailState';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesApplication {
|
||||
@@ -57,6 +58,9 @@ export class SaleEstimatesApplication {
|
||||
@Inject()
|
||||
private sendEstimateMailService: SendSaleEstimateMail;
|
||||
|
||||
@Inject()
|
||||
private getSaleEstimateMailStateService: GetSaleEstimateMailState;
|
||||
|
||||
@Inject()
|
||||
private getSaleEstimateStateService: GetSaleEstimateState;
|
||||
|
||||
@@ -220,6 +224,18 @@ export class SaleEstimatesApplication {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the HTML content of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
*/
|
||||
public getSaleEstimateHtml(tenantId: number, saleEstimateId: number) {
|
||||
return this.saleEstimatesPdfService.saleEstimateHtml(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the reminder mail of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
@@ -238,6 +254,22 @@ export class SaleEstimatesApplication {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sale estimate mail state.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<SaleEstimateMailOptions>}
|
||||
*/
|
||||
public getSaleEstimateMailState(
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<SaleEstimateMailOptions> {
|
||||
return this.getSaleEstimateMailStateService.getEstimateMailState(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { renderEstimatePaperTemplateHtml, EstimatePaperTemplateProps } from '@bigcapital/pdf-templates';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesPdf {
|
||||
@@ -17,9 +16,6 @@ export class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
@Inject()
|
||||
private getSaleEstimate: GetSaleEstimate;
|
||||
|
||||
@@ -29,6 +25,22 @@ export class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve sale estimate html content.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} invoiceId -
|
||||
*/
|
||||
public async saleEstimateHtml(
|
||||
tenantId: number,
|
||||
estimateId: number
|
||||
): Promise<string> {
|
||||
const brandingAttributes = await this.getEstimateBrandingAttributes(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return renderEstimatePaperTemplateHtml({ ...brandingAttributes });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
@@ -42,15 +54,10 @@ export class SaleEstimatesPdf {
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
const brandingAttributes = await this.getEstimateBrandingAttributes(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/estimate-regular',
|
||||
brandingAttributes
|
||||
);
|
||||
// Retireves the sale estimate html.
|
||||
const htmlContent = await this.saleEstimateHtml(tenantId, saleEstimateId);
|
||||
|
||||
// Converts the html content to pdf.
|
||||
const content = await this.chromiumlyTenancy.convertHtmlContent(
|
||||
tenantId,
|
||||
htmlContent
|
||||
@@ -88,7 +95,7 @@ export class SaleEstimatesPdf {
|
||||
async getEstimateBrandingAttributes(
|
||||
tenantId: number,
|
||||
estimateId: number
|
||||
): Promise<EstimatePdfBrandingAttributes> {
|
||||
): Promise<EstimatePaperTemplateProps> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
const saleEstimate = await this.getSaleEstimate.getEstimate(
|
||||
tenantId,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { transformEstimateToMailDataArgs } from './utils';
|
||||
import { GetEstimateMailTemplate } from './GetEstimateMailTemplate';
|
||||
|
||||
@Service()
|
||||
export class SendSaleEstimateMail {
|
||||
@@ -32,12 +33,15 @@ export class SendSaleEstimateMail {
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
@Inject()
|
||||
private getEstimateMailTemplate: GetEstimateMailTemplate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Triggers the reminder mail of the given sale estimate.
|
||||
* @param {number} tenantId -
|
||||
@@ -76,7 +80,13 @@ export class SendSaleEstimateMail {
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return transformEstimateToMailDataArgs(estimate);
|
||||
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
|
||||
tenantId
|
||||
);
|
||||
return {
|
||||
...commonArgs,
|
||||
...transformEstimateToMailDataArgs(estimate),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,9 +142,45 @@ export class SendSaleEstimateMail {
|
||||
mailOptions,
|
||||
formatterArgs
|
||||
);
|
||||
return { ...formattedOptions };
|
||||
// Retrieves the estimate mail template.
|
||||
const message = await this.getEstimateMailTemplate.getMailTemplate(
|
||||
tenantId,
|
||||
saleEstimateId,
|
||||
{
|
||||
message: formattedOptions.message,
|
||||
preview: formattedOptions.message,
|
||||
}
|
||||
);
|
||||
return { ...formattedOptions, message };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted mail options.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @param {SaleEstimateMailOptionsDTO} messageOptions
|
||||
* @returns
|
||||
*/
|
||||
public async getFormattedMailOptions(
|
||||
tenantId: number,
|
||||
saleEstimateId: number,
|
||||
messageOptions: SaleEstimateMailOptionsDTO
|
||||
): Promise<SaleEstimateMailOptions> {
|
||||
const defaultMessageOptions = await this.getMailOptions(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
const parsedMessageOptions = mergeAndValidateMailOptions(
|
||||
defaultMessageOptions,
|
||||
messageOptions
|
||||
);
|
||||
return this.formatMailOptions(
|
||||
tenantId,
|
||||
saleEstimateId,
|
||||
parsedMessageOptions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the mail notification of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
@@ -147,20 +193,10 @@ export class SendSaleEstimateMail {
|
||||
saleEstimateId: number,
|
||||
messageOptions: SaleEstimateMailOptionsDTO
|
||||
): Promise<void> {
|
||||
const localMessageOpts = await this.getMailOptions(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
// Overrides and validates the given mail options.
|
||||
const parsedMessageOptions = mergeAndValidateMailOptions(
|
||||
localMessageOpts,
|
||||
messageOptions
|
||||
) as SaleEstimateMailOptions;
|
||||
|
||||
const formattedOptions = await this.formatMailOptions(
|
||||
const formattedOptions = await this.getFormattedMailOptions(
|
||||
tenantId,
|
||||
saleEstimateId,
|
||||
parsedMessageOptions
|
||||
messageOptions
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(formattedOptions.subject)
|
||||
@@ -182,7 +218,6 @@ export class SendSaleEstimateMail {
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const eventPayload = {
|
||||
tenantId,
|
||||
saleEstimateId,
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
|
||||
'Estimate {Estimate Number} is awaiting your approval';
|
||||
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {Customer Name}</p>
|
||||
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
|
||||
<p>
|
||||
Estimate <strong>#{Estimate Number}</strong><br />
|
||||
Expiration Date : <strong>{Estimate Expiration Date}</strong><br />
|
||||
Amount : <strong>{Estimate Amount}</strong></br />
|
||||
</p>
|
||||
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `Hi {Customer Name},
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{Company Name}</i>
|
||||
</p>
|
||||
`;
|
||||
Here's estimate # {Estimate Number} for {Estimate Amount}
|
||||
|
||||
This estimate is valid until {Estimate Expiration Date}, and we’re happy to discuss any adjustments you or questions may have.
|
||||
|
||||
Please find your estimate attached to this email for your reference.
|
||||
|
||||
If you have any questions, please let us know.
|
||||
|
||||
Thanks,
|
||||
{Company Name}`;
|
||||
|
||||
export const ERRORS = {
|
||||
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||
@@ -255,18 +254,27 @@ export interface EstimatePdfBrandingAttributes {
|
||||
companyAddress: string;
|
||||
billedToLabel: string;
|
||||
|
||||
// # Total
|
||||
total: string;
|
||||
totalLabel: string;
|
||||
showTotal: boolean;
|
||||
|
||||
// # Discount
|
||||
discount: string;
|
||||
showDiscount: boolean;
|
||||
discountLabel: string;
|
||||
|
||||
// # Subtotal
|
||||
subtotal: string;
|
||||
subtotalLabel: string;
|
||||
showSubtotal: boolean;
|
||||
|
||||
// # Customer Note
|
||||
showCustomerNote: boolean;
|
||||
customerNote: string;
|
||||
customerNoteLabel: string;
|
||||
|
||||
// # Terms & Conditions
|
||||
showTermsConditions: boolean;
|
||||
termsConditions: string;
|
||||
termsConditionsLabel: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { EstimatePaperTemplateProps } from '@bigcapital/pdf-templates';
|
||||
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||
import { EstimatePdfBrandingAttributes } from './constants';
|
||||
|
||||
export const transformEstimateToPdfTemplate = (
|
||||
estimate
|
||||
): Partial<EstimatePdfBrandingAttributes> => {
|
||||
): Partial<EstimatePaperTemplateProps> => {
|
||||
return {
|
||||
expirationDate: estimate.formattedExpirationDate,
|
||||
estimateNumebr: estimate.estimateNumber,
|
||||
@@ -13,13 +13,20 @@ export const transformEstimateToPdfTemplate = (
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
discount: entry.discountFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
total: estimate.formattedSubtotal,
|
||||
total: estimate.totalFormatted,
|
||||
subtotal: estimate.formattedSubtotal,
|
||||
adjustment: estimate.adjustmentFormatted,
|
||||
customerNote: estimate.note,
|
||||
termsConditions: estimate.termsConditions,
|
||||
customerAddress: contactAddressTextFormat(estimate.customer),
|
||||
showLineDiscount: estimate.entries.some((entry) => entry.discountFormatted),
|
||||
discount: estimate.discountAmountFormatted,
|
||||
discountLabel: estimate.discountPercentageFormatted
|
||||
? `Discount [${estimate.discountPercentageFormatted}]`
|
||||
: 'Discount',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -154,6 +154,6 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
* @returns {number}
|
||||
*/
|
||||
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
|
||||
return sumBy(entries, (e) => e.amount);
|
||||
return sumBy(entries, (e) => e.total);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
|
||||
'invoiceNumber',
|
||||
'invoiceNumberLabel',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLabel',
|
||||
|
||||
'discount',
|
||||
'discountLabel',
|
||||
|
||||
'adjustment',
|
||||
'adjustmentLabel',
|
||||
|
||||
'total',
|
||||
'totalLabel',
|
||||
|
||||
@@ -76,6 +85,30 @@ export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
|
||||
return 'Invoice # {invoiceNumber}';
|
||||
}
|
||||
|
||||
public subtotal(): string {
|
||||
return this.options.invoice?.subtotalFormatted;
|
||||
}
|
||||
|
||||
public subtotalLabel(): string {
|
||||
return 'Subtotal';
|
||||
}
|
||||
|
||||
public discount(): string {
|
||||
return this.options.invoice?.discountAmountFormatted;
|
||||
}
|
||||
|
||||
public discountLabel(): string {
|
||||
return 'Discount';
|
||||
}
|
||||
|
||||
public adjustment(): string {
|
||||
return this.options.invoice?.adjustmentFormatted;
|
||||
}
|
||||
|
||||
public adjustmentLabel(): string {
|
||||
return 'Adjustment';
|
||||
}
|
||||
|
||||
public total(): string {
|
||||
return this.options.invoice?.totalFormatted;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,15 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
|
||||
'total',
|
||||
'totalFormatted',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountFormatted',
|
||||
'discountPercentage',
|
||||
'discountPercentageFormatted',
|
||||
'discountLabel',
|
||||
|
||||
'adjustment',
|
||||
'adjustmentFormatted',
|
||||
|
||||
'subtotal',
|
||||
'subtotalFormatted',
|
||||
|
||||
@@ -76,6 +85,17 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
|
||||
return invoice.pdfTemplate?.attributes?.primaryColor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the discount label of the estimate.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountLabel(invoice) {
|
||||
return invoice.discountType === 'percentage'
|
||||
? `Discount [${invoice.discountPercentageFormatted}]`
|
||||
: 'Discount';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param invoice
|
||||
|
||||
@@ -44,18 +44,31 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
// Find or create the A/R account.
|
||||
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
|
||||
saleInvoice.currencyCode, {}, trx
|
||||
saleInvoice.currencyCode,
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Find or create tax payable account.
|
||||
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Find or create the discount expense account.
|
||||
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Find or create the other charges account.
|
||||
const otherChargesAccount =
|
||||
await accountRepository.findOrCreateOtherChargesAccount({}, trx);
|
||||
|
||||
// Retrieves the ledger of the invoice.
|
||||
const ledger = this.getInvoiceGLedger(
|
||||
saleInvoice,
|
||||
ARAccount.id,
|
||||
taxPayableAccount.id
|
||||
taxPayableAccount.id,
|
||||
discountAccount.id,
|
||||
otherChargesAccount.id
|
||||
);
|
||||
// Commits the ledger entries to the storage as UOW.
|
||||
await this.ledegrRepository.commit(tenantId, ledger, trx);
|
||||
@@ -107,12 +120,16 @@ export class SaleInvoiceGLEntries {
|
||||
public getInvoiceGLedger = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
taxPayableAccountId: number,
|
||||
discountAccountId: number,
|
||||
otherChargesAccountId: number
|
||||
): ILedger => {
|
||||
const entries = this.getInvoiceGLEntries(
|
||||
saleInvoice,
|
||||
ARAccountId,
|
||||
taxPayableAccountId
|
||||
taxPayableAccountId,
|
||||
discountAccountId,
|
||||
otherChargesAccountId
|
||||
);
|
||||
return new Ledger(entries);
|
||||
};
|
||||
@@ -127,6 +144,7 @@ export class SaleInvoiceGLEntries {
|
||||
): Partial<ILedgerEntry> => ({
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
|
||||
currencyCode: saleInvoice.currencyCode,
|
||||
exchangeRate: saleInvoice.exchangeRate,
|
||||
|
||||
@@ -181,7 +199,7 @@ export class SaleInvoiceGLEntries {
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate;
|
||||
const localAmount = entry.totalExcludingTax * saleInvoice.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
@@ -249,6 +267,50 @@ export class SaleInvoiceGLEntries {
|
||||
return nonZeroTaxEntries.map(transformTaxEntry);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice discount GL entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} discountAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceDiscountEntry = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
discountAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleInvoice.discountAmountLocal,
|
||||
accountId: discountAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
} as ILedgerEntry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice adjustment GL entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} adjustmentAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAdjustmentEntry = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
otherChargesAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
const adjustmentAmount = Math.abs(saleInvoice.adjustmentLocal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleInvoice.adjustmentLocal < 0 ? adjustmentAmount : 0,
|
||||
credit: saleInvoice.adjustmentLocal > 0 ? adjustmentAmount : 0,
|
||||
accountId: otherChargesAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
@@ -258,7 +320,9 @@ export class SaleInvoiceGLEntries {
|
||||
public getInvoiceGLEntries = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
taxPayableAccountId: number,
|
||||
discountAccountId: number,
|
||||
otherChargesAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const receivableEntry = this.getInvoiceReceivableEntry(
|
||||
saleInvoice,
|
||||
@@ -271,6 +335,20 @@ export class SaleInvoiceGLEntries {
|
||||
saleInvoice,
|
||||
taxPayableAccountId
|
||||
);
|
||||
return [receivableEntry, ...creditEntries, ...taxEntries];
|
||||
const discountEntry = this.getInvoiceDiscountEntry(
|
||||
saleInvoice,
|
||||
discountAccountId
|
||||
);
|
||||
const adjustmentEntry = this.getAdjustmentEntry(
|
||||
saleInvoice,
|
||||
otherChargesAccountId
|
||||
);
|
||||
return [
|
||||
receivableEntry,
|
||||
...creditEntries,
|
||||
...taxEntries,
|
||||
discountEntry,
|
||||
adjustmentEntry,
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IItemEntry } from '@/interfaces';
|
||||
import { DiscountType, IItemEntry } from '@/interfaces';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from '@/utils';
|
||||
|
||||
@@ -8,7 +8,13 @@ export class ItemEntryTransformer extends Transformer {
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
|
||||
return [
|
||||
'quantityFormatted',
|
||||
'rateFormatted',
|
||||
'totalFormatted',
|
||||
'discountFormatted',
|
||||
'discountAmountFormatted',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,4 +49,34 @@ export class ItemEntryTransformer extends Transformer {
|
||||
money: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount of item entry.
|
||||
* @param {IItemEntry} entry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountFormatted = (entry: IItemEntry): string => {
|
||||
if (!entry.discount) {
|
||||
return '';
|
||||
}
|
||||
return entry.discountType === DiscountType.Percentage
|
||||
? `${entry.discount}%`
|
||||
: formatNumber(entry.discount, {
|
||||
currencyCode: this.context.currencyCode,
|
||||
money: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount amount of item entry.
|
||||
* @param {IItemEntry} entry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (entry: IItemEntry): string => {
|
||||
return formatNumber(entry.discountAmount, {
|
||||
currencyCode: this.context.currencyCode,
|
||||
money: false,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
|
||||
import {
|
||||
renderInvoicePaperTemplateHtml,
|
||||
InvoicePaperTemplateProps,
|
||||
} from '@bigcapital/pdf-templates';
|
||||
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';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { renderInvoicePaymentEmail } from '@bigcapital/email-components';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdf {
|
||||
@@ -102,7 +102,7 @@ export class SaleInvoicePdf {
|
||||
async getInvoiceBrandingAttributes(
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<InvoicePdfTemplateAttributes> {
|
||||
): Promise<InvoicePaperTemplateProps> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const invoice = await this.getInvoiceService.getSaleInvoice(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { formatNumber } from 'utils';
|
||||
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer';
|
||||
import { ItemEntryTransformer } from './ItemEntryTransformer';
|
||||
import { AttachmentTransformer } from '@/services/Attachments/AttachmentTransformer';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
|
||||
export class SaleInvoiceTransformer extends Transformer {
|
||||
/**
|
||||
@@ -23,6 +24,9 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
'subtotalExludingTaxFormatted',
|
||||
'taxAmountWithheldFormatted',
|
||||
'taxAmountWithheldLocalFormatted',
|
||||
'discountAmountFormatted',
|
||||
'discountPercentageFormatted',
|
||||
'adjustmentFormatted',
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
'taxes',
|
||||
@@ -158,6 +162,41 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount amount.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.discountAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount percentage.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountPercentageFormatted = (invoice): string => {
|
||||
return invoice.discountType === DiscountType.Percentage
|
||||
? `${invoice.discount}%`
|
||||
: '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted adjustment amount.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (invoice): string => {
|
||||
return this.formatMoney(invoice.adjustment, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted total in foreign currency.
|
||||
* @param invoice
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { pickBy } from 'lodash';
|
||||
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
|
||||
import { ISaleInvoice } from '@/interfaces';
|
||||
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||
import { InvoicePaperTemplateProps } from '@bigcapital/pdf-templates';
|
||||
|
||||
export const mergePdfTemplateWithDefaultAttributes = (
|
||||
brandingTemplate?: Record<string, any>,
|
||||
@@ -18,7 +19,7 @@ export const mergePdfTemplateWithDefaultAttributes = (
|
||||
|
||||
export const transformInvoiceToPdfTemplate = (
|
||||
invoice: ISaleInvoice
|
||||
): Partial<InvoicePdfTemplateAttributes> => {
|
||||
): Partial<InvoicePaperTemplateProps> => {
|
||||
return {
|
||||
dueDate: invoice.dueDateFormatted,
|
||||
dateIssue: invoice.invoiceDateFormatted,
|
||||
@@ -28,6 +29,11 @@ export const transformInvoiceToPdfTemplate = (
|
||||
subtotal: invoice.subtotalFormatted,
|
||||
paymentMade: invoice.paymentAmountFormatted,
|
||||
dueAmount: invoice.dueAmountFormatted,
|
||||
discount: invoice.discountAmountFormatted,
|
||||
adjustment: invoice.adjustmentFormatted,
|
||||
discountLabel: invoice.discountPercentageFormatted
|
||||
? `Discount [${invoice.discountPercentageFormatted}]`
|
||||
: 'Discount',
|
||||
|
||||
termsConditions: invoice.termsConditions,
|
||||
statement: invoice.invoiceMessage,
|
||||
@@ -37,13 +43,14 @@ export const transformInvoiceToPdfTemplate = (
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
discount: entry.discountFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
taxes: invoice.taxes.map((tax) => ({
|
||||
label: tax.name,
|
||||
amount: tax.taxRateAmountFormatted,
|
||||
})),
|
||||
|
||||
showLineDiscount: invoice.entries.some((entry) => entry.discountFormatted),
|
||||
customerAddress: contactAddressTextFormat(invoice.customer),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { PaymentReceiveMailOpts } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { GetPaymentReceivedMailStateTransformer } from './GetPaymentReceivedMailStateTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
|
||||
|
||||
@Service()
|
||||
export class GetPaymentReceivedMailState {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private paymentReceivedMail: SendPaymentReceiveMailNotification;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the default payment mail options.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} paymentReceiveId - Payment receive id.
|
||||
* @returns {Promise<PaymentReceiveMailOpts>}
|
||||
*/
|
||||
public getMailOptions = async (
|
||||
tenantId: number,
|
||||
paymentId: number
|
||||
): Promise<PaymentReceiveMailOpts> => {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentReceive = await PaymentReceive.query()
|
||||
.findById(paymentId)
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('entries.invoice')
|
||||
.withGraphFetched('pdfTemplate')
|
||||
.throwIfNotFound();
|
||||
|
||||
const mailOptions = await this.paymentReceivedMail.getMailOptions(
|
||||
tenantId,
|
||||
paymentId
|
||||
);
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
paymentReceive,
|
||||
new GetPaymentReceivedMailStateTransformer(),
|
||||
{
|
||||
mailOptions,
|
||||
}
|
||||
);
|
||||
return transformed;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { PaymentReceiveEntry } from '@/models';
|
||||
import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer';
|
||||
import { PaymentReceivedEntryTransfromer } from './PaymentReceivedEntryTransformer';
|
||||
|
||||
export class GetPaymentReceivedMailStateTransformer extends PaymentReceiveTransfromer {
|
||||
/**
|
||||
* Exclude these attributes from user object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'paymentDate',
|
||||
'paymentDateFormatted',
|
||||
|
||||
'paymentAmount',
|
||||
'paymentAmountFormatted',
|
||||
|
||||
'total',
|
||||
'totalFormatted',
|
||||
|
||||
'subtotal',
|
||||
'subtotalFormatted',
|
||||
|
||||
'paymentNumber',
|
||||
|
||||
'entries',
|
||||
|
||||
'companyName',
|
||||
'companyLogoUri',
|
||||
|
||||
'primaryColor',
|
||||
|
||||
'customerName',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the customer name of the payment.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected customerName = (payment) => {
|
||||
return payment.customer.displayName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company name.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected companyName = () => {
|
||||
return this.context.organization.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company logo uri.
|
||||
* @returns {string | null}
|
||||
*/
|
||||
protected companyLogoUri = (payment) => {
|
||||
return payment.pdfTemplate?.companyLogoUri;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the primary color.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected primaryColor = (payment) => {
|
||||
return payment.pdfTemplate?.attributes?.primaryColor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted payment date.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected paymentDateFormatted = (payment) => {
|
||||
return this.formatDate(payment.paymentDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment amount.
|
||||
* @param payment
|
||||
* @returns {number}
|
||||
*/
|
||||
protected total = (payment) => {
|
||||
return this.formatNumber(payment.amount, {
|
||||
money: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted payment amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (payment) => {
|
||||
return this.formatMoney(payment.amount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment amount.
|
||||
* @param payment
|
||||
* @returns {number}
|
||||
*/
|
||||
protected subtotal = (payment) => {
|
||||
return this.formatNumber(payment.amount, {
|
||||
money: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted payment amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalFormatted = (payment) => {
|
||||
return this.formatMoney(payment.amount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment number.
|
||||
* @param payment
|
||||
* @returns {string}
|
||||
*/
|
||||
protected paymentNumber = (payment) => {
|
||||
return payment.paymentReceiveNo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment entries.
|
||||
* @param {IPaymentReceived} payment
|
||||
* @returns {IPaymentReceivedEntry[]}
|
||||
*/
|
||||
protected entries = (payment) => {
|
||||
return this.item(payment.entries, new GetPaymentReceivedEntryMailState());
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges the mail options with the invoice object.
|
||||
*/
|
||||
public transform = (object: any) => {
|
||||
return {
|
||||
...this.options.mailOptions,
|
||||
...object,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class GetPaymentReceivedEntryMailState extends PaymentReceivedEntryTransfromer {
|
||||
/**
|
||||
* Include these attributes to payment receive entry object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['paidAmount', 'invoiceNumber'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude these attributes from user object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the paid amount.
|
||||
* @param entry
|
||||
* @returns {string}
|
||||
*/
|
||||
public paidAmount = (entry) => {
|
||||
return this.paymentAmountFormatted(entry);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice number.
|
||||
* @param entry
|
||||
* @returns {string}
|
||||
*/
|
||||
public invoiceNumber = (entry) => {
|
||||
return entry.invoice.invoiceNo;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||
import { GetPaymentReceived } from './GetPaymentReceived';
|
||||
import { GetPaymentReceivedMailTemplateAttrsTransformer } from './GetPaymentReceivedMailTemplateAttrsTransformer';
|
||||
import {
|
||||
PaymentReceivedEmailTemplateProps,
|
||||
renderPaymentReceivedEmailTemplate,
|
||||
} from '@bigcapital/email-components';
|
||||
|
||||
@Service()
|
||||
export class GetPaymentReceivedMailTemplate {
|
||||
@Inject()
|
||||
private getPaymentReceivedService: GetPaymentReceived;
|
||||
|
||||
@Inject()
|
||||
private getBrandingTemplate: GetPdfTemplate;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the mail template attributes of the given payment received.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} paymentReceivedId - Payment received id.
|
||||
* @returns {Promise<PaymentReceivedEmailTemplateProps>}
|
||||
*/
|
||||
public async getMailTemplateAttributes(
|
||||
tenantId: number,
|
||||
paymentReceivedId: number
|
||||
): Promise<PaymentReceivedEmailTemplateProps> {
|
||||
const paymentReceived =
|
||||
await this.getPaymentReceivedService.getPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceivedId
|
||||
);
|
||||
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
|
||||
tenantId,
|
||||
paymentReceived.pdfTemplateId
|
||||
);
|
||||
const mailTemplateAttributes = await this.transformer.transform(
|
||||
tenantId,
|
||||
paymentReceived,
|
||||
new GetPaymentReceivedMailTemplateAttrsTransformer(),
|
||||
{
|
||||
paymentReceived,
|
||||
brandingTemplate,
|
||||
}
|
||||
);
|
||||
return mailTemplateAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail template html content.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceivedId
|
||||
* @param {Partial<PaymentReceivedEmailTemplateProps>} overrideAttributes
|
||||
* @returns
|
||||
*/
|
||||
public async getMailTemplate(
|
||||
tenantId: number,
|
||||
paymentReceivedId: number,
|
||||
overrideAttributes?: Partial<PaymentReceivedEmailTemplateProps>
|
||||
): Promise<string> {
|
||||
const mailTemplateAttributes = await this.getMailTemplateAttributes(
|
||||
tenantId,
|
||||
paymentReceivedId
|
||||
);
|
||||
const mergedAttributes = {
|
||||
...mailTemplateAttributes,
|
||||
...overrideAttributes,
|
||||
};
|
||||
return renderPaymentReceivedEmailTemplate(mergedAttributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class GetPaymentReceivedMailTemplateAttrsTransformer extends Transformer {
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'companyLogoUri',
|
||||
'companyName',
|
||||
'primaryColor',
|
||||
'total',
|
||||
'totalLabel',
|
||||
'subtotal',
|
||||
'subtotalLabel',
|
||||
'paymentNumberLabel',
|
||||
'paymentNumber',
|
||||
'items',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Company logo uri.
|
||||
* @returns {string}
|
||||
*/
|
||||
public companyLogoUri(): string {
|
||||
return this.options.brandingTemplate?.companyLogoUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Company name.
|
||||
* @returns {string}
|
||||
*/
|
||||
public companyName(): string {
|
||||
return this.context.organization.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary color
|
||||
* @returns {string}
|
||||
*/
|
||||
public primaryColor(): string {
|
||||
return this.options?.brandingTemplate?.attributes?.primaryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total.
|
||||
* @returns {string}
|
||||
*/
|
||||
public total(): string {
|
||||
return this.options.paymentReceived.formattedAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public totalLabel(): string {
|
||||
return 'Total';
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal.
|
||||
* @returns {string}
|
||||
*/
|
||||
public subtotal(): string {
|
||||
return this.options.paymentReceived.formattedAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public subtotalLabel(): string {
|
||||
return 'Subtotal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment number label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public paymentNumberLabel(): string {
|
||||
return 'Payment # {paymentNumber}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment number.
|
||||
* @returns {string}
|
||||
*/
|
||||
public paymentNumber(): string {
|
||||
return this.options.paymentReceived.paymentReceiveNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Items.
|
||||
* @returns
|
||||
*/
|
||||
public items() {
|
||||
return this.item(
|
||||
this.options.paymentReceived.entries,
|
||||
new GetPaymentReceivedMailTemplateItemAttrsTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetPaymentReceivedMailTemplateItemAttrsTransformer extends Transformer {
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = () => {
|
||||
return ['label', 'total'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Excluded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = () => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public label(entry) {
|
||||
return entry.invoice.invoiceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public total(entry) {
|
||||
return entry.paymentAmountFormatted;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates';
|
||||
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';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export default class GetPaymentReceivedPdf {
|
||||
@@ -17,9 +17,6 @@ export default class GetPaymentReceivedPdf {
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
@Inject()
|
||||
private getPaymentService: GetPaymentReceived;
|
||||
|
||||
@@ -29,6 +26,23 @@ export default class GetPaymentReceivedPdf {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieves payment received html content.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceivedId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async getPaymentReceivedHtml(
|
||||
tenantId: number,
|
||||
paymentReceivedId: number
|
||||
): Promise<string> {
|
||||
const brandingAttributes = await this.getPaymentBrandingAttributes(
|
||||
tenantId,
|
||||
paymentReceivedId
|
||||
);
|
||||
return renderPaymentReceivedPaperTemplateHtml(brandingAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
@@ -39,15 +53,10 @@ export default class GetPaymentReceivedPdf {
|
||||
tenantId: number,
|
||||
paymentReceivedId: number
|
||||
): Promise<[Buffer, string]> {
|
||||
const brandingAttributes = await this.getPaymentBrandingAttributes(
|
||||
const htmlContent = await this.getPaymentReceivedHtml(
|
||||
tenantId,
|
||||
paymentReceivedId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/payment-receive-standard',
|
||||
brandingAttributes
|
||||
);
|
||||
const filename = await this.getPaymentReceivedFilename(
|
||||
tenantId,
|
||||
paymentReceivedId
|
||||
|
||||
@@ -20,6 +20,7 @@ import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify';
|
||||
import GetPaymentReceivedPdf from './GetPaymentReceivedPdf';
|
||||
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
|
||||
import { GetPaymentReceivedState } from './GetPaymentReceivedState';
|
||||
import { GetPaymentReceivedMailState } from './GetPaymentReceivedMailState';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceivesApplication {
|
||||
@@ -53,6 +54,9 @@ export class PaymentReceivesApplication {
|
||||
@Inject()
|
||||
private getPaymentReceivedStateService: GetPaymentReceivedState;
|
||||
|
||||
@Inject()
|
||||
private getPaymentReceivedMailStateService: GetPaymentReceivedMailState;
|
||||
|
||||
/**
|
||||
* Creates a new payment receive.
|
||||
* @param {number} tenantId
|
||||
@@ -204,12 +208,15 @@ export class PaymentReceivesApplication {
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given payment transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} paymentReceiveId - Payment received id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) {
|
||||
return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId);
|
||||
return this.getPaymentReceivedMailStateService.getMailOptions(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,6 +235,22 @@ export class PaymentReceivesApplication {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given payment receive html document.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public getPaymentReceivedHtml = (
|
||||
tenantId: number,
|
||||
paymentReceiveId: number
|
||||
) => {
|
||||
return this.getPaymentReceivePdfService.getPaymentReceivedHtml(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the create/edit initial state of the payment received.
|
||||
* @param {number} tenantId - The ID of the tenant.
|
||||
|
||||
@@ -15,8 +15,9 @@ import { GetPaymentReceived } from './GetPaymentReceived';
|
||||
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { transformPaymentReceivedToMailDataArgs } from './utils';
|
||||
import { GetPaymentReceivedMailTemplate } from './GetPaymentReceivedMailTemplate';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class SendPaymentReceiveMailNotification {
|
||||
@@ -29,12 +30,15 @@ export class SendPaymentReceiveMailNotification {
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private paymentMailTemplate: GetPaymentReceivedMailTemplate;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Sends the mail of the given payment receive.
|
||||
* @param {number} tenantId
|
||||
@@ -62,37 +66,6 @@ export class SendPaymentReceiveMailNotification {
|
||||
} as PaymentReceiveMailPresendEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default payment mail options.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} paymentReceiveId - Payment receive id.
|
||||
* @returns {Promise<PaymentReceiveMailOpts>}
|
||||
*/
|
||||
public getMailOptions = async (
|
||||
tenantId: number,
|
||||
paymentId: number
|
||||
): Promise<PaymentReceiveMailOpts> => {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentReceive = await PaymentReceive.query()
|
||||
.findById(paymentId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const formatArgs = await this.textFormatter(tenantId, paymentId);
|
||||
|
||||
const mailOptions =
|
||||
await this.contactMailNotification.getDefaultMailOptions(
|
||||
tenantId,
|
||||
paymentReceive.customerId
|
||||
);
|
||||
return {
|
||||
...mailOptions,
|
||||
subject: DEFAULT_PAYMENT_MAIL_SUBJECT,
|
||||
message: DEFAULT_PAYMENT_MAIL_CONTENT,
|
||||
...formatArgs,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted text of the given sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
@@ -108,7 +81,82 @@ export class SendPaymentReceiveMailNotification {
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
return transformPaymentReceivedToMailDataArgs(payment);
|
||||
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
|
||||
tenantId
|
||||
);
|
||||
const paymentArgs = transformPaymentReceivedToMailDataArgs(payment);
|
||||
|
||||
return {
|
||||
...commonArgs,
|
||||
...paymentArgs,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the mail options of the given payment received.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} paymentReceivedId - Payment received id.
|
||||
* @param {string} defaultSubject - Default subject of the mail.
|
||||
* @param {string} defaultContent - Default content of the mail.
|
||||
* @returns
|
||||
*/
|
||||
public getMailOptions = async (
|
||||
tenantId: number,
|
||||
paymentReceivedId: number,
|
||||
defaultSubject: string = DEFAULT_PAYMENT_MAIL_SUBJECT,
|
||||
defaultContent: string = DEFAULT_PAYMENT_MAIL_CONTENT
|
||||
): Promise<PaymentReceiveMailOpts> => {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentReceived = await PaymentReceive.query().findById(
|
||||
paymentReceivedId
|
||||
);
|
||||
const formatArgs = await this.textFormatter(tenantId, paymentReceivedId);
|
||||
|
||||
// Retrieves the default mail options.
|
||||
const mailOptions =
|
||||
await this.contactMailNotification.getDefaultMailOptions(
|
||||
tenantId,
|
||||
paymentReceived.customerId
|
||||
);
|
||||
return {
|
||||
...mailOptions,
|
||||
message: defaultContent,
|
||||
subject: defaultSubject,
|
||||
attachPdf: true,
|
||||
formatArgs,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the mail options of the given payment receive.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {PaymentReceiveMailOpts} mailOptions
|
||||
* @returns {Promise<PaymentReceiveMailOpts>}
|
||||
*/
|
||||
public formattedMailOptions = async (
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
mailOptions: PaymentReceiveMailOpts
|
||||
): Promise<PaymentReceiveMailOpts> => {
|
||||
const formatterArgs = await this.textFormatter(tenantId, paymentReceiveId);
|
||||
const formattedOptions =
|
||||
await this.contactMailNotification.formatMailOptions(
|
||||
tenantId,
|
||||
mailOptions,
|
||||
formatterArgs
|
||||
);
|
||||
// Retrieves the mail template.
|
||||
const message = await this.paymentMailTemplate.getMailTemplate(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
{
|
||||
message: formattedOptions.message,
|
||||
preview: formattedOptions.message,
|
||||
}
|
||||
);
|
||||
return { ...formattedOptions, message };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -136,10 +184,10 @@ export class SendPaymentReceiveMailNotification {
|
||||
messageDTO
|
||||
);
|
||||
// Formats the message options.
|
||||
return this.contactMailNotification.formatMailOptions(
|
||||
return this.formattedMailOptions(
|
||||
tenantId,
|
||||
parsedMessageOpts,
|
||||
formatterArgs
|
||||
paymentReceiveId,
|
||||
parsedMessageOpts
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
export const DEFAULT_PAYMENT_MAIL_SUBJECT =
|
||||
'Payment Received for {Customer Name} from {Company Name}';
|
||||
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
|
||||
<p>Dear {Customer Name}</p>
|
||||
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
|
||||
<p>
|
||||
Payment Date : <strong>{Payment Date}</strong><br />
|
||||
Amount : <strong>{Payment Amount}</strong></br />
|
||||
</p>
|
||||
' Payment Confirmation from {Company Name} – Thank You!';
|
||||
export const DEFAULT_PAYMENT_MAIL_CONTENT = `Dear {Customer Name}
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{Company Name}</i>
|
||||
</p>
|
||||
`;
|
||||
Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!
|
||||
|
||||
Payment Transaction: {Payment Number}
|
||||
Payment Date : {Payment Date}
|
||||
Amount : {Payment Amount}
|
||||
|
||||
Regards,
|
||||
{Company Name}`;
|
||||
|
||||
export const ERRORS = {
|
||||
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||
import { GetSaleReceiptMailStateTransformer } from './GetSaleReceiptMailStateTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetSaleReceiptMailState {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private receiptMail: SaleReceiptMailNotification;
|
||||
|
||||
/**
|
||||
* Retrieves the sale receipt mail state of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
*/
|
||||
public async getMailState(tenantId: number, saleReceiptId: number) {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleReceipt = await SaleReceipt.query()
|
||||
.findById(saleReceiptId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('customer')
|
||||
.throwIfNotFound();
|
||||
|
||||
const mailOptions = await this.receiptMail.getMailOptions(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
saleReceipt,
|
||||
new GetSaleReceiptMailStateTransformer(),
|
||||
{
|
||||
mailOptions,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
|
||||
import { DiscountType } from '@/interfaces';
|
||||
import { SaleReceiptTransformer } from './SaleReceiptTransformer';
|
||||
|
||||
export class GetSaleReceiptMailStateTransformer extends SaleReceiptTransformer {
|
||||
/**
|
||||
* Exclude these attributes from user object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'companyName',
|
||||
'companyLogoUri',
|
||||
'primaryColor',
|
||||
'customerName',
|
||||
|
||||
'total',
|
||||
'totalFormatted',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountFormatted',
|
||||
'discountPercentage',
|
||||
'discountPercentageFormatted',
|
||||
'discountLabel',
|
||||
|
||||
'adjustment',
|
||||
'adjustmentFormatted',
|
||||
|
||||
'subtotal',
|
||||
'subtotalFormatted',
|
||||
|
||||
'receiptDate',
|
||||
'receiptDateFormatted',
|
||||
|
||||
'closedAtDate',
|
||||
'closedAtDateFormatted',
|
||||
|
||||
'receiptNumber',
|
||||
'entries',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the customer name of the invoice.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected customerName = (receipt) => {
|
||||
return receipt.customer.displayName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company name.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected companyName = () => {
|
||||
return this.context.organization.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company logo uri.
|
||||
* @returns {string | null}
|
||||
*/
|
||||
protected companyLogoUri = (receipt) => {
|
||||
return receipt.pdfTemplate?.companyLogoUri;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the primary color.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected primaryColor = (receipt) => {
|
||||
return receipt.pdfTemplate?.attributes?.primaryColor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total amount.
|
||||
* @param receipt
|
||||
* @returns
|
||||
*/
|
||||
protected total = (receipt) => {
|
||||
return receipt.total;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total amount.
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (receipt) => {
|
||||
return this.formatMoney(receipt.total, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the discount label of the estimate.
|
||||
* @param estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountLabel(receipt) {
|
||||
return receipt.discountType === DiscountType.Percentage
|
||||
? `Discount [${receipt.discountPercentageFormatted}]`
|
||||
: 'Discount';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the subtotal of the receipt.
|
||||
* @param receipt
|
||||
* @returns
|
||||
*/
|
||||
protected subtotal = (receipt) => {
|
||||
return receipt.subtotal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted subtotal of the receipt.
|
||||
* @param receipt
|
||||
* @returns
|
||||
*/
|
||||
protected subtotalFormatted = (receipt) => {
|
||||
return this.formatMoney(receipt.subtotal, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the receipt date.
|
||||
* @param receipt
|
||||
* @returns
|
||||
*/
|
||||
protected receiptDate = (receipt): string => {
|
||||
return receipt.receiptDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted receipt date.
|
||||
* @param {ISaleReceipt} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected receiptDateFormatted = (receipt): string => {
|
||||
return this.formatDate(receipt.receiptDate);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param receipt
|
||||
* @returns
|
||||
*/
|
||||
protected closedAtDate = (receipt): string => {
|
||||
return receipt.closedAt;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted estimate closed at date.
|
||||
* @param {ISaleReceipt} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected closedAtDateFormatted = (receipt): string => {
|
||||
return this.formatDate(receipt.closedAt);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param invoice
|
||||
* @returns
|
||||
*/
|
||||
protected entries = (receipt) => {
|
||||
return this.item(
|
||||
receipt.entries,
|
||||
new GetSaleReceiptEntryMailStateTransformer(),
|
||||
{
|
||||
currencyCode: receipt.currencyCode,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges the mail options with the invoice object.
|
||||
*/
|
||||
public transform = (object: any) => {
|
||||
return {
|
||||
...this.options.mailOptions,
|
||||
...object,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class GetSaleReceiptEntryMailStateTransformer extends ItemEntryTransformer {
|
||||
/**
|
||||
* Exclude these attributes from user object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
public name = (entry) => {
|
||||
return entry.item.name;
|
||||
};
|
||||
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'name',
|
||||
'quantity',
|
||||
'quantityFormatted',
|
||||
'rate',
|
||||
'rateFormatted',
|
||||
'total',
|
||||
'totalFormatted',
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
ReceiptEmailTemplateProps,
|
||||
renderReceiptEmailTemplate,
|
||||
} from '@bigcapital/email-components';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetSaleReceiptMailTemplateAttributesTransformer } from './GetSaleReceiptMailTemplateAttributesTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetSaleReceiptMailTemplate {
|
||||
@Inject()
|
||||
private getReceiptService: GetSaleReceipt;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private getBrandingTemplate: GetPdfTemplate;
|
||||
|
||||
/**
|
||||
* Retrieves the mail template attributes of the given estimate.
|
||||
* Estimate template attributes are composed of the estimate and branding template attributes.
|
||||
* @param {number} tenantId
|
||||
* @param {number} receiptId - Receipt id.
|
||||
* @returns {Promise<EstimatePaymentEmailProps>}
|
||||
*/
|
||||
public async getMailTemplateAttributes(
|
||||
tenantId: number,
|
||||
receiptId: number
|
||||
): Promise<ReceiptEmailTemplateProps> {
|
||||
const receipt = await this.getReceiptService.getSaleReceipt(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
|
||||
tenantId,
|
||||
receipt.pdfTemplateId
|
||||
);
|
||||
const mailTemplateAttributes = await this.transformer.transform(
|
||||
tenantId,
|
||||
receipt,
|
||||
new GetSaleReceiptMailTemplateAttributesTransformer(),
|
||||
{
|
||||
receipt,
|
||||
brandingTemplate,
|
||||
}
|
||||
);
|
||||
return mailTemplateAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail template html content.
|
||||
* @param {number} tenantId
|
||||
* @param {number} estimateId
|
||||
* @param overrideAttributes
|
||||
* @returns
|
||||
*/
|
||||
public async getMailTemplate(
|
||||
tenantId: number,
|
||||
estimateId: number,
|
||||
overrideAttributes?: Partial<any>
|
||||
): Promise<string> {
|
||||
const attributes = await this.getMailTemplateAttributes(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
const mergedAttributes = {
|
||||
...attributes,
|
||||
...overrideAttributes,
|
||||
};
|
||||
return renderReceiptEmailTemplate(mergedAttributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class GetSaleReceiptMailTemplateAttributesTransformer extends Transformer {
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'companyLogoUri',
|
||||
'companyName',
|
||||
|
||||
'primaryColor',
|
||||
|
||||
'receiptAmount',
|
||||
'receiptMessage',
|
||||
|
||||
'date',
|
||||
'dateLabel',
|
||||
|
||||
'receiptNumber',
|
||||
'receiptNumberLabel',
|
||||
|
||||
'total',
|
||||
'totalLabel',
|
||||
|
||||
'discount',
|
||||
'discountLabel',
|
||||
|
||||
'adjustment',
|
||||
'adjustmentLabel',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLabel',
|
||||
|
||||
'paidAmount',
|
||||
'paidAmountLabel',
|
||||
|
||||
'items',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Company logo uri.
|
||||
* @returns {string}
|
||||
*/
|
||||
public companyLogoUri(): string {
|
||||
return this.options.brandingTemplate?.companyLogoUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Company name.
|
||||
* @returns {string}
|
||||
*/
|
||||
public companyName(): string {
|
||||
return this.context.organization.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary color
|
||||
* @returns {string}
|
||||
*/
|
||||
public primaryColor(): string {
|
||||
return this.options?.brandingTemplate?.attributes?.primaryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt number.
|
||||
* @returns {string}
|
||||
*/
|
||||
public receiptNumber(): string {
|
||||
return this.options.receipt.receiptNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt number label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public receiptNumberLabel(): string {
|
||||
return 'Receipt # {receiptNumber}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Date.
|
||||
* @returns {string}
|
||||
*/
|
||||
public date(): string {
|
||||
return this.options.receipt.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Date label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public dateLabel(): string {
|
||||
return 'Date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt total.
|
||||
*/
|
||||
public total(): string {
|
||||
return this.options.receipt.totalFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt total label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public totalLabel(): string {
|
||||
return 'Total';
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt discount.
|
||||
* @returns {string}
|
||||
*/
|
||||
public discount(): string {
|
||||
return this.options.receipt?.discountAmountFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt discount label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public discountLabel(): string {
|
||||
return 'Discount';
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt adjustment.
|
||||
* @returns {string}
|
||||
*/
|
||||
public adjustment(): string {
|
||||
return this.options.receipt?.adjustmentFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt adjustment label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public adjustmentLabel(): string {
|
||||
return 'Adjustment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt subtotal.
|
||||
* @returns {string}
|
||||
*/
|
||||
public subtotal(): string {
|
||||
return this.options.receipt.subtotalFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt subtotal label.
|
||||
* @returns {string}
|
||||
*/
|
||||
public subtotalLabel(): string {
|
||||
return 'Subtotal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt mail items attributes.
|
||||
*/
|
||||
public items(): any[] {
|
||||
return this.item(
|
||||
this.options.receipt.entries,
|
||||
new GetSaleReceiptMailTemplateEntryAttributesTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetSaleReceiptMailTemplateEntryAttributesTransformer extends Transformer {
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['label', 'quantity', 'rate', 'total'];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
public label(entry): string {
|
||||
return entry?.item?.name;
|
||||
}
|
||||
|
||||
public quantity(entry): string {
|
||||
return entry?.quantity;
|
||||
}
|
||||
|
||||
public rate(entry): string {
|
||||
return entry?.rateFormatted;
|
||||
}
|
||||
|
||||
public total(entry): string {
|
||||
return entry?.totalFormatted;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export class GetSaleReceiptState {
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retireves the sale receipt state.
|
||||
* Retrieves the sale receipt state.
|
||||
* @param {Number} tenantId -
|
||||
* @return {Promise<ISaleReceiptState>}
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,7 @@ import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
||||
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
|
||||
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||
import { GetSaleReceiptState } from './GetSaleReceiptState';
|
||||
import { GetSaleReceiptMailState } from './GetSaleReceiptMailState';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptApplication {
|
||||
@@ -51,6 +52,9 @@ export class SaleReceiptApplication {
|
||||
@Inject()
|
||||
private getSaleReceiptStateService: GetSaleReceiptState;
|
||||
|
||||
@Inject()
|
||||
private getSaleReceiptMailStateService: GetSaleReceiptMailState;
|
||||
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @param {number} tenantId
|
||||
@@ -152,6 +156,19 @@ export class SaleReceiptApplication {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given sale receipt html.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public getSaleReceiptHtml(tenantId: number, saleReceiptId: number) {
|
||||
return this.getSaleReceiptPdfService.saleReceiptHtml(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify receipt customer by SMS of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
@@ -221,4 +238,20 @@ export class SaleReceiptApplication {
|
||||
public getSaleReceiptState(tenantId: number): Promise<ISaleReceiptState> {
|
||||
return this.getSaleReceiptStateService.getSaleReceiptState(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail state of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
* @returns
|
||||
*/
|
||||
public getSaleReceiptMailState(
|
||||
tenantId: number,
|
||||
saleReceiptId: number
|
||||
): Promise<ISaleReceiptState> {
|
||||
return this.getSaleReceiptMailStateService.getMailState(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +31,27 @@ export class SaleReceiptGLEntries {
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const saleReceipt = await SaleReceipt.query(trx)
|
||||
.findById(saleReceiptId)
|
||||
.withGraphFetched('entries.item');
|
||||
|
||||
// Find or create the discount expense account.
|
||||
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Find or create the other charges account.
|
||||
const otherChargesAccount =
|
||||
await accountRepository.findOrCreateOtherChargesAccount({}, trx);
|
||||
|
||||
// Retrieve the income entries ledger.
|
||||
const incomeLedger = this.getIncomeEntriesLedger(saleReceipt);
|
||||
const incomeLedger = this.getIncomeEntriesLedger(
|
||||
saleReceipt,
|
||||
discountAccount.id,
|
||||
otherChargesAccount.id
|
||||
);
|
||||
|
||||
// Commits the ledger entries to the storage.
|
||||
await this.ledgerStorage.commit(tenantId, incomeLedger, trx);
|
||||
@@ -87,8 +101,16 @@ export class SaleReceiptGLEntries {
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => {
|
||||
const entries = this.getIncomeGLEntries(saleReceipt);
|
||||
private getIncomeEntriesLedger = (
|
||||
saleReceipt: ISaleReceipt,
|
||||
discountAccountId: number,
|
||||
otherChargesAccountId: number
|
||||
): Ledger => {
|
||||
const entries = this.getIncomeGLEntries(
|
||||
saleReceipt,
|
||||
discountAccountId,
|
||||
otherChargesAccountId
|
||||
);
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
@@ -121,10 +143,10 @@ export class SaleReceiptGLEntries {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve receipt income item GL entry.
|
||||
* @param {ISaleReceipt} saleReceipt -
|
||||
* @param {IItemEntry} entry -
|
||||
* @param {number} index -
|
||||
* Retrieve receipt income item G/L entry.
|
||||
* @param {ISaleReceipt} saleReceipt -
|
||||
* @param {IItemEntry} entry -
|
||||
* @param {number} index -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getReceiptIncomeItemEntry = R.curry(
|
||||
@@ -134,11 +156,11 @@ export class SaleReceiptGLEntries {
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
|
||||
const itemIncome = entry.amount * saleReceipt.exchangeRate;
|
||||
const totalLocal = entry.totalExcludingTax * saleReceipt.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: itemIncome,
|
||||
credit: totalLocal,
|
||||
accountId: entry.item.sellAccountId,
|
||||
note: entry.description,
|
||||
index: index + 2,
|
||||
@@ -161,24 +183,76 @@ export class SaleReceiptGLEntries {
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleReceipt.localAmount,
|
||||
debit: saleReceipt.totalLocal,
|
||||
accountId: saleReceipt.depositAccountId,
|
||||
index: 1,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the discount GL entry.
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @param {number} discountAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getDiscountEntry = (
|
||||
saleReceipt: ISaleReceipt,
|
||||
discountAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleReceipt.discountAmountLocal,
|
||||
accountId: discountAccountId,
|
||||
index: 1,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the adjustment GL entry.
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @param {number} adjustmentAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAdjustmentEntry = (
|
||||
saleReceipt: ISaleReceipt,
|
||||
adjustmentAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
|
||||
const adjustmentAmount = Math.abs(saleReceipt.adjustmentLocal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleReceipt.adjustmentLocal < 0 ? adjustmentAmount : 0,
|
||||
credit: saleReceipt.adjustmentLocal > 0 ? adjustmentAmount : 0,
|
||||
accountId: adjustmentAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the income GL entries.
|
||||
* @param {ISaleReceipt} saleReceipt -
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => {
|
||||
private getIncomeGLEntries = (
|
||||
saleReceipt: ISaleReceipt,
|
||||
discountAccountId: number,
|
||||
otherChargesAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt);
|
||||
|
||||
const creditEntries = saleReceipt.entries.map(getItemEntry);
|
||||
const depositEntry = this.getReceiptDepositEntry(saleReceipt);
|
||||
|
||||
return [depositEntry, ...creditEntries];
|
||||
const discountEntry = this.getDiscountEntry(saleReceipt, discountAccountId);
|
||||
const adjustmentEntry = this.getAdjustmentEntry(
|
||||
saleReceipt,
|
||||
otherChargesAccountId
|
||||
);
|
||||
return [depositEntry, ...creditEntries, discountEntry, adjustmentEntry];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { transformReceiptToMailDataArgs } from './utils';
|
||||
import { GetSaleReceiptMailTemplate } from './GetSaleReceiptMailTemplate';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptMailNotification {
|
||||
@@ -32,6 +33,9 @@ export class SaleReceiptMailNotification {
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
@Inject()
|
||||
private getReceiptMailTemplate: GetSaleReceiptMailTemplate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@@ -111,7 +115,13 @@ export class SaleReceiptMailNotification {
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
return transformReceiptToMailDataArgs(receipt);
|
||||
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
|
||||
tenantId
|
||||
);
|
||||
return {
|
||||
...commonArgs,
|
||||
...transformReceiptToMailDataArgs(receipt),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -133,7 +143,15 @@ export class SaleReceiptMailNotification {
|
||||
mailOptions,
|
||||
formatterArgs
|
||||
)) as SaleReceiptMailOpts;
|
||||
return formattedOptions;
|
||||
|
||||
const message = await this.getReceiptMailTemplate.getMailTemplate(
|
||||
tenantId,
|
||||
receiptId,
|
||||
{
|
||||
message: formattedOptions.message,
|
||||
}
|
||||
);
|
||||
return { ...formattedOptions, message };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,11 +13,24 @@ export class SaleReceiptTransformer extends Transformer {
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedSubtotal',
|
||||
'discountAmountFormatted',
|
||||
'discountPercentageFormatted',
|
||||
'discountAmountLocalFormatted',
|
||||
|
||||
'subtotalFormatted',
|
||||
'subtotalLocalFormatted',
|
||||
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
|
||||
'adjustmentFormatted',
|
||||
'adjustmentLocalFormatted',
|
||||
|
||||
'formattedAmount',
|
||||
'formattedReceiptDate',
|
||||
'formattedClosedAtDate',
|
||||
'formattedCreatedAt',
|
||||
'paidFormatted',
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
@@ -43,7 +56,7 @@ export class SaleReceiptTransformer extends Transformer {
|
||||
|
||||
/**
|
||||
* Retrieve formatted receipt created at date.
|
||||
* @param receipt
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreatedAt = (receipt: ISaleReceipt): string => {
|
||||
@@ -55,8 +68,41 @@ export class SaleReceiptTransformer extends Transformer {
|
||||
* @param {ISaleReceipt} receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedSubtotal = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.amount, { money: false });
|
||||
protected subtotalFormatted = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.subtotal, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the estimate formatted subtotal in local currency.
|
||||
* @param {ISaleReceipt} receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalLocalFormatted = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.subtotalLocal, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the receipt formatted total.
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.total, { currencyCode: receipt.currencyCode });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the receipt formatted total in local currency.
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalLocalFormatted = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.totalLocal, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -64,12 +110,57 @@ export class SaleReceiptTransformer extends Transformer {
|
||||
* @param {ISaleReceipt} estimate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (receipt: ISaleReceipt): string => {
|
||||
protected amountFormatted = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.amount, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount amount.
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.discountAmount, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount percentage.
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountPercentageFormatted = (receipt: ISaleReceipt): string => {
|
||||
return receipt.discountPercentage ? `${receipt.discountPercentage}%` : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted paid amount.
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected paidFormatted = (receipt: ISaleReceipt): string => {
|
||||
return formatNumber(receipt.paid, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted adjustment amount.
|
||||
* @param receipt
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (receipt: ISaleReceipt): string => {
|
||||
return this.formatMoney(receipt.adjustment, {
|
||||
currencyCode: receipt.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the credit note.
|
||||
* @param {ISaleReceipt} credit
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import {
|
||||
renderReceiptPaperTemplateHtml,
|
||||
ReceiptPaperTemplateProps,
|
||||
} from '@bigcapital/pdf-templates';
|
||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
|
||||
import { transformReceiptToBrandingTemplateAttributes } from './utils';
|
||||
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@@ -17,9 +19,6 @@ export class SaleReceiptsPdf {
|
||||
@Inject()
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
@Inject()
|
||||
private getSaleReceiptService: GetSaleReceipt;
|
||||
|
||||
@@ -29,6 +28,19 @@ export class SaleReceiptsPdf {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieves sale receipt html content.
|
||||
* @param {number} tennatId
|
||||
* @param {number} saleReceiptId
|
||||
*/
|
||||
public async saleReceiptHtml(tennatId: number, saleReceiptId: number) {
|
||||
const brandingAttributes = await this.getReceiptBrandingAttributes(
|
||||
tennatId,
|
||||
saleReceiptId
|
||||
);
|
||||
return renderReceiptPaperTemplateHtml(brandingAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
@@ -41,16 +53,9 @@ export class SaleReceiptsPdf {
|
||||
): Promise<[Buffer, string]> {
|
||||
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
|
||||
|
||||
const brandingAttributes = await this.getReceiptBrandingAttributes(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
// Converts the receipt template to html content.
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/receipt-regular',
|
||||
brandingAttributes
|
||||
);
|
||||
const htmlContent = await this.saleReceiptHtml(tenantId, saleReceiptId);
|
||||
|
||||
// Renders the html content to pdf document.
|
||||
const content = await this.chromiumlyTenancy.convertHtmlContent(
|
||||
tenantId,
|
||||
@@ -87,12 +92,12 @@ export class SaleReceiptsPdf {
|
||||
* Retrieves receipt branding attributes.
|
||||
* @param {number} tenantId
|
||||
* @param {number} receiptId
|
||||
* @returns {Promise<ISaleReceiptBrandingTemplateAttributes>}
|
||||
* @returns {Promise<ReceiptPaperTemplateProps>}
|
||||
*/
|
||||
public async getReceiptBrandingAttributes(
|
||||
tenantId: number,
|
||||
receiptId: number
|
||||
): Promise<ISaleReceiptBrandingTemplateAttributes> {
|
||||
): Promise<ReceiptPaperTemplateProps> {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
|
||||
'Receipt {Receipt Number} from {Company Name}';
|
||||
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
|
||||
<p>Dear {Customer Name}</p>
|
||||
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
|
||||
<p>
|
||||
Receipt <strong>#{Receipt Number}</strong><br />
|
||||
Amount : <strong>{Receipt Amount}</strong></br />
|
||||
</p>
|
||||
export const DEFAULT_RECEIPT_MAIL_CONTENT = `Hi {Customer Name},
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{Company Name}</i>
|
||||
</p>
|
||||
`;
|
||||
Here's receipt # {Receipt Number} for Receipt {Receipt Amount}
|
||||
|
||||
The receipt paid on {Receipt Date}, and the total amount paid is {Receipt Amount}.
|
||||
|
||||
Please find your sale receipt attached to this email for your reference
|
||||
|
||||
If you have any questions, please let us know.
|
||||
|
||||
Thanks,
|
||||
{Company Name}`;
|
||||
|
||||
export const ERRORS = {
|
||||
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import {
|
||||
ISaleReceipt,
|
||||
ISaleReceiptBrandingTemplateAttributes,
|
||||
} from '@/interfaces';
|
||||
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||
import { ReceiptPaperTemplateProps } from '@bigcapital/pdf-templates';
|
||||
|
||||
export const transformReceiptToBrandingTemplateAttributes = (
|
||||
saleReceipt: ISaleReceipt
|
||||
): Partial<ISaleReceiptBrandingTemplateAttributes> => {
|
||||
saleReceipt
|
||||
): Partial<ReceiptPaperTemplateProps> => {
|
||||
return {
|
||||
total: saleReceipt.formattedAmount,
|
||||
subtotal: saleReceipt.formattedSubtotal,
|
||||
total: saleReceipt.totalFormatted,
|
||||
subtotal: saleReceipt.subtotalFormatted,
|
||||
lines: saleReceipt.entries?.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
discount: entry.discountFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
receiptNumber: saleReceipt.receiptNumber,
|
||||
receiptDate: saleReceipt.formattedReceiptDate,
|
||||
discount: saleReceipt.discountAmountFormatted,
|
||||
discountLabel: saleReceipt.discountPercentageFormatted
|
||||
? `Discount [${saleReceipt.discountPercentageFormatted}]`
|
||||
: 'Discount',
|
||||
showLineDiscount: saleReceipt.entries.some(
|
||||
(entry) => entry.discountFormatted
|
||||
),
|
||||
adjustment: saleReceipt.adjustmentFormatted,
|
||||
customerAddress: contactAddressTextFormat(saleReceipt.customer),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { keyBy, sumBy } from 'lodash';
|
||||
import { ItemEntry } from '@/models';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces';
|
||||
import { IItemEntry } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class ItemEntriesTaxTransactions {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('api_keys', (table) => {
|
||||
table.increments('id').primary();
|
||||
|
||||
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
|
||||
table.integer('user_id').unsigned().index().references('id').inTable('users');
|
||||
|
||||
table.string('name');
|
||||
table.text('key');
|
||||
|
||||
table.dateTime('expires_at').nullable();
|
||||
table.dateTime('revoked_at').nullable();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('api_keys');
|
||||
};
|
||||
@@ -413,7 +413,10 @@ export const formatSmsMessage = (message: string, args) => {
|
||||
const variable = `{${key}}`;
|
||||
const value = _.defaultTo(args[key], '');
|
||||
|
||||
formattedMessage = formattedMessage.replace(variable, value);
|
||||
formattedMessage = formattedMessage.replace(
|
||||
new RegExp(variable, 'g'),
|
||||
value
|
||||
);
|
||||
});
|
||||
return formattedMessage;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@bigcapital/utils": "*",
|
||||
"@bigcapital/pdf-templates": "*",
|
||||
"@bigcapital/email-components": "*",
|
||||
"@blueprintjs-formik/core": "^0.3.7",
|
||||
"@blueprintjs-formik/datetime": "^0.3.7",
|
||||
"@blueprintjs-formik/select": "^0.3.5",
|
||||
|
||||
@@ -62,6 +62,9 @@ export function DataTable(props) {
|
||||
initialPageIndex = 0,
|
||||
initialPageSize = 20,
|
||||
|
||||
// Hidden columns.
|
||||
initialHiddenColumns = [],
|
||||
|
||||
updateDebounceTime = 200,
|
||||
selectionColumnWidth = 42,
|
||||
|
||||
@@ -115,6 +118,7 @@ export function DataTable(props) {
|
||||
columnResizing: {
|
||||
columnWidths: initialColumnsWidths || {},
|
||||
},
|
||||
hiddenColumns: initialHiddenColumns,
|
||||
},
|
||||
manualPagination,
|
||||
pageCount: controlledPageCount,
|
||||
|
||||
@@ -11,7 +11,6 @@ import QuickPaymentMadeFormDialog from '@/containers/Dialogs/QuickPaymentMadeFor
|
||||
import AllocateLandedCostDialog from '@/containers/Dialogs/AllocateLandedCostDialog';
|
||||
import InvoicePdfPreviewDialog from '@/containers/Dialogs/InvoicePdfPreviewDialog';
|
||||
import EstimatePdfPreviewDialog from '@/containers/Dialogs/EstimatePdfPreviewDialog';
|
||||
import ReceiptPdfPreviewDialog from '@/containers/Dialogs/ReceiptPdfPreviewDialog';
|
||||
import MoneyInDialog from '@/containers/CashFlow/MoneyInDialog';
|
||||
import MoneyOutDialog from '@/containers/CashFlow/MoneyOutDialog';
|
||||
import BadDebtDialog from '@/containers/Dialogs/BadDebtDialog';
|
||||
@@ -45,9 +44,6 @@ import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/P
|
||||
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
|
||||
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
||||
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
||||
import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog';
|
||||
import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
|
||||
import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog';
|
||||
import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog';
|
||||
@@ -81,7 +77,6 @@ export default function DialogsContainer() {
|
||||
/>
|
||||
<InvoicePdfPreviewDialog dialogName={DialogsName.InvoicePdfForm} />
|
||||
<EstimatePdfPreviewDialog dialogName={DialogsName.EstimatePdfForm} />
|
||||
<ReceiptPdfPreviewDialog dialogName={DialogsName.ReceiptPdfForm} />
|
||||
<MoneyInDialog dialogName={DialogsName.MoneyInForm} />
|
||||
<MoneyOutDialog dialogName={DialogsName.MoneyOutForm} />
|
||||
|
||||
@@ -143,9 +138,6 @@ export default function DialogsContainer() {
|
||||
<InvoiceExchangeRateChangeDialog
|
||||
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
|
||||
/>
|
||||
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
||||
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
||||
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
||||
<ExportDialog dialogName={DialogsName.Export} />
|
||||
<RuleFormDialog dialogName={DialogsName.BankRuleForm} />
|
||||
<DisconnectBankAccountDialog
|
||||
|
||||
@@ -32,6 +32,9 @@ import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/Branding
|
||||
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer';
|
||||
import { EstimateSendMailDrawer } from '@/containers/Sales/Estimates/EstimateSendMailDrawer';
|
||||
import { ReceiptSendMailDrawer } from '@/containers/Sales/Receipts/ReceiptSendMailDrawer';
|
||||
import { PaymentReceivedSendMailDrawer } from '@/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer';
|
||||
|
||||
/**
|
||||
* Drawers container of the dashboard.
|
||||
@@ -81,6 +84,9 @@ export default function DrawersContainer() {
|
||||
/>
|
||||
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
|
||||
<InvoiceSendMailDrawer name={DRAWERS.INVOICE_SEND_MAIL} />
|
||||
<EstimateSendMailDrawer name={DRAWERS.ESTIMATE_SEND_MAIL} />
|
||||
<ReceiptSendMailDrawer name={DRAWERS.RECEIPT_SEND_MAIL} />
|
||||
<PaymentReceivedSendMailDrawer name={DRAWERS.PAYMENT_RECEIVED_SEND_MAIL} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Money } from '@/components';
|
||||
|
||||
import '@/style/components/BigAmount.scss';
|
||||
|
||||
export function PageFormBigNumber({ label, amount, currencyCode }) {
|
||||
interface PageFormBigNumberProps {
|
||||
label: string;
|
||||
amount: string | number;
|
||||
}
|
||||
export function PageFormBigNumber({
|
||||
label,
|
||||
amount,
|
||||
}: PageFormBigNumberProps) {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
|
||||
<div class="big-amount">
|
||||
<span class="big-amount__label">{label}</span>
|
||||
<h1 class="big-amount__number">
|
||||
<Money amount={amount} currency={currencyCode} />
|
||||
</h1>
|
||||
<h1 class="big-amount__number">{amount}</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { x } from '@xstyled/emotion';
|
||||
export const TotalLineBorderStyle = {
|
||||
None: 'None',
|
||||
SingleDark: 'SingleDark',
|
||||
@@ -32,14 +32,14 @@ export function TotalLines({
|
||||
|
||||
export function TotalLine({ title, value, borderStyle, textStyle, className }) {
|
||||
return (
|
||||
<TotalLineRoot
|
||||
<TotalLinePrimitive
|
||||
borderStyle={borderStyle}
|
||||
textStyle={textStyle}
|
||||
className={className}
|
||||
>
|
||||
<div class="title">{title}</div>
|
||||
<div class="amount">{value}</div>
|
||||
</TotalLineRoot>
|
||||
</TotalLinePrimitive>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export const TotalLinesRoot = styled.div`
|
||||
`}
|
||||
`;
|
||||
|
||||
export const TotalLineRoot = styled.div`
|
||||
export const TotalLinePrimitive = styled.div`
|
||||
display: table-row;
|
||||
|
||||
.amount,
|
||||
@@ -96,5 +96,17 @@ export const TotalLineRoot = styled.div`
|
||||
|
||||
.amount {
|
||||
text-align: right;
|
||||
width: 25%;
|
||||
}
|
||||
`;
|
||||
|
||||
const TotalLineAmount = (props) => {
|
||||
return <x.div display={'table-cell'} padding={'8px'} textAlign={'right'} {...props} />;
|
||||
};
|
||||
|
||||
export const TotalLineTitle = (props) => {
|
||||
return <x.div display={'table-cell'} padding={'8px'} {...props} />;
|
||||
};
|
||||
|
||||
TotalLinePrimitive.Amount = TotalLineAmount;
|
||||
TotalLinePrimitive.Title = TotalLineTitle;
|
||||
|
||||
@@ -34,5 +34,8 @@ export enum DRAWERS {
|
||||
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES',
|
||||
PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW',
|
||||
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT',
|
||||
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL'
|
||||
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL',
|
||||
ESTIMATE_SEND_MAIL = 'ESTIMATE_SEND_MAIL',
|
||||
RECEIPT_SEND_MAIL = 'RECEIPT_SEND_MAIL',
|
||||
PAYMENT_RECEIVED_SEND_MAIL = 'PAYMENT_RECEIVED_SEND_MAIL',
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { safeSumBy } from '@/utils';
|
||||
import { PageFormBigNumber, FormattedMessage as T } from '@/components';
|
||||
import MakeJournalEntriesHeaderFields from './MakeJournalEntriesHeaderFields';
|
||||
import { useManualJournalTotalFormatted } from './utils';
|
||||
|
||||
export default function MakeJournalEntriesHeader() {
|
||||
return (
|
||||
@@ -21,19 +20,9 @@ export default function MakeJournalEntriesHeader() {
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function MakeJournalHeaderBigNumber() {
|
||||
const {
|
||||
values: { entries, currency_code },
|
||||
} = useFormikContext();
|
||||
const totalCredit = safeSumBy(entries, 'credit');
|
||||
const totalDebit = safeSumBy(entries, 'debit');
|
||||
|
||||
const total = Math.max(totalCredit, totalDebit);
|
||||
const totalFormatted = useManualJournalTotalFormatted();
|
||||
|
||||
return (
|
||||
<PageFormBigNumber
|
||||
label={<T id={'amount'} />}
|
||||
amount={total}
|
||||
currencyCode={currency_code}
|
||||
/>
|
||||
<PageFormBigNumber label={<T id={'amount'} />} amount={totalFormatted} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,14 @@ import {
|
||||
TotalLineBorderStyle,
|
||||
TotalLineTextStyle,
|
||||
} from '@/components';
|
||||
import { useJournalTotals } from './utils';
|
||||
import {
|
||||
useManualJournalSubtotalFormatted,
|
||||
useManualJournalTotalFormatted,
|
||||
} from './utils';
|
||||
|
||||
export function MakeJournalFormFooterRight() {
|
||||
const { formattedSubtotal, formattedTotal } = useJournalTotals();
|
||||
const formattedSubtotal = useManualJournalSubtotalFormatted();
|
||||
const formattedTotal = useManualJournalTotalFormatted();
|
||||
|
||||
return (
|
||||
<MakeJouranlTotalLines>
|
||||
@@ -29,7 +33,7 @@ export function MakeJournalFormFooterRight() {
|
||||
);
|
||||
}
|
||||
|
||||
const MakeJouranlTotalLines =styled(TotalLines)`
|
||||
const MakeJouranlTotalLines = styled(TotalLines)`
|
||||
width: 100%;
|
||||
color: #555555;
|
||||
`;
|
||||
|
||||
@@ -226,34 +226,73 @@ export const useSetPrimaryBranchToForm = () => {
|
||||
}, [isBranchesSuccess, setFieldValue, branches]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the Journal totals.
|
||||
*/
|
||||
export const useJournalTotals = () => {
|
||||
const {
|
||||
values: { entries, currency_code: currencyCode },
|
||||
} = useFormikContext();
|
||||
export const useManualJournalCreditTotal = () => {
|
||||
const { values } = useFormikContext();
|
||||
const totalCredit = safeSumBy(values.entries, 'credit');
|
||||
|
||||
// Retrieves the invoice entries total.
|
||||
const totalCredit = safeSumBy(entries, 'credit');
|
||||
const totalDebit = safeSumBy(entries, 'debit');
|
||||
return totalCredit;
|
||||
};
|
||||
|
||||
const total = Math.max(totalCredit, totalDebit);
|
||||
// Retrieves the formatted total money.
|
||||
const formattedTotal = React.useMemo(
|
||||
() => formattedAmount(total, currencyCode),
|
||||
[total, currencyCode],
|
||||
);
|
||||
// Retrieves the formatted subtotal.
|
||||
const formattedSubtotal = React.useMemo(
|
||||
() => formattedAmount(total, currencyCode, { money: false }),
|
||||
[total, currencyCode],
|
||||
);
|
||||
export const useManualJournalCreditTotalFormatted = () => {
|
||||
const totalCredit = useManualJournalCreditTotal();
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return {
|
||||
formattedTotal,
|
||||
formattedSubtotal,
|
||||
};
|
||||
return formattedAmount(totalCredit, values.currency_code);
|
||||
};
|
||||
|
||||
export const useManualJournalDebitTotal = () => {
|
||||
const { values } = useFormikContext();
|
||||
const totalDebit = safeSumBy(values.entries, 'debit');
|
||||
|
||||
return totalDebit;
|
||||
};
|
||||
|
||||
export const useManualJournalDebitTotalFormatted = () => {
|
||||
const totalDebit = useManualJournalDebitTotal();
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return formattedAmount(totalDebit, values.currency_code);
|
||||
};
|
||||
|
||||
export const useManualJournalSubtotal = () => {
|
||||
const totalCredit = useManualJournalCreditTotal();
|
||||
const totalDebit = useManualJournalDebitTotal();
|
||||
|
||||
return Math.max(totalCredit, totalDebit);
|
||||
};
|
||||
|
||||
export const useManualJournalSubtotalFormatted = () => {
|
||||
const subtotal = useManualJournalSubtotal();
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return formattedAmount(subtotal, values.currency_code);
|
||||
};
|
||||
|
||||
export const useManualJournalTotalDifference = () => {
|
||||
const totalCredit = useManualJournalCreditTotal();
|
||||
const totalDebit = useManualJournalDebitTotal();
|
||||
|
||||
return Math.abs(totalCredit - totalDebit);
|
||||
};
|
||||
|
||||
export const useManualJournalTotalDifferenceFormatted = () => {
|
||||
const difference = useManualJournalTotalDifference();
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return formattedAmount(difference, values.currency_code);
|
||||
};
|
||||
|
||||
export const useManualJournalTotal = () => {
|
||||
const total = useManualJournalSubtotal();
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
export const useManualJournalTotalFormatted = () => {
|
||||
const total = useManualJournalTotal();
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return formattedAmount(total, values.currency_code);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,10 @@ export default function BillDetailTable() {
|
||||
<CommercialDocEntriesTable
|
||||
columns={columns}
|
||||
data={entries}
|
||||
initialHiddenColumns={
|
||||
// If any entry has no discount, hide the discount column.
|
||||
entries?.some((e) => e.discount_formatted) ? [] : ['discount']
|
||||
}
|
||||
styleName={TableStyle.Constrant}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,23 @@ export function BillDetailTableFooter() {
|
||||
textStyle={TotalLineTextStyle.Regular}
|
||||
/>
|
||||
))}
|
||||
{bill.discount_amount > 0 && (
|
||||
<TotalLine
|
||||
title={
|
||||
bill.discount_percentage_formatted
|
||||
? `Discount [${bill.discount_percentage_formatted}]`
|
||||
: 'Discount'
|
||||
}
|
||||
value={bill.discount_amount_formatted}
|
||||
textStyle={TotalLineTextStyle.Regular}
|
||||
/>
|
||||
)}
|
||||
{bill.adjustment_formatted && (
|
||||
<TotalLine
|
||||
title={'Adjustment'}
|
||||
value={bill.adjustment_formatted}
|
||||
/>
|
||||
)}
|
||||
<TotalLine
|
||||
title={<T id={'bill.details.total'} />}
|
||||
value={bill.total_formatted}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user