Compare commits

..

22 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
ccbb399685 Merge pull request #720 from bigcapitalhq/add-estimate-customer-note
fix Customer note does not appear in pdf document
2024-10-19 16:28:33 +02:00
Ahmed Bouhuolia
f381d433ec fix Customer note does not appear in pdf document 2024-10-19 16:28:01 +02:00
Ahmed Bouhuolia
2ee49f7496 Merge pull request #719 from bigcapitalhq/track-pdf-documents-views-events
feat: Track events of pdf documents views
2024-10-19 13:40:18 +02:00
Ahmed Bouhuolia
bb299aa595 feat: Track events of pdf documents views 2024-10-19 13:38:28 +02:00
Ahmed Bouhuolia
a66867463e Merge pull request #718 from bigcapitalhq/invoice-number-filename-document
feat: Invoice number in downloaded pdf document
2024-10-19 13:16:48 +02:00
Ahmed Bouhuolia
de50b89e5c feat: Invoice number in downloaded pdf document 2024-10-19 13:16:06 +02:00
Ahmed Bouhuolia
c4ee143354 Merge pull request #717 from bigcapitalhq/preline-statements
ffeat: Pre-line invoice statements
2024-10-19 11:00:31 +02:00
Ahmed Bouhuolia
a2227016e5 feat: Pre-line invoice statements 2024-10-19 10:59:46 +02:00
Ahmed Bouhuolia
4d6f65b179 Merge pull request #716 from bigcapitalhq/add-quantity-column-pdf-templates
feat: Add quantity column to pdf templates
2024-10-17 16:01:02 +02:00
Ahmed Bouhuolia
758ebbe261 feat: Add qty column to server-side pdf templates 2024-10-17 16:00:19 +02:00
Ahmed Bouhuolia
279890e922 feat: Add qty column to preview pdf templates: 2024-10-17 15:58:19 +02:00
Ahmed Bouhuolia
44fae36b82 Merge pull request #715 from bigcapitalhq/sync-account-norma-cashflow
fix: Sync account normal of cashflow GL entries
2024-10-16 20:12:51 +02:00
Ahmed Bouhuolia
fc2fac80af fix: Sync account normal of cashflow GL entries 2024-10-16 20:12:25 +02:00
Ahmed Bouhuolia
5ad9a9654b Merge pull request #714 from bigcapitalhq/sync-plaid-credit-card-account-type
fix: Sync Plaid credit card account type
2024-10-16 19:47:13 +02:00
Ahmed Bouhuolia
8a4034cc5d fix: Sync Plaid credit card account type 2024-10-16 19:46:39 +02:00
Ahmed Bouhuolia
5649657bf0 Merge pull request #713 from bigcapitalhq/i18napply
chore: Move i18nApply localization to the account transformer
2024-10-15 19:27:43 +02:00
Ahmed Bouhuolia
c929a7cb27 chore: Move i18nApply localization to the account transformer 2024-10-15 19:27:15 +02:00
Ahmed Bouhuolia
eeedb789a9 Merge pull request #711 from bigcapitalhq/fix-parse-non-lowercase-import
fix: Parse the uppercase values in importing
2024-10-14 20:02:31 +02:00
Ahmed Bouhuolia
321af8c271 fix: Parse the uppercase values in importing 2024-10-14 20:01:52 +02:00
Ahmed Bouhuolia
fd4d86e797 Merge pull request #710 from bigcapitalhq/fix-import-category-on-items
fix: Import category column of item resource
2024-10-14 19:49:54 +02:00
Ahmed Bouhuolia
49988e27a2 fix: Import category column of item resource 2024-10-14 19:49:32 +02:00
Ahmed Bouhuolia
8c94ee5982 Dump CHANGELOG 2024-10-14 13:58:57 +02:00
43 changed files with 625 additions and 131 deletions

View File

@@ -2,6 +2,43 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
# [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
* fix: Invoice form layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/705
* refactor: Invoice, estimate, receipt, credit note and payment received date input fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/707
* feat: Add customize templates button to edit forms by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/708
* feat: Track account, invoice and item viewed events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/709
# [0.20.4]
* fix: Delete company logo from the PDF template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/699
* fix: Set max width/height to company logo of pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/700
# [0.20.3]
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
# [0.20.2]
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
# [0.20.1]
* fix: Getting uploaded object uri by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/684
# [0.20.0] # [0.20.0]
* feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667 * feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667

View File

@@ -108,7 +108,14 @@ block head
.#{prefix}-table__cell--right { .#{prefix}-table__cell--right {
text-align: 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -143,7 +150,7 @@ block head
color: #666; color: #666;
} }
.#{prefix}-statement__value { .#{prefix}-statement__value {
/* Styles for statement value */ white-space: pre-line;
} }
block content block content
@@ -183,17 +190,20 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead thead
tr tr
th(class=`${prefix}-table__header`) #{'Item'} th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{'Item'}
th(class=`${prefix}-table__header`) #{'Description'} th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{'Quantity'}
th(class=`${prefix}-table__header`) #{'Rate'} th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{'Rate'}
th(class=`${prefix}-table__header`) #{'Total'} th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{'Total'}
tbody tbody
each line in lines each line in lines
tr(class=`${prefix}-table__row`) tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`) #{line.item} td(class=`${prefix}-table__cell ${prefix}-table__cell--item ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`) #{line.description} div.item
td(class=`${prefix}-table__cell--right`) #{line.rate} div.item__label #{line.item}
td(class=`${prefix}-table__cell--right`) #{line.total} 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}
div(class=`${prefix}-totals`) div(class=`${prefix}-totals`)
if showSubtotal if showSubtotal

View File

@@ -96,6 +96,9 @@ block head
.#{prefix}-table__header--right { .#{prefix}-table__header--right {
text-align: right; text-align: right;
} }
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell { .#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6; border-bottom: 1px solid #F6F6F6;
padding: 12px 10px; padding: 12px 10px;
@@ -109,6 +112,14 @@ block head
.#{prefix}-table__cell:last-of-type { .#{prefix}-table__cell:last-of-type {
padding-right: 0; 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -143,6 +154,7 @@ block head
color: #666; color: #666;
} }
.#{prefix}-statement__value { .#{prefix}-statement__value {
white-space: pre-line;
} }
block content block content
@@ -192,17 +204,20 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead thead
tr tr
th(class=`${prefix}-table__header`) Item th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header`) Description th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody tbody
each line in lines each line in lines
tr tr
td(class=`${prefix}-table__cell`) #{line.item} td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`) #{line.description} div.item
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate} div.item__label #{line.item}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total} 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 //- Totals section
div(class=`${prefix}-totals`) div(class=`${prefix}-totals`)
@@ -216,12 +231,12 @@ block content
div(class=`${prefix}-totals__item-amount`) #{total} div(class=`${prefix}-totals__item-amount`) #{total}
//- Statements section //- Statements section
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
div(class=`${prefix}-statement__value`) #{termsConditions}
if showCustomerNote && customerNote if showCustomerNote && customerNote
div(class=`${prefix}-statement`) div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{customerNoteLabel} div(class=`${prefix}-statement__label`) #{customerNoteLabel}
div(class=`${prefix}-statement__value`) #{customerNote} div(class=`${prefix}-statement__value`) #{customerNote}
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
div(class=`${prefix}-statement__value`) #{termsConditions}

