diff --git a/packages/server/resources/scss/base.css b/packages/server/resources/scss/base.css new file mode 100644 index 000000000..d7d148cab --- /dev/null +++ b/packages/server/resources/scss/base.css @@ -0,0 +1,42 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap'); + +*, +*::before, +*::after { + box-sizing: border-box; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; +} + +body{ + margin: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #000; + background-color: #fff; + direction: ltr; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; + + font-family: "Noto Sans", sans-serif; + font-optical-sizing: auto; + /* font-weight: ; */ + font-style: normal; + /* font-variation-settings: + "wdth" 100; */ +} \ No newline at end of file diff --git a/packages/server/resources/scss/base.scss b/packages/server/resources/scss/base.scss index bf7b32cc6..f7ed229d8 100644 --- a/packages/server/resources/scss/base.scss +++ b/packages/server/resources/scss/base.scss @@ -1,35 +1 @@ @import "./normalize.scss"; - -*, -*::before, -*::after { - box-sizing: border-box; -} - -th { - text-align: inherit; // 2 - text-align: -webkit-match-parent; // 3 -} - -thead, -tbody, -tfoot, -tr, -td, -th { - border-color: inherit; - border-style: solid; - border-width: 0; -} - -body{ - margin: 0; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #212529; - background-color: #fff; - direction: ltr; - -webkit-text-size-adjust: 100%; - -webkit-tap-highlight-color: transparent; -} diff --git a/packages/server/resources/scss/normalize.css b/packages/server/resources/scss/normalize.css new file mode 100644 index 000000000..631be2581 --- /dev/null +++ b/packages/server/resources/scss/normalize.css @@ -0,0 +1,379 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; + /* 1 */ + height: 0; + /* 1 */ + overflow: visible; + /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; + /* 1 */ + text-decoration: underline; + /* 2 */ + text-decoration: underline dotted; + /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: 1.15; + /* 1 */ + margin: 0; + /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { + /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { + /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; + /* 1 */ + color: inherit; + /* 2 */ + display: table; + /* 1 */ + max-width: 100%; + /* 1 */ + padding: 0; + /* 3 */ + white-space: normal; + /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/packages/server/resources/views/PaperTemplateLayout.pug b/packages/server/resources/views/PaperTemplateLayout.pug index 4678ae166..b93bc7388 100644 --- a/packages/server/resources/views/PaperTemplateLayout.pug +++ b/packages/server/resources/views/PaperTemplateLayout.pug @@ -1,6 +1,9 @@ html(lang=locale) head title My Site - #{title} + style + include ../scss/normalize.css + include ../scss/base.css block head body div.paper-template diff --git a/packages/server/resources/views/modules/invoice-regular.pug b/packages/server/resources/views/modules/invoice-regular.pug deleted file mode 100644 index edddcaf9c..000000000 --- a/packages/server/resources/views/modules/invoice-regular.pug +++ /dev/null @@ -1,92 +0,0 @@ -extends ../PaperTemplateLayout.pug - -block head - style - if (isRtl) - include ../../css/modules/invoice-rtl.css - else - include ../../css/modules/invoice.css - -block content - div.invoice - div.invoice__header - div.paper - h1.title #{__("invoice.paper.invoice")} - if saleInvoice.invoiceNo - span.invoiceNo #{saleInvoice.invoiceNo} - - div.organization - h3.title #{organizationName} - if organizationEmail - span.email #{organizationEmail} - - div.invoice__due-amount - div.label #{__('invoice.paper.invoice_amount')} - div.amount #{saleInvoice.totalFormatted} - - div.invoice__meta - div.invoice__meta-item.invoice__meta-item--amount - span.label #{__('invoice.paper.due_amount')} - span.value #{saleInvoice.dueAmountFormatted} - - div.invoice__meta-item.invoice__meta-item--billed-to - span.label #{__("invoice.paper.billed_to")} - span.value #{saleInvoice.customer.displayName} - - div.invoice__meta-item.invoice__meta-item--invoice-date - span.label #{__("invoice.paper.invoice_date")} - span.value #{saleInvoice.invoiceDateFormatted} - - div.invoice__meta-item.invoice__meta-item--due-date - span.label #{__("invoice.paper.due_date")} - span.value #{saleInvoice.dueDateFormatted} - - div.invoice__table - table - thead - tr - th.item #{__("item_entry.paper.item_name")} - th.rate #{__("item_entry.paper.rate")} - th.quantity #{__("item_entry.paper.quantity")} - th.total #{__("item_entry.paper.total")} - tbody - each entry in saleInvoice.entries - tr - td.item - div.title=entry.item.name - span.description=entry.description - td.rate=entry.rate - td.quantity=entry.quantity - td.total=entry.amount - - div.invoice__table-after - div.invoice__table-total - table - tbody - tr.subtotal - td #{__('invoice.paper.subtotal')} - td #{saleInvoice.subtotalFormatted} - each tax in saleInvoice.taxes - tr.tax_line - td #{tax.name} [#{tax.taxRate}%] - td #{tax.taxRateAmountFormatted} - tr.total - td #{__('invoice.paper.total')} - td #{saleInvoice.totalFormatted} - tr.payment-amount - td #{__('invoice.paper.payment_amount')} - td #{saleInvoice.paymentAmountFormatted} - tr.blanace-due - td #{__('invoice.paper.balance_due')} - td #{saleInvoice.dueAmountFormatted} - - div.invoice__footer - if saleInvoice.termsConditions - div.invoice__conditions - h3 #{__("invoice.paper.conditions_title")} - p #{saleInvoice.termsConditions} - - if saleInvoice.invoiceMessage - div.invoice__notes - h3 #{__("invoice.paper.notes_title")} - p #{saleInvoice.invoiceMessage} \ 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 new file mode 100644 index 000000000..d5b172a8d --- /dev/null +++ b/packages/server/resources/views/modules/invoice-standard.pug @@ -0,0 +1,244 @@ +extends ../PaperTemplateLayout.pug + +block head + - var prefix = 'bc' + style. + .#{prefix}-root { + background-color: #fff; + 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; + border-radius: 5px; + overflow: hidden; + } + .#{prefix}-details { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 24px; + } + .#{prefix}-detail { + display: flex; + flex-direction: row; + gap: 12px; + } + .#{prefix}-detail__label { + min-width: 120px; + color: #333; + } + .#{prefix}-detail__value { + /* Styles for detail values */ + } + .#{prefix}-address-root { + box-sizing: border-box; + display: flex; + flex-flow: wrap; + -webkit-box-align: center; + align-items: center; + -webkit-box-pack: start; + justify-content: flex-start; + gap: 10px; + margin-bottom: 24px; + } + .#{prefix}-address-from { + flex: 1; + } + .#{prefix}-address-from__item { + /* Styles for items in the billed-from address */ + } + .#{prefix}-address-to { + flex: 1; + } + .#{prefix}-address-to__item { + /* Styles for items in the billed-to address */ + } + .#{prefix}-table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: inherit; + } + .#{prefix}-table__header { + font-weight: 400; + border-bottom: 1px solid #000; + padding: 2px 10px; + color: #333; + } + .#{prefix}-table__header:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__header:last-of-type{ + padding-right: 0; + } + .#{prefix}-table__header--right { + text-align: right; + } + .#{prefix}-table__cell { + border-bottom: 1px solid #F6F6F6; + padding: 12px 10px; + } + .#{prefix}-table__cell:first-of-type{ + padding-left: 0; + } + .#{prefix}-table__cell:last-of-type { + padding-right: 0; + } + .#{prefix}-table__cell--right { + text-align: right; + } + .#{prefix}-totals { + display: flex; + flex-direction: column; + margin-left: auto; + width: 300px; + margin-bottom: 24px; + } + .#{prefix}-totals__item { + display: flex; + padding: 4px 0; + } + .#{prefix}-totals__item--border-gray { + border-bottom: 1px solid #DADADA; + } + .#{prefix}-totals__item--border-dark { + border-bottom: 1px solid #000; + } + .#{prefix}-totals__item--font-weight-bold { + font-weight: bold; + /* Additional styles for total items with bold font weight */ + } + .#{prefix}-totals__item-label { + min-width: 160px; + } + .#{prefix}-totals__item-amount { + flex: 1 1 auto; + text-align: right; + } + .#{prefix}-paragraph { + margin-bottom: 20px; + } + .#{prefix}-paragraph__label { + color: #666; + } + .#{prefix}-paragraph__value { + /* Styles for values within the paragraph section */ + } +block content + //- block head + div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) + //- Title and company logo + h1(class=`${prefix}-big-title`) Invoice + + if showCompanyLogo + div(class=`${prefix}-logo-wrap`) + img(alt="", src=companyLogo) + + //- Invoice details + div(class=`${prefix}-details`) + if showInvoiceNumber + div(class=`${prefix}-detail`) + div(class=`${prefix}-detail__label`) #{invoiceNumberLabel} + div(class=`${prefix}-detail__value`) #{invoiceNumber} + + if showDateIssue + div(class=`${prefix}-detail`) + div(class=`${prefix}-detail__label`) #{dateIssueLabel} + div(class=`${prefix}-detail__value`) #{dateIssue} + + if showDueDate + div(class=`${prefix}-detail`) + div(class=`${prefix}-detail__label`) #{dueDateLabel} + div(class=`${prefix}-detail__value`) #{dueDate} + + //- Address section + div(class=`${prefix}-address-root`) + if showBilledFromAddress + div(class=`${prefix}-address-from`) + strong #{companyName} + each item in billedFromAddres + div(class=`${prefix}-address-from__item`) #{item} + + if showBillingToAddress + div(class=`${prefix}-address-to`) + strong #{billedToLabel} + each item in billedToAddress + div(class=`${prefix}-address-to__item`) #{item} + + //- Invoice table + table(class=`${prefix}-table`) + thead + tr + th(class=`${prefix}-table__header`) #{lineItemLabel} + th(class=`${prefix}-table__header`) #{lineDescriptionLabel} + th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel} + th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel} + tbody + each line in lines + tr + td(class=`${prefix}-table__cell`) #{line.item} + td(class=`${prefix}-table__cell`) #{line.description} + td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate} + td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total} + + //- Totals section + div(class=`${prefix}-totals`) + if showSubtotal + div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`) + div(class=`${prefix}-totals__item-label`) #{subtotalLabel} + div(class=`${prefix}-totals__item-amount`) #{subtotal} + + if showDiscount + div(class=`${prefix}-totals__item`) + div(class=`${prefix}-totals__item-label`) #{discountLabel} + div(class=`${prefix}-totals__item-amount`) #{discount} + + if showTaxes + each tax in taxes + div(class=`${prefix}-totals__item`) + div(class=`${prefix}-totals__item-label`) #{tax.label} + div(class=`${prefix}-totals__item-amount`) #{tax.amount} + + if showTotal + div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`) + div(class=`${prefix}-totals__item-label`) #{totalLabel} + div(class=`${prefix}-totals__item-amount`) #{total} + + if showPaymentMade + div(class=`${prefix}-totals__item`) + div(class=`${prefix}-totals__item-label`) #{paymentMadeLabel} + div(class=`${prefix}-totals__item-amount`) #{paymentMade} + + if showBalanceDue + div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`) + div(class=`${prefix}-totals__item-label`) #{balanceDueLabel} + div(class=`${prefix}-totals__item-amount`) #{balanceDue} + + //- Footer section + if showTermsConditions + div(class=`${prefix}-paragraph`) + if termsConditionsLabel + div(class=`${prefix}-paragraph__label`) #{termsConditionsLabel} + div(class=`${prefix}-paragraph__value`) #{termsConditions} + + if showStatement + div(class=`${prefix}-paragraph`) + if statementLabel + div(class=`${prefix}-paragraph__label`) #{statementLabel} + div(class=`${prefix}-paragraph__value`) #{statement} diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 03827d229..4493782b8 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -45,6 +45,11 @@ export interface ISaleInvoice { subtotal: number; subtotalLocal: number; subtotalExludingTax: number; + + termsConditions: string; + invoiceMessage: string; + + pdfTemplateId?: number; } export interface ISaleInvoiceDTO { @@ -217,3 +222,83 @@ export interface ISaleInvoiceMailSent { saleInvoiceId: number; messageOptions: SendInvoiceMailDTO; } + + +// Invoice Pdf Document +export interface InvoicePdfLine { + item: string; + description: string; + rate: string; + quantity: string; + total: string; +} + +export interface InvoicePdfTax { + label: string; + amount: string; +} + +export interface InvoicePdfTemplateAttributes { + primaryColor: string; + secondaryColor: string; + + companyName: string; + + showCompanyLogo: boolean; + companyLogo: string; + + dueDate: string; + dueDateLabel: string; + showDueDate: boolean; + + dateIssue: string; + dateIssueLabel: string; + showDateIssue: boolean; + + invoiceNumberLabel: string; + invoiceNumber: string; + showInvoiceNumber: boolean; + + showBillingToAddress: boolean; + showBilledFromAddress: boolean; + billedToLabel: string; + + lineItemLabel: string; + lineDescriptionLabel: string; + lineRateLabel: string; + lineTotalLabel: string; + + totalLabel: string; + subtotalLabel: string; + discountLabel: string; + paymentMadeLabel: string; + balanceDueLabel: string; + + showTotal: boolean; + showSubtotal: boolean; + showDiscount: boolean; + showTaxes: boolean; + showPaymentMade: boolean; + showDueAmount: boolean; + showBalanceDue: boolean; + + total: string; + subtotal: string; + discount: string; + paymentMade: string; + balanceDue: string; + + termsConditionsLabel: string; + showTermsConditions: boolean; + termsConditions: string; + + lines: InvoicePdfLine[]; + taxes: InvoicePdfTax[]; + + statementLabel: string; + showStatement: boolean; + statement: string; + + billedToAddress: string[]; + billedFromAddres: string[]; +} \ No newline at end of file diff --git a/packages/server/src/models/PdfTemplate.ts b/packages/server/src/models/PdfTemplate.ts index 67f53ac9d..91920a4dd 100644 --- a/packages/server/src/models/PdfTemplate.ts +++ b/packages/server/src/models/PdfTemplate.ts @@ -15,6 +15,9 @@ export class PdfTemplate extends TenantModel { return ['createdAt', 'updatedAt']; } + /** + * Json schema. + */ static get jsonSchema() { return { type: 'object', diff --git a/packages/server/src/services/ChromiumlyTenancy/ChromiumlyTenancy.ts b/packages/server/src/services/ChromiumlyTenancy/ChromiumlyTenancy.ts index 21df50a53..3e8a37daf 100644 --- a/packages/server/src/services/ChromiumlyTenancy/ChromiumlyTenancy.ts +++ b/packages/server/src/services/ChromiumlyTenancy/ChromiumlyTenancy.ts @@ -20,6 +20,10 @@ export class ChromiumlyTenancy { properties?: PageProperties, pdfFormat?: PdfFormat ) { - return this.htmlConvert.convert(tenantId, content, properties, pdfFormat); + const parsedProperties = { + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + ...properties, + } + return this.htmlConvert.convert(tenantId, content, parsedProperties, pdfFormat); } } diff --git a/packages/server/src/services/Items/ItemsEntriesService.ts b/packages/server/src/services/Items/ItemsEntriesService.ts index f307087b2..bab37c367 100644 --- a/packages/server/src/services/Items/ItemsEntriesService.ts +++ b/packages/server/src/services/Items/ItemsEntriesService.ts @@ -238,7 +238,7 @@ export default class ItemsEntriesService { * Sets the cost/sell accounts to the invoice entries. */ public setItemsEntriesDefaultAccounts(tenantId: number) { - return async (entries: IItemEntry[]) => { + return async (entries: IItemEntry[]) => { const { Item } = this.tenancy.models(tenantId); const entriesItemsIds = entries.map((e) => e.itemId); diff --git a/packages/server/src/services/PdfTemplate/AssignPdfTemplateDefault.ts b/packages/server/src/services/PdfTemplate/AssignPdfTemplateDefault.ts index ac038c79d..04a7c5e4f 100644 --- a/packages/server/src/services/PdfTemplate/AssignPdfTemplateDefault.ts +++ b/packages/server/src/services/PdfTemplate/AssignPdfTemplateDefault.ts @@ -33,6 +33,14 @@ export class AssignPdfTemplateDefault { return this.uow.withTransaction( tenantId, async (trx?: Knex.Transaction) => { + // Triggers `onPdfTemplateAssigningDefault` event. + await this.eventPublisher.emitAsync( + events.pdfTemplate.onAssigningDefault, + { + tenantId, + templateId, + } + ); await PdfTemplate.query(trx) .where('resource', oldPdfTempalte.resource) .patch({ default: false }); @@ -41,6 +49,7 @@ export class AssignPdfTemplateDefault { .findById(templateId) .patch({ default: true }); + // Triggers `onPdfTemplateAssignedDefault` event. await this.eventPublisher.emitAsync( events.pdfTemplate.onAssignedDefault, { diff --git a/packages/server/src/services/PdfTemplate/BrandingTemplateDTOTransformer.ts b/packages/server/src/services/PdfTemplate/BrandingTemplateDTOTransformer.ts new file mode 100644 index 000000000..379f097ca --- /dev/null +++ b/packages/server/src/services/PdfTemplate/BrandingTemplateDTOTransformer.ts @@ -0,0 +1,39 @@ +import * as R from 'ramda'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; + +@Service() +export class BrandingTemplateDTOTransformer { + @Inject() + private tenancy: HasTenancyService; + + /** + * Associates the default branding template id. + * @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'; + + const defaultTemplate = await PdfTemplate.query().findOne({ + resource, + default: true, + }); + console.log(defaultTemplate); + + if (!defaultTemplate) { + return object; + } + return { + ...object, + [attributeName]: defaultTemplate.id, + }; + }, +} diff --git a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts index 8013fe7af..e6a080c04 100644 --- a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts @@ -19,6 +19,7 @@ import { formatDateFields } from 'utils'; import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions'; import { assocItemEntriesDefaultIndex } from '@/services/Items/utils'; import { ItemEntry } from '@/models'; +import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer'; @Service() export class CommandSaleInvoiceDTOTransformer { @@ -40,6 +41,9 @@ export class CommandSaleInvoiceDTOTransformer { @Inject() private taxDTOTransformer: ItemEntriesTaxTransactions; + @Inject() + private brandingTemplatesTransformer: BrandingTemplateDTOTransformer; + /** * Transformes the create DTO to invoice object model. * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. @@ -113,11 +117,19 @@ export class CommandSaleInvoiceDTOTransformer { userId: authorizedUser.id, } as ISaleInvoice; + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + tenantId, + 'SaleInvoice' + ) + )(initialDTO); + return R.compose( this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, this.branchDTOTransform.transformDTO(tenantId), this.warehouseDTOTransform.transformDTO(tenantId) - )(initialDTO); + )(initialAsyncDTO); } /** diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts index 2bbe7e003..0146478b3 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts @@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { GetSaleInvoice } from './GetSaleInvoice'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { transformInvoiceToPdfTemplate } from './utils'; +import { InvoicePdfTemplateAttributes } from '@/interfaces'; +import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate'; @Service() export class SaleInvoicePdf { + @Inject() + private tenancy: HasTenancyService; + @Inject() private chromiumlyTenancy: ChromiumlyTenancy; @@ -14,6 +21,9 @@ export class SaleInvoicePdf { @Inject() private getInvoiceService: GetSaleInvoice; + @Inject() + private invoiceBrandingTemplateService: SaleInvoicePdfTemplate; + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - Tenant Id. @@ -24,19 +34,54 @@ export class SaleInvoicePdf { tenantId: number, invoiceId: number ): Promise { - const saleInvoice = await this.getInvoiceService.getSaleInvoice( + const brandingAttributes = await this.getInvoiceBrandingAttributes( tenantId, invoiceId ); const htmlContent = await this.templateInjectable.render( tenantId, - 'modules/invoice-regular', - { - saleInvoice, - } + 'modules/invoice-standard', + brandingAttributes ); - return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, { - margins: { top: 0, bottom: 0, left: 0, right: 0 }, - }); + // Converts the given html content to pdf document. + return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent); + } + + /** + * Retrieves the branding attributes of the given sale invoice. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + async getInvoiceBrandingAttributes( + tenantId: number, + invoiceId: number + ): Promise { + const { PdfTemplate } = this.tenancy.models(tenantId); + + const invoice = await this.getInvoiceService.getSaleInvoice( + tenantId, + invoiceId + ); + // Retrieve the invoice template id of not found get the default template id. + const templateId = + invoice.pdfTemplateId ?? + ( + await PdfTemplate.query().findOne({ + resource: 'SaleInvoice', + default: true, + }) + )?.id; + // Getting the branding template attributes. + const brandingTemplate = + await this.invoiceBrandingTemplateService.getInvoicePdfTemplate( + tenantId, + templateId + ); + // Merge the branding template attributes with the invoice. + return { + ...brandingTemplate.attributes, + ...transformInvoiceToPdfTemplate(invoice), + }; } } diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicePdfTemplate.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdfTemplate.ts new file mode 100644 index 000000000..563fd3a20 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdfTemplate.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import { mergePdfTemplateWithDefaultAttributes } from './utils'; +import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate'; +import { defaultInvoicePdfTemplateAttributes } from './constants'; + +@Service() +export class SaleInvoicePdfTemplate { + @Inject() + private getPdfTemplateService: GetPdfTemplate; + + /** + * Retrieves the invoice pdf template. + * @param {number} tenantId + * @param {number} invoiceTemplateId + * @returns + */ + async getInvoicePdfTemplate(tenantId: number, invoiceTemplateId: number){ + const template = await this.getPdfTemplateService.getPdfTemplate( + tenantId, + invoiceTemplateId + ); + const attributes = mergePdfTemplateWithDefaultAttributes( + template.attributes, + defaultInvoicePdfTemplateAttributes + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server/src/services/Sales/Invoices/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts index 4ed0e6bbd..ffb1db409 100644 --- a/packages/server/src/services/Sales/Invoices/constants.ts +++ b/packages/server/src/services/Sales/Invoices/constants.ts @@ -158,3 +158,88 @@ export const SaleInvoicesSampleData = [ Description: 'Description', }, ]; + +export const defaultInvoicePdfTemplateAttributes = { + primaryColor: 'red', + secondaryColor: 'red', + + companyName: 'Bigcapital Technology, Inc.', + + showCompanyLogo: true, + companyLogo: '', + + dueDateLabel: 'Date due', + showDueDate: true, + + dateIssueLabel: 'Date of issue', + showDateIssue: true, + + // dateIssue, + invoiceNumberLabel: 'Invoice number', + showInvoiceNumber: true, + + // Address + showBillingToAddress: true, + showBilledFromAddress: true, + billedToLabel: 'Billed To', + + // Entries + lineItemLabel: 'Item', + lineDescriptionLabel: 'Description', + lineRateLabel: 'Rate', + lineTotalLabel: 'Total', + + totalLabel: 'Total', + subtotalLabel: 'Subtotal', + discountLabel: 'Discount', + paymentMadeLabel: 'Payment Made', + balanceDueLabel: 'Balance Due', + + // Totals + showTotal: true, + showSubtotal: true, + showDiscount: true, + showTaxes: true, + showPaymentMade: true, + showDueAmount: true, + showBalanceDue: true, + + discount: '0.00', + + // Footer paragraphs. + termsConditionsLabel: 'Terms & Conditions', + showTermsConditions: true, + + lines: [ + { + item: 'Simply dummy text', + description: 'Simply dummy text of the printing and typesetting', + rate: '1', + quantity: '1000', + total: '$1000.00', + }, + ], + taxes: [ + { label: 'Sample Tax1 (4.70%)', amount: '11.75' }, + { label: 'Sample Tax2 (7.00%)', amount: '21.74' }, + ], + + statementLabel: 'Statement', + showStatement: true, + billedToAddress: [ + 'Bigcapital Technology, Inc.', + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], + billedFromAddres: [ + '131 Continental Dr Suite 305 Newark,', + 'Delaware 19713', + 'United States', + '+1 762-339-5634', + 'ahmed@bigcapital.app', + ], +} + diff --git a/packages/server/src/services/Sales/Invoices/utils.ts b/packages/server/src/services/Sales/Invoices/utils.ts new file mode 100644 index 000000000..ed8235678 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/utils.ts @@ -0,0 +1,43 @@ +import { pickBy } from 'lodash'; +import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces'; + +export const mergePdfTemplateWithDefaultAttributes = ( + brandingTemplate?: Record, + defaultAttributes: Record = {} +) => { + const brandingAttributes = pickBy( + brandingTemplate, + (val, key) => val !== null && Object.keys(defaultAttributes).includes(key) + ); + + return { + ...defaultAttributes, + ...brandingAttributes, + }; +}; + +export const transformInvoiceToPdfTemplate = ( + invoice: ISaleInvoice +): Partial => { + return { + dueDate: invoice.dueDateFormatted, + dateIssue: invoice.invoiceDateFormatted, + invoiceNumber: invoice.invoiceNo, + + total: invoice.totalFormatted, + subtotal: invoice.subtotalFormatted, + paymentMade: invoice.paymentAmountFormatted, + balanceDue: invoice.balanceAmountFormatted, + + termsConditions: invoice.termsConditions, + statement: invoice.invoiceMessage, + + lines: invoice.entries.map((entry) => ({ + item: entry.item.name, + description: entry.description, + rate: entry.rateFormatted, + quantity: entry.quantityFormatted, + total: entry.totalFormatted, + })), + }; +}; diff --git a/packages/server/src/services/TemplateInjectable/TemplateInjectable.ts b/packages/server/src/services/TemplateInjectable/TemplateInjectable.ts index fd2c965a8..31e059ae3 100644 --- a/packages/server/src/services/TemplateInjectable/TemplateInjectable.ts +++ b/packages/server/src/services/TemplateInjectable/TemplateInjectable.ts @@ -17,7 +17,7 @@ export class TemplateInjectable { public async render( tenantId: number, filename: string, - options: Record + options: Record ) { const i18n = this.tenancy.i18n(tenantId); diff --git a/packages/webapp/src/components/Forms/Select.tsx b/packages/webapp/src/components/Forms/Select.tsx index c77c51ce2..93d6fc8ed 100644 --- a/packages/webapp/src/components/Forms/Select.tsx +++ b/packages/webapp/src/components/Forms/Select.tsx @@ -6,16 +6,14 @@ import styled from 'styled-components'; import clsx from 'classnames'; export function FSelect({ ...props }) { - const input = ({ activeItem, text, label, value }) => { - return ( - - ); - }; + const input = ({ activeItem, text, label, value }) => ( + + ); return