diff --git a/packages/server/resources/scss/modules/export-resource-table.scss b/packages/server/resources/scss/modules/export-resource-table.scss new file mode 100644 index 000000000..aa2c5497b --- /dev/null +++ b/packages/server/resources/scss/modules/export-resource-table.scss @@ -0,0 +1,38 @@ +@import "../base.scss"; + +body { + font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 12px; + line-height: 1.4; + margin: 0; +} +.sheet__title{ + margin-bottom: 18px; +} +.sheet__title h2{ + line-height: 1; + margin-top: 0; + margin-bottom: 10px; + font-size: 16px; +} +.sheet__table { + font-size: inherit; + line-height: inherit; + width: 100%; +} +.sheet__table { + table-layout: auto; + border-collapse: collapse; + width: 100%; +} +.sheet__table thead tr th { + border-top: 1px solid #000; + border-bottom: 1px solid #000; + background: #fff; + padding: 8px; + line-height: 1.2; +} +.sheet__table tbody tr td { + padding: 4px 8px; + border-bottom: 1px solid #CCC; +} \ No newline at end of file diff --git a/packages/server/resources/views/modules/export-resource-table.pug b/packages/server/resources/views/modules/export-resource-table.pug new file mode 100644 index 000000000..1d1486586 --- /dev/null +++ b/packages/server/resources/views/modules/export-resource-table.pug @@ -0,0 +1,24 @@ +block head + style + include ../../css/modules/export-resource-table.css + +style. + !{customCSS} + +block content + .sheet + .sheet__title + h2.sheetTitle= sheetTitle + p.sheetDesc= sheetDescription + + table.sheet__table + thead + tr + each column in table.columns + th(style=column.style class='column--' + column.key)= column.name + tbody + each row in table.rows + tr(class=row.classNames) + each cell in row.cells + td(class='cell--' + cell.key) + span!= cell.value \ No newline at end of file diff --git a/packages/server/scripts/gulpConfig.js b/packages/server/scripts/gulpConfig.js index 1caa9af23..92324d0cc 100644 --- a/packages/server/scripts/gulpConfig.js +++ b/packages/server/scripts/gulpConfig.js @@ -70,6 +70,10 @@ module.exports = { src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`, dest: `${RESOURCES_PATH}/css/modules`, }, + { + src: `${RESOURCES_PATH}/scss/modules/export-resource-table.scss`, + dest: `${RESOURCES_PATH}/css/modules`, + }, ], // RTL builds. rtl: [ diff --git a/packages/server/src/api/controllers/Export/ExportController.ts b/packages/server/src/api/controllers/Export/ExportController.ts index 8ef90ca53..7ba493453 100644 --- a/packages/server/src/api/controllers/Export/ExportController.ts +++ b/packages/server/src/api/controllers/Export/ExportController.ts @@ -5,6 +5,7 @@ import BaseController from '@/api/controllers/BaseController'; import { ServiceError } from '@/exceptions'; import { ExportApplication } from '@/services/Export/ExportApplication'; import { ACCEPT_TYPE } from '@/interfaces/Http'; +import { convertAcceptFormatToFormat } from './_utils'; @Service() export class ExportController extends BaseController { @@ -25,7 +26,6 @@ export class ExportController extends BaseController { ], this.validationResult, this.export.bind(this), - this.catchServiceErrors ); return router; } @@ -48,10 +48,12 @@ export class ExportController extends BaseController { ACCEPT_TYPE.APPLICATION_CSV, ACCEPT_TYPE.APPLICATION_PDF, ]); + const applicationFormat = convertAcceptFormatToFormat(acceptType); + const data = await this.exportResourceApp.export( tenantId, query.resource, - acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv' + applicationFormat ); // Retrieves the csv format. if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { @@ -70,31 +72,16 @@ export class ExportController extends BaseController { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ); return res.send(data); + // + } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Length': data.length, + }); + res.send(data); } } catch (error) { next(error); } } - - /** - * Transforms service errors to response. - * @param {Error} - * @param {Request} req - * @param {Response} res - * @param {ServiceError} error - */ - private catchServiceErrors( - error, - req: Request, - res: Response, - next: NextFunction - ) { - if (error instanceof ServiceError) { - return res.status(400).send({ - errors: [{ type: error.errorType }], - }); - } - - next(error); - } } diff --git a/packages/server/src/api/controllers/Export/_utils.ts b/packages/server/src/api/controllers/Export/_utils.ts new file mode 100644 index 000000000..30758c72e --- /dev/null +++ b/packages/server/src/api/controllers/Export/_utils.ts @@ -0,0 +1,13 @@ +import { ACCEPT_TYPE } from '@/interfaces/Http'; +import { ExportFormat } from '@/services/Export/common'; + +export const convertAcceptFormatToFormat = (accept: string): ExportFormat => { + switch (accept) { + case ACCEPT_TYPE.APPLICATION_CSV: + return ExportFormat.Csv; + case ACCEPT_TYPE.APPLICATION_PDF: + return ExportFormat.Pdf; + case ACCEPT_TYPE.APPLICATION_XLSX: + return ExportFormat.Xlsx; + } +}; diff --git a/packages/server/src/database/migrations/20231108170207_create_documents_table.js b/packages/server/src/database/migrations/20231108170207_create_documents_table.js index 8ae0cc542..44e2f269c 100644 --- a/packages/server/src/database/migrations/20231108170207_create_documents_table.js +++ b/packages/server/src/database/migrations/20231108170207_create_documents_table.js @@ -3,7 +3,7 @@ exports.up = function (knex) { table.increments('id').primary(); table.string('key').notNullable(); table.string('mime_type').notNullable(); - table.integer('size').unsigned().notNullable(); + table.integer('size').unsigned(); table.string('origin_name'); table.timestamps(); }); diff --git a/packages/server/src/interfaces/Model.ts b/packages/server/src/interfaces/Model.ts index 93bb1f7fc..bb6e7720b 100644 --- a/packages/server/src/interfaces/Model.ts +++ b/packages/server/src/interfaces/Model.ts @@ -122,6 +122,10 @@ export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon & export type IModelMetaRelationField = IModelMetaRelationFieldCommon & IModelMetaRelationEnumerationField; +interface IModelPrintMeta{ + pageTitle: string; +} + export interface IModelMeta { defaultFilterField: string; defaultSort: IModelMetaDefaultSort; @@ -134,6 +138,8 @@ export interface IModelMeta { importAggregateOn?: string; importAggregateBy?: string; + print?: IModelPrintMeta; + fields: { [key: string]: IModelMetaField }; columns: { [key: string]: IModelMetaColumn }; } diff --git a/packages/server/src/models/Account.Settings.ts b/packages/server/src/models/Account.Settings.ts index 7be8cf404..0d4690041 100644 --- a/packages/server/src/models/Account.Settings.ts +++ b/packages/server/src/models/Account.Settings.ts @@ -8,6 +8,9 @@ export default { }, importable: true, exportable: true, + print: { + pageTitle: 'Chart of Accounts', + }, fields: { name: { name: 'account.field.name', @@ -121,7 +124,7 @@ export default { }, balance: { name: 'account.field.balance', - accessor: 'amount', + accessor: 'formattedAmount', }, description: { name: 'account.field.description', @@ -133,6 +136,7 @@ export default { }, createdAt: { name: 'account.field.created_at', + printable: false, }, }, fields2: { diff --git a/packages/server/src/models/Bill.Settings.ts b/packages/server/src/models/Bill.Settings.ts index 890a9635a..0d9e2bda6 100644 --- a/packages/server/src/models/Bill.Settings.ts +++ b/packages/server/src/models/Bill.Settings.ts @@ -10,6 +10,9 @@ export default { importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'billNumber', + print: { + pageTitle: 'Bills', + }, fields: { vendor: { name: 'bill.field.vendor', @@ -83,6 +86,10 @@ export default { }, }, columns: { + billDate: { + name: 'Date', + accessor: 'formattedBillDate', + }, billNumber: { name: 'Bill No.', type: 'text', @@ -91,13 +98,10 @@ export default { name: 'Reference No.', type: 'text', }, - billDate: { - name: 'Date', - type: 'date', - }, dueDate: { name: 'Due Date', type: 'date', + accessor: 'formattedDueDate', }, vendorId: { name: 'Vendor', @@ -111,10 +115,12 @@ export default { exchangeRate: { name: 'Exchange Rate', type: 'number', + printable: false, }, currencyCode: { name: 'Currency Code', type: 'text', + printable: false, }, dueAmount: { name: 'Due Amount', @@ -127,10 +133,12 @@ export default { note: { name: 'Note', type: 'text', + printable: false, }, open: { name: 'Open', type: 'boolean', + printable: false, }, entries: { name: 'Entries', diff --git a/packages/server/src/models/BillPayment.Settings.ts b/packages/server/src/models/BillPayment.Settings.ts index 4d7de9239..d255ac3d2 100644 --- a/packages/server/src/models/BillPayment.Settings.ts +++ b/packages/server/src/models/BillPayment.Settings.ts @@ -77,6 +77,7 @@ export default { paymentDate: { name: 'bill_payment.field.payment_date', type: 'date', + accessor: 'formattedPaymentDate' }, paymentNumber: { name: 'bill_payment.field.payment_number', @@ -94,14 +95,17 @@ export default { currencyCode: { name: 'Currency Code', type: 'text', + printable: false, }, exchangeRate: { name: 'bill_payment.field.exchange_rate', type: 'number', + printable: false, }, statement: { name: 'bill_payment.field.note', type: 'text', + printable: false, }, reference: { name: 'bill_payment.field.reference', diff --git a/packages/server/src/models/CreditNote.Meta.ts b/packages/server/src/models/CreditNote.Meta.ts index 5da0c1d9e..95e8595b9 100644 --- a/packages/server/src/models/CreditNote.Meta.ts +++ b/packages/server/src/models/CreditNote.Meta.ts @@ -20,6 +20,10 @@ export default { importAggregateOn: 'entries', importAggregateBy: 'creditNoteNumber', + print: { + pageTitle: 'Credit Notes', + }, + fields: { customer: { name: 'credit_note.field.customer', @@ -88,36 +92,34 @@ export default { columns: { customer: { name: 'Customer', - type: 'relation', accessor: 'customer.displayName', }, exchangeRate: { name: 'Exchange Rate', - type: 'number', + printable: false, }, creditNoteDate: { name: 'Credit Note Date', - type: 'date', + accessor: 'formattedCreditNoteDate' }, referenceNo: { name: 'Reference No.', - type: 'text', }, note: { name: 'Note', - type: 'text', }, termsConditions: { name: 'Terms & Conditions', - type: 'text', + printable: false, }, creditNoteNumber: { name: 'Credit Note Number', - type: 'text', + printable: false, }, open: { name: 'Open', type: 'boolean', + printable: false, }, entries: { name: 'Entries', diff --git a/packages/server/src/models/Customer.Settings.ts b/packages/server/src/models/Customer.Settings.ts index 71f631032..96e75c017 100644 --- a/packages/server/src/models/Customer.Settings.ts +++ b/packages/server/src/models/Customer.Settings.ts @@ -6,6 +6,9 @@ export default { sortOrder: 'DESC', sortField: 'created_at', }, + print: { + pageTitle: 'Customers', + }, fields: { first_name: { name: 'vendor.field.first_name', @@ -127,100 +130,121 @@ export default { balance: { name: 'vendor.field.balance', type: 'number', + accessor: 'formattedBalance', }, openingBalance: { name: 'vendor.field.opening_balance', type: 'number', + printable: false }, openingBalanceAt: { name: 'vendor.field.opening_balance_at', type: 'date', + printable: false }, currencyCode: { name: 'vendor.field.currency', type: 'text', + printable: false }, status: { name: 'vendor.field.status', + printable: false }, note: { name: 'vendor.field.note', + printable: false }, // Billing Address billingAddress1: { name: 'Billing Address 1', column: 'billing_address1', type: 'text', + printable: false }, billingAddress2: { name: 'Billing Address 2', column: 'billing_address2', type: 'text', + printable: false }, billingAddressCity: { name: 'Billing Address City', column: 'billing_address_city', type: 'text', + printable: false }, billingAddressCountry: { name: 'Billing Address Country', column: 'billing_address_country', type: 'text', + printable: false }, billingAddressPostcode: { name: 'Billing Address Postcode', column: 'billing_address_postcode', type: 'text', + printable: false }, billingAddressState: { name: 'Billing Address State', column: 'billing_address_state', type: 'text', + printable: false }, billingAddressPhone: { name: 'Billing Address Phone', column: 'billing_address_phone', type: 'text', + printable: false }, // Shipping Address shippingAddress1: { name: 'Shipping Address 1', column: 'shipping_address1', type: 'text', + printable: false }, shippingAddress2: { name: 'Shipping Address 2', column: 'shipping_address2', type: 'text', + printable: false }, shippingAddressCity: { name: 'Shipping Address City', column: 'shipping_address_city', type: 'text', + printable: false }, shippingAddressCountry: { name: 'Shipping Address Country', column: 'shipping_address_country', type: 'text', + printable: false }, shippingAddressPostcode: { name: 'Shipping Address Postcode', column: 'shipping_address_postcode', type: 'text', + printable: false }, shippingAddressPhone: { name: 'Shipping Address Phone', column: 'shipping_address_phone', type: 'text', + printable: false }, shippingAddressState: { name: 'Shipping Address State', column: 'shipping_address_state', type: 'text', + printable: false }, createdAt: { name: 'vendor.field.created_at', type: 'date', + printable: false }, }, fields2: { diff --git a/packages/server/src/models/Expense.Settings.ts b/packages/server/src/models/Expense.Settings.ts index 12c539782..0ba73c4d9 100644 --- a/packages/server/src/models/Expense.Settings.ts +++ b/packages/server/src/models/Expense.Settings.ts @@ -10,6 +10,9 @@ export default { importable: true, exportFlattenOn: 'categories', exportable: true, + print: { + pageTitle: 'Expenses', + }, fields: { payment_date: { name: 'expense.field.payment_date', @@ -67,7 +70,7 @@ export default { paymentReceive: { name: 'expense.field.payment_account', type: 'text', - accessor: 'paymentAccount.name' + accessor: 'paymentAccount.name', }, referenceNo: { name: 'expense.field.reference_no', @@ -75,15 +78,18 @@ export default { }, paymentDate: { name: 'expense.field.payment_date', + accessor: 'formattedDate', type: 'date', }, currencyCode: { name: 'expense.field.currency_code', type: 'text', + printable: false, }, exchangeRate: { name: 'expense.field.exchange_rate', type: 'number', + printable: false, }, description: { name: 'expense.field.description', @@ -111,6 +117,7 @@ export default { publish: { name: 'expense.field.publish', type: 'boolean', + printable: false, }, }, fields2: { diff --git a/packages/server/src/models/Item.Settings.ts b/packages/server/src/models/Item.Settings.ts index 9c8a50ce8..af8968366 100644 --- a/packages/server/src/models/Item.Settings.ts +++ b/packages/server/src/models/Item.Settings.ts @@ -6,6 +6,9 @@ export default { sortField: 'name', sortOrder: 'DESC', }, + print: { + pageTitle: 'Items', + }, fields: { type: { name: 'item.field.type', @@ -127,6 +130,7 @@ export default { name: 'item.field.type', type: 'text', exportable: true, + accessor: 'typeFormatted', }, name: { name: 'item.field.name', @@ -142,11 +146,13 @@ export default { name: 'item.field.sellable', type: 'boolean', exportable: true, + printable: false, }, purchasable: { name: 'item.field.purchasable', type: 'boolean', exportable: true, + printable: false, }, sellPrice: { name: 'item.field.cost_price', @@ -163,12 +169,14 @@ export default { type: 'text', accessor: 'costAccount.name', exportable: true, + printable: false, }, sellAccount: { name: 'item.field.sell_description', type: 'text', accessor: 'sellAccount.name', exportable: true, + printable: false, }, inventoryAccount: { name: 'item.field.inventory_account', @@ -180,11 +188,13 @@ export default { name: 'Sell description', type: 'text', exportable: true, + printable: false, }, purchaseDescription: { name: 'Purchase description', type: 'text', exportable: true, + printable: false, }, quantityOnHand: { name: 'item.field.quantity_on_hand', @@ -206,11 +216,13 @@ export default { name: 'item.field.active', fieldType: 'boolean', exportable: true, + printable: false, }, createdAt: { name: 'item.field.created_at', type: 'date', exportable: true, + printable: false, }, }, fields2: { diff --git a/packages/server/src/models/ManualJournal.Settings.ts b/packages/server/src/models/ManualJournal.Settings.ts index db2712220..84ce31367 100644 --- a/packages/server/src/models/ManualJournal.Settings.ts +++ b/packages/server/src/models/ManualJournal.Settings.ts @@ -11,6 +11,11 @@ export default { importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'journalNumber', + + print: { + pageTitle: 'Manual Journals', + }, + fields: { date: { name: 'manual_journal.field.date', @@ -63,6 +68,7 @@ export default { date: { name: 'manual_journal.field.date', type: 'date', + accessor: 'formattedDate', }, journalNumber: { name: 'manual_journal.field.journal_number', @@ -83,10 +89,12 @@ export default { currencyCode: { name: 'manual_journal.field.currency', type: 'text', + printable: false, }, exchangeRate: { name: 'manual_journal.field.exchange_rate', type: 'number', + printable: false, }, description: { name: 'manual_journal.field.description', @@ -120,13 +128,17 @@ export default { publish: { name: 'Publish', type: 'boolean', + printable: false, }, publishedAt: { name: 'Published At', + printable: false, }, }, createdAt: { name: 'Created At', + accessor: 'formattedCreatedAt', + printable: false, }, }, fields2: { diff --git a/packages/server/src/models/PaymentReceive.Settings.ts b/packages/server/src/models/PaymentReceive.Settings.ts index 663b5884d..15d287d66 100644 --- a/packages/server/src/models/PaymentReceive.Settings.ts +++ b/packages/server/src/models/PaymentReceive.Settings.ts @@ -67,10 +67,12 @@ export default { paymentDate: { name: 'payment_receive.field.payment_date', type: 'date', + accessor: 'formattedPaymentDate', }, amount: { name: 'payment_receive.field.amount', type: 'number', + accessor: 'formattedAmount' }, referenceNo: { name: 'payment_receive.field.reference_no', @@ -88,10 +90,12 @@ export default { statement: { name: 'payment_receive.field.statement', type: 'text', + printable: false, }, created_at: { name: 'payment_receive.field.created_at', type: 'date', + printable: false, }, }, fields2: { diff --git a/packages/server/src/models/SaleEstimate.Settings.ts b/packages/server/src/models/SaleEstimate.Settings.ts index a9577b4f4..5462e9717 100644 --- a/packages/server/src/models/SaleEstimate.Settings.ts +++ b/packages/server/src/models/SaleEstimate.Settings.ts @@ -11,6 +11,11 @@ export default { importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'estimateNumber', + + print: { + pageTitle: 'Sale Estimates' + }, + fields: { amount: { name: 'estimate.field.amount', @@ -86,11 +91,13 @@ export default { estimateDate: { name: 'Estimate Date', type: 'date', + accessor: 'formattedEstimateDate', exportable: true, }, expirationDate: { name: 'Expiration Date', type: 'date', + accessor: 'formattedExpirationDate', exportable: true, }, estimateNumber: { @@ -112,26 +119,31 @@ export default { name: 'Exchange Rate', type: 'number', exportable: true, + printable: false, }, currencyCode: { name: 'Currency', type: 'text', exportable: true, + printable: false, }, note: { name: 'Note', type: 'text', exportable: true, + printable: false, }, termsConditions: { name: 'Terms & Conditions', type: 'text', exportable: true, + printable: false, }, delivered: { name: 'Delivered', type: 'boolean', exportable: true, + printable: false, }, entries: { name: 'Entries', @@ -153,6 +165,7 @@ export default { }, description: { name: 'Item Description', + printable: false, }, amount: { name: 'Item Amount', diff --git a/packages/server/src/models/SaleInvoice.Settings.ts b/packages/server/src/models/SaleInvoice.Settings.ts index 24728522e..da6719ba2 100644 --- a/packages/server/src/models/SaleInvoice.Settings.ts +++ b/packages/server/src/models/SaleInvoice.Settings.ts @@ -11,6 +11,10 @@ export default { importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'invoiceNo', + + print: { + pageTitle: 'Sale invoices', + }, fields: { customer: { name: 'invoice.field.customer', @@ -94,10 +98,12 @@ export default { invoiceDate: { name: 'invoice.field.invoice_date', type: 'date', + accessor: 'invoiceDateFormatted', }, dueDate: { name: 'invoice.field.due_date', type: 'date', + accessor: 'dueDateFormatted', }, referenceNo: { name: 'invoice.field.reference_no', @@ -120,10 +126,12 @@ export default { exchangeRate: { name: 'invoice.field.exchange_rate', type: 'number', + printable: false, }, currencyCode: { name: 'invoice.field.currency', type: 'text', + printable: false, }, paidAmount: { name: 'Paid Amount', @@ -136,14 +144,17 @@ export default { invoiceMessage: { name: 'invoice.field.invoice_message', type: 'text', + printable: false, }, termsConditions: { name: 'invoice.field.terms_conditions', type: 'text', + printable: false, }, delivered: { name: 'invoice.field.delivered', type: 'boolean', + printable: false, }, entries: { name: 'Entries', @@ -165,6 +176,7 @@ export default { }, description: { name: 'Item Description', + printable: false, }, amount: { name: 'Item Amount', @@ -202,18 +214,22 @@ export default { exchangeRate: { name: 'invoice.field.exchange_rate', fieldType: 'number', + printable: false, }, currencyCode: { name: 'invoice.field.currency', fieldType: 'text', + printable: false, }, invoiceMessage: { name: 'invoice.field.invoice_message', fieldType: 'text', + printable: false, }, termsConditions: { name: 'invoice.field.terms_conditions', fieldType: 'text', + printable: false, }, entries: { name: 'invoice.field.entries', @@ -249,6 +265,7 @@ export default { delivered: { name: 'invoice.field.delivered', fieldType: 'boolean', + printable: false, }, }, }; diff --git a/packages/server/src/models/SaleReceipt.Settings.ts b/packages/server/src/models/SaleReceipt.Settings.ts index 3fecd0480..54230128b 100644 --- a/packages/server/src/models/SaleReceipt.Settings.ts +++ b/packages/server/src/models/SaleReceipt.Settings.ts @@ -11,6 +11,10 @@ export default { importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'receiptNumber', + + print: { + pageTitle: 'Sale Receipts', + }, fields: { amount: { name: 'receipt.field.amount', @@ -81,11 +85,6 @@ export default { }, }, columns: { - amount: { - name: 'receipt.field.amount', - column: 'amount', - type: 'number', - }, depositAccount: { name: 'receipt.field.deposit_account', type: 'text', @@ -98,6 +97,7 @@ export default { }, receiptDate: { name: 'receipt.field.receipt_date', + accessor: 'formattedReceiptDate', type: 'date', }, receiptNumber: { @@ -114,10 +114,17 @@ export default { name: 'receipt.field.receipt_message', column: 'receipt_message', type: 'text', + printable: false, + }, + amount: { + name: 'receipt.field.amount', + accessor: 'formattedAmount', + type: 'number', }, statement: { name: 'receipt.field.statement', type: 'text', + printable: false, }, status: { name: 'receipt.field.status', @@ -127,6 +134,7 @@ export default { { key: 'closed', label: 'receipt.field.status.closed' }, ], exportable: true, + printable: false, }, entries: { name: 'Entries', @@ -148,6 +156,7 @@ export default { }, description: { name: 'Item Description', + printable: false, }, amount: { name: 'Item Amount', @@ -158,6 +167,7 @@ export default { createdAt: { name: 'receipt.field.created_at', type: 'date', + printable: false, }, }, fields2: { diff --git a/packages/server/src/models/Vendor.Settings.ts b/packages/server/src/models/Vendor.Settings.ts index 7681dfa10..cf007f278 100644 --- a/packages/server/src/models/Vendor.Settings.ts +++ b/packages/server/src/models/Vendor.Settings.ts @@ -131,21 +131,26 @@ export default { openingBalance: { name: 'vendor.field.opening_balance', type: 'number', + printable: false }, openingBalanceAt: { name: 'vendor.field.opening_balance_at', type: 'date', + printable: false }, currencyCode: { name: 'vendor.field.currency', type: 'text', + printable: false }, status: { name: 'vendor.field.status', + printable: false }, note: { name: 'vendor.field.note', type: 'text', + printable: false }, // Billing Address billingAddress1: { @@ -153,42 +158,49 @@ export default { column: 'billing_address1', type: 'text', exportable: true, + printable: false }, billingAddress2: { name: 'Billing Address 2', column: 'billing_address2', type: 'text', exportable: true, + printable: false }, billingAddressCity: { name: 'Billing Address City', column: 'billing_address_city', type: 'text', exportable: true, + printable: false }, billingAddressCountry: { name: 'Billing Address Country', column: 'billing_address_country', type: 'text', exportable: true, + printable: false }, billingAddressPostcode: { name: 'Billing Address Postcode', column: 'billing_address_postcode', type: 'text', exportable: true, + printable: false }, billingAddressState: { name: 'Billing Address State', column: 'billing_address_state', type: 'text', exportable: true, + printable: false }, billingAddressPhone: { name: 'Billing Address Phone', column: 'billing_address_phone', type: 'text', exportable: true, + printable: false }, // Shipping Address shippingAddress1: { @@ -196,47 +208,55 @@ export default { column: 'shipping_address1', type: 'text', exportable: true, + printable: false }, shippingAddress2: { name: 'Shipping Address 2', column: 'shipping_address2', type: 'text', exportable: true, + printable: false }, shippingAddressCity: { name: 'Shipping Address City', column: 'shipping_address_city', type: 'text', exportable: true, + printable: false }, shippingAddressCountry: { name: 'Shipping Address Country', column: 'shipping_address_country', type: 'text', exportable: true, + printable: false }, shippingAddressPostcode: { name: 'Shipping Address Postcode', column: 'shipping_address_postcode', type: 'text', exportable: true, + printable: false }, shippingAddressState: { name: 'Shipping Address State', column: 'shipping_address_state', type: 'text', exportable: true, + printable: false }, shippingAddressPhone: { name: 'Shipping Address Phone', column: 'shipping_address_phone', type: 'text', exportable: true, + printable: false }, createdAt: { name: 'vendor.field.created_at', type: 'date', exportable: true, + printable: false }, }, fields2: { diff --git a/packages/server/src/models/VendorCredit.Meta.ts b/packages/server/src/models/VendorCredit.Meta.ts index b57cc275c..3834bc5ec 100644 --- a/packages/server/src/models/VendorCredit.Meta.ts +++ b/packages/server/src/models/VendorCredit.Meta.ts @@ -20,6 +20,9 @@ export default { importAggregateOn: 'entries', importAggregateBy: 'vendorCreditNumber', + print: { + pageTitle: 'Vendor Credits', + }, fields: { vendor: { name: 'vendor_credit.field.vendor', @@ -89,6 +92,7 @@ export default { exchangeRate: { name: 'Echange Rate', type: 'text', + printable: false, }, vendorCreditNumber: { name: 'Vendor Credit No.', @@ -100,7 +104,7 @@ export default { }, vendorCreditDate: { name: 'Vendor Credit Date', - type: 'date', + accessor: 'formattedVendorCreditDate', }, amount: { name: 'Amount', @@ -109,10 +113,12 @@ export default { creditRemaining: { name: 'Credits Remaining', accessor: 'formattedCreditsRemaining', + printable: false, }, refundedAmount: { name: 'Refunded Amount', accessor: 'refundedAmount', + printable: false, }, invoicedAmount: { name: 'Invoiced Amount', @@ -121,10 +127,12 @@ export default { note: { name: 'Note', type: 'text', + printable: false, }, open: { name: 'Open', type: 'boolean', + printable: false, }, entries: { name: 'Entries', diff --git a/packages/server/src/services/ChromiumlyTenancy/ChromiumlyHtmlConvert.ts b/packages/server/src/services/ChromiumlyTenancy/ChromiumlyHtmlConvert.ts index f6e90e3e4..51cf77f7c 100644 --- a/packages/server/src/services/ChromiumlyTenancy/ChromiumlyHtmlConvert.ts +++ b/packages/server/src/services/ChromiumlyTenancy/ChromiumlyHtmlConvert.ts @@ -5,7 +5,11 @@ import { PageProperties, PdfFormat } from '@/lib/Chromiumly/_types'; import { UrlConverter } from '@/lib/Chromiumly/UrlConvert'; import HasTenancyService from '../Tenancy/TenancyService'; import { Chromiumly } from '@/lib/Chromiumly/Chromiumly'; -import { PDF_FILE_EXPIRE_IN, getPdfFilesStorageDir } from './utils'; +import { + PDF_FILE_EXPIRE_IN, + getPdfFilePath, + getPdfFilesStorageDir, +} from './utils'; @Service() export class ChromiumlyHtmlConvert { @@ -22,22 +26,16 @@ export class ChromiumlyHtmlConvert { tenantId: number, content: string ): Promise<[string, () => Promise]> { - const { Attachment } = this.tenancy.models(tenantId); + const { Document } = this.tenancy.models(tenantId); - const filename = `document-${Date.now()}.html`; - const storageDir = getPdfFilesStorageDir(filename); - const filePath = path.join(global.__storage_dir, storageDir); + const filename = `document-print-${Date.now()}.html`; + const filePath = getPdfFilePath(filename); await fs.writeFile(filePath, content); - await Attachment.query().insert({ - key: filename, - path: storageDir, - expire_in: PDF_FILE_EXPIRE_IN, // ms - extension: 'html', - }); + await Document.query().insert({ key: filename, mimeType: 'text/html' }); const cleanup = async () => { await fs.unlink(filePath); - await Attachment.query().where('key', filename).delete(); + await Document.query().where('key', filename).delete(); }; return [filename, cleanup]; } @@ -60,6 +58,7 @@ export class ChromiumlyHtmlConvert { html ); const fileDir = getPdfFilesStorageDir(filename); + const url = path.join(Chromiumly.GOTENBERG_DOCS_ENDPOINT, fileDir); const urlConverter = new UrlConverter(); diff --git a/packages/server/src/services/ChromiumlyTenancy/utils.ts b/packages/server/src/services/ChromiumlyTenancy/utils.ts index d7e5f1223..fd7bf7bce 100644 --- a/packages/server/src/services/ChromiumlyTenancy/utils.ts +++ b/packages/server/src/services/ChromiumlyTenancy/utils.ts @@ -5,4 +5,10 @@ export const PDF_FILE_EXPIRE_IN = 40; // ms export const getPdfFilesStorageDir = (filename: string) => { return path.join(PDF_FILE_SUB_DIR, filename); -} \ No newline at end of file +}; + +export const getPdfFilePath = (filename: string) => { + const storageDir = getPdfFilesStorageDir(filename); + + return path.join(global.__storage_dir, storageDir); +}; diff --git a/packages/server/src/services/Export/ExportApplication.ts b/packages/server/src/services/Export/ExportApplication.ts index 44a7dc73f..490c788d2 100644 --- a/packages/server/src/services/Export/ExportApplication.ts +++ b/packages/server/src/services/Export/ExportApplication.ts @@ -1,5 +1,6 @@ import { Inject, Service } from 'typedi'; import { ExportResourceService } from './ExportService'; +import { ExportFormat } from './common'; @Service() export class ExportApplication { @@ -9,9 +10,9 @@ export class ExportApplication { /** * Exports the given resource to csv, xlsx or pdf format. * @param {string} reosurce - * @param {string} format + * @param {ExportFormat} format */ - public export(tenantId: number, resource: string, format: string) { + public export(tenantId: number, resource: string, format: ExportFormat) { return this.exportResource.export(tenantId, resource, format); } } diff --git a/packages/server/src/services/Export/ExportPdf.ts b/packages/server/src/services/Export/ExportPdf.ts new file mode 100644 index 000000000..f6d2e1f27 --- /dev/null +++ b/packages/server/src/services/Export/ExportPdf.ts @@ -0,0 +1,47 @@ +import { Inject, Service } from 'typedi'; +import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy'; +import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable'; +import { mapPdfRows } from './utils'; + +@Service() +export class ExportPdf { + @Inject() + private templateInjectable: TemplateInjectable; + + @Inject() + private chromiumlyTenancy: ChromiumlyTenancy; + + /** + * Generates the pdf table sheet for the given data and columns. + * @param {number} tenantId + * @param {} columns + * @param {Record} data + * @param {string} sheetTitle + * @param {string} sheetDescription + * @returns + */ + public async pdf( + tenantId: number, + columns: { accessor: string }, + data: Record, + sheetTitle: string = '', + sheetDescription: string = '' + ) { + const rows = mapPdfRows(columns, data); + + const htmlContent = await this.templateInjectable.render( + tenantId, + 'modules/export-resource-table', + { + table: { rows, columns }, + sheetTitle, + sheetDescription, + } + ); + // Convert the HTML content to PDF + return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, { + margins: { top: 0.2, bottom: 0.2, left: 0.2, right: 0.2 }, + landscape: true, + }); + } +} diff --git a/packages/server/src/services/Export/ExportService.ts b/packages/server/src/services/Export/ExportService.ts index c9c3dc432..eec34c959 100644 --- a/packages/server/src/services/Export/ExportService.ts +++ b/packages/server/src/services/Export/ExportService.ts @@ -6,9 +6,10 @@ import { sanitizeResourceName } from '../Import/_utils'; import ResourceService from '../Resource/ResourceService'; import { ExportableResources } from './ExportResources'; import { ServiceError } from '@/exceptions'; -import { Errors } from './common'; +import { Errors, ExportFormat } from './common'; import { IModelMeta, IModelMetaColumn } from '@/interfaces'; import { flatDataCollections, getDataAccessor } from './utils'; +import { ExportPdf } from './ExportPdf'; @Service() export class ExportResourceService { @@ -18,13 +19,20 @@ export class ExportResourceService { @Inject() private exportableResources: ExportableResources; + @Inject() + private exportPdf: ExportPdf; + /** * Exports the given resource data through csv, xlsx or pdf. * @param {number} tenantId - Tenant id. * @param {string} resourceName - Resource name. - * @param {string} format - File format. + * @param {ExportFormat} format - File format. */ - public async export(tenantId: number, resourceName: string, format: string = 'csv') { + public async export( + tenantId: number, + resourceName: string, + format: ExportFormat = ExportFormat.Csv + ) { const resource = sanitizeResourceName(resourceName); const resourceMeta = this.getResourceMeta(tenantId, resource); @@ -32,10 +40,24 @@ export class ExportResourceService { const data = await this.getExportableData(tenantId, resource); const transformed = this.transformExportedData(tenantId, resource, data); - const exportableColumns = this.getExportableColumns(resourceMeta); - const workbook = this.createWorkbook(transformed, exportableColumns); - return this.exportWorkbook(workbook, format); + // Returns the csv, xlsx format. + if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) { + const exportableColumns = this.getExportableColumns(resourceMeta); + const workbook = this.createWorkbook(transformed, exportableColumns); + + return this.exportWorkbook(workbook, format); + // Returns the pdf format. + } else if (format === ExportFormat.Pdf) { + const printableColumns = this.getPrintableColumns(resourceMeta); + + return this.exportPdf.pdf( + tenantId, + printableColumns, + transformed, + resourceMeta?.print?.pageTitle + ); + } } /** @@ -91,6 +113,7 @@ export class ExportResourceService { private async getExportableData(tenantId: number, resource: string) { const exportable = this.exportableResources.registry.getExportable(resource); + return exportable.exportable(tenantId, {}); } @@ -125,6 +148,32 @@ export class ExportResourceService { return processColumns(resourceMeta.columns); } + private getPrintableColumns(resourceMeta: IModelMeta) { + const processColumns = ( + columns: { [key: string]: IModelMetaColumn }, + parent = '' + ) => { + return Object.entries(columns) + .filter(([_, value]) => value.printable !== false) + .flatMap(([key, value]) => { + if (value.type === 'collection' && value.collectionOf === 'object') { + return processColumns(value.columns, key); + } else { + const group = parent; + return [ + { + name: value.name, + type: value.type || 'text', + accessor: value.accessor || key, + group, + }, + ]; + } + }); + }; + return processColumns(resourceMeta.columns); + } + /** * Creates a workbook from the provided data and columns. * @param {any[]} data - The data to be included in the workbook. @@ -136,7 +185,6 @@ export class ExportResourceService { const worksheetData = data.map((item) => exportableColumns.map((col) => get(item, getDataAccessor(col))) ); - worksheetData.unshift(exportableColumns.map((col) => col.name)); const worksheet = xlsx.utils.aoa_to_sheet(worksheetData); diff --git a/packages/server/src/services/Export/common.ts b/packages/server/src/services/Export/common.ts index 5895e3367..71f6ef281 100644 --- a/packages/server/src/services/Export/common.ts +++ b/packages/server/src/services/Export/common.ts @@ -1,3 +1,9 @@ export enum Errors { RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE', } + +export enum ExportFormat { + Csv = 'csv', + Pdf = 'pdf', + Xlsx = 'xlsx', +} diff --git a/packages/server/src/services/Export/utils.ts b/packages/server/src/services/Export/utils.ts index e1436d8ab..f21515c46 100644 --- a/packages/server/src/services/Export/utils.ts +++ b/packages/server/src/services/Export/utils.ts @@ -1,4 +1,4 @@ -import { flatMap } from 'lodash'; +import { flatMap, get } from 'lodash'; /** * Flattens the data based on a specified attribute. * @param data - The data to be flattened. @@ -25,3 +25,21 @@ export const flatDataCollections = ( export const getDataAccessor = (col: any) => { return col.group ? `${col.group}.${col.accessor}` : col.accessor; }; + +/** + * Maps the data retrieved from the service layer to the pdf document. + * @param {any} columns + * @param {Record} data + * @returns + */ +export const mapPdfRows = (columns: any, data: Record) => { + return data.map((item) => { + const cells = columns.map((column) => { + return { + key: column.accessor, + value: get(item, getDataAccessor(column)), + }; + }); + return { cells, classNames: '' }; + }); +}; diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx index c6a8c447a..5861febfb 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx @@ -30,6 +30,7 @@ import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf'; import { compose } from '@/utils'; import { DialogsName } from '@/constants/dialogs'; @@ -50,7 +51,7 @@ function ManualJournalActionsBar({ addSetting, // #withDialogActions - openDialog + openDialog, }) { // History context. const history = useHistory(); @@ -58,6 +59,9 @@ function ManualJournalActionsBar({ // Manual journals context. const { journalsViews, fields } = useManualJournalsContext(); + // Exports pdf document. + const { downloadAsync: downloadExportPdf } = useDownloadExportPdf(); + // Manual journals refresh action. const { refresh } = useRefreshJournals(); @@ -91,6 +95,11 @@ function ManualJournalActionsBar({ openDialog(DialogsName.Export, { resource: 'manual_journal' }); }; + // Handle the pdf print button click. + const handlePdfPrintBtnSubmit = () => { + downloadExportPdf({ resource: 'ManualJournal' }); + }; + return ( @@ -134,10 +143,12 @@ function ManualJournalActionsBar({ /> +