From 2c790427faf2b2ab77a88746e628cf9eed7df28a Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 17 Sep 2024 13:53:57 +0200 Subject: [PATCH] feat: rendering pdf templates on the server-side --- .../views/modules/credit-note-standard.pug | 260 +++++++++++++----- .../views/modules/estimate-regular.pug | 258 ++++++++++++----- .../views/modules/invoice-standard.pug | 4 +- .../modules/payment-receive-standard.pug | 230 ++++++++++++---- .../views/modules/receipt-regular.pug | 241 +++++++++++----- packages/server/src/interfaces/CreditNote.ts | 46 ++++ .../server/src/interfaces/PaymentReceive.ts | 68 +++++ .../server/src/interfaces/SaleEstimate.ts | 1 + .../services/CreditNotes/CreateCreditNote.ts | 2 +- .../CreditNotes/CreditNoteBrandingTemplate.ts | 30 ++ .../src/services/CreditNotes/CreditNotes.ts | 19 +- .../services/CreditNotes/EditCreditNote.ts | 2 +- .../services/CreditNotes/GetCreditNotePdf.ts | 66 ++++- .../src/services/CreditNotes/constants.ts | 71 ++++- .../server/src/services/CreditNotes/utils.ts | 9 + .../BrandingTemplateDTOTransformer.ts | 46 ++-- .../Estimates/SaleEstimateDTOTransformer.ts | 15 +- .../Sales/Estimates/SaleEstimatesPdf.ts | 54 +++- .../src/services/Sales/Estimates/constants.ts | 119 ++++++++ .../src/services/Sales/Estimates/utils.ts | 7 + .../Sales/Invoices/SaleEstimatePdfTemplate.ts | 31 +++ .../PaymentReceived/GetPaymentReceivedPdf.ts | 57 +++- .../PaymentReceivedBrandingTemplate.ts | 35 +++ .../PaymentReceivedDTOTransformer.ts | 15 +- .../Sales/PaymentReceived/constants.ts | 50 ++++ .../services/Sales/PaymentReceived/utils.ts | 12 + .../Receipts/SaleReceiptBrandingTemplate.ts | 35 +++ .../Receipts/SaleReceiptDTOTransformer.ts | 14 +- .../Sales/Receipts/SaleReceiptsPdfService.ts | 60 +++- .../src/services/Sales/Receipts/constants.ts | 98 +++++-- .../src/services/Sales/Receipts/utils.ts | 6 + .../CreditNoteFloatingActions.tsx | 28 +- .../CreditNoteForm/CreditNoteFormProvider.tsx | 12 +- .../CreditNotes/CreditNoteForm/utils.tsx | 14 +- .../EstimateForm/EstimateFloatingActions.tsx | 28 +- .../EstimateForm/EstimateFormProvider.tsx | 21 +- .../Sales/Estimates/EstimateForm/utils.tsx | 13 +- .../EstimatesLanding/EstimatesActionsBar.tsx | 5 + .../PaymentReceiveFloatingActions.tsx | 29 +- .../PaymentReceiveFormProvider.tsx | 20 +- .../PaymentReceiveForm/utils.tsx | 13 +- .../ReceiptFormFloatingActions.tsx | 28 +- .../ReceiptForm/ReceiptFormProvider.tsx | 13 +- .../Sales/Receipts/ReceiptForm/utils.tsx | 11 + 44 files changed, 1833 insertions(+), 363 deletions(-) create mode 100644 packages/server/src/services/CreditNotes/CreditNoteBrandingTemplate.ts create mode 100644 packages/server/src/services/CreditNotes/utils.ts create mode 100644 packages/server/src/services/Sales/Estimates/utils.ts create mode 100644 packages/server/src/services/Sales/Invoices/SaleEstimatePdfTemplate.ts create mode 100644 packages/server/src/services/Sales/PaymentReceived/PaymentReceivedBrandingTemplate.ts create mode 100644 packages/server/src/services/Sales/PaymentReceived/utils.ts create mode 100644 packages/server/src/services/Sales/Receipts/SaleReceiptBrandingTemplate.ts create mode 100644 packages/server/src/services/Sales/Receipts/utils.ts diff --git a/packages/server/resources/views/modules/credit-note-standard.pug b/packages/server/resources/views/modules/credit-note-standard.pug index 8f9367a6b..16b1c8834 100644 --- a/packages/server/resources/views/modules/credit-note-standard.pug +++ b/packages/server/resources/views/modules/credit-note-standard.pug @@ -1,81 +1,205 @@ extends ../PaperTemplateLayout.pug block head - style - if (isRtl) - include ../../css/modules/credit-rtl.css - else - include ../../css/modules/credit.css + - var prefix = 'bc' + style. + .#{prefix}-root { + color: #111; + padding: 24px 30px; + font-size: 12px; + position: relative; + box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); + } + .#{prefix}-big-title { + font-size: 60px; + margin: 0; + line-height: 1; + margin-bottom: 25px; + font-weight: 500; + color: #333; + } + .#{prefix}-logo-wrap { + height: 120px; + width: 120px; + position: absolute; + right: 26px; + top: 26px; + overflow: hidden; + } + .#{prefix}-terms-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 24px; + } + .#{prefix}-terms-item { + display: flex; + flex-direction: row; + gap: 12px; + } + .#{prefix}-terms-item__label { + min-width: 120px; + color: #333; + } + .#{prefix}-terms-item__value { + /* Styles for the term value */ + } + + .#{prefix}-group { + box-sizing: border-box; + display: flex; + flex-flow: wrap; + -webkit-box-align: center; + align-items: center; + -webkit-box-pack: start; + justify-content: flex-start; + gap: 10px; + margin-bottom: 24px; + } + + + .#{prefix}-address { + /* Styles for each address block */ + } + + .#{prefix}-table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: inherit; + } + .#{prefix}-table__header { + font-weight: 400; + border-bottom: 1px solid #000; + padding: 2px 10px; + color: #333; + } + .#{prefix}-table__header:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__header:last-of-type{ + padding-right: 0; + } + .#{prefix}-table__header--right { + text-align: right; + } + .#{prefix}-table__cell { + border-bottom: 1px solid #F6F6F6; + padding: 12px 10px; + } + .#{prefix}-table__cell:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__cell:last-of-type { + padding-right: 0; + } + .#{prefix}-table__cell--right { + text-align: right; + } + + .#{prefix}-totals { + display: flex; + flex-direction: column; + margin-left: auto; + width: 300px; + margin-bottom: 24px; + } + .#{prefix}-totals__item { + display: flex; + padding: 4px 0; + } + .#{prefix}-totals__item--border-gray { + border-bottom: 1px solid #DADADA; + } + .#{prefix}-totals__item--border-dark { + border-bottom: 1px solid #000; + } + .#{prefix}-totals__item--font-weight-bold { + font-weight: bold; + } + .#{prefix}-totals__item-label { + min-width: 160px; + } + .#{prefix}-totals__item-amount { + flex: 1 1 auto; + text-align: right; + } + + .#{prefix}-statement { + /* Styles for customer note/terms statement section */ + } + + .#{prefix}-statement__label { + /* Styles for statement label */ + } + + .#{prefix}-statement__value { + /* Styles for statement value */ + } block content - div.credit - div.credit__header - div.paper - h1.title #{__('credit.paper.credit_note')} - if creditNote.creditNoteNumber - span.creditNoteNumber #{creditNote.creditNoteNumber} + div(class=`${prefix}-root`) + div(class=`${prefix}-big-title`) Credit Note - div.organization - h3.title #{organizationName} - if organizationEmail - span.email #{organizationEmail} + if showCompanyLogo + div(class=`${prefix}-logo-wrap`) + img(src=companyLogo alt=`Company Logo`) + + div(class=`${prefix}-terms-list`) + if showCreditNoteNumber + div(class=`${prefix}-terms-item`) + div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}: + div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr} - div.credit__full-amount - div.label #{__('credit.paper.amount')} - div.amount #{creditNote.formattedAmount} + if showCreditNoteDate + div(class=`${prefix}-terms-item`) + div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}: + div(class=`${prefix}-terms-item__value`) #{creditNoteDate} - div.credit__meta - div.credit__meta-item.credit__meta-item--amount - span.label #{__('credit.paper.remaining')} - span.value #{creditNote.formattedCreditsRemaining} + div(class=`${prefix}-group`) + if showBilledFromAddress + div(class=`${prefix}-address`) + strong #{companyName} + each address in billedFromAddress + div #{address} + if showBilledToAddress + div(class=`${prefix}-address`) + strong #{billedToLabel} + each address in billedToAddress + div #{address} - div.credit__meta-item.credit__meta-item--billed-to - span.label #{__("credit.paper.billed_to")} - span.value #{creditNote.customer.displayName} - - div.credit__meta-item.credit__meta-item--credit-date - span.label #{__("credit.paper.credit_date")} - span.value #{creditNote.formattedCreditNoteDate} - - div.credit__table - table - thead - tr - th.item #{__("item_entry.paper.item_name")} - th.rate #{__("item_entry.paper.rate")} - th.quantity #{__("item_entry.paper.quantity")} - th.total #{__("item_entry.paper.total")} + table(class=`${prefix}-table`) + thead(class=`${prefix}-table__header`) + tr + th #{'Item'} + th #{'Description'} + th #{'Rate'} + th #{'Total'} tbody - each entry in creditNote.entries - tr - td.item - div.title=entry.item.name - span.description=entry.description - td.rate=entry.rate - td.quantity=entry.quantity - td.total=entry.amount + each line in lines + tr(class=`${prefix}-table__row`) + td #{line.item} + td #{line.description} + td(class=`${prefix}-table__column--right`) #{line.rate} + td(class=`${prefix}-table__column--right`) #{line.total} - div.credit__table-after - div.credit__table-total - table - tbody - tr.total - td #{__('credit.paper.total')} - td #{creditNote.formattedAmount} - tr.payment-amount - td #{__('credit.paper.credits_used')} - td #{creditNote.formattedCreditsUsed} - tr.blanace-due - td #{__('credit.paper.credits_remaining')} - td #{creditNote.formattedCreditsRemaining} + div(class=`${prefix}-totals`) + if showSubtotal + div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`) + div #{subtotalLabel}: + div #{subtotal} - div.credit__footer - if creditNote.termsConditions - div.credit__conditions - h3 #{__("credit.paper.terms_conditions")} - p #{creditNote.termsConditions} + if showTotal + div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark`) + div #{totalLabel}: + div #{total} - if creditNote.note - div.credit__notes - h3 #{__("credit.paper.notes")} - p #{creditNote.note} \ No newline at end of file + if showCustomerNote + div(class=`${prefix}-statement`) + div(class=`${prefix}-statement__label`) #{customerNoteLabel}: + div(class=`${prefix}-statement__value`) #{customerNote} + + if showTermsConditions + div(class=`${prefix}-statement`) + div(class=`${prefix}-statement__label`) #{termsConditionsLabel}: + div(class=`${prefix}-statement__value`) #{termsConditions} diff --git a/packages/server/resources/views/modules/estimate-regular.pug b/packages/server/resources/views/modules/estimate-regular.pug index 37cf85bfa..393cbceaf 100644 --- a/packages/server/resources/views/modules/estimate-regular.pug +++ b/packages/server/resources/views/modules/estimate-regular.pug @@ -1,82 +1,202 @@ extends ../PaperTemplateLayout.pug -block head - style - if (isRtl) - include ../../css/modules/estimate-rtl.css - else - include ../../css/modules/estimate.css +block head + - var prefix = 'bc' + style. + .#{prefix}-root { + color: #111; + padding: 24px 30px; + font-size: 12px; + position: relative; + box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); + } + .#{prefix}-big-title { + font-size: 60px; + margin: 0; + line-height: 1; + margin-bottom: 25px; + font-weight: 500; + color: #333; + } + .#{prefix}-logo-wrap { + height: 120px; + width: 120px; + position: absolute; + right: 26px; + top: 26px; + overflow: hidden; + } + .#{prefix}-terms-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 24px; + } + .#{prefix}-terms-item { + display: flex; + flex-direction: row; + gap: 12px; + } + .#{prefix}-terms-item__label { + min-width: 120px; + color: #333; + } + .#{prefix}-terms-item__value { + } + .#{prefix}-address-section{ + box-sizing: border-box; + display: flex; + flex-flow: wrap; + -webkit-box-align: center; + align-items: center; + -webkit-box-pack: start; + justify-content: flex-start; + gap: 10px; + margin-bottom: 24px; + } + .#{prefix}-address { + } + .#{prefix}-address__item { + } + .#{prefix}-table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: inherit; + } + .#{prefix}-table__header { + font-weight: 400; + border-bottom: 1px solid #000; + padding: 2px 10px; + color: #333; + } + .#{prefix}-table__header:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__header:last-of-type{ + padding-right: 0; + } + .#{prefix}-table__header--right { + text-align: right; + } + .#{prefix}-table__cell { + border-bottom: 1px solid #F6F6F6; + padding: 12px 10px; + } + .#{prefix}-table__cell--right{ + text-align: right; + } + .#{prefix}-table__cell:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__cell:last-of-type { + padding-right: 0; + } + .#{prefix}-totals { + display: flex; + flex-direction: column; + margin-left: auto; + width: 300px; + margin-bottom: 24px; + } + .#{prefix}-totals__item { + display: flex; + padding: 4px 0; + } + .#{prefix}-totals__item--border-gray { + border-bottom: 1px solid #DADADA; + } + .#{prefix}-totals__item--border-dark { + border-bottom: 1px solid #000; + } + .#{prefix}-totals__item--font-weight-bold { + font-weight: bold; + } + .#{prefix}-totals__item-label { + min-width: 160px; + } + .#{prefix}-totals__item-amount { + flex: 1 1 auto; + text-align: right; + } + .#{prefix}-statement { + } + .#{prefix}-statement__label { + } + .#{prefix}-statement__value { + } block content - div.estimate - div.estimate__header - div.paper - h1.title #{__("estimate.paper.estimate")} - span.email #{saleEstimate.estimateNumber} + div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) + h1(class=`${prefix}-big-title`) Estimate - div.organization - h3.title #{organizationName} - if organizationEmail - span.email #{organizationEmail} + if showCompanyLogo + div(class=`${prefix}-logo-wrap`) + img(alt="", src=companyLogo) - div.estimate__estimate-amount - div.label #{__('estimate.paper.estimate_amount')} - div.amount #{saleEstimate.formattedAmount} + //- Terms List + div(class=`${prefix}-terms`) + if showEstimateNumber + div(class=`${prefix}-terms-item`) + div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel} + div(class=`${prefix}-terms-item__value`) #{estimateNumebr} + if showEstimateDate + div(class=`${prefix}-terms-item`) + div(class=`${prefix}-terms-item__label`) #{estimateDateLabel} + div(class=`${prefix}-terms-item__value`) #{estimateDate} + if showExpirationDate + div(class=`${prefix}-terms-item`) + div(class=`${prefix}-terms-item__label`) #{expirationDateLabel} + div(class=`${prefix}-terms-item__value`) #{expirationDate} - div.estimate__meta - if saleEstimate.estimateNumber - div.estimate__meta-item.estimate__meta-item--estimate-number - span.label #{__("estimate.paper.estimate_number")} - span.value #{saleEstimate.estimateNumber} + //- Addresses (Group section) + div(class=`${prefix}-address-section`) + if showBilledFromAddress + div(class=`${prefix}-address`) + strong #{companyName} + each item in billedFromAddress + div(class=`${prefix}-address__item`) #{item} - div.estimate__meta-item.estimate__meta-item--billed-to - span.label #{__("estimate.paper.billed_to")} - span.value #{saleEstimate.customer.displayName} + if showBilledToAddress + div(class=`${prefix}-address`) + strong #{billedToLabel} + each item in billedToAddress + div(class=`${prefix}-address__item`) #{item} - div.estimate__meta-item.estimate__meta-item--estimate-date - span.label #{__("estimate.paper.estimate_date")} - span.value #{saleEstimate.formattedEstimateDate} - - div.estimate__meta-item.estimate__meta-item--due-date - span.label #{__("estimate.paper.expiration_date")} - span.value #{saleEstimate.formattedExpirationDate} - - div.estimate__table - table - thead - tr - th.item #{__("item_entry.paper.item_name")} - th.rate #{__("item_entry.paper.rate")} - th.quantity #{__("item_entry.paper.quantity")} - th.total #{__("item_entry.paper.total")} + //- Table section (Line items) + table(class=`${prefix}-table`) + thead + tr + th(class=`${prefix}-table__header`) Item + th(class=`${prefix}-table__header`) Description + th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate + th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total tbody - each entry in saleEstimate.entries - tr - td.item - div.title=entry.item.name - span.description=entry.description - td.rate=entry.rate - td.quantity=entry.quantity - td.total=entry.amount + each line in lines + tr + td(class=`${prefix}-table__cell`) #{line.item} + td(class=`${prefix}-table__cell`) #{line.description} + td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate} + td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total} - div.estimate__table-after - div.estimate__table-total - table - tbody - tr.subtotal - td #{__('estimate.paper.subtotal')} - td #{saleEstimate.formattedAmount} - tr.total - td #{__('estimate.paper.total')} - td #{saleEstimate.formattedAmount} + //- Totals section + div(class=`${prefix}-totals`) + if showSubtotal + div(class=`${prefix}-totals__item`) + div(class=`${prefix}-totals__item-label`) #{subtotalLabel} + div(class=`${prefix}-totals__item-amount`) #{subtotal} + if showTotal + div(class=`${prefix}-totals__item`) + div(class=`${prefix}-totals__item-label`) #{totalLabel} + div(class=`${prefix}-totals__item-amount`) #{total} - div.estimate__footer - if saleEstimate.termsConditions - div.estimate__conditions - h3 #{__("estimate.paper.conditions_title")} - p #{saleEstimate.termsConditions} + //- Statements section + if showCustomerNote + div(class=`${prefix}-statement`) + div(class=`${prefix}-statement__label`) #{customerNoteLabel} + div(class=`${prefix}-statement__value`) #{customerNote} - if saleEstimate.note - div.estimate__notes - h3 #{__("estimate.paper.notes_title")} - p #{saleEstimate.note} \ No newline at end of file + if showTermsConditions + div(class=`${prefix}-statement`) + div(class=`${prefix}-statement__label`) #{termsConditionsLabel} + div(class=`${prefix}-statement__value`) #{termsConditions} \ No newline at end of file diff --git a/packages/server/resources/views/modules/invoice-standard.pug b/packages/server/resources/views/modules/invoice-standard.pug index d5b172a8d..006e33569 100644 --- a/packages/server/resources/views/modules/invoice-standard.pug +++ b/packages/server/resources/views/modules/invoice-standard.pug @@ -4,7 +4,6 @@ block head - var prefix = 'bc' style. .#{prefix}-root { - background-color: #fff; color: #111; padding: 24px 30px; font-size: 12px; @@ -25,7 +24,6 @@ block head position: absolute; right: 26px; top: 26px; - border-radius: 5px; overflow: hidden; } .#{prefix}-details { @@ -122,7 +120,6 @@ block head } .#{prefix}-totals__item--font-weight-bold { font-weight: bold; - /* Additional styles for total items with bold font weight */ } .#{prefix}-totals__item-label { min-width: 160px; @@ -143,6 +140,7 @@ block head block content //- block head div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) + //- Title and company logo h1(class=`${prefix}-big-title`) Invoice diff --git a/packages/server/resources/views/modules/payment-receive-standard.pug b/packages/server/resources/views/modules/payment-receive-standard.pug index ee415b768..2c6e417cd 100644 --- a/packages/server/resources/views/modules/payment-receive-standard.pug +++ b/packages/server/resources/views/modules/payment-receive-standard.pug @@ -1,67 +1,187 @@ extends ../PaperTemplateLayout.pug block head - style - if (isRtl) - include ../../css/modules/payment-rtl.css - else - include ../../css/modules/payment.css + - var prefix = 'bp3'; + + style. + .#{prefix}-root{ + color: #111; + padding: 24px 30px; + font-size: 12px; + position: relative; + box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); + } + .#{prefix}-big-title{ + font-size: 60px; + margin: 0; + line-height: 1; + margin-bottom: 25px; + font-weight: 500; + color: #333; + } + .#{prefix}-logo-wrap{ + height: 120px; + width: 120px; + position: absolute; + right: 26px; + top: 26px; + overflow: hidden; + } + .#{prefix}-terms-list{ + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 24px; + } + .#{prefix}-terms-item{ + display: flex; + flex-direction: row; + gap: 12px; + } + .#{prefix}-terms-item__label{ + min-width: 120px; + color: #333; + } + .#{prefix}-group{ + } + .#{prefix}-addresses{ + box-sizing: border-box; + display: flex; + flex-flow: wrap; + -webkit-box-align: center; + align-items: center; + -webkit-box-pack: start; + justify-content: flex-start; + gap: 10px; + margin-bottom: 24px; + } + .#{prefix}-addresses > * { + flex: 1 1; + } + .#{prefix}-address__label{ + + } + .#{prefix}-table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: inherit; + } + .#{prefix}-table__header { + font-weight: 400; + border-bottom: 1px solid #000; + padding: 2px 10px; + color: #333; + } + .#{prefix}-table__header:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__header:last-of-type{ + padding-right: 0; + } + .#{prefix}-table__header--right { + text-align: right; + } + .#{prefix}-table__cell { + border-bottom: 1px solid #F6F6F6; + padding: 12px 10px; + } + .#{prefix}-table__cell:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__cell:last-of-type { + padding-right: 0; + } + .#{prefix}-table__cell--right { + text-align: right; + } + .#{prefix}-table__column{ + + } + .#{prefix}-table__column--right{ + + } + .#{prefix}-totals { + display: flex; + flex-direction: column; + margin-left: auto; + width: 300px; + margin-bottom: 24px; + } + .#{prefix}-totals__item { + display: flex; + padding: 4px 0; + } + .#{prefix}-totals__item--gray-border { + border-bottom: 1px solid #DADADA; + } + .#{prefix}-totals__item--dark-border { + border-bottom: 1px solid #000; + } + .#{prefix}-totals__item--bold { + font-weight: bold; + } + .#{prefix}-totals__item-label { + min-width: 160px; + } + .#{prefix}-totals__item-amount { + flex: 1 1 auto; + text-align: right; + } block content - div.payment - div.payment__header - div.paper - h1.title #{__("payment.paper.payment_receipt")} - if paymentReceive.paymentReceiveNo - span.paymentNumber #{paymentReceive.paymentReceiveNo} + div(class=`${prefix}-root`) + div(class=`${prefix}-big-title`) Payment - div.organization - h3.title #{organizationName} - if organizationEmail - span.email #{organizationEmail} + if showCompanyLogo + div(class=`${prefix}-logo-wrap`) + img(src=companyLogo alt="Company Logo") + + div(class=`${prefix}-terms-list`) + if showPaymentReceivedNumber + div(class=`${prefix}-terms-item`) + div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel} + div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr} - div.payment__received-amount - div.label #{__('payment.paper.amount_received')} - div.amount #{paymentReceive.formattedAmount} + if showPaymentReceivedDate + div(class=`${prefix}-terms-item`) + div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel} + div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate} + + div(class=`${prefix}-addresses`) + if showBilledFromAddress + div(class=`${prefix}-address`) + strong(class=`${prefix}-address__item`) #{companyName} + each addressLine in billedFromAddress + div(class=`${prefix}-address__item`) #{addressLine} - div.payment__meta - div.payment__meta-item.payment__meta-item--billed-to - span.label #{__("payment.paper.billed_to")} - span.value #{paymentReceive.customer.displayName} + if showBillingToAddress + div(class=`${prefix}-address`) + strong(class=`${prefix}-address__item`) #{billedToLabel} + each addressLine in billedToAddress + div(class=`${prefix}-address__item`) #{addressLine} - div.payment__meta-item.payment__meta-item--payment-date - span.label #{__("payment.paper.payment_date")} - span.value #{paymentReceive.formattedPaymentDate} + table(class=`${prefix}-table`) + thead + tr + th(class=`${prefix}-table__header`) Invoice # + th(class=`${prefix}-table__header ${prefix}-table__header--right`) Invoice Amount + th(class=`${prefix}-table__header ${prefix}-table__header--right`) Paid Amount - div.payment__table - table - thead - tr - th.item #{__("payment.paper.invoice_number")} - th.date #{__("payment.paper.invoice_date")} - th.invoiceAmount #{__("payment.paper.invoice_amount")} - th.paymentAmount #{__("payment.paper.payment_amount")} - tbody - each entry in paymentReceive.entries - tr - td.item=entry.invoice.invoiceNo - td.date=entry.invoice.invoiceDateFormatted - td.invoiceAmount=entry.invoice.totalFormatted - td.paymentAmount=entry.invoice.paymentAmountFormatted + tbody + each line in lines + tr + td(class=`${prefix}-table__cell`) #{line.invoiceNumber} + td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.invoiceAmount} + td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.paidAmount} - div.payment__table-after - div.payment__table-total - table - tbody - tr.payment-amount - td #{__('payment.paper.payment_amount')} - td #{paymentReceive.formattedAmount} - tr.blanace-due - td #{__('payment.paper.balance_due')} - td #{paymentReceive.customer.closingBalance} + div(class=`${prefix}-totals`) + if showSubtotal + div(class=`${prefix}-totals__item`) + div(class=`${prefix}-totals__item-label`) #{subtotalLabel} + div(class=`${prefix}-totals__item-amount`) #{subtotal} - div.payment__footer - if paymentReceive.statement - div.payment__notes - h3 #{__("payment.paper.statement")} - p #{paymentReceive.statement} \ No newline at end of file + if showTotal + div(class=`${prefix}-totals__item`) + div(class=`${prefix}-totals__item-label`) #{totalLabel} + div(class=`${prefix}-totals__item-amount`) #{total} diff --git a/packages/server/resources/views/modules/receipt-regular.pug b/packages/server/resources/views/modules/receipt-regular.pug index a0cbb1f61..f9ccd1a72 100644 --- a/packages/server/resources/views/modules/receipt-regular.pug +++ b/packages/server/resources/views/modules/receipt-regular.pug @@ -1,77 +1,190 @@ extends ../PaperTemplateLayout.pug block head - style - if (isRtl) - include ../../css/modules/receipt-rtl.css - else - include ../../css/modules/receipt.css + - var prefix = 'bc' + style. + .#{prefix}-root { + color: #111; + padding: 24px 30px; + font-size: 12px; + position: relative; + box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); + } + .#{prefix}-logo-wrap { + font-size: 60px; + margin: 0; + line-height: 1; + margin-bottom: 25px; + font-weight: 500; + color: #333; + } + .#{prefix}-big-title { + height: 120px; + width: 120px; + position: absolute; + right: 26px; + top: 26px; + overflow: hidden; + } + .#{prefix}-terms-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 24px; + } + .#{prefix}-terms-item { + display: flex; + flex-direction: row; + gap: 12px; + } + .#{prefix}-terms-item__label { + min-width: 120px; + color: #333; + } + .#{prefix}-terms-item__value {} + .#{prefix}-address-section { + box-sizing: border-box; + display: flex; + flex-flow: wrap; + -webkit-box-align: center; + align-items: center; + -webkit-box-pack: start; + justify-content: flex-start; + gap: 10px; + margin-bottom: 24px; + } + .#{prefix}-address {} + .#{prefix}-table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: inherit; + } + .#{prefix}-table__header { + font-weight: 400; + border-bottom: 1px solid #000; + padding: 2px 10px; + color: #333; + } + .#{prefix}-table__header:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__header:last-of-type{ + padding-right: 0; + } + .#{prefix}-table__header--right { + text-align: right; + } + .#{prefix}-table__cell { + border-bottom: 1px solid #F6F6F6; + padding: 12px 10px; + } + .#{prefix}-table__cell:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__cell:last-of-type { + padding-right: 0; + } + .#{prefix}-table__cell--right { + text-align: right; + } + .#{prefix}-totals { + display: flex; + flex-direction: column; + margin-left: auto; + width: 300px; + margin-bottom: 24px; + } + .#{prefix}-totals__line { + display: flex; + padding: 4px 0; + } + .#{prefix}-totals__line--gray-border {} + .#{prefix}-totals__line--dark-border {} + .#{prefix}-totals__line__label { + min-width: 160px; + } + .#{prefix}-totals__line__amount { + flex: 1 1 auto; + text-align: right; + } + .#{prefix}-statement { + margin-bottom: 20px; + } + .#{prefix}-statement__label {} + .#{prefix}-statement__value {} block content - div.receipt - div.receipt__header - div.paper - h1.title #{__("receipt.paper.receipt")} - span.receiptNumber #{saleReceipt.receiptNumber} + //- block head + div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) + + //- Title and company logo + h1(class=`${prefix}-big-title`) Receipt - div.organization - h3.title #{organizationName} + if showCompanyLogo + div(class=`${prefix}-logo-wrap`) + img(src=companyLogo alt=`Company Logo`) - div.receipt__receipt-amount - div.label #{__('receipt.paper.receipt_amount')} - div.amount #{saleReceipt.formattedAmount} + //- Terms List + div(class=`${prefix}-terms-list`) + if showReceiptNumber + div(class=`${prefix}-terms-item`) + span(class=`${prefix}-terms-item__label`)= receiptNumberLabel + span(class=`${prefix}-terms-item__value`)= receiptNumber + if showReceiptDate + div(class=`${prefix}-terms-item`) + span(class=`${prefix}-terms-item__label`)= receiptDateLabel + span(class=`${prefix}-terms-item__value`)= receiptDate - div.receipt__meta - div.receipt__meta-item.receipt__meta-item--billed-to - span.label #{__("receipt.paper.billed_to")} - span.value #{saleReceipt.customer.displayName} + //- Address Section + div(class=`${prefix}-address-section`) + if showBilledFromAddress + div(class=`${prefix}-address`) + strong= companyName + each addressLine in billedFromAddress + div= addressLine - div.receipt__meta-item.receipt__meta-item--invoice-date - span.label #{__("receipt.paper.receipt_date")} - span.value #{saleReceipt.formattedReceiptDate} + if showBilledToAddress + div(class=`${prefix}-address`) + strong= billedToLabel + each addressLine in billedToAddress + div= addressLine - if saleReceipt.receiptNumber - div.receipt__meta-item.receipt__meta-item--invoice-number - span.label #{__("receipt.paper.receipt_number")} - span.value #{saleReceipt.receiptNumber} + //- Table Section + table(class=`${prefix}-table`) + thead(class=`${prefix}-table__header`) + tr + th(class=`${prefix}-table__header`) Item + th(class=`${prefix}-table__header`) Description + th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate + th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total + tbody + each line in lines + tr(class=`${prefix}-table__row`) + td(class=`${prefix}-table__column`)= line.item + td(class=`${prefix}-table__column`)= line.description + td(class=`${prefix}-table__column ${prefix}-table__column--right`)= line.rate + td(class=`${prefix}-table__column ${prefix}-table__column--right`)= line.total - div.receipt__table - table - thead - tr - th.item #{__("item_entry.paper.item_name")} - th.rate #{__("item_entry.paper.rate")} - th.quantity #{__("item_entry.paper.quantity")} - th.total #{__("item_entry.paper.total")} - tbody - each entry in saleReceipt.entries - tr - td.item=entry.item.name - td.rate=entry.rate - td.quantity=entry.quantity - td.total=entry.amount - - div.receipt__table-after - div.receipt__table-total - table - tbody - tr.total - td #{__('receipt.paper.total')} - td #{saleReceipt.formattedAmount} - tr.payment-amount - td #{__('receipt.paper.payment_amount')} - td #{saleReceipt.formattedAmount} - tr.blanace-due - td #{__('receipt.paper.balance_due')} - td #{'$0'} + //- Totals Section + div(class=`${prefix}-totals`) + if showSubtotal + div(class=`${prefix}-totals__line #{prefix}-totals__line--gray-border`) + span(class=`${prefix}-totals__line__label`)= subtotalLabel + span(class=`${prefix}-totals__line__amount`)= subtotal + 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 - div.receipt__footer - if saleReceipt.statement - div.receipt__conditions - h3 #{__("receipt.paper.statement")} - p #{saleReceipt.statement} + //- Customer Note Section + if showCustomerNote + div(class=`${prefix}-statement`) + span(class=`${prefix}-statement__label`)= customerNoteLabel + p(class=`${prefix}-statement__value`)= customerNote - if saleReceipt.receiptMessage - div.receipt__notes - h3 #{__("receipt.paper.notes")} - p #{saleReceipt.receiptMessage} \ No newline at end of file + //- Terms & Conditions Section + if showTermsConditions + div(class=`${prefix}-statement`) + span(class=`${prefix}-statement__label`)= termsConditionsLabel + p(class=`${prefix}-statement__value`)= termsConditions diff --git a/packages/server/src/interfaces/CreditNote.ts b/packages/server/src/interfaces/CreditNote.ts index 22ff521d7..136425818 100644 --- a/packages/server/src/interfaces/CreditNote.ts +++ b/packages/server/src/interfaces/CreditNote.ts @@ -258,3 +258,49 @@ export type ICreditNoteGLCommonEntry = Pick< | 'debit' | 'branchId' >; + +export interface CreditNotePdfTemplateAttributes { + primaryColor: string; + secondaryColor: string; + showCompanyLogo: boolean; + companyLogo: string; + companyName: string; + + billedToAddress: string[]; + billedFromAddress: string[]; + showBilledToAddress: boolean; + showBilledFromAddress: boolean; + billedToLabel: string; + + total: string; + totalLabel: string; + showTotal: boolean; + + subtotal: string; + subtotalLabel: string; + showSubtotal: boolean; + + showCustomerNote: boolean; + customerNote: string; + customerNoteLabel: string; + + showTermsConditions: boolean; + termsConditions: string; + termsConditionsLabel: string; + + lines: Array<{ + item: string; + description: string; + rate: string; + quantity: string; + total: string; + }>; + + showCreditNoteNumber: boolean; + creditNoteNumberLabel: string; + creditNoteNumebr: string; + + creditNoteDate: string; + showCreditNoteDate: boolean; + creditNoteDateLabel: string; +} diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts index ea90e58af..fbc24ba43 100644 --- a/packages/server/src/interfaces/PaymentReceive.ts +++ b/packages/server/src/interfaces/PaymentReceive.ts @@ -25,6 +25,7 @@ export interface IPaymentReceived { updatedAt: Date; localAmount?: number; branchId?: number; + pdfTemplateId?: number; } export interface IPaymentReceivedCreateDTO { customerId: number; @@ -185,3 +186,70 @@ export interface PaymentReceiveMailPresendEvent { paymentReceiveId: number; messageOptions: PaymentReceiveMailOptsDTO; } + +export interface PaymentReceivedPdfLineItem { + item: string; + description: string; + rate: string; + quantity: string; + total: string; +} + +export interface PaymentReceivedPdfTax { + label: string; + amount: string; +} + +export interface PaymentReceivedPdfTemplateAttributes { + primaryColor: string; + secondaryColor: string; + companyName: string; + + showCompanyLogo: boolean; + companyLogo: string; + + dueDateLabel: string; + showDueDate: boolean; + + dateIssueLabel: string; + showDateIssue: boolean; + + invoiceNumberLabel: string; + showInvoiceNumber: boolean; + + showBillingToAddress: boolean; + showBilledFromAddress: boolean; + billedToLabel: string; + + lineItemLabel: string; + lineDescriptionLabel: string; + lineRateLabel: string; + lineTotalLabel: string; + + totalLabel: string; + subtotalLabel: string; + discountLabel: string; + paymentMadeLabel: string; + balanceDueLabel: string; + + showTotal: boolean; + showSubtotal: boolean; + showDiscount: boolean; + showTaxes: boolean; + showPaymentMade: boolean; + showDueAmount: boolean; + showBalanceDue: boolean; + + discount: string; + + termsConditionsLabel: string; + showTermsConditions: boolean; + + lines: PaymentReceivedPdfLineItem[]; + taxes: PaymentReceivedPdfTax[]; + + statementLabel: string; + showStatement: boolean; + billedToAddress: string[]; + billedFromAddress: string[]; +} diff --git a/packages/server/src/interfaces/SaleEstimate.ts b/packages/server/src/interfaces/SaleEstimate.ts index bc6231fc4..f53693b52 100644 --- a/packages/server/src/interfaces/SaleEstimate.ts +++ b/packages/server/src/interfaces/SaleEstimate.ts @@ -143,3 +143,4 @@ export interface ISaleEstimateMailPresendEvent { saleEstimateId: number; messageOptions: SaleEstimateMailOptionsDTO; } + diff --git a/packages/server/src/services/CreditNotes/CreateCreditNote.ts b/packages/server/src/services/CreditNotes/CreateCreditNote.ts index c47587fbe..d48a1371f 100644 --- a/packages/server/src/services/CreditNotes/CreateCreditNote.ts +++ b/packages/server/src/services/CreditNotes/CreateCreditNote.ts @@ -59,7 +59,7 @@ export default class CreateCreditNote extends BaseCreditNotes { creditNoteDTO.entries ); // Transformes the given DTO to storage layer data. - const creditNoteModel = this.transformCreateEditDTOToModel( + const creditNoteModel = await this.transformCreateEditDTOToModel( tenantId, creditNoteDTO, customer.currencyCode diff --git a/packages/server/src/services/CreditNotes/CreditNoteBrandingTemplate.ts b/packages/server/src/services/CreditNotes/CreditNoteBrandingTemplate.ts new file mode 100644 index 000000000..1b35dc29b --- /dev/null +++ b/packages/server/src/services/CreditNotes/CreditNoteBrandingTemplate.ts @@ -0,0 +1,30 @@ +import { Inject } from "typedi"; +import { GetPdfTemplate } from "../PdfTemplate/GetPdfTemplate"; +import { defaultCreditNoteBrandingAttributes } from "./constants"; +import { mergePdfTemplateWithDefaultAttributes } from "../Sales/Invoices/utils"; + +export class CreditNoteBrandingTemplate { + @Inject() + private getPdfTemplateService: GetPdfTemplate; + + /** + * Retrieves the credit note branding template. + * @param {number} tenantId + * @param {number} templateId + * @returns {} + */ + public async getCreditNoteBrandingTemplate(tenantId: number, templateId: number) { + const template = await this.getPdfTemplateService.getPdfTemplate( + tenantId, + templateId + ); + const attributes = mergePdfTemplateWithDefaultAttributes( + template.attributes, + defaultCreditNoteBrandingAttributes + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server/src/services/CreditNotes/CreditNotes.ts b/packages/server/src/services/CreditNotes/CreditNotes.ts index 414be0e2a..5f3a57a0d 100644 --- a/packages/server/src/services/CreditNotes/CreditNotes.ts +++ b/packages/server/src/services/CreditNotes/CreditNotes.ts @@ -2,6 +2,7 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; import { omit } from 'lodash'; import * as R from 'ramda'; +import composeAsync from 'async/compose'; import { ServiceError } from '@/exceptions'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { ERRORS } from './constants'; @@ -16,6 +17,7 @@ import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersServ import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { assocItemEntriesDefaultIndex } from '../Items/utils'; +import { BrandingTemplateDTOTransformer } from '../PdfTemplate/BrandingTemplateDTOTransformer'; @Service() export default class BaseCreditNotes { @@ -34,17 +36,20 @@ export default class BaseCreditNotes { @Inject() private warehouseDTOTransform: WarehouseTransactionDTOTransform; + @Inject() + private brandingTemplatesTransformer: BrandingTemplateDTOTransformer; + /** * Transformes the credit/edit DTO to model. * @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO * @param {string} customerCurrencyCode - */ - protected transformCreateEditDTOToModel = ( + protected transformCreateEditDTOToModel = async ( tenantId: number, creditNoteDTO: ICreditNoteNewDTO | ICreditNoteEditDTO, customerCurrencyCode: string, oldCreditNote?: ICreditNote - ): ICreditNote => { + ): Promise => { // Retrieve the total amount of the given items entries. const amount = this.itemsEntriesService.getTotalItemsEntries( creditNoteDTO.entries @@ -83,10 +88,18 @@ export default class BaseCreditNotes { refundedAmount: 0, invoicesAmount: 0, }; + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + tenantId, + 'CreditNote' + ) + )(initialDTO); + return R.compose( this.branchDTOTransform.transformDTO(tenantId), this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); + )(initialAsyncDTO); }; /** diff --git a/packages/server/src/services/CreditNotes/EditCreditNote.ts b/packages/server/src/services/CreditNotes/EditCreditNote.ts index 0e045227d..cbeed9ab2 100644 --- a/packages/server/src/services/CreditNotes/EditCreditNote.ts +++ b/packages/server/src/services/CreditNotes/EditCreditNote.ts @@ -63,7 +63,7 @@ export default class EditCreditNote extends BaseCreditNotes { creditNoteEditDTO.entries ); // Transformes the given DTO to storage layer data. - const creditNoteModel = this.transformCreateEditDTOToModel( + const creditNoteModel = await this.transformCreateEditDTOToModel( tenantId, creditNoteEditDTO, customer.currencyCode, diff --git a/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts b/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts index c54c8096e..5a1ea0faf 100644 --- a/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts +++ b/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts @@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable'; import GetCreditNote from './GetCreditNote'; +import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate'; +import { CreditNotePdfTemplateAttributes } from '@/interfaces'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { transformCreditNoteToPdfTemplate } from './utils'; @Service() export default class GetCreditNotePdf { + @Inject() + private tenancy: HasTenancyService; + @Inject() private chromiumlyTenancy: ChromiumlyTenancy; @@ -14,25 +21,62 @@ export default class GetCreditNotePdf { @Inject() private getCreditNoteService: GetCreditNote; + @Inject() + private creditNoteBrandingTemplate: CreditNoteBrandingTemplate; + /** - * Retrieve sale invoice pdf content. + * Retrieves sale invoice pdf content. * @param {number} tenantId - Tenant id. * @param {number} creditNoteId - Credit note id. */ public async getCreditNotePdf(tenantId: number, creditNoteId: number) { + const brandingAttributes = await this.getCreditNoteBrandingAttributes( + tenantId, + creditNoteId + ); + console.log(brandingAttributes, 'brandingAttributes'); + + const htmlContent = await this.templateInjectable.render( + tenantId, + 'modules/credit-note-standard', + brandingAttributes + ); + return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); + } + + /** + * Retrieves credit note branding attributes. + * @param {number} tenantId - The ID of the tenant. + * @param {number} creditNoteId - The ID of the credit note. + * @returns {Promise} The credit note branding attributes. + */ + public async getCreditNoteBrandingAttributes( + tenantId: number, + creditNoteId: number + ): Promise { + const { PdfTemplate } = this.tenancy.models(tenantId); const creditNote = await this.getCreditNoteService.getCreditNote( tenantId, creditNoteId ); - const htmlContent = await this.templateInjectable.render( - tenantId, - 'modules/credit-note-standard', - { - creditNote, - } - ); - return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, { - margins: { top: 0, bottom: 0, left: 0, right: 0 }, - }); + // Retrieve the invoice template id of not found get the default template id. + const templateId = + creditNote.pdfTemplateId ?? + ( + await PdfTemplate.query().findOne({ + resource: 'CreditNote', + default: true, + }) + )?.id; + // Retrieves the credit note branding template. + const brandingTemplate = + await this.creditNoteBrandingTemplate.getCreditNoteBrandingTemplate( + tenantId, + templateId + ); + return { + ...brandingTemplate.attributes, + ...transformCreditNoteToPdfTemplate(creditNote), + }; } } diff --git a/packages/server/src/services/CreditNotes/constants.ts b/packages/server/src/services/CreditNotes/constants.ts index 9d0060075..9691a4b77 100644 --- a/packages/server/src/services/CreditNotes/constants.ts +++ b/packages/server/src/services/CreditNotes/constants.ts @@ -9,7 +9,7 @@ export const ERRORS = { 'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND', CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS', CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES', - CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES' + CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES', }; export const DEFAULT_VIEW_COLUMNS = []; @@ -66,3 +66,72 @@ export const DEFAULT_VIEWS = [ columns: DEFAULT_VIEW_COLUMNS, }, ]; + +export const defaultCreditNoteBrandingAttributes = { + primaryColor: '', + secondaryColor: '', + showCompanyLogo: true, + companyLogo: '', + companyName: 'Bigcapital Technology, Inc.', + + // Address + billedToAddress: [ + 'Bigcapital Technology, Inc.', + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + billedFromAddress: [ + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + showBilledToAddress: true, + showBilledFromAddress: true, + billedToLabel: 'Billed To', + + // Total + total: '$1000.00', + totalLabel: 'Total', + showTotal: true, + + // Subtotal + subtotal: '1000/00', + subtotalLabel: 'Subtotal', + showSubtotal: true, + + // Customer note + showCustomerNote: true, + customerNote: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + customerNoteLabel: 'Customer Note', + + // Terms & conditions + showTermsConditions: true, + termsConditions: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + termsConditionsLabel: 'Terms & Conditions', + + lines: [ + { + item: 'Simply dummy text', + description: 'Simply dummy text of the printing and typesetting', + rate: '1', + quantity: '1000', + total: '$1000.00', + }, + ], + // Credit note number. + showCreditNoteNumber: true, + creditNoteNumberLabel: 'Credit Note Number', + creditNoteNumebr: '346D3D40-0001', + + // Credit note date. + creditNoteDate: 'September 3, 2024', + showCreditNoteDate: true, + creditNoteDateLabel: 'Credit Note Date', +}; diff --git a/packages/server/src/services/CreditNotes/utils.ts b/packages/server/src/services/CreditNotes/utils.ts new file mode 100644 index 000000000..e81de43f2 --- /dev/null +++ b/packages/server/src/services/CreditNotes/utils.ts @@ -0,0 +1,9 @@ +import { CreditNotePdfTemplateAttributes } from "@/interfaces"; +import CreditNote from "@/models/CreditNote"; + + +export const transformCreditNoteToPdfTemplate = (creditNote: CreditNote): Partial { + return { + + }; +} \ No newline at end of file diff --git a/packages/server/src/services/PdfTemplate/BrandingTemplateDTOTransformer.ts b/packages/server/src/services/PdfTemplate/BrandingTemplateDTOTransformer.ts index 379f097ca..88aea05fb 100644 --- a/packages/server/src/services/PdfTemplate/BrandingTemplateDTOTransformer.ts +++ b/packages/server/src/services/PdfTemplate/BrandingTemplateDTOTransformer.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import HasTenancyService from '../Tenancy/TenancyService'; import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; @Service() export class BrandingTemplateDTOTransformer { @@ -9,31 +10,28 @@ export class BrandingTemplateDTOTransformer { /** * Associates the default branding template id. - * @param {number} tenantId - * @param {string} resource - * @param {Record} object - * @param {string} attributeName - * @returns + * @param {number} tenantId + * @param {string} resource + * @param {Record} object + * @param {string} attributeName + * @returns */ - public assocDefaultBrandingTemplate = ( - tenantId: number, - resource: string, - ) => async (object: Record) => { - const { PdfTemplate } = this.tenancy.models(tenantId); - const attributeName = 'pdfTemplateId'; + public assocDefaultBrandingTemplate = + (tenantId: number, resource: string) => + async (object: Record) => { + const { PdfTemplate } = this.tenancy.models(tenantId); + const attributeName = 'pdfTemplateId'; - const defaultTemplate = await PdfTemplate.query().findOne({ - resource, - default: true, - }); - console.log(defaultTemplate); - - if (!defaultTemplate) { - return object; - } - return { - ...object, - [attributeName]: defaultTemplate.id, + const defaultTemplate = await PdfTemplate.query().findOne({ + resource, + default: true, + }); + if (!defaultTemplate || !isEmpty(object[attributeName])) { + return object; + } + return { + ...object, + [attributeName]: defaultTemplate.id, + }; }; - }, } diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts b/packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts index 80879af7f..32b1245bd 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimateDTOTransformer.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import { Inject, Service } from 'typedi'; import { omit, sumBy } from 'lodash'; +import composeAsync from 'async/compose'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces'; import { SaleEstimateValidators } from './SaleEstimateValidators'; @@ -10,6 +11,7 @@ import { formatDateFields } from '@/utils'; import moment from 'moment'; import { SaleEstimateIncrement } from './SaleEstimateIncrement'; import { assocItemEntriesDefaultIndex } from '@/services/Items/utils'; +import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer'; @Service() export class SaleEstimateDTOTransformer { @@ -28,6 +30,9 @@ export class SaleEstimateDTOTransformer { @Inject() private estimateIncrement: SaleEstimateIncrement; + @Inject() + private brandingTemplatesTransformer: BrandingTemplateDTOTransformer; + /** * Transform create DTO object ot model object. * @param {number} tenantId @@ -81,10 +86,18 @@ export class SaleEstimateDTOTransformer { deliveredAt: moment().toMySqlDateTime(), }), }; + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + tenantId, + 'SaleEstimate' + ) + )(initialDTO); + return R.compose( this.branchDTOTransform.transformDTO(tenantId), this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); + )(initialAsyncDTO); } /** diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts index af1d2098c..ef89fc919 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts @@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { GetSaleEstimate } from './GetSaleEstimate'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate'; +import { transformEstimateToPdfTemplate } from './utils'; +import { EstimatePdfBrandingAttributes } from './constants'; @Service() export class SaleEstimatesPdf { + @Inject() + private tenancy: HasTenancyService; + @Inject() private chromiumlyTenancy: ChromiumlyTenancy; @@ -14,25 +21,58 @@ export class SaleEstimatesPdf { @Inject() private getSaleEstimate: GetSaleEstimate; + @Inject() + private estimatePdfTemplate: SaleEstimatePdfTemplate; + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - * @param {ISaleInvoice} saleInvoice - */ public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { - const saleEstimate = await this.getSaleEstimate.getEstimate( + const brandingAttributes = await this.getEstimateBrandingAttributes( tenantId, saleEstimateId ); const htmlContent = await this.templateInjectable.render( tenantId, 'modules/estimate-regular', - { - saleEstimate, - } + brandingAttributes ); - return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, { - margins: { top: 0, bottom: 0, left: 0, right: 0 }, - }); + return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); + } + + /** + * + * @param {number} tenantId + * @param {number} estimateId + */ + async getEstimateBrandingAttributes( + tenantId: number, + estimateId: number + ): Promise { + const { PdfTemplate } = this.tenancy.models(tenantId); + const saleEstimate = await this.getSaleEstimate.getEstimate( + tenantId, + estimateId + ); + // Retrieve the invoice template id of not found get the default template id. + const templateId = + saleEstimate.pdfTemplateId ?? + ( + await PdfTemplate.query().findOne({ + resource: 'SaleEstimate', + default: true, + }) + )?.id; + const brandingTemplate = + await this.estimatePdfTemplate.getEstimatePdfTemplate( + tenantId, + templateId + ); + return { + ...brandingTemplate.attributes, + ...transformEstimateToPdfTemplate(saleEstimate), + }; } } diff --git a/packages/server/src/services/Sales/Estimates/constants.ts b/packages/server/src/services/Sales/Estimates/constants.ts index 1870c01d2..a5f9a9520 100644 --- a/packages/server/src/services/Sales/Estimates/constants.ts +++ b/packages/server/src/services/Sales/Estimates/constants.ts @@ -173,3 +173,122 @@ export const SaleEstimatesSampleData = [ 'Line Description': 'Qui suscipit ducimus qui qui.', }, ]; + +export const defaultEstimatePdfBrandingAttributes = { + primaryColor: '#000', + secondaryColor: '#000', + showCompanyLogo: true, + companyLogo: '', + companyName: '', + + billedToAddress: [ + 'Bigcapital Technology, Inc.', + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + billedFromAddress: [ + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + showBilledFromAddress: true, + showBilledToAddress: true, + billedToLabel: 'Billed To', + + total: '$1000.00', + totalLabel: 'Total', + showTotal: true, + + subtotal: '1000/00', + subtotalLabel: 'Subtotal', + showSubtotal: true, + + showCustomerNote: true, + customerNote: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + customerNoteLabel: 'Customer Note', + + showTermsConditions: true, + termsConditions: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + termsConditionsLabel: 'Terms & Conditions', + + lines: [ + { + item: 'Simply dummy text', + description: 'Simply dummy text of the printing and typesetting', + rate: '1', + quantity: '1000', + total: '$1000.00', + }, + ], + showEstimateNumber: true, + estimateNumberLabel: 'Estimate Number', + estimateNumebr: '346D3D40-0001', + + estimateDate: 'September 3, 2024', + showEstimateDate: true, + estimateDateLabel: 'Estimate Date', + + expirationDateLabel: 'Expiration Date', + showExpirationDate: true, + expirationDate: 'September 3, 2024', +}; + + +interface EstimatePdfBrandingLineItem { + item: string; + description: string; + rate: string; + quantity: string; + total: string; +} + +export interface EstimatePdfBrandingAttributes { + primaryColor: string; + secondaryColor: string; + showCompanyLogo: boolean; + companyLogo: string; + companyName: string; + + billedToAddress: string[]; + billedFromAddress: string[]; + showBilledFromAddress: boolean; + showBilledToAddress: boolean; + billedToLabel: string; + + total: string; + totalLabel: string; + showTotal: boolean; + + subtotal: string; + subtotalLabel: string; + showSubtotal: boolean; + + showCustomerNote: boolean; + customerNote: string; + customerNoteLabel: string; + + showTermsConditions: boolean; + termsConditions: string; + termsConditionsLabel: string; + + lines: EstimatePdfBrandingLineItem[]; + + showEstimateNumber: boolean; + estimateNumberLabel: string; + estimateNumebr: string; + + estimateDate: string; + showEstimateDate: boolean; + estimateDateLabel: string; + + expirationDateLabel: string; + showExpirationDate: boolean; + expirationDate: string; +} \ No newline at end of file diff --git a/packages/server/src/services/Sales/Estimates/utils.ts b/packages/server/src/services/Sales/Estimates/utils.ts new file mode 100644 index 000000000..2b7c7fd1b --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/utils.ts @@ -0,0 +1,7 @@ +import { EstimatePdfBrandingAttributes } from './constants'; + +export const transformEstimateToPdfTemplate = ( + estimate +): Partial => { + return {}; +}; diff --git a/packages/server/src/services/Sales/Invoices/SaleEstimatePdfTemplate.ts b/packages/server/src/services/Sales/Invoices/SaleEstimatePdfTemplate.ts new file mode 100644 index 000000000..de324ea90 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SaleEstimatePdfTemplate.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import { mergePdfTemplateWithDefaultAttributes } from './utils'; +import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate'; +import { defaultEstimatePdfBrandingAttributes } from '../Estimates/constants'; + +@Service() +export class SaleEstimatePdfTemplate { + @Inject() + private getPdfTemplateService: GetPdfTemplate; + + /** + * Retrieves the estimate pdf template. + * @param {number} tenantId + * @param {number} invoiceTemplateId + * @returns + */ + async getEstimatePdfTemplate(tenantId: number, estimateTemplateId: number) { + const template = await this.getPdfTemplateService.getPdfTemplate( + tenantId, + estimateTemplateId + ); + const attributes = mergePdfTemplateWithDefaultAttributes( + template.attributes, + defaultEstimatePdfBrandingAttributes + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts b/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts index 0a357e5ea..afae67359 100644 --- a/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts +++ b/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts @@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { GetPaymentReceived } from './GetPaymentReceived'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate'; +import { transformPaymentReceivedToPdfTemplate } from './utils'; +import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces'; @Service() export default class GetPaymentReceivedPdf { + @Inject() + private tenancy: HasTenancyService; + @Inject() private chromiumlyTenancy: ChromiumlyTenancy; @@ -14,6 +21,9 @@ export default class GetPaymentReceivedPdf { @Inject() private getPaymentService: GetPaymentReceived; + @Inject() + private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate; + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - @@ -24,19 +34,52 @@ export default class GetPaymentReceivedPdf { tenantId: number, paymentReceiveId: number ): Promise { - const paymentReceive = await this.getPaymentService.getPaymentReceive( + const brandingAttributes = await this.getPaymentBrandingAttributes( tenantId, paymentReceiveId ); const htmlContent = await this.templateInjectable.render( tenantId, 'modules/payment-receive-standard', - { - paymentReceive, - } + brandingAttributes ); - return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, { - margins: { top: 0, bottom: 0, left: 0, right: 0 }, - }); + // Converts the given html content to pdf document. + return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); + } + + /** + * Retrieves the given payment received branding attributes. + * @param {number} tenantId + * @param {number} paymentReceivedId + * @returns {Promise} + */ + async getPaymentBrandingAttributes( + tenantId: number, + paymentReceivedId: number + ): Promise { + const { PdfTemplate } = this.tenancy.models(tenantId); + + const paymentReceived = await this.getPaymentService.getPaymentReceive( + tenantId, + paymentReceivedId + ); + const templateId = + paymentReceived?.pdfTemplateId ?? + ( + await PdfTemplate.query().findOne({ + resource: 'PaymentReceive', + default: true, + }) + )?.id; + + const brandingTemplate = + await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate( + tenantId, + templateId + ); + return { + ...brandingTemplate.attributes, + ...transformPaymentReceivedToPdfTemplate(paymentReceived), + }; } } diff --git a/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedBrandingTemplate.ts b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedBrandingTemplate.ts new file mode 100644 index 000000000..20e01d7e6 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedBrandingTemplate.ts @@ -0,0 +1,35 @@ +import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate'; +import { Inject, Service } from 'typedi'; +import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils'; +import { defaultPaymentReceivedPdfTemplateAttributes } from './constants'; +import { PdfTemplate } from '@/models/PdfTemplate'; + +@Service() +export class PaymentReceivedBrandingTemplate { + @Inject() + private getPdfTemplateService: GetPdfTemplate; + + /** + * Retrieves the payment received pdf template. + * @param {number} tenantId + * @param {number} paymentTemplateId + * @returns + */ + public async getPaymentReceivedPdfTemplate( + tenantId: number, + paymentTemplateId: number + ): Promise { + const template = await this.getPdfTemplateService.getPdfTemplate( + tenantId, + paymentTemplateId + ); + const attributes = mergePdfTemplateWithDefaultAttributes( + template.attributes, + defaultPaymentReceivedPdfTemplateAttributes + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedDTOTransformer.ts b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedDTOTransformer.ts index 24f0b0737..047607df4 100644 --- a/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedDTOTransformer.ts +++ b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedDTOTransformer.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import { Inject, Service } from 'typedi'; import { omit, sumBy } from 'lodash'; +import composeAsync from 'async/compose'; import { ICustomer, IPaymentReceived, @@ -12,6 +13,7 @@ import { PaymentReceivedIncrement } from './PaymentReceivedIncrement'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { formatDateFields } from '@/utils'; import { assocItemEntriesDefaultIndex } from '@/services/Items/utils'; +import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer'; @Service() export class PaymentReceiveDTOTransformer { @@ -24,6 +26,9 @@ export class PaymentReceiveDTOTransformer { @Inject() private branchDTOTransform: BranchTransactionDTOTransform; + @Inject() + private brandingTemplatesTransformer: BrandingTemplateDTOTransformer; + /** * Transformes the create payment receive DTO to model object. * @param {number} tenantId @@ -68,8 +73,16 @@ export class PaymentReceiveDTOTransformer { exchangeRate: paymentReceiveDTO.exchangeRate || 1, entries, }; + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + tenantId, + 'SaleInvoice' + ) + )(initialDTO); + return R.compose( this.branchDTOTransform.transformDTO(tenantId) - )(initialDTO); + )(initialAsyncDTO); } } diff --git a/packages/server/src/services/Sales/PaymentReceived/constants.ts b/packages/server/src/services/Sales/PaymentReceived/constants.ts index 0f48e39fe..3d9b6af88 100644 --- a/packages/server/src/services/Sales/PaymentReceived/constants.ts +++ b/packages/server/src/services/Sales/PaymentReceived/constants.ts @@ -45,3 +45,53 @@ export const PaymentsReceiveSampleData = [ 'Payment Amount': 850, }, ]; + +export const defaultPaymentReceivedPdfTemplateAttributes = { + primaryColor: '#000', + secondaryColor: '#000', + showCompanyLogo: true, + companyLogo: '', + companyName: 'Bigcapital Technology, Inc.', + + billedToAddress: [ + 'Bigcapital Technology, Inc.', + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + billedFromAddress: [ + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + showBilledFromAddress: true, + showBillingToAddress: true, + billedToLabel: 'Billed To', + + total: '$1000.00', + totalLabel: 'Total', + showTotal: true, + + subtotal: '1000/00', + subtotalLabel: 'Subtotal', + showSubtotal: true, + + lines: [ + { + invoiceNumber: 'INV-00001', + invoiceAmount: '$1000.00', + paidAmount: '$1000.00', + }, + ], + showPaymentReceivedNumber: true, + paymentReceivedNumberLabel: 'Payment Number', + paymentReceivedNumebr: '346D3D40-0001', + + paymentReceivedDate: 'September 3, 2024', + showPaymentReceivedDate: true, + paymentReceivedDateLabel: 'Payment Date', +}; diff --git a/packages/server/src/services/Sales/PaymentReceived/utils.ts b/packages/server/src/services/Sales/PaymentReceived/utils.ts new file mode 100644 index 000000000..beda5e0b9 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceived/utils.ts @@ -0,0 +1,12 @@ +import { + IPaymentReceived, + PaymentReceivedPdfTemplateAttributes, +} from '@/interfaces'; + +export const transformPaymentReceivedToPdfTemplate = ( + payment: IPaymentReceived +): Partial => { + return { + // ...payment + }; +}; diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptBrandingTemplate.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptBrandingTemplate.ts new file mode 100644 index 000000000..5d7794421 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptBrandingTemplate.ts @@ -0,0 +1,35 @@ +import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate'; +import { Inject, Service } from 'typedi'; +import { defaultSaleReceiptBrandingAttributes } from './constants'; +import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils'; + +@Service() +export class SaleReceiptBrandingTemplate { + @Inject() + private getPdfTemplateService: GetPdfTemplate; + + + /** + * Retrieves the sale receipt branding template. + * @param {number} tenantId - The ID of the tenant. + * @param {number} templateId - The ID of the PDF template. + * @returns {Promise} The sale receipt branding template with merged attributes. + */ + public async getSaleReceiptBrandingTemplate( + tenantId: number, + templateId: number + ) { + const template = await this.getPdfTemplateService.getPdfTemplate( + tenantId, + templateId + ); + const attributes = mergePdfTemplateWithDefaultAttributes( + template.attributes, + defaultSaleReceiptBrandingAttributes + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts index ae492099a..78d14b3b8 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptDTOTransformer.ts @@ -12,6 +12,7 @@ import { formatDateFields } from '@/utils'; import { SaleReceiptIncrement } from './SaleReceiptIncrement'; import { ItemEntry } from '@/models'; import { assocItemEntriesDefaultIndex } from '@/services/Items/utils'; +import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer'; @Service() export class SaleReceiptDTOTransformer { @@ -30,6 +31,9 @@ export class SaleReceiptDTOTransformer { @Inject() private receiptIncrement: SaleReceiptIncrement; + @Inject() + private brandingTemplatesTransformer: BrandingTemplateDTOTransformer; + /** * Transform create DTO object to model object. * @param {ISaleReceiptDTO} saleReceiptDTO - @@ -88,9 +92,17 @@ export class SaleReceiptDTOTransformer { }), entries, }; + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + tenantId, + 'SaleReceipt' + ) + )(initialDTO); + return R.compose( this.branchDTOTransform.transformDTO(tenantId), this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); + )(initialAsyncDTO); } } diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts index cad2b5f93..c72c9b0c0 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -2,9 +2,15 @@ import { Inject, Service } from 'typedi'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { GetSaleReceipt } from './GetSaleReceipt'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate'; +import { transformReceiptToBrandingTemplateAttributes } from './utils'; @Service() export class SaleReceiptsPdf { + @Inject() + private tenancy: HasTenancyService; + @Inject() private chromiumlyTenancy: ChromiumlyTenancy; @@ -14,26 +20,64 @@ export class SaleReceiptsPdf { @Inject() private getSaleReceiptService: GetSaleReceipt; + @Inject() + private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate; + /** * Retrieves sale invoice pdf content. - * @param {number} tenantId - + * @param {number} tenantId - * @param {number} saleInvoiceId - * @returns {Promise} */ public async saleReceiptPdf(tenantId: number, saleReceiptId: number) { - const saleReceipt = await this.getSaleReceiptService.getSaleReceipt( + const brandingAttributes = await this.getReceiptBrandingAttributes( tenantId, saleReceiptId ); + console.log(brandingAttributes, 'attributes'); + const htmlContent = await this.templateInjectable.render( tenantId, 'modules/receipt-regular', - { - saleReceipt, - } + brandingAttributes ); - return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, { - margins: { top: 0, bottom: 0, left: 0, right: 0 }, - }); + return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); + } + + /** + * Retrieves receipt branding attributes. + * @param {number} tenantId + * @param {number] receiptId + * @returns + */ + public async getReceiptBrandingAttributes( + tenantId: number, + receiptId: number + ) { + const { PdfTemplate } = this.tenancy.models(tenantId); + + const saleReceipt = await this.getSaleReceiptService.getSaleReceipt( + tenantId, + receiptId + ); + // Retrieve the invoice template id of not found get the default template id. + const templateId = + saleReceipt.pdfTemplateId ?? + ( + await PdfTemplate.query().findOne({ + resource: 'SaleReceipt', + default: true, + }) + )?.id; + // Retrieves the receipt branding template. + const brandingTemplate = + await this.saleReceiptBrandingTemplate.getSaleReceiptBrandingTemplate( + tenantId, + templateId + ); + return { + ...brandingTemplate.attributes, + ...transformReceiptToBrandingTemplateAttributes(saleReceipt), + }; } } diff --git a/packages/server/src/services/Sales/Receipts/constants.ts b/packages/server/src/services/Sales/Receipts/constants.ts index 06df9a615..491369c74 100644 --- a/packages/server/src/services/Sales/Receipts/constants.ts +++ b/packages/server/src/services/Sales/Receipts/constants.ts @@ -22,7 +22,7 @@ export const ERRORS = { SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED', SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED', CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES', - NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR' + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR', }; export const DEFAULT_VIEW_COLUMNS = []; @@ -47,22 +47,84 @@ export const DEFAULT_VIEWS = [ }, ]; - export const SaleReceiptsSampleData = [ { - "Receipt Date": "2023-01-01", - "Customer": "Randall Kohler", - "Deposit Account": "Petty Cash", - "Exchange Rate": "", - "Receipt Number": "REC-00001", - "Reference No.": "REF-0001", - "Statement": "Delectus unde aut soluta et accusamus placeat.", - "Receipt Message": "Vitae asperiores dicta.", - "Closed": "T", - "Item": "Schmitt Group", - "Quantity": 100, - "Rate": 200, - "Line Description": "Distinctio distinctio sit veritatis consequatur iste quod veritatis." - } - -] \ No newline at end of file + 'Receipt Date': '2023-01-01', + Customer: 'Randall Kohler', + 'Deposit Account': 'Petty Cash', + 'Exchange Rate': '', + 'Receipt Number': 'REC-00001', + 'Reference No.': 'REF-0001', + Statement: 'Delectus unde aut soluta et accusamus placeat.', + 'Receipt Message': 'Vitae asperiores dicta.', + Closed: 'T', + Item: 'Schmitt Group', + Quantity: 100, + Rate: 200, + 'Line Description': + 'Distinctio distinctio sit veritatis consequatur iste quod veritatis.', + }, +]; + +export const defaultSaleReceiptBrandingAttributes = { + primaryColor: '', + secondaryColor: '', + showCompanyLogo: true, + companyLogo: '', + companyName: 'Bigcapital Technology, Inc.', + + // # Address + billedToAddress: [ + 'Bigcapital Technology, Inc.', + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + billedFromAddress: [ + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + showBilledFromAddress: true, + showBilledToAddress: true, + billedToLabel: 'Billed To', + + total: '$1000.00', + totalLabel: 'Total', + showTotal: true, + + subtotal: '1000/00', + subtotalLabel: 'Subtotal', + showSubtotal: true, + + showCustomerNote: true, + customerNote: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + customerNoteLabel: 'Customer Note', + + showTermsConditions: true, + termsConditions: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + termsConditionsLabel: 'Terms & Conditions', + + lines: [ + { + item: 'Simply dummy text', + description: 'Simply dummy text of the printing and typesetting', + rate: '1', + quantity: '1000', + total: '$1000.00', + }, + ], + showReceiptNumber: true, + receiptNumberLabel: 'Receipt Number', + receiptNumebr: '346D3D40-0001', + + receiptDate: 'September 3, 2024', + showReceiptDate: true, + receiptDateLabel: 'Receipt Date', +}; diff --git a/packages/server/src/services/Sales/Receipts/utils.ts b/packages/server/src/services/Sales/Receipts/utils.ts new file mode 100644 index 000000000..548ad7001 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/utils.ts @@ -0,0 +1,6 @@ + + + +export const transformReceiptToBrandingTemplateAttributes = () => { + return {}; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFloatingActions.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFloatingActions.tsx index bfdc6c57f..85bd40d93 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFloatingActions.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFloatingActions.tsx @@ -12,10 +12,15 @@ import { Menu, MenuItem, } from '@blueprintjs/core'; -import { If, Icon, FormattedMessage as T, Group } from '@/components'; +import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components'; import { CLASSES } from '@/constants/classes'; import classNames from 'classnames'; import { useCreditNoteFormContext } from './CreditNoteFormProvider'; +import { + BrandingThemeFormGroup, + BrandingThemeSelectButton, +} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; +import { useCreditNoteFormBrandingTemplatesOptions } from './utils'; /** * Credit note floating actions. @@ -74,6 +79,8 @@ export default function CreditNoteFloatingActions() { resetForm(); }; + const brandingTemplatesOptions = useCreditNoteFormBrandingTemplatesOptions(); + return ( } /> + + {/* ----------- Branding Template Select ----------- */} + + ( + + )} + filterable={false} + popoverProps={{ minimal: true }} + /> + ); } diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormProvider.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormProvider.tsx index 3e4d95261..19c02eb61 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormProvider.tsx @@ -18,6 +18,7 @@ import { useSettingsCreditNotes, useInvoice, } from '@/hooks/query'; +import { useGetPdfTemplates } from '@/hooks/query/pdf-templates'; const CreditNoteFormContext = React.createContext(); @@ -73,6 +74,10 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) { isSuccess: isBranchesSuccess, } = useBranches({}, { enabled: isBranchFeatureCan }); + // Fetches branding templates of invoice. + const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } = + useGetPdfTemplates({ resource: 'PaymentReceive' }); + // Handle fetching settings. useSettingsCreditNotes(); @@ -115,13 +120,18 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) { createCreditNoteMutate, editCreditNoteMutate, setSubmitPayload, + + // Branding templates. + brandingTemplates, + isBrandingTemplatesLoading, }; const isLoading = isItemsLoading || isCustomersLoading || isCreditNoteLoading || - isInvoiceLoading; + isInvoiceLoading || + isBrandingTemplatesLoading; return ( diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/utils.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/utils.tsx index 52af20853..fad59288f 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/utils.tsx @@ -24,6 +24,7 @@ import { transformAttachmentsToForm, transformAttachmentsToRequest, } from '@/containers/Attachments/utils'; +import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; export const MIN_LINES_NUMBER = 1; @@ -54,7 +55,8 @@ export const defaultCreditNote = { exchange_rate: 1, currency_code: '', entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)], - attachments: [] + attachments: [], + pdf_template_id: '', }; /** @@ -214,3 +216,13 @@ export const useCreditNoteIsForeignCustomer = () => { ); return isForeignCustomer; }; + + +export const useCreditNoteFormBrandingTemplatesOptions = () => { + const { brandingTemplates } = useCreditNoteFormContext(); + + return React.useMemo( + () => convertBrandingTemplatesToOptions(brandingTemplates), + [brandingTemplates], + ); +}; diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFloatingActions.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFloatingActions.tsx index 906d703d9..31e7cf8dc 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFloatingActions.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFloatingActions.tsx @@ -11,11 +11,16 @@ import { Menu, MenuItem, } from '@blueprintjs/core'; -import { If, Icon, FormattedMessage as T, Group } from '@/components'; +import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components'; import { CLASSES } from '@/constants/classes'; import { useHistory } from 'react-router-dom'; import { useFormikContext } from 'formik'; import { useEstimateFormContext } from './EstimateFormProvider'; +import { useEstimateFormBrandingTemplatesOptions } from './utils'; +import { + BrandingThemeFormGroup, + BrandingThemeSelectButton, +} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; /** * Estimate floating actions bar. @@ -73,6 +78,8 @@ export default function EstimateFloatingActions() { resetForm(); }; + const brandingTemplatesOptions = useEstimateFormBrandingTemplatesOptions(); + return ( } /> + + {/* ----------- Branding Template Select ----------- */} + + ( + + )} + filterable={false} + popoverProps={{ minimal: true }} + /> + ); } diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormProvider.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormProvider.tsx index 87196dc24..6cba0125e 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormProvider.tsx @@ -12,8 +12,9 @@ import { useCreateEstimate, useEditEstimate, } from '@/hooks/query'; -import { Features } from '@/constants'; import { useProjects } from '@/containers/Projects/hooks'; +import { useGetPdfTemplates } from '@/hooks/query/pdf-templates'; +import { Features } from '@/constants'; import { useFeatureCan } from '@/hooks/state'; import { ITEMS_FILTER_ROLES } from './utils'; @@ -71,6 +72,10 @@ function EstimateFormProvider({ query, estimateId, ...props }) { isLoading: isProjectsLoading, } = useProjects({}, { enabled: !!isProjectsFeatureCan }); + // Fetches branding templates of invoice. + const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } = + useGetPdfTemplates({ resource: 'SaleEstimate' }); + // Handle fetch settings. useSettingsEstimates(); @@ -112,13 +117,19 @@ function EstimateFormProvider({ query, estimateId, ...props }) { createEstimateMutate, editEstimateMutate, + + brandingTemplates, + isBrandingTemplatesLoading, }; + const isLoading = + isCustomersLoading || + isItemsLoading || + isEstimateLoading || + isBrandingTemplatesLoading; + return ( - + ); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/utils.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/utils.tsx index 48088a524..355c7619e 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/utils.tsx @@ -22,6 +22,7 @@ import { transformAttachmentsToForm, transformAttachmentsToRequest, } from '@/containers/Attachments/utils'; +import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; export const MIN_LINES_NUMBER = 1; @@ -60,7 +61,8 @@ export const defaultEstimate = { exchange_rate: 1, currency_code: '', entries: [...repeatValue(defaultEstimateEntry, MIN_LINES_NUMBER)], - attachments: [] + attachments: [], + pdf_template_id: '', }; const ERRORS = { @@ -262,3 +264,12 @@ export const resetFormState = ({ initialValues, values, resetForm }) => { }, }); }; + +export const useEstimateFormBrandingTemplatesOptions = () => { + const { brandingTemplates } = useEstimateFormContext(); + + return React.useMemo( + () => convertBrandingTemplatesToOptions(brandingTemplates), + [brandingTemplates], + ); +}; diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx index 3f6d874dc..e403822b4 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx @@ -25,6 +25,7 @@ import { DashboardFilterButton, DashboardRowsHeightButton, DashboardActionsBar, + FSelect, } from '@/components'; import withEstimates from './withEstimates'; @@ -42,6 +43,10 @@ import { compose } from '@/utils'; import { DialogsName } from '@/constants/dialogs'; import withDrawerActions from '@/containers/Drawer/withDrawerActions'; import { DRAWERS } from '@/constants/drawers'; +import { + BrandingThemeFormGroup, + BrandingThemeSelectButton, +} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; /** * Estimates list actions bar. diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFloatingActions.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFloatingActions.tsx index 351b88a88..53f4e3b4b 100644 --- a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFloatingActions.tsx +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFloatingActions.tsx @@ -12,10 +12,15 @@ import { MenuItem, } from '@blueprintjs/core'; import { useHistory } from 'react-router-dom'; -import { Group, Icon, FormattedMessage as T } from '@/components'; +import { FSelect, Group, Icon, FormattedMessage as T } from '@/components'; import { useFormikContext } from 'formik'; import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider'; import { CLASSES } from '@/constants/classes'; +import { + BrandingThemeFormGroup, + BrandingThemeSelectButton, +} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; +import { usePaymentReceivedFormBrandingTemplatesOptions } from './utils'; /** * Payment receive floating actions bar. @@ -53,6 +58,9 @@ export default function PaymentReceiveFormFloatingActions() { submitForm(); }; + const brandingTemplatesOpts = + usePaymentReceivedFormBrandingTemplatesOptions(); + return ( } /> + + {/* ----------- Branding Template Select ----------- */} + + ( + + )} + filterable={false} + popoverProps={{ minimal: true }} + /> + ); } diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFormProvider.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFormProvider.tsx index c6f5a4729..d98e26b4d 100644 --- a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/PaymentReceiveFormProvider.tsx @@ -13,6 +13,7 @@ import { useCreatePaymentReceive, useEditPaymentReceive, } from '@/hooks/query'; +import { useGetPdfTemplates } from '@/hooks/query/pdf-templates'; // Payment receive form context. const PaymentReceiveFormContext = createContext(); @@ -65,6 +66,10 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) { isLoading: isProjectsLoading, } = useProjects({}, { enabled: !!isProjectsFeatureCan }); + // Fetches branding templates of payment received module. + const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } = + useGetPdfTemplates({ resource: 'PaymentReceive' }); + // Detarmines whether the new mode. const isNewMode = !paymentReceiveId; @@ -102,13 +107,20 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) { isExcessConfirmed, setIsExcessConfirmed, + + // Branding templates + brandingTemplates, + isBrandingTemplatesLoading, }; + const isLoading = + isPaymentLoading || + isAccountsLoading || + isCustomersLoading || + isBrandingTemplatesLoading; + return ( - + ); diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/utils.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/utils.tsx index bbd416248..4464b8ea9 100644 --- a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceiveForm/utils.tsx @@ -19,6 +19,7 @@ import { transformAttachmentsToForm, transformAttachmentsToRequest, } from '@/containers/Attachments/utils'; +import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; // Default payment receive entry. export const defaultPaymentReceiveEntry = { @@ -44,10 +45,11 @@ export const defaultPaymentReceive = { statement: '', amount: '', currency_code: '', - branch_id: '', exchange_rate: 1, entries: [], attachments: [], + branch_id: '', + pdf_template_id: '', }; export const defaultRequestPaymentEntry = { @@ -303,3 +305,12 @@ export const getExceededAmountFromValues = (values) => { return totalAmount - totalApplied; }; + +export const usePaymentReceivedFormBrandingTemplatesOptions = () => { + const { brandingTemplates } = usePaymentReceiveFormContext(); + + return React.useMemo( + () => convertBrandingTemplatesToOptions(brandingTemplates), + [brandingTemplates], + ); +}; diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormFloatingActions.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormFloatingActions.tsx index aa156618d..8e198ea27 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormFloatingActions.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormFloatingActions.tsx @@ -11,12 +11,17 @@ import { Menu, MenuItem, } from '@blueprintjs/core'; -import { Group, FormattedMessage as T } from '@/components'; +import { FSelect, Group, FormattedMessage as T } from '@/components'; import { useFormikContext } from 'formik'; import { useHistory } from 'react-router-dom'; import { CLASSES } from '@/constants/classes'; import { If, Icon } from '@/components'; import { useReceiptFormContext } from './ReceiptFormProvider'; +import { useReceiptFormBrandingTemplatesOptions } from './utils'; +import { + BrandingThemeFormGroup, + BrandingThemeSelectButton, +} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields'; /** * Receipt floating actions bar. @@ -76,6 +81,8 @@ export default function ReceiptFormFloatingActions() { resetForm(); }; + const brandingTemplatesOptions = useReceiptFormBrandingTemplatesOptions(); + return ( } /> + + {/* ----------- Branding Template Select ----------- */} + + ( + + )} + filterable={false} + popoverProps={{ minimal: true }} + /> + ); } diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormProvider.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormProvider.tsx index 23f432115..d51877c01 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormProvider.tsx @@ -15,6 +15,7 @@ import { useEditReceipt, } from '@/hooks/query'; import { useProjects } from '@/containers/Projects/hooks'; +import { useGetPdfTemplates } from '@/hooks/query/pdf-templates'; const ReceiptFormContext = createContext(); @@ -77,7 +78,7 @@ function ReceiptFormProvider({ receiptId, ...props }) { [], ); - // Handle fetch Items data table or list + // Handle fetch Items data table or list. const { data: { items }, isLoading: isItemsLoading, @@ -85,13 +86,16 @@ function ReceiptFormProvider({ receiptId, ...props }) { page_size: 10000, stringified_filter_roles: stringifiedFilterRoles, }); - // Fetch project list. const { data: { projects }, isLoading: isProjectsLoading, } = useProjects({}, { enabled: !!isProjectsFeatureCan }); + // Fetches branding templates of receipt. + const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } = + useGetPdfTemplates({ resource: 'SaleReceipt' }); + // Fetch receipt settings. const { isLoading: isSettingLoading } = useSettingsReceipts(); @@ -101,7 +105,6 @@ function ReceiptFormProvider({ receiptId, ...props }) { const [submitPayload, setSubmitPayload] = useState({}); const isNewMode = !receiptId; - const isFeatureLoading = isWarehouesLoading || isBranchesLoading; const provider = { @@ -130,6 +133,10 @@ function ReceiptFormProvider({ receiptId, ...props }) { createReceiptMutate, editReceiptMutate, setSubmitPayload, + + // Branding templates + brandingTemplates, + isBrandingTemplatesLoading }; return ( { }, }); }; + +export const useReceiptFormBrandingTemplatesOptions = () => { + const { brandingTemplates } = useReceiptFormContext(); + + return React.useMemo( + () => convertBrandingTemplatesToOptions(brandingTemplates), + [brandingTemplates], + ); +};