View File

@@ -102,6 +102,9 @@ block head
.#{prefix}-table__header--right { .#{prefix}-table__header--right {
text-align: right; text-align: right;
} }
.#{prefix}-table__header--item {
width: 50%;
}
.#{prefix}-table__cell { .#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6; border-bottom: 1px solid #F6F6F6;
padding: 12px 10px; padding: 12px 10px;
@@ -115,6 +118,14 @@ block head
.#{prefix}-table__cell--right { .#{prefix}-table__cell--right {
text-align: 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -149,7 +160,7 @@ block head
color: #666; color: #666;
} }
.#{prefix}-paragraph__value { .#{prefix}-paragraph__value {
/* Styles for values within the paragraph section */ white-space: pre-line;
} }
block content block content
//- block head //- block head
@@ -199,15 +210,18 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead thead
tr tr
th(class=`${prefix}-table__header`) #{lineItemLabel} th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{lineItemLabel}
th(class=`${prefix}-table__header`) #{lineDescriptionLabel} th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{lineQuantityLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel} th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{lineRateLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel} th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{lineTotalLabel}
tbody tbody
each line in lines each line in lines
tr tr
td(class=`${prefix}-table__cell`) #{line.item} td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`) #{line.description} 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.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total} td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}

View File

@@ -92,6 +92,9 @@ block head
.#{prefix}-table__header--right { .#{prefix}-table__header--right {
text-align: right; text-align: right;
} }
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell { .#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6; border-bottom: 1px solid #F6F6F6;
padding: 12px 10px; padding: 12px 10px;
@@ -105,6 +108,14 @@ block head
.#{prefix}-table__cell--right { .#{prefix}-table__cell--right {
text-align: 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -133,7 +144,9 @@ block head
margin-bottom: 20px; margin-bottom: 20px;
} }
.#{prefix}-statement__label {} .#{prefix}-statement__label {}
.#{prefix}-statement__value {} .#{prefix}-statement__value {
white-space: pre-line;
}
block content block content
//- block head //- block head
@@ -178,17 +191,20 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead(class=`${prefix}-table__header`) thead(class=`${prefix}-table__header`)
tr tr
th(class=`${prefix}-table__header`) Item th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header`) Description th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody tbody
each line in lines each line in lines
tr(class=`${prefix}-table__row`) tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`)= line.item td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`)= line.description div.item
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.rate div.item__label #{line.item}
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.total 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 //- Totals Section
div(class=`${prefix}-totals`) div(class=`${prefix}-totals`)

View File

@@ -471,13 +471,14 @@ export default class PaymentReceivesController extends BaseController {
ACCEPT_TYPE.APPLICATION_PDF, ACCEPT_TYPE.APPLICATION_PDF,
]); ]);
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.creditNotePdf.getCreditNotePdf( const [pdfContent, filename] = await this.creditNotePdf.getCreditNotePdf(
tenantId, tenantId,
creditNoteId creditNoteId
); );
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
} else { } else {

View File

@@ -408,7 +408,7 @@ export default class PaymentReceivesController extends BaseController {
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
const { tenantId } = req; const { tenantId } = req;
try { try {
const data = await this.paymentReceiveApplication.getPaymentReceivedState( const data = await this.paymentReceiveApplication.getPaymentReceivedState(
@@ -473,7 +473,7 @@ export default class PaymentReceivesController extends BaseController {
]); ]);
// Response in pdf format. // Response in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = const [pdfContent, filename] =
await this.paymentReceiveApplication.getPaymentReceivePdf( await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId, tenantId,
paymentReceiveId paymentReceiveId
@@ -481,6 +481,7 @@ export default class PaymentReceivesController extends BaseController {
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Response in json format. // Response in json format.

View File

@@ -398,13 +398,15 @@ export default class SalesEstimatesController extends BaseController {
]); ]);
// Retrieves estimate in pdf format. // Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf( const [pdfContent, filename] =
tenantId, await this.saleEstimatesApplication.getSaleEstimatePdf(
estimateId tenantId,
); estimateId
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Retrieves estimates in json format. // Retrieves estimates in json format.

View File

@@ -441,13 +441,15 @@ export default class SaleInvoicesController extends BaseController {
]); ]);
// Retrieves invoice in pdf format. // Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf( const [pdfContent, filename] =
tenantId, await this.saleInvoiceApplication.saleInvoicePdf(
saleInvoiceId tenantId,
); saleInvoiceId
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Retrieves invoice in json format. // Retrieves invoice in json format.

View File

@@ -113,7 +113,7 @@ export default class SalesReceiptsController extends BaseController {
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt), CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
asyncMiddleware(this.getSaleReceiptState.bind(this)), asyncMiddleware(this.getSaleReceiptState.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
router.get( router.get(
'/:id', '/:id',
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt), CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
@@ -356,13 +356,15 @@ export default class SalesReceiptsController extends BaseController {
]); ]);
// Retrieves receipt in pdf format. // Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf( const [pdfContent, filename] =
tenantId, await this.saleReceiptsApplication.getSaleReceiptPdf(
saleReceiptId tenantId,
); saleReceiptId
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Retrieves receipt in json format. // Retrieves receipt in json format.

View File

@@ -3,14 +3,21 @@ export const SALE_INVOICE_EDITED = 'Sale invoice edited';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted'; export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered'; export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
export const SALE_INVOICE_VIEWED = 'Sale invoice viewed'; export const SALE_INVOICE_VIEWED = 'Sale invoice viewed';
export const SALE_INVOICE_PDF_VIEWED = 'Sale invoice PDF viewed';
export const SALE_ESTIMATE_CREATED = 'Sale estimate created'; export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited'; export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted'; export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
export const SALE_ESTIMATE_PDF_VIEWED = 'Sale estimate PDF viewed';
export const PAYMENT_RECEIVED_CREATED = 'Payment received created'; export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited'; export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted'; export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
export const PAYMENT_RECEIVED_PDF_VIEWED = 'Payment received PDF viewed';
export const SALE_RECEIPT_PDF_VIEWED = 'Sale credit PDF viewed';
export const CREDIT_NOTE_PDF_VIEWED = 'Credit note PDF viewed';
export const BILL_CREATED = 'Bill created'; export const BILL_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited'; export const BILL_EDITED = 'Bill edited';
@@ -82,7 +89,8 @@ export const PAYMENT_METHOD_DELETED = 'Payment method deleted';
export const INVOICE_PAYMENT_LINK_GENERATED = 'Invoice payment link generated'; export const INVOICE_PAYMENT_LINK_GENERATED = 'Invoice payment link generated';
export const STRIPE_INTEGRAION_CONNECTED = 'Stripe integration oauth2 connected'; export const STRIPE_INTEGRAION_CONNECTED =
'Stripe integration oauth2 connected';
// # Event Groups // # Event Groups
export const ACCOUNT_GROUP = 'Account'; export const ACCOUNT_GROUP = 'Account';

View File

@@ -129,6 +129,7 @@ export const ACCOUNT_TYPES = [
normal: ACCOUNT_NORMAL.CREDIT, normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY, rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
multiCurrency: true,
balanceSheet: true, balanceSheet: true,
incomeSheet: false, incomeSheet: false,
}, },

View File

@@ -294,7 +294,7 @@ export default {
name: 'item.field.note', name: 'item.field.note',
fieldType: 'text', fieldType: 'text',
}, },
category: { categoryId: {
name: 'item.field.category', name: 'item.field.category',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'ItemCategory', relationModel: 'ItemCategory',

View File

@@ -11,6 +11,12 @@ export default class UncategorizedCashflowTransaction extends mixin(
) { ) {
id!: number; id!: number;
date!: Date | string; date!: Date | string;
/**
* Transaction amount.
* Negative represents to spending and positive to deposit/card charge.
* @param {number}
*/
amount!: number; amount!: number;
categorized!: boolean; categorized!: boolean;
accountId!: number; accountId!: number;

View File

@@ -14,6 +14,8 @@ export class AccountTransformer extends Transformer {
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return [ return [
'accountTypeLabel',
'accountNormalFormatted',
'formattedAmount', 'formattedAmount',
'flattenName', 'flattenName',
'bankBalanceFormatted', 'bankBalanceFormatted',
@@ -84,6 +86,22 @@ export class AccountTransformer extends Transformer {
return account.plaidItem?.isPaused || false; return account.plaidItem?.isPaused || false;
}; };
/**
* Retrieves formatted account type label.
* @returns {string}
*/
protected accountTypeLabel = (account: any): string => {
return this.context.i18n.__(account.accountTypeLabel);
};
/**
* Retrieves formatted account normal.
* @returns {string}
*/
protected accountNormalFormatted = (account: any): string => {
return this.context.i18n.__(account.accountNormalFormatted);
};
/** /**
* Transformes the accounts collection to flat or nested array. * Transformes the accounts collection to flat or nested array.
* @param {IAccount[]} * @param {IAccount[]}

View File

@@ -51,10 +51,6 @@ export class GetAccount {
// Triggers `onAccountViewed` event. // Triggers `onAccountViewed` event.
await this.eventPublisher.emitAsync(events.accounts.onViewed, eventPayload); await this.eventPublisher.emitAsync(events.accounts.onViewed, eventPayload);
return this.i18nService.i18nApply( return transformed;
[['accountTypeLabel'], ['accountNormalFormatted']],
transformed,
tenantId
);
}; };
} }

View File

@@ -4,11 +4,27 @@ import {
Institution as PlaidInstitution, Institution as PlaidInstitution,
AccountBase as PlaidAccount, AccountBase as PlaidAccount,
TransactionBase as PlaidTransactionBase, TransactionBase as PlaidTransactionBase,
AccountType as PlaidAccountType,
} from 'plaid'; } from 'plaid';
import { import {
CreateUncategorizedTransactionDTO, CreateUncategorizedTransactionDTO,
IAccountCreateDTO, IAccountCreateDTO,
} from '@/interfaces'; } from '@/interfaces';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
/**
* Retrieves the system account type from the given Plaid account type.
* @param {PlaidAccountType} plaidAccountType
* @returns {string}
*/
const getAccountTypeFromPlaidAccountType = (
plaidAccountType: PlaidAccountType
) => {
if (plaidAccountType === PlaidAccountType.Credit) {
return ACCOUNT_TYPE.CREDIT_CARD;
}
return ACCOUNT_TYPE.BANK;
};
/** /**
* Transformes the Plaid account to create cashflow account DTO. * Transformes the Plaid account to create cashflow account DTO.
@@ -28,7 +44,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
code: '', code: '',
description: plaidAccount.official_name, description: plaidAccount.official_name,
currencyCode: plaidAccount.balances.iso_currency_code, currencyCode: plaidAccount.balances.iso_currency_code,
accountType: 'cash', accountType: getAccountTypeFromPlaidAccountType(plaidAccount.type),
active: true, active: true,
bankBalance: plaidAccount.balances.current, bankBalance: plaidAccount.balances.current,
accountMask: plaidAccount.mask, accountMask: plaidAccount.mask,

View File

@@ -1,14 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import { ILedgerEntry, ICashflowTransaction } from '../../interfaces';
ILedgerEntry, import { transformCashflowTransactionType } from './utils';
ICashflowTransaction,
AccountNormal,
} from '../../interfaces';
import {
transformCashflowTransactionType,
getCashflowAccountTransactionsTypes,
} from './utils';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
@@ -70,7 +63,7 @@ export default class CashflowTransactionJournalEntries {
debit: cashflowTransaction.isCashDebit debit: cashflowTransaction.isCashDebit
? cashflowTransaction.localAmount ? cashflowTransaction.localAmount
: 0, : 0,
accountNormal: AccountNormal.DEBIT, accountNormal: cashflowTransaction?.cashflowAccount?.accountNormal,
index: 1, index: 1,
}; };
}; };
@@ -143,6 +136,7 @@ export default class CashflowTransactionJournalEntries {
// Retrieves the cashflow transactions with associated entries. // Retrieves the cashflow transactions with associated entries.
const transaction = await CashflowTransaction.query(trx) const transaction = await CashflowTransaction.query(trx)
.findById(cashflowTransactionId) .findById(cashflowTransactionId)
.withGraphFetched('cashflowAccount')
.withGraphFetched('creditAccount'); .withGraphFetched('creditAccount');
// Retrieves the cashflow transaction ledger. // Retrieves the cashflow transaction ledger.

View File

@@ -4,6 +4,7 @@ import { CashflowAccountTransformer } from './CashflowAccountTransformer';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
@Service() @Service()
export default class GetCashflowAccountsService { export default class GetCashflowAccountsService {
@@ -41,14 +42,20 @@ export default class GetCashflowAccountsService {
const accounts = await CashflowAccount.query().onBuild((builder) => { const accounts = await CashflowAccount.query().onBuild((builder) => {
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
builder.whereIn('account_type', ['bank', 'cash']); builder.whereIn('account_type', [
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.CREDIT_CARD,
]);
builder.modify('inactiveMode', filter.inactiveMode); builder.modify('inactiveMode', filter.inactiveMode);
}); });
// Retrieves the transformed accounts. // Retrieves the transformed accounts.
return this.transformer.transform( const transformed = await this.transformer.transform(
tenantId, tenantId,
accounts, accounts,
new CashflowAccountTransformer() new CashflowAccountTransformer()
); );
return transformed;
} }
} }

View File

@@ -12,7 +12,7 @@ export class GetCashflowTransactionService {
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject() @Inject()
private transfromer: TransformerInjectable; private transformer: TransformerInjectable;
/** /**
* Retrieve the given cashflow transaction. * Retrieve the given cashflow transaction.
@@ -37,7 +37,7 @@ export class GetCashflowTransactionService {
this.throwErrorCashflowTranscationNotFound(cashflowTransaction); this.throwErrorCashflowTranscationNotFound(cashflowTransaction);
// Transformes the cashflow transaction model to POJO. // Transformes the cashflow transaction model to POJO.
return this.transfromer.transform( return this.transformer.transform(
tenantId, tenantId,
cashflowTransaction, cashflowTransaction,
new CashflowTransactionTransformer() new CashflowTransactionTransformer()

View File

@@ -6,6 +6,8 @@ import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
import { CreditNotePdfTemplateAttributes } from '@/interfaces'; import { CreditNotePdfTemplateAttributes } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { transformCreditNoteToPdfTemplate } from './utils'; import { transformCreditNoteToPdfTemplate } from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export default class GetCreditNotePdf { export default class GetCreditNotePdf {
@@ -24,12 +26,19 @@ export default class GetCreditNotePdf {
@Inject() @Inject()
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate; private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieves sale invoice pdf content. * Retrieves sale invoice pdf content.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id. * @param {number} creditNoteId - Credit note id.
* @returns {Promise<[Buffer, string]>}
*/ */
public async getCreditNotePdf(tenantId: number, creditNoteId: number) { public async getCreditNotePdf(
tenantId: number,
creditNoteId: number
): Promise<[Buffer, string]> {
const brandingAttributes = await this.getCreditNoteBrandingAttributes( const brandingAttributes = await this.getCreditNoteBrandingAttributes(
tenantId, tenantId,
creditNoteId creditNoteId
@@ -39,7 +48,37 @@ export default class GetCreditNotePdf {
'modules/credit-note-standard', 'modules/credit-note-standard',
brandingAttributes brandingAttributes
); );
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); const filename = await this.getCreditNoteFilename(tenantId, creditNoteId);
const document = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
);
const eventPayload = { tenantId, creditNoteId };
// Triggers the `onCreditNotePdfViewed` event.
await this.eventPublisher.emitAsync(
events.creditNote.onPdfViewed,
eventPayload
);
return [document, filename];
}
/**
* Retrieves the filename pdf document of the given credit note.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<string>}
*/
public async getCreditNoteFilename(
tenantId: number,
creditNoteId: number
): Promise<string> {
const { CreditNote } = this.tenancy.models(tenantId);
const creditNote = await CreditNote.query().findById(creditNoteId);
return `Credit-${creditNote.creditNoteNumber}`;
} }
/** /**

View File

@@ -11,6 +11,7 @@ import {
PAYMENT_RECEIVED_CREATED, PAYMENT_RECEIVED_CREATED,
PAYMENT_RECEIVED_EDITED, PAYMENT_RECEIVED_EDITED,
PAYMENT_RECEIVED_DELETED, PAYMENT_RECEIVED_DELETED,
PAYMENT_RECEIVED_PDF_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@Service() @Service()
@@ -34,6 +35,10 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
events.paymentReceive.onDeleted, events.paymentReceive.onDeleted,
this.handleTrackDeletedPaymentReceivedEvent this.handleTrackDeletedPaymentReceivedEvent
); );
bus.subscribe(
events.paymentReceive.onPdfViewed,
this.handleTrackPdfViewedPaymentReceivedEvent
);
} }
private handleTrackPaymentReceivedCreatedEvent = ({ private handleTrackPaymentReceivedCreatedEvent = ({
@@ -65,4 +70,14 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
properties: {}, properties: {},
}); });
}; };
private handleTrackPdfViewedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_PDF_VIEWED,
properties: {},
});
};
} }

View File

@@ -11,6 +11,7 @@ import {
SALE_ESTIMATE_CREATED, SALE_ESTIMATE_CREATED,
SALE_ESTIMATE_EDITED, SALE_ESTIMATE_EDITED,
SALE_ESTIMATE_DELETED, SALE_ESTIMATE_DELETED,
SALE_ESTIMATE_PDF_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@Service() @Service()
@@ -34,6 +35,10 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
events.saleEstimate.onDeleted, events.saleEstimate.onDeleted,
this.handleTrackDeletedEstimateEvent this.handleTrackDeletedEstimateEvent
); );
bus.subscribe(
events.saleEstimate.onPdfViewed,
this.handleTrackPdfViewedEstimateEvent
);
} }
private handleTrackEstimateCreatedEvent = ({ private handleTrackEstimateCreatedEvent = ({
@@ -65,4 +70,14 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
properties: {}, properties: {},
}); });
}; };
private handleTrackPdfViewedEstimateEvent = ({
tenantId,
}: ISaleEstimateDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_PDF_VIEWED,
properties: {},
});
};
} }

View File

@@ -10,6 +10,7 @@ import {
SALE_INVOICE_CREATED, SALE_INVOICE_CREATED,
SALE_INVOICE_DELETED, SALE_INVOICE_DELETED,
SALE_INVOICE_EDITED, SALE_INVOICE_EDITED,
SALE_INVOICE_PDF_VIEWED,
SALE_INVOICE_VIEWED, SALE_INVOICE_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@@ -38,6 +39,10 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
events.saleInvoice.onViewed, events.saleInvoice.onViewed,
this.handleTrackViewedInvoiceEvent this.handleTrackViewedInvoiceEvent
); );
bus.subscribe(
events.saleInvoice.onPdfViewed,
this.handleTrackPdfViewedInvoiceEvent
);
} }
private handleTrackInvoiceCreatedEvent = ({ private handleTrackInvoiceCreatedEvent = ({
@@ -77,4 +82,12 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
properties: {}, properties: {},
}); });
}; };
private handleTrackPdfViewedInvoiceEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_PDF_VIEWED,
properties: {},
});
};
} }

View File

@@ -299,7 +299,7 @@ export const valueParser =
// Parses the enumeration value. // Parses the enumeration value.
} else if (field.fieldType === 'enumeration') { } else if (field.fieldType === 'enumeration') {
const option = get(field, 'options', []).find( const option = get(field, 'options', []).find(
(option) => option.label === value (option) => option.label?.toLowerCase() === value?.toLowerCase()
); );
_value = get(option, 'key'); _value = get(option, 'key');
// Parses the numeric value. // Parses the numeric value.
@@ -433,8 +433,8 @@ export const getMapToPath = (to: string, group = '') =>
group ? `${group}.${to}` : to; group ? `${group}.${to}` : to;
export const getImportsStoragePath = () => { export const getImportsStoragePath = () => {
return path.join(global.__storage_dir, `/imports`); return path.join(global.__storage_dir, `/imports`);
} };
/** /**
* Deletes the imported file from the storage and database. * Deletes the imported file from the storage and database.

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate'; import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
import { transformEstimateToPdfTemplate } from './utils'; import { transformEstimateToPdfTemplate } from './utils';
import { EstimatePdfBrandingAttributes } from './constants'; import { EstimatePdfBrandingAttributes } from './constants';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service() @Service()
export class SaleEstimatesPdf { export class SaleEstimatesPdf {
@@ -24,12 +26,22 @@ export class SaleEstimatesPdf {
@Inject() @Inject()
private estimatePdfTemplate: SaleEstimatePdfTemplate; private estimatePdfTemplate: SaleEstimatePdfTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve sale invoice pdf content. * Retrieve sale invoice pdf content.
* @param {number} tenantId - * @param {number} tenantId -
* @param {ISaleInvoice} saleInvoice - * @param {ISaleInvoice} saleInvoice -
*/ */
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { public async getSaleEstimatePdf(
tenantId: number,
saleEstimateId: number
): Promise<[Buffer, string]> {
const filename = await this.getSaleEstimateFilename(
tenantId,
saleEstimateId
);
const brandingAttributes = await this.getEstimateBrandingAttributes( const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId, tenantId,
saleEstimateId saleEstimateId
@@ -39,7 +51,32 @@ export class SaleEstimatesPdf {
'modules/estimate-regular', 'modules/estimate-regular',
brandingAttributes brandingAttributes
); );
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
);
const eventPayload = { tenantId, saleEstimateId };
// Triggers the `onSaleEstimatePdfViewed` event.
await this.eventPublisher.emitAsync(
events.saleEstimate.onPdfViewed,
eventPayload
);
return [content, filename];
}
/**
* Retrieves the filename file document of the given estimate.
* @param {number} tenantId
* @param {number} estimateId
* @returns {Promise<string>}
*/
private async getSaleEstimateFilename(tenantId: number, estimateId: number) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const estimate = await SaleEstimate.query().findById(estimateId);
return `Estimate-${estimate.estimateNumber}`;
} }
/** /**

View File

@@ -17,7 +17,7 @@ export const transformEstimateToPdfTemplate = (
})), })),
total: estimate.formattedSubtotal, total: estimate.formattedSubtotal,
subtotal: estimate.formattedSubtotal, subtotal: estimate.formattedSubtotal,
customerNote: estimate.customerNote, customerNote: estimate.note,
termsConditions: estimate.termsConditions, termsConditions: estimate.termsConditions,
customerAddress: contactAddressTextFormat(estimate.customer), customerAddress: contactAddressTextFormat(estimate.customer),
}; };

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformInvoiceToPdfTemplate } from './utils'; import { transformInvoiceToPdfTemplate } from './utils';
import { InvoicePdfTemplateAttributes } from '@/interfaces'; import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate'; import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export class SaleInvoicePdf { export class SaleInvoicePdf {
@@ -24,6 +26,9 @@ export class SaleInvoicePdf {
@Inject() @Inject()
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate; private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve sale invoice pdf content. * Retrieve sale invoice pdf content.
* @param {number} tenantId - Tenant Id. * @param {number} tenantId - Tenant Id.
@@ -33,7 +38,9 @@ export class SaleInvoicePdf {
public async saleInvoicePdf( public async saleInvoicePdf(
tenantId: number, tenantId: number,
invoiceId: number invoiceId: number
): Promise<Buffer> { ): Promise<[Buffer, string]> {
const filename = await this.getInvoicePdfFilename(tenantId, invoiceId);
const brandingAttributes = await this.getInvoiceBrandingAttributes( const brandingAttributes = await this.getInvoiceBrandingAttributes(
tenantId, tenantId,
invoiceId invoiceId
@@ -44,7 +51,35 @@ export class SaleInvoicePdf {
brandingAttributes brandingAttributes
); );
// Converts the given html content to pdf document. // Converts the given html content to pdf document.
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); const buffer = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
);
const eventPayload = { tenantId, saleInvoiceId: invoiceId };
// Triggers the `onSaleInvoicePdfViewed` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onPdfViewed,
eventPayload
);
return [buffer, filename];
}
/**
* Retrieves the filename pdf document of the given invoice.
* @param {number} tenantId
* @param {number} invoiceId
* @returns {Promise<string>}
*/
private async getInvoicePdfFilename(
tenantId: number,
invoiceId: number
): Promise<string> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoice = await SaleInvoice.query().findById(invoiceId);
return `Invoice-${invoice.invoiceNo}`;
} }
/** /**

View File

@@ -194,7 +194,7 @@ export const defaultInvoicePdfTemplateAttributes = {
// Entries // Entries
lineItemLabel: 'Item', lineItemLabel: 'Item',
lineDescriptionLabel: 'Description', lineQuantityLabel: 'Qty',
lineRateLabel: 'Rate', lineRateLabel: 'Rate',
lineTotalLabel: 'Total', lineTotalLabel: 'Total',

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate'; import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
import { transformPaymentReceivedToPdfTemplate } from './utils'; import { transformPaymentReceivedToPdfTemplate } from './utils';
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces'; import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service() @Service()
export default class GetPaymentReceivedPdf { export default class GetPaymentReceivedPdf {
@@ -24,6 +26,9 @@ export default class GetPaymentReceivedPdf {
@Inject() @Inject()
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate; private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve sale invoice pdf content. * Retrieve sale invoice pdf content.
* @param {number} tenantId - * @param {number} tenantId -
@@ -32,19 +37,51 @@ export default class GetPaymentReceivedPdf {
*/ */
async getPaymentReceivePdf( async getPaymentReceivePdf(
tenantId: number, tenantId: number,
paymentReceiveId: number paymentReceivedId: number
): Promise<Buffer> { ): Promise<[Buffer, string]> {
const brandingAttributes = await this.getPaymentBrandingAttributes( const brandingAttributes = await this.getPaymentBrandingAttributes(
tenantId, tenantId,
paymentReceiveId paymentReceivedId
); );
const htmlContent = await this.templateInjectable.render( const htmlContent = await this.templateInjectable.render(
tenantId, tenantId,
'modules/payment-receive-standard', 'modules/payment-receive-standard',
brandingAttributes brandingAttributes
); );
const filename = await this.getPaymentReceivedFilename(
tenantId,
paymentReceivedId
);
// Converts the given html content to pdf document. // Converts the given html content to pdf document.
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
);
const eventPayload = { tenantId, paymentReceivedId };
// Triggers the `onCreditNotePdfViewed` event.
await this.eventPublisher.emitAsync(
events.paymentReceive.onPdfViewed,
eventPayload
);
return [content, filename];
}
/**
* Retrieves the filename of the given payment.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @returns {Promise<string>}
*/
private async getPaymentReceivedFilename(
tenantId: number,
paymentReceivedId: number
): Promise<string> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const payment = await PaymentReceive.query().findById(paymentReceivedId);
return `Payment-${payment.paymentReceiveNo}`;
} }
/** /**

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate'; import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
import { transformReceiptToBrandingTemplateAttributes } from './utils'; import { transformReceiptToBrandingTemplateAttributes } from './utils';
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces'; import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export class SaleReceiptsPdf { export class SaleReceiptsPdf {
@@ -24,6 +26,9 @@ export class SaleReceiptsPdf {
@Inject() @Inject()
private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate; private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieves sale invoice pdf content. * Retrieves sale invoice pdf content.
* @param {number} tenantId - * @param {number} tenantId -
@@ -31,6 +36,8 @@ export class SaleReceiptsPdf {
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) { public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
const brandingAttributes = await this.getReceiptBrandingAttributes( const brandingAttributes = await this.getReceiptBrandingAttributes(
tenantId, tenantId,
saleReceiptId saleReceiptId
@@ -42,7 +49,35 @@ export class SaleReceiptsPdf {
brandingAttributes brandingAttributes
); );
// Renders the html content to pdf document. // Renders the html content to pdf document.
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
);
const eventPayload = { tenantId, saleReceiptId };
// Triggers the `onSaleReceiptPdfViewed` event.
await this.eventPublisher.emitAsync(
events.saleReceipt.onPdfViewed,
eventPayload
);
return [content, filename];
}
/**
* Retrieves the filename file document of the given sale receipt.
* @param {number} tenantId
* @param {number} receiptId
* @returns {Promise<string>}
*/
public async getSaleReceiptFilename(
tenantId: number,
receiptId: number
): Promise<string> {
const { SaleReceipt } = this.tenancy.models(tenantId);
const receipt = await SaleReceipt.query().findById(receiptId);
return `Receipt-${receipt.receiptNumber}`;
} }
/** /**

View File

@@ -133,6 +133,8 @@ export default {
onViewed: 'onSaleInvoiceItemViewed', onViewed: 'onSaleInvoiceItemViewed',
onListViewed: 'onSaleInvoiceListViewed', onListViewed: 'onSaleInvoiceListViewed',
onPdfViewed: 'onSaleInvoicePdfViewed',
onCreate: 'onSaleInvoiceCreate', onCreate: 'onSaleInvoiceCreate',
onCreating: 'onSaleInvoiceCreating', onCreating: 'onSaleInvoiceCreating',
onCreated: 'onSaleInvoiceCreated', onCreated: 'onSaleInvoiceCreated',
@@ -178,6 +180,8 @@ export default {
* Sales estimates service. * Sales estimates service.
*/ */
saleEstimate: { saleEstimate: {
onPdfViewed: 'onSaleEstimatePdfViewed',
onCreating: 'onSaleEstimateCreating', onCreating: 'onSaleEstimateCreating',
onCreated: 'onSaleEstimateCreated', onCreated: 'onSaleEstimateCreated',
@@ -215,6 +219,8 @@ export default {
* Sales receipts service. * Sales receipts service.
*/ */
saleReceipt: { saleReceipt: {
onPdfViewed: 'onSaleReceiptPdfViewed',
onCreating: 'onSaleReceiptsCreating', onCreating: 'onSaleReceiptsCreating',
onCreated: 'onSaleReceiptsCreated', onCreated: 'onSaleReceiptsCreated',
@@ -242,6 +248,8 @@ export default {
* Payment receipts service. * Payment receipts service.
*/ */
paymentReceive: { paymentReceive: {
onPdfViewed: 'onPaymentReceivedPdfViewed',
onCreated: 'onPaymentReceiveCreated', onCreated: 'onPaymentReceiveCreated',
onCreating: 'onPaymentReceiveCreating', onCreating: 'onPaymentReceiveCreating',
@@ -345,7 +353,7 @@ export default {
*/ */
item: { item: {
onViewed: 'onItemViewed', onViewed: 'onItemViewed',
onCreated: 'onItemCreated', onCreated: 'onItemCreated',
onCreating: 'onItemCreating', onCreating: 'onItemCreating',
@@ -464,6 +472,8 @@ export default {
* Credit note service. * Credit note service.
*/ */
creditNote: { creditNote: {
onPdfViewed: 'onCreditNotePdfViewed',
onCreate: 'onCreditNoteCreate', onCreate: 'onCreditNoteCreate',
onCreating: 'onCreditNoteCreating', onCreating: 'onCreditNoteCreating',
onCreated: 'onCreditNoteCreated', onCreated: 'onCreditNoteCreated',
@@ -722,7 +732,7 @@ export default {
// Payment methods integrations // Payment methods integrations
paymentIntegrationLink: { paymentIntegrationLink: {
onPaymentIntegrationLink: 'onPaymentIntegrationLink', onPaymentIntegrationLink: 'onPaymentIntegrationLink',
onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink' onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink',
}, },
// Stripe Payment Integration // Stripe Payment Integration
@@ -739,6 +749,6 @@ export default {
// Stripe Payment Webhooks // Stripe Payment Webhooks
stripeWebhooks: { stripeWebhooks: {
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted', onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
onAccountUpdated: 'onStripeAccountUpdated' onAccountUpdated: 'onStripeAccountUpdated',
} },
}; };

View File

@@ -11,8 +11,8 @@ import { compose } from '@/utils';
function CreditNotePdfPreviewDialogContent({ function CreditNotePdfPreviewDialogContent({
subscriptionForm: { creditNoteId }, subscriptionForm: { creditNoteId },
}) { }) {
const { isLoading, pdfUrl } = usePdfCreditNote(creditNoteId); const { isLoading, pdfUrl, filename } = usePdfCreditNote(creditNoteId);
return ( return (
<DialogContent> <DialogContent>
<div class="dialog__header-actions"> <div class="dialog__header-actions">
@@ -27,7 +27,7 @@ function CreditNotePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'creditNote.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -14,7 +14,7 @@ function EstimatePdfPreviewDialogContent({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
const { isLoading, pdfUrl } = usePdfEstimate(estimateId); const { isLoading, pdfUrl, filename } = usePdfEstimate(estimateId);
return ( return (
<DialogContent> <DialogContent>
@@ -30,7 +30,7 @@ function EstimatePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'estimate.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -13,7 +13,7 @@ function InvoicePdfPreviewDialogContent({
// #withDialog // #withDialog
closeDialog, closeDialog,
}) { }) {
const { isLoading, pdfUrl } = usePdfInvoice(invoiceId); const { isLoading, pdfUrl, filename } = usePdfInvoice(invoiceId);
return ( return (
<DialogContent> <DialogContent>
@@ -29,7 +29,7 @@ function InvoicePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'invoice.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -11,7 +11,7 @@ import { compose } from '@/utils';
function PaymentReceivePdfPreviewDialogContent({ function PaymentReceivePdfPreviewDialogContent({
subscriptionForm: { paymentReceiveId }, subscriptionForm: { paymentReceiveId },
}) { }) {
const { isLoading, pdfUrl } = usePdfPaymentReceive(paymentReceiveId); const { isLoading, pdfUrl, filename } = usePdfPaymentReceive(paymentReceiveId);
return ( return (
<DialogContent> <DialogContent>
@@ -27,7 +27,7 @@ function PaymentReceivePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'payment.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -13,7 +13,7 @@ function ReceiptPdfPreviewDialogContent({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
const { isLoading, pdfUrl } = usePdfReceipt(receiptId); const { isLoading, pdfUrl, filename } = usePdfReceipt(receiptId);
return ( return (
<DialogContent> <DialogContent>
@@ -29,7 +29,7 @@ function ReceiptPdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'receipt.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -1,3 +1,4 @@
import { Classes, Text } from '@blueprintjs/core';
import { Box, Group, Stack } from '@/components'; import { Box, Group, Stack } from '@/components';
import { import {
PaperTemplate, PaperTemplate,
@@ -67,6 +68,12 @@ export interface CreditNotePaperTemplateProps extends PaperTemplateProps {
creditNoteNumebr?: string; creditNoteNumebr?: string;
creditNoteNumberLabel?: string; creditNoteNumberLabel?: string;
showCreditNoteNumber?: boolean; showCreditNoteNumber?: boolean;
// Entries
lineItemLabel?: string;
lineQuantityLabel?: string;
lineRateLabel?: string;
lineTotalLabel?: string;
} }
export function CreditNotePaperTemplate({ export function CreditNotePaperTemplate({
@@ -127,6 +134,12 @@ export function CreditNotePaperTemplate({
creditNoteDate = 'September 3, 2024', creditNoteDate = 'September 3, 2024',
showCreditNoteDate = true, showCreditNoteDate = true,
creditNoteDateLabel = 'Credit Note Date', creditNoteDateLabel = 'Credit Note Date',
// Entries
lineItemLabel = 'Item',
lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate',
lineTotalLabel = 'Total',
}: CreditNotePaperTemplateProps) { }: CreditNotePaperTemplateProps) {
return ( return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}> <PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
@@ -172,10 +185,23 @@ export function CreditNotePaperTemplate({
<Stack spacing={0}> <Stack spacing={0}>
<PaperTemplate.Table <PaperTemplate.Table
columns={[ columns={[
{ label: 'Item', accessor: 'item' }, {
{ label: 'Description', accessor: 'description' }, label: lineItemLabel,
{ label: 'Rate', accessor: 'rate', align: 'right' }, accessor: (data) => (
{ label: 'Total', accessor: 'total', align: 'right' }, <Stack spacing={2}>
<Text>{data.item}</Text>
<Text
className={Classes.TEXT_MUTED}
style={{ fontSize: 12 }}
>
{data.description}
</Text>
</Stack>
),
},
{ label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
]} ]}
data={lines} data={lines}
/> />

View File

@@ -1,3 +1,4 @@
import { Classes, Text } from '@blueprintjs/core';
import { Box, Group, Stack } from '@/components'; import { Box, Group, Stack } from '@/components';
import { import {
PaperTemplate, PaperTemplate,
@@ -69,6 +70,13 @@ export interface EstimatePaperTemplateProps extends PaperTemplateProps {
quantity: string; quantity: string;
total: string; total: string;
}>; }>;
// Lines
lineItemLabel?: string,
lineQuantityLabel?: string,
lineRateLabel?: string,
lineTotalLabel?: string,
} }
export function EstimatePaperTemplate({ export function EstimatePaperTemplate({
@@ -134,6 +142,13 @@ export function EstimatePaperTemplate({
expirationDateLabel = 'Expiration Date', expirationDateLabel = 'Expiration Date',
showExpirationDate = true, showExpirationDate = true,
expirationDate = 'September 3, 2024', expirationDate = 'September 3, 2024',
// Entries
lineItemLabel = 'Item',
lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate',
lineTotalLabel = 'Total',
}: EstimatePaperTemplateProps) { }: EstimatePaperTemplateProps) {
return ( return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}> <PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
@@ -183,10 +198,23 @@ export function EstimatePaperTemplate({
<Stack spacing={0}> <Stack spacing={0}>
<PaperTemplate.Table <PaperTemplate.Table
columns={[ columns={[
{ label: 'Item', accessor: 'item' }, {
{ label: 'Description', accessor: 'description' }, label: lineItemLabel,
{ label: 'Rate', accessor: 'rate', align: 'right' }, accessor: (data) => (
{ label: 'Total', accessor: 'total', align: 'right' }, <Stack spacing={2}>
<Text>{data.item}</Text>
<Text
className={Classes.TEXT_MUTED}
style={{ fontSize: 12 }}
>
{data.description}
</Text>
</Stack>
),
},
{ label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
]} ]}
data={lines} data={lines}
/> />

View File

@@ -1,4 +1,4 @@
import React from 'react'; import { Classes, Text } from '@blueprintjs/core';
import { PaperTemplate, PaperTemplateTotalBorder } from './PaperTemplate'; import { PaperTemplate, PaperTemplateTotalBorder } from './PaperTemplate';
import { Box, Group, Stack } from '@/components'; import { Box, Group, Stack } from '@/components';
import { import {
@@ -9,6 +9,7 @@ import {
DefaultPdfTemplateAddressBilledTo, DefaultPdfTemplateAddressBilledTo,
DefaultPdfTemplateAddressBilledFrom, DefaultPdfTemplateAddressBilledFrom,
} from '@/constants/PdfTemplates'; } from '@/constants/PdfTemplates';
interface PapaerLine { interface PapaerLine {
item?: string; item?: string;
description?: string; description?: string;
@@ -55,7 +56,7 @@ export interface InvoicePaperTemplateProps {
// Entries // Entries
lineItemLabel?: string; lineItemLabel?: string;
lineDescriptionLabel?: string; lineQuantityLabel?: string;
lineRateLabel?: string; lineRateLabel?: string;
lineTotalLabel?: string; lineTotalLabel?: string;
@@ -129,7 +130,7 @@ export function InvoicePaperTemplate({
// Entries // Entries
lineItemLabel = 'Item', lineItemLabel = 'Item',
lineDescriptionLabel = 'Description', lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate', lineRateLabel = 'Rate',
lineTotalLabel = 'Total', lineTotalLabel = 'Total',
@@ -214,7 +215,6 @@ export function InvoicePaperTemplate({
<Box dangerouslySetInnerHTML={{ __html: companyAddress }} /> <Box dangerouslySetInnerHTML={{ __html: companyAddress }} />
</PaperTemplate.Address> </PaperTemplate.Address>
)} )}
{showCustomerAddress && ( {showCustomerAddress && (
<PaperTemplate.Address> <PaperTemplate.Address>
<strong>{billedToLabel}</strong> <strong>{billedToLabel}</strong>
@@ -226,8 +226,21 @@ export function InvoicePaperTemplate({
<Stack spacing={0}> <Stack spacing={0}>
<PaperTemplate.Table <PaperTemplate.Table
columns={[ columns={[
{ label: lineItemLabel, accessor: 'item' }, {
{ label: lineDescriptionLabel, accessor: 'description' }, label: lineItemLabel,
accessor: (data) => (
<Stack spacing={2}>
<Text>{data.item}</Text>
<Text
className={Classes.TEXT_MUTED}
style={{ fontSize: 12 }}
>
{data.description}
</Text>
</Stack>
),
},
{ label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' }, { label: lineRateLabel, accessor: 'rate', align: 'right' },
{ label: lineTotalLabel, accessor: 'total', align: 'right' }, { label: lineTotalLabel, accessor: 'total', align: 'right' },
]} ]}

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import clsx from 'classnames'; import clsx from 'classnames';
import { get } from 'lodash'; import { get, isFunction } from 'lodash';
import { Box, Group, GroupProps } from '@/components'; import { Box, Group, GroupProps } from '@/components';
import styles from './InvoicePaperTemplate.module.scss'; import styles from './InvoicePaperTemplate.module.scss';
@@ -26,8 +26,9 @@ export function PaperTemplate({
interface PaperTemplateTableProps { interface PaperTemplateTableProps {
columns: Array<{ columns: Array<{
accessor: string; accessor: string | ((data: Record<string, any>) => JSX.Element);
label: string; label: string;
value?: JSX.Element;
align?: 'left' | 'center' | 'right'; align?: 'left' | 'center' | 'right';
}>; }>;
data: Array<Record<string, any>>; data: Array<Record<string, any>>;
@@ -71,7 +72,9 @@ PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => {
<tr> <tr>
{columns.map((column, index) => ( {columns.map((column, index) => (
<td align={column.align} key={index}> <td align={column.align} key={index}>
{get(_data, column.accessor)} {isFunction(column?.accessor)
? column?.accessor(_data)
: get(_data, column.accessor)}
</td> </td>
))} ))}
</tr> </tr>
@@ -115,9 +118,9 @@ PaperTemplate.TotalLine = ({
); );
}; };
PaperTemplate.MutedText = () => {}; PaperTemplate.MutedText = () => { };
PaperTemplate.Text = () => {}; PaperTemplate.Text = () => { };
PaperTemplate.AddressesGroup = (props: GroupProps) => { PaperTemplate.AddressesGroup = (props: GroupProps) => {
return ( return (

View File

@@ -1,3 +1,4 @@
import { Classes, Text } from '@blueprintjs/core';
import { Box, Group, Stack } from '@/components'; import { Box, Group, Stack } from '@/components';
import { import {
PaperTemplate, PaperTemplate,
@@ -67,6 +68,12 @@ export interface ReceiptPaperTemplateProps extends PaperTemplateProps {
receiptNumebr?: string; receiptNumebr?: string;
receiptNumberLabel?: string; receiptNumberLabel?: string;
showReceiptNumber?: boolean; showReceiptNumber?: boolean;
// Entries
lineItemLabel?: string;
lineQuantityLabel?: string;
lineRateLabel?: string;
lineTotalLabel?: string;
} }
export function ReceiptPaperTemplate({ export function ReceiptPaperTemplate({
@@ -115,13 +122,22 @@ export function ReceiptPaperTemplate({
total: '$1000.00', total: '$1000.00',
}, },
], ],
// Receipt Number
showReceiptNumber = true, showReceiptNumber = true,
receiptNumberLabel = 'Receipt Number', receiptNumberLabel = 'Receipt Number',
receiptNumebr = '346D3D40-0001', receiptNumebr = '346D3D40-0001',
// Receipt Date
receiptDate = 'September 3, 2024', receiptDate = 'September 3, 2024',
showReceiptDate = true, showReceiptDate = true,
receiptDateLabel = 'Receipt Date', receiptDateLabel = 'Receipt Date',
// Entries
lineItemLabel = 'Item',
lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate',
lineTotalLabel = 'Total',
}: ReceiptPaperTemplateProps) { }: ReceiptPaperTemplateProps) {
return ( return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}> <PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
@@ -167,10 +183,23 @@ export function ReceiptPaperTemplate({
<Stack spacing={0}> <Stack spacing={0}>
<PaperTemplate.Table <PaperTemplate.Table
columns={[ columns={[
{ label: 'Item', accessor: 'item' }, {
{ label: 'Description', accessor: 'description' }, label: lineItemLabel,
{ label: 'Rate', accessor: 'rate', align: 'right' }, accessor: (data) => (
{ label: 'Total', accessor: 'total', align: 'right' }, <Stack spacing={2}>
<Text>{data.item}</Text>
<Text
className={Classes.TEXT_MUTED}
style={{ fontSize: 12 }}
>
{data.description}
</Text>
</Stack>
),
},
{ label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
]} ]}
data={lines} data={lines}
/> />

View File

@@ -8,6 +8,7 @@ export const useRequestPdf = (httpProps) => {
const [isLoaded, setIsLoaded] = React.useState(false); const [isLoaded, setIsLoaded] = React.useState(false);
const [pdfUrl, setPdfUrl] = React.useState(''); const [pdfUrl, setPdfUrl] = React.useState('');
const [response, setResponse] = React.useState(null); const [response, setResponse] = React.useState(null);
const [filename, setFilename] = React.useState<string>('');
React.useEffect(() => { React.useEffect(() => {
setIsLoading(true); setIsLoading(true);
@@ -25,10 +26,21 @@ export const useRequestPdf = (httpProps) => {
// Build a URL from the file // Build a URL from the file
const fileURL = URL.createObjectURL(file); const fileURL = URL.createObjectURL(file);
// Extract the filename from the Content-Disposition header
const contentDisposition = response.headers.get('Content-Disposition');
let _filename = 'default.pdf'; // Default filename if not provided by server
if (contentDisposition && contentDisposition.includes('filename=')) {
const matches = contentDisposition.match(/filename="(.+)"/);
if (matches && matches[1]) {
_filename = matches[1];
}
}
setPdfUrl(fileURL); setPdfUrl(fileURL);
setIsLoading(false); setIsLoading(false);
setIsLoaded(true); setIsLoaded(true);
setResponse(response); setResponse(response);
setFilename(_filename);
}); });
}, []); }, []);
@@ -37,5 +49,6 @@ export const useRequestPdf = (httpProps) => {
isLoaded, isLoaded,
pdfUrl, pdfUrl,
response, response,
filename
}; };
}; };