diff --git a/packages/server/resources/locales/en.json b/packages/server/resources/locales/en.json index 13fc8a174..ba4c158b8 100644 --- a/packages/server/resources/locales/en.json +++ b/packages/server/resources/locales/en.json @@ -244,6 +244,7 @@ "account.field.active": "Active", "account.field.currency": "Currency", "account.field.balance": "Balance", + "account.field.bank_balance": "Bank Balance", "account.field.parent_account": "Parent Account", "account.field.created_at": "Created at", "item.field.type": "Item Type", @@ -331,7 +332,7 @@ "bill_payment.field.reference_no": "Reference No.", "bill_payment.field.description": "Description", "bill_payment.field.exchange_rate": "Exchange Rate", - "bill_payment.field.statement": "Statement", + "bill_payment.field.note": "Note", "bill_payment.field.entries.bill": "Bill No.", "bill_payment.field.entries.payment_amount": "Payment Amount", "bill_payment.field.reference": "Reference No.", @@ -431,6 +432,7 @@ "vendor.field.created_at": "Created at", "vendor.field.balance": "Balance", "vendor.field.status": "Status", + "vendor.field.note": "Note", "vendor.field.currency": "Currency", "vendor.field.status.active": "Active", "vendor.field.status.inactive": "Inactive", diff --git a/packages/server/src/api/controllers/Export/ExportController.ts b/packages/server/src/api/controllers/Export/ExportController.ts new file mode 100644 index 000000000..632c84932 --- /dev/null +++ b/packages/server/src/api/controllers/Export/ExportController.ts @@ -0,0 +1,99 @@ +import { Inject, Service } from 'typedi'; +import { Router, Request, Response, NextFunction } from 'express'; +import { query } from 'express-validator'; +import BaseController from '@/api/controllers/BaseController'; +import { ServiceError } from '@/exceptions'; +import { ExportApplication } from '@/services/Export/ExportApplication'; +import { ACCEPT_TYPE } from '@/interfaces/Http'; + +@Service() +export class ExportController extends BaseController { + @Inject() + private exportResourceApp: ExportApplication; + + /** + * Router constructor method. + */ + router() { + const router = Router(); + + router.get( + '/', + [ + query('resource').exists(), + query('format').isIn(['csv', 'xlsx']).optional(), + ], + this.validationResult, + this.export.bind(this), + this.catchServiceErrors + ); + return router; + } + + /** + * Imports xlsx/csv to the given resource type. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + private async export(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const query = this.matchedQueryData(req); + + try { + const accept = this.accepts(req); + + const acceptType = accept.types([ + ACCEPT_TYPE.APPLICATION_XLSX, + ACCEPT_TYPE.APPLICATION_CSV, + ACCEPT_TYPE.APPLICATION_PDF, + ]); + const data = await this.exportResourceApp.export( + tenantId, + query.resource, + acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv' + ); + if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { + res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); + res.setHeader('Content-Type', 'text/csv'); + + return res.send(data); + // Retrieves the xlsx format. + } else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) { + res.setHeader( + 'Content-Disposition', + 'attachment; filename=output.xlsx' + ); + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ); + return 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/index.ts b/packages/server/src/api/index.ts index 623e5a0f7..33b47e616 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -61,6 +61,7 @@ import { TaxRatesController } from './controllers/TaxRates/TaxRates'; import { ImportController } from './controllers/Import/ImportController'; import { BankingController } from './controllers/Banking/BankingController'; import { Webhooks } from './controllers/Webhooks/Webhooks'; +import { ExportController } from './controllers/Export/ExportController'; export default () => { const app = Router(); @@ -141,6 +142,7 @@ export default () => { dashboard.use('/projects', Container.get(ProjectsController).router()); dashboard.use('/tax-rates', Container.get(TaxRatesController).router()); dashboard.use('/import', Container.get(ImportController).router()); + dashboard.use('/export', Container.get(ExportController).router()) dashboard.use('/', Container.get(ProjectTasksController).router()); dashboard.use('/', Container.get(ProjectTimesController).router()); diff --git a/packages/server/src/interfaces/Model.ts b/packages/server/src/interfaces/Model.ts index 87912bafe..93bb1f7fc 100644 --- a/packages/server/src/interfaces/Model.ts +++ b/packages/server/src/interfaces/Model.ts @@ -126,13 +126,16 @@ export interface IModelMeta { defaultFilterField: string; defaultSort: IModelMetaDefaultSort; - importable?: boolean; + exportable?: boolean; + exportFlattenOn?: string; + importable?: boolean; importAggregator?: string; importAggregateOn?: string; importAggregateBy?: string; fields: { [key: string]: IModelMetaField }; + columns: { [key: string]: IModelMetaColumn }; } // ---- @@ -161,3 +164,22 @@ export type IModelMetaField2 = IModelMetaFieldCommon2 & | IModelMetaRelationField2 | IModelMetaCollectionField ); + +export interface ImodelMetaColumnMeta { + name: string; + accessor?: string; + exportable?: boolean; +} + +interface IModelMetaColumnText { + type: 'text;'; +} + +interface IModelMetaColumnCollection { + type: 'collection'; + collectionOf: 'object'; + columns: { [key: string]: ImodelMetaColumnMeta & IModelMetaColumnText }; +} + +export type IModelMetaColumn = ImodelMetaColumnMeta & + (IModelMetaColumnText | IModelMetaColumnCollection); diff --git a/packages/server/src/models/Account.Settings.ts b/packages/server/src/models/Account.Settings.ts index 5fb8ea8a7..7be8cf404 100644 --- a/packages/server/src/models/Account.Settings.ts +++ b/packages/server/src/models/Account.Settings.ts @@ -7,6 +7,7 @@ export default { sortField: 'name', }, importable: true, + exportable: true, fields: { name: { name: 'account.field.name', @@ -85,6 +86,55 @@ export default { fieldType: 'date', }, }, + columns: { + name: { + name: 'account.field.name', + type: 'text', + }, + code: { + name: 'account.field.code', + type: 'text', + }, + rootType: { + name: 'account.field.root_type', + type: 'text', + accessor: 'accountRootType', + }, + accountType: { + name: 'account.field.type', + accessor: 'accountTypeLabel', + type: 'text', + }, + accountNormal: { + name: 'account.field.normal', + accessor: 'accountNormalFormatted', + }, + currencyCode: { + name: 'account.field.currency', + type: 'text', + }, + bankBalance: { + name: 'account.field.bank_balance', + accessor: 'bankBalanceFormatted', + type: 'text', + exportable: true, + }, + balance: { + name: 'account.field.balance', + accessor: 'amount', + }, + description: { + name: 'account.field.description', + type: 'text', + }, + active: { + name: 'account.field.active', + type: 'boolean', + }, + createdAt: { + name: 'account.field.created_at', + }, + }, fields2: { name: { name: 'account.field.name', diff --git a/packages/server/src/models/Bill.Settings.ts b/packages/server/src/models/Bill.Settings.ts index 2842ff926..890a9635a 100644 --- a/packages/server/src/models/Bill.Settings.ts +++ b/packages/server/src/models/Bill.Settings.ts @@ -5,6 +5,8 @@ export default { sortField: 'bill_date', }, importable: true, + exportFlattenOn: 'entries', + exportable: true, importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'billNumber', @@ -80,6 +82,84 @@ export default { fieldType: 'date', }, }, + columns: { + billNumber: { + name: 'Bill No.', + type: 'text', + }, + referenceNo: { + name: 'Reference No.', + type: 'text', + }, + billDate: { + name: 'Date', + type: 'date', + }, + dueDate: { + name: 'Due Date', + type: 'date', + }, + vendorId: { + name: 'Vendor', + accessor: 'vendor.displayName', + type: 'text', + }, + amount: { + name: 'Amount', + accessor: 'formattedAmount', + }, + exchangeRate: { + name: 'Exchange Rate', + type: 'number', + }, + currencyCode: { + name: 'Currency Code', + type: 'text', + }, + dueAmount: { + name: 'Due Amount', + accessor: 'formattedDueAmount', + }, + paidAmount: { + name: 'Paid Amount', + accessor: 'formattedPaymentAmount', + }, + note: { + name: 'Note', + type: 'text', + }, + open: { + name: 'Open', + type: 'boolean', + }, + entries: { + name: 'Entries', + accessor: 'entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + }, fields2: { billNumber: { name: 'Bill No.', @@ -132,7 +212,7 @@ export default { relationModel: 'Item', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the item name or code." + importHint: 'Matches the item name or code.', }, rate: { name: 'Rate', diff --git a/packages/server/src/models/BillPayment.Settings.ts b/packages/server/src/models/BillPayment.Settings.ts index 1f2369f21..4d7de9239 100644 --- a/packages/server/src/models/BillPayment.Settings.ts +++ b/packages/server/src/models/BillPayment.Settings.ts @@ -4,6 +4,7 @@ export default { sortOrder: 'DESC', sortField: 'bill_date', }, + exportable: true, importable: true, importAggregator: 'group', importAggregateOn: 'entries', @@ -67,6 +68,46 @@ export default { fieldType: 'date', }, }, + columns: { + vendor: { + name: 'bill_payment.field.vendor', + type: 'relation', + accessor: 'vendor.displayName', + }, + paymentDate: { + name: 'bill_payment.field.payment_date', + type: 'date', + }, + paymentNumber: { + name: 'bill_payment.field.payment_number', + type: 'text', + }, + paymentAccount: { + name: 'bill_payment.field.payment_account', + accessor: 'paymentAccount.name', + type: 'text', + }, + amount: { + name: 'Amount', + accessor: 'formattedAmount', + }, + currencyCode: { + name: 'Currency Code', + type: 'text', + }, + exchangeRate: { + name: 'bill_payment.field.exchange_rate', + type: 'number', + }, + statement: { + name: 'bill_payment.field.note', + type: 'text', + }, + reference: { + name: 'bill_payment.field.reference', + type: 'text', + }, + }, fields2: { vendorId: { name: 'bill_payment.field.vendor', @@ -84,7 +125,7 @@ export default { name: 'bill_payment.field.payment_number', fieldType: 'text', unique: true, - importHint: "The payment number should be unique." + importHint: 'The payment number should be unique.', }, paymentAccountId: { name: 'bill_payment.field.payment_account', @@ -92,14 +133,14 @@ export default { relationModel: 'Account', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the account name or code." + importHint: 'Matches the account name or code.', }, exchangeRate: { name: 'bill_payment.field.exchange_rate', fieldType: 'number', }, statement: { - name: 'bill_payment.field.statement', + name: 'bill_payment.field.note', fieldType: 'text', }, reference: { @@ -120,7 +161,7 @@ export default { relationModel: 'Bill', relationImportMatch: 'billNumber', required: true, - importHint: "Matches the bill number." + importHint: 'Matches the bill number.', }, paymentAmount: { name: 'bill_payment.field.entries.payment_amount', diff --git a/packages/server/src/models/CreditNote.Meta.ts b/packages/server/src/models/CreditNote.Meta.ts index f6317528b..5da0c1d9e 100644 --- a/packages/server/src/models/CreditNote.Meta.ts +++ b/packages/server/src/models/CreditNote.Meta.ts @@ -12,10 +12,14 @@ export default { sortOrder: 'DESC', sortField: 'name', }, + exportable: true, + exportFlattenOn: 'entries', + importable: true, importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'creditNoteNumber', + fields: { customer: { name: 'credit_note.field.customer', @@ -81,6 +85,67 @@ export default { fieldType: 'date', }, }, + columns: { + customer: { + name: 'Customer', + type: 'relation', + accessor: 'customer.displayName', + }, + exchangeRate: { + name: 'Exchange Rate', + type: 'number', + }, + creditNoteDate: { + name: 'Credit Note Date', + type: 'date', + }, + referenceNo: { + name: 'Reference No.', + type: 'text', + }, + note: { + name: 'Note', + type: 'text', + }, + termsConditions: { + name: 'Terms & Conditions', + type: 'text', + }, + creditNoteNumber: { + name: 'Credit Note Number', + type: 'text', + }, + open: { + name: 'Open', + type: 'boolean', + }, + entries: { + name: 'Entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + }, fields2: { customerId: { name: 'Customer', diff --git a/packages/server/src/models/Customer.Settings.ts b/packages/server/src/models/Customer.Settings.ts index fa7bcc2d2..71f631032 100644 --- a/packages/server/src/models/Customer.Settings.ts +++ b/packages/server/src/models/Customer.Settings.ts @@ -1,5 +1,6 @@ export default { importable: true, + exportable: true, defaultFilterField: 'displayName', defaultSort: { sortOrder: 'DESC', @@ -90,6 +91,138 @@ export default { }, }, }, + columns: { + firstName: { + name: 'vendor.field.first_name', + type: 'text', + }, + lastName: { + name: 'vendor.field.last_name', + type: 'text', + }, + displayName: { + name: 'vendor.field.display_name', + type: 'text', + }, + email: { + name: 'vendor.field.email', + type: 'text', + }, + workPhone: { + name: 'vendor.field.work_phone', + type: 'text', + }, + personalPhone: { + name: 'vendor.field.personal_phone', + type: 'text', + }, + companyName: { + name: 'vendor.field.company_name', + type: 'text', + }, + website: { + name: 'vendor.field.website', + type: 'text', + }, + balance: { + name: 'vendor.field.balance', + type: 'number', + }, + openingBalance: { + name: 'vendor.field.opening_balance', + type: 'number', + }, + openingBalanceAt: { + name: 'vendor.field.opening_balance_at', + type: 'date', + }, + currencyCode: { + name: 'vendor.field.currency', + type: 'text', + }, + status: { + name: 'vendor.field.status', + }, + note: { + name: 'vendor.field.note', + }, + // Billing Address + billingAddress1: { + name: 'Billing Address 1', + column: 'billing_address1', + type: 'text', + }, + billingAddress2: { + name: 'Billing Address 2', + column: 'billing_address2', + type: 'text', + }, + billingAddressCity: { + name: 'Billing Address City', + column: 'billing_address_city', + type: 'text', + }, + billingAddressCountry: { + name: 'Billing Address Country', + column: 'billing_address_country', + type: 'text', + }, + billingAddressPostcode: { + name: 'Billing Address Postcode', + column: 'billing_address_postcode', + type: 'text', + }, + billingAddressState: { + name: 'Billing Address State', + column: 'billing_address_state', + type: 'text', + }, + billingAddressPhone: { + name: 'Billing Address Phone', + column: 'billing_address_phone', + type: 'text', + }, + // Shipping Address + shippingAddress1: { + name: 'Shipping Address 1', + column: 'shipping_address1', + type: 'text', + }, + shippingAddress2: { + name: 'Shipping Address 2', + column: 'shipping_address2', + type: 'text', + }, + shippingAddressCity: { + name: 'Shipping Address City', + column: 'shipping_address_city', + type: 'text', + }, + shippingAddressCountry: { + name: 'Shipping Address Country', + column: 'shipping_address_country', + type: 'text', + }, + shippingAddressPostcode: { + name: 'Shipping Address Postcode', + column: 'shipping_address_postcode', + type: 'text', + }, + shippingAddressPhone: { + name: 'Shipping Address Phone', + column: 'shipping_address_phone', + type: 'text', + }, + shippingAddressState: { + name: 'Shipping Address State', + column: 'shipping_address_state', + type: 'text', + }, + createdAt: { + name: 'vendor.field.created_at', + type: 'date', + }, + }, fields2: { customerType: { name: 'Customer Type', diff --git a/packages/server/src/models/Expense.Settings.ts b/packages/server/src/models/Expense.Settings.ts index c1d670526..12c539782 100644 --- a/packages/server/src/models/Expense.Settings.ts +++ b/packages/server/src/models/Expense.Settings.ts @@ -8,6 +8,8 @@ export default { sortField: 'name', }, importable: true, + exportFlattenOn: 'categories', + exportable: true, fields: { payment_date: { name: 'expense.field.payment_date', @@ -61,6 +63,56 @@ export default { fieldType: 'date', }, }, + columns: { + paymentReceive: { + name: 'expense.field.payment_account', + type: 'text', + accessor: 'paymentAccount.name' + }, + referenceNo: { + name: 'expense.field.reference_no', + type: 'text', + }, + paymentDate: { + name: 'expense.field.payment_date', + type: 'date', + }, + currencyCode: { + name: 'expense.field.currency_code', + type: 'text', + }, + exchangeRate: { + name: 'expense.field.exchange_rate', + type: 'number', + }, + description: { + name: 'expense.field.description', + type: 'text', + }, + categories: { + name: 'expense.field.categories', + type: 'collection', + collectionOf: 'object', + columns: { + expenseAccount: { + name: 'expense.field.expense_account', + accessor: 'expenseAccount.name', + }, + amount: { + name: 'expense.field.amount', + accessor: 'amountFormatted', + }, + description: { + name: 'expense.field.line_description', + type: 'text', + }, + }, + }, + publish: { + name: 'expense.field.publish', + type: 'boolean', + }, + }, fields2: { paymentAccountId: { name: 'expense.field.payment_account', @@ -68,7 +120,7 @@ export default { relationModel: 'Account', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the account name or code." + importHint: 'Matches the account name or code.', }, referenceNo: { name: 'expense.field.reference_no', @@ -102,7 +154,7 @@ export default { relationModel: 'Account', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the account name or code." + importHint: 'Matches the account name or code.', }, amount: { name: 'expense.field.amount', diff --git a/packages/server/src/models/InventoryAdjustment.Settings.ts b/packages/server/src/models/InventoryAdjustment.Settings.ts index 9ef90cbc5..9d7f65237 100644 --- a/packages/server/src/models/InventoryAdjustment.Settings.ts +++ b/packages/server/src/models/InventoryAdjustment.Settings.ts @@ -4,6 +4,54 @@ export default { sortOrder: 'DESC', sortField: 'date', }, + columns: { + date: { + name: 'inventory_adjustment.field.date', + column: 'date', + fieldType: 'date', + exportable: true, + }, + type: { + name: 'inventory_adjustment.field.type', + column: 'type', + fieldType: 'enumeration', + options: [ + { key: 'increment', name: 'inventory_adjustment.field.type.increment' }, + { key: 'decrement', name: 'inventory_adjustment.field.type.decrement' }, + ], + exportable: true, + }, + adjustmentAccount: { + name: 'inventory_adjustment.field.adjustment_account', + type: 'adjustment_account_id', + exportable: true, + }, + reason: { + name: 'inventory_adjustment.field.reason', + type: 'text', + exportable: true, + }, + referenceNo: { + name: 'inventory_adjustment.field.reference_no', + type: 'text', + exportable: true, + }, + description: { + name: 'inventory_adjustment.field.description', + type: 'text', + exportable: true, + }, + publishedAt: { + name: 'inventory_adjustment.field.published_at', + type: 'date', + exportable: true, + }, + createdAt: { + name: 'inventory_adjustment.field.created_at', + type: 'date', + exportable: true, + }, + }, fields: { date: { name: 'inventory_adjustment.field.date', diff --git a/packages/server/src/models/Item.Settings.ts b/packages/server/src/models/Item.Settings.ts index 640c271ea..9c8a50ce8 100644 --- a/packages/server/src/models/Item.Settings.ts +++ b/packages/server/src/models/Item.Settings.ts @@ -1,5 +1,6 @@ export default { importable: true, + exportable: true, defaultFilterField: 'name', defaultSort: { sortField: 'name', @@ -121,6 +122,97 @@ export default { fieldType: 'date', }, }, + columns: { + type: { + name: 'item.field.type', + type: 'text', + exportable: true, + }, + name: { + name: 'item.field.name', + type: 'text', + exportable: true, + }, + code: { + name: 'item.field.code', + type: 'text', + exportable: true, + }, + sellable: { + name: 'item.field.sellable', + type: 'boolean', + exportable: true, + }, + purchasable: { + name: 'item.field.purchasable', + type: 'boolean', + exportable: true, + }, + sellPrice: { + name: 'item.field.cost_price', + type: 'number', + exportable: true, + }, + costPrice: { + name: 'item.field.cost_account', + type: 'number', + exportable: true, + }, + costAccount: { + name: 'item.field.sell_account', + type: 'text', + accessor: 'costAccount.name', + exportable: true, + }, + sellAccount: { + name: 'item.field.sell_description', + type: 'text', + accessor: 'sellAccount.name', + exportable: true, + }, + inventoryAccount: { + name: 'item.field.inventory_account', + type: 'text', + accessor: 'inventoryAccount.name', + exportable: true, + }, + sellDescription: { + name: 'Sell description', + type: 'text', + exportable: true, + }, + purchaseDescription: { + name: 'Purchase description', + type: 'text', + exportable: true, + }, + quantityOnHand: { + name: 'item.field.quantity_on_hand', + type: 'number', + exportable: true, + }, + note: { + name: 'item.field.note', + type: 'text', + exportable: true, + }, + category: { + name: 'item.field.category', + type: 'text', + accessor: 'category.name', + exportable: true, + }, + active: { + name: 'item.field.active', + fieldType: 'boolean', + exportable: true, + }, + createdAt: { + name: 'item.field.created_at', + type: 'date', + exportable: true, + }, + }, fields2: { type: { name: 'item.field.type', @@ -195,7 +287,7 @@ export default { fieldType: 'relation', relationModel: 'ItemCategory', relationImportMatch: ['name'], - importHint: "Matches the category name." + importHint: 'Matches the category name.', }, active: { name: 'item.field.active', diff --git a/packages/server/src/models/ItemCategory.Settings.ts b/packages/server/src/models/ItemCategory.Settings.ts index 32a29cd10..66b7af28f 100644 --- a/packages/server/src/models/ItemCategory.Settings.ts +++ b/packages/server/src/models/ItemCategory.Settings.ts @@ -5,6 +5,7 @@ export default { sortOrder: 'DESC', }, importable: true, + exportable: true, fields: { name: { name: 'item_category.field.name', @@ -28,6 +29,24 @@ export default { columnType: 'date', }, }, + columns: { + name: { + name: 'item_category.field.name', + type: 'text', + }, + description: { + name: 'item_category.field.description', + type: 'text', + }, + count: { + name: 'item_category.field.count', + type: 'text', + }, + createdAt: { + name: 'item_category.field.created_at', + type: 'text', + }, + }, fields2: { name: { name: 'item_category.field.name', diff --git a/packages/server/src/models/ManualJournal.Settings.ts b/packages/server/src/models/ManualJournal.Settings.ts index 11409e1aa..db2712220 100644 --- a/packages/server/src/models/ManualJournal.Settings.ts +++ b/packages/server/src/models/ManualJournal.Settings.ts @@ -5,6 +5,9 @@ export default { sortField: 'name', }, importable: true, + exportFlattenOn: 'entries', + + exportable: true, importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'journalNumber', @@ -56,6 +59,76 @@ export default { fieldType: 'date', }, }, + columns: { + date: { + name: 'manual_journal.field.date', + type: 'date', + }, + journalNumber: { + name: 'manual_journal.field.journal_number', + type: 'text', + }, + reference: { + name: 'manual_journal.field.reference', + type: 'text', + }, + journalType: { + name: 'manual_journal.field.journal_type', + type: 'text', + }, + amount: { + name: 'Amount', + accessor: 'formattedAmount', + }, + currencyCode: { + name: 'manual_journal.field.currency', + type: 'text', + }, + exchangeRate: { + name: 'manual_journal.field.exchange_rate', + type: 'number', + }, + description: { + name: 'manual_journal.field.description', + type: 'text', + }, + entries: { + name: 'Entries', + type: 'collection', + collectionOf: 'object', + columns: { + credit: { + name: 'Credit', + type: 'text', + }, + debit: { + name: 'Debit', + type: 'text', + }, + account: { + name: 'Account', + accessor: 'account.name', + }, + contact: { + name: 'Contact', + accessor: 'contact.displayName', + }, + note: { + name: 'Note', + }, + }, + publish: { + name: 'Publish', + type: 'boolean', + }, + publishedAt: { + name: 'Published At', + }, + }, + createdAt: { + name: 'Created At', + }, + }, fields2: { date: { name: 'manual_journal.field.date', diff --git a/packages/server/src/models/PaymentReceive.Settings.ts b/packages/server/src/models/PaymentReceive.Settings.ts index e96e1c7b2..663b5884d 100644 --- a/packages/server/src/models/PaymentReceive.Settings.ts +++ b/packages/server/src/models/PaymentReceive.Settings.ts @@ -1,5 +1,6 @@ export default { importable: true, + exportable: true, importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'paymentReceiveNo', @@ -57,6 +58,42 @@ export default { fieldDate: 'date', }, }, + columns: { + customer: { + name: 'payment_receive.field.customer', + accessor: 'customer.displayName', + type: 'text', + }, + paymentDate: { + name: 'payment_receive.field.payment_date', + type: 'date', + }, + amount: { + name: 'payment_receive.field.amount', + type: 'number', + }, + referenceNo: { + name: 'payment_receive.field.reference_no', + type: 'text', + }, + depositAccount: { + name: 'payment_receive.field.deposit_account', + accessor: 'depositAccount.name', + type: 'text', + }, + paymentReceiveNo: { + name: 'payment_receive.field.payment_receive_no', + type: 'text', + }, + statement: { + name: 'payment_receive.field.statement', + type: 'text', + }, + created_at: { + name: 'payment_receive.field.created_at', + type: 'date', + }, + }, fields2: { customerId: { name: 'payment_receive.field.customer', @@ -84,12 +121,12 @@ export default { relationModel: 'Account', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the account name or code." + importHint: 'Matches the account name or code.', }, paymentReceiveNo: { name: 'payment_receive.field.payment_receive_no', fieldType: 'text', - importHint: "The payment number should be unique." + importHint: 'The payment number should be unique.', }, statement: { name: 'payment_receive.field.statement', @@ -108,7 +145,7 @@ export default { relationModel: 'SaleInvoice', relationImportMatch: 'invoiceNo', required: true, - importHint: "Matches the invoice number." + importHint: 'Matches the invoice number.', }, paymentAmount: { name: 'payment_receive.field.entries.payment_amount', diff --git a/packages/server/src/models/SaleEstimate.Settings.ts b/packages/server/src/models/SaleEstimate.Settings.ts index f92735f34..a9577b4f4 100644 --- a/packages/server/src/models/SaleEstimate.Settings.ts +++ b/packages/server/src/models/SaleEstimate.Settings.ts @@ -4,6 +4,9 @@ export default { sortOrder: 'DESC', sortField: 'estimate_date', }, + exportable: true, + exportFlattenOn: 'entries', + importable: true, importAggregator: 'group', importAggregateOn: 'entries', @@ -73,6 +76,91 @@ export default { columnType: 'date', }, }, + columns: { + customer: { + name: 'Customer', + type: 'text', + accessor: 'customer.displayName', + exportable: true, + }, + estimateDate: { + name: 'Estimate Date', + type: 'date', + exportable: true, + }, + expirationDate: { + name: 'Expiration Date', + type: 'date', + exportable: true, + }, + estimateNumber: { + name: 'Estimate No.', + type: 'text', + exportable: true, + }, + reference: { + name: 'Reference No.', + type: 'text', + exportable: true, + }, + amount: { + name: 'Amount', + accessor: 'formattedAmount', + type: 'text', + }, + exchangeRate: { + name: 'Exchange Rate', + type: 'number', + exportable: true, + }, + currencyCode: { + name: 'Currency', + type: 'text', + exportable: true, + }, + note: { + name: 'Note', + type: 'text', + exportable: true, + }, + termsConditions: { + name: 'Terms & Conditions', + type: 'text', + exportable: true, + }, + delivered: { + name: 'Delivered', + type: 'boolean', + exportable: true, + }, + entries: { + name: 'Entries', + accessor: 'entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + }, fields2: { customerId: { name: 'Customer', @@ -132,7 +220,7 @@ export default { relationModel: 'Item', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the item name or code." + importHint: 'Matches the item name or code.', }, rate: { name: 'invoice.field.rate', diff --git a/packages/server/src/models/SaleInvoice.Settings.ts b/packages/server/src/models/SaleInvoice.Settings.ts index c48befbcb..24728522e 100644 --- a/packages/server/src/models/SaleInvoice.Settings.ts +++ b/packages/server/src/models/SaleInvoice.Settings.ts @@ -4,6 +4,9 @@ export default { sortOrder: 'DESC', sortField: 'created_at', }, + exportable: true, + exportFlattenOn: 'entries', + importable: true, importAggregator: 'group', importAggregateOn: 'entries', @@ -87,6 +90,89 @@ export default { fieldType: 'date', }, }, + columns: { + invoiceDate: { + name: 'invoice.field.invoice_date', + type: 'date', + }, + dueDate: { + name: 'invoice.field.due_date', + type: 'date', + }, + referenceNo: { + name: 'invoice.field.reference_no', + type: 'text', + }, + invoiceNo: { + name: 'invoice.field.invoice_no', + type: 'text', + }, + customer: { + name: 'invoice.field.customer', + type: 'text', + accessor: 'customer.displayName', + }, + amount: { + name: 'invoice.field.amount', + type: 'text', + accessor: 'balanceAmountFormatted', + }, + exchangeRate: { + name: 'invoice.field.exchange_rate', + type: 'number', + }, + currencyCode: { + name: 'invoice.field.currency', + type: 'text', + }, + paidAmount: { + name: 'Paid Amount', + accessor: 'paymentAmountFormatted', + }, + dueAmount: { + name: 'Due Amount', + accessor: 'dueAmountFormatted', + }, + invoiceMessage: { + name: 'invoice.field.invoice_message', + type: 'text', + }, + termsConditions: { + name: 'invoice.field.terms_conditions', + type: 'text', + }, + delivered: { + name: 'invoice.field.delivered', + type: 'boolean', + }, + entries: { + name: 'Entries', + accessor: 'entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + }, fields2: { invoiceDate: { name: 'invoice.field.invoice_date', @@ -142,7 +228,7 @@ export default { relationModel: 'Item', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the item name or code." + importHint: 'Matches the item name or code.', }, rate: { name: 'invoice.field.rate', diff --git a/packages/server/src/models/SaleReceipt.Settings.ts b/packages/server/src/models/SaleReceipt.Settings.ts index 2b2fe6fa2..3fecd0480 100644 --- a/packages/server/src/models/SaleReceipt.Settings.ts +++ b/packages/server/src/models/SaleReceipt.Settings.ts @@ -4,6 +4,9 @@ export default { sortOrder: 'DESC', sortField: 'created_at', }, + exportable: true, + exportFlattenOn: 'entries', + importable: true, importAggregator: 'group', importAggregateOn: 'entries', @@ -77,6 +80,86 @@ export default { sortCustomQuery: StatusFieldSortQuery, }, }, + columns: { + amount: { + name: 'receipt.field.amount', + column: 'amount', + type: 'number', + }, + depositAccount: { + name: 'receipt.field.deposit_account', + type: 'text', + accessor: 'depositAccount.name', + }, + customer: { + name: 'receipt.field.customer', + type: 'text', + accessor: 'customer.displayName', + }, + receiptDate: { + name: 'receipt.field.receipt_date', + type: 'date', + }, + receiptNumber: { + name: 'receipt.field.receipt_number', + type: 'text', + }, + referenceNo: { + name: 'receipt.field.reference_no', + column: 'reference_no', + type: 'text', + exportable: true, + }, + receiptMessage: { + name: 'receipt.field.receipt_message', + column: 'receipt_message', + type: 'text', + }, + statement: { + name: 'receipt.field.statement', + type: 'text', + }, + status: { + name: 'receipt.field.status', + type: 'enumeration', + options: [ + { key: 'draft', label: 'receipt.field.status.draft' }, + { key: 'closed', label: 'receipt.field.status.closed' }, + ], + exportable: true, + }, + entries: { + name: 'Entries', + accessor: 'entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + createdAt: { + name: 'receipt.field.created_at', + type: 'date', + }, + }, fields2: { receiptDate: { name: 'Receipt Date', @@ -126,7 +209,7 @@ export default { relationModel: 'Item', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the item name or code." + importHint: 'Matches the item name or code.', }, rate: { name: 'invoice.field.rate', diff --git a/packages/server/src/models/Vendor.Settings.ts b/packages/server/src/models/Vendor.Settings.ts index 7f22d5ba4..7681dfa10 100644 --- a/packages/server/src/models/Vendor.Settings.ts +++ b/packages/server/src/models/Vendor.Settings.ts @@ -5,6 +5,7 @@ export default { sortField: 'created_at', }, importable: true, + exportable: true, fields: { first_name: { name: 'vendor.field.first_name', @@ -32,7 +33,7 @@ export default { fieldType: 'text', }, personal_phone: { - name: 'vendor.field.personal_pone', + name: 'vendor.field.personal_phone', column: 'personal_phone', fieldType: 'text', }, @@ -90,6 +91,154 @@ export default { }, }, }, + columns: { + firstName: { + name: 'vendor.field.first_name', + type: 'text', + }, + lastName: { + name: 'vendor.field.last_name', + type: 'text', + }, + displayName: { + name: 'vendor.field.display_name', + type: 'text', + }, + email: { + name: 'vendor.field.email', + type: 'text', + }, + workPhone: { + name: 'vendor.field.work_phone', + type: 'text', + }, + personalPhone: { + name: 'vendor.field.personal_phone', + type: 'text', + }, + companyName: { + name: 'vendor.field.company_name', + type: 'text', + }, + website: { + name: 'vendor.field.website', + type: 'text', + }, + balance: { + name: 'vendor.field.balance', + type: 'number', + }, + openingBalance: { + name: 'vendor.field.opening_balance', + type: 'number', + }, + openingBalanceAt: { + name: 'vendor.field.opening_balance_at', + type: 'date', + }, + currencyCode: { + name: 'vendor.field.currency', + type: 'text', + }, + status: { + name: 'vendor.field.status', + }, + note: { + name: 'vendor.field.note', + type: 'text', + }, + // Billing Address + billingAddress1: { + name: 'Billing Address 1', + column: 'billing_address1', + type: 'text', + exportable: true, + }, + billingAddress2: { + name: 'Billing Address 2', + column: 'billing_address2', + type: 'text', + exportable: true, + }, + billingAddressCity: { + name: 'Billing Address City', + column: 'billing_address_city', + type: 'text', + exportable: true, + }, + billingAddressCountry: { + name: 'Billing Address Country', + column: 'billing_address_country', + type: 'text', + exportable: true, + }, + billingAddressPostcode: { + name: 'Billing Address Postcode', + column: 'billing_address_postcode', + type: 'text', + exportable: true, + }, + billingAddressState: { + name: 'Billing Address State', + column: 'billing_address_state', + type: 'text', + exportable: true, + }, + billingAddressPhone: { + name: 'Billing Address Phone', + column: 'billing_address_phone', + type: 'text', + exportable: true, + }, + // Shipping Address + shippingAddress1: { + name: 'Shipping Address 1', + column: 'shipping_address1', + type: 'text', + exportable: true, + }, + shippingAddress2: { + name: 'Shipping Address 2', + column: 'shipping_address2', + type: 'text', + exportable: true, + }, + shippingAddressCity: { + name: 'Shipping Address City', + column: 'shipping_address_city', + type: 'text', + exportable: true, + }, + shippingAddressCountry: { + name: 'Shipping Address Country', + column: 'shipping_address_country', + type: 'text', + exportable: true, + }, + shippingAddressPostcode: { + name: 'Shipping Address Postcode', + column: 'shipping_address_postcode', + type: 'text', + exportable: true, + }, + shippingAddressState: { + name: 'Shipping Address State', + column: 'shipping_address_state', + type: 'text', + exportable: true, + }, + shippingAddressPhone: { + name: 'Shipping Address Phone', + column: 'shipping_address_phone', + type: 'text', + exportable: true, + }, + createdAt: { + name: 'vendor.field.created_at', + type: 'date', + exportable: true, + }, + }, fields2: { firstName: { name: 'vendor.field.first_name', diff --git a/packages/server/src/models/VendorCredit.Meta.ts b/packages/server/src/models/VendorCredit.Meta.ts index 86a3aa103..b57cc275c 100644 --- a/packages/server/src/models/VendorCredit.Meta.ts +++ b/packages/server/src/models/VendorCredit.Meta.ts @@ -12,10 +12,14 @@ export default { sortOrder: 'DESC', sortField: 'name', }, + exportable: true, + exportFlattenOn: 'entries', + importable: true, importAggregator: 'group', importAggregateOn: 'entries', importAggregateBy: 'vendorCreditNumber', + fields: { vendor: { name: 'vendor_credit.field.vendor', @@ -76,6 +80,79 @@ export default { fieldType: 'date', }, }, + columns: { + vendorId: { + name: 'Vendor', + type: 'relation', + accessor: 'vendor.displayName', + }, + exchangeRate: { + name: 'Echange Rate', + type: 'text', + }, + vendorCreditNumber: { + name: 'Vendor Credit No.', + type: 'text', + }, + referenceNo: { + name: 'Refernece No.', + type: 'text', + }, + vendorCreditDate: { + name: 'Vendor Credit Date', + type: 'date', + }, + amount: { + name: 'Amount', + accessor: 'formattedAmount', + }, + creditRemaining: { + name: 'Credits Remaining', + accessor: 'formattedCreditsRemaining', + }, + refundedAmount: { + name: 'Refunded Amount', + accessor: 'refundedAmount', + }, + invoicedAmount: { + name: 'Invoiced Amount', + accessor: 'formattedInvoicedAmount', + }, + note: { + name: 'Note', + type: 'text', + }, + open: { + name: 'Open', + type: 'boolean', + }, + entries: { + name: 'Entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + }, fields2: { vendorId: { name: 'Vendor', @@ -122,7 +199,7 @@ export default { relationModel: 'Item', relationImportMatch: ['name', 'code'], required: true, - importHint: "Matches the item name or code." + importHint: 'Matches the item name or code.', }, rate: { name: 'Rate', diff --git a/packages/server/src/models/WarehouseTransfer.Settings.ts b/packages/server/src/models/WarehouseTransfer.Settings.ts index 8d0bc635d..d20064187 100644 --- a/packages/server/src/models/WarehouseTransfer.Settings.ts +++ b/packages/server/src/models/WarehouseTransfer.Settings.ts @@ -8,6 +8,34 @@ export default { sortField: 'name', sortOrder: 'DESC', }, + columns: { + date: { + name: 'warehouse_transfer.field.date', + type: 'date', + exportable: true, + }, + transaction_number: { + name: 'warehouse_transfer.field.transaction_number', + type: 'text', + exportable: true, + }, + status: { + name: 'warehouse_transfer.field.status', + fieldType: 'enumeration', + options: [ + { key: 'draft', label: 'Draft' }, + { key: 'in-transit', label: 'In Transit' }, + { key: 'transferred', label: 'Transferred' }, + ], + sortable: false, + }, + created_at: { + name: 'warehouse_transfer.field.created_at', + column: 'created_at', + columnType: 'date', + fieldType: 'date', + }, + }, fields: { date: { name: 'warehouse_transfer.field.date', diff --git a/packages/server/src/services/Accounts/AccountsExportable.ts b/packages/server/src/services/Accounts/AccountsExportable.ts new file mode 100644 index 000000000..86a6293d8 --- /dev/null +++ b/packages/server/src/services/Accounts/AccountsExportable.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import { AccountsApplication } from './AccountsApplication'; +import { Exportable } from '../Export/Exportable'; +import { IAccountsFilter, IAccountsStructureType } from '@/interfaces'; + +@Service() +export class AccountsExportable extends Exportable { + @Inject() + private accountsApplication: AccountsApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IAccountsFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + inactiveMode: false, + ...query, + structure: IAccountsStructureType.Flat, + pageSize: 12000, + page: 1, + } as IAccountsFilter; + + return this.accountsApplication + .getAccounts(tenantId, parsedQuery) + .then((output) => output.accounts); + } +} diff --git a/packages/server/src/services/Contacts/Customers/CustomersExportable.ts b/packages/server/src/services/Contacts/Customers/CustomersExportable.ts new file mode 100644 index 000000000..8654fa31f --- /dev/null +++ b/packages/server/src/services/Contacts/Customers/CustomersExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { IItemsFilter } from '@/interfaces'; +import { CustomersApplication } from './CustomersApplication'; +import { Exportable } from '@/services/Export/Exportable'; + +@Service() +export class CustomersExportable extends Exportable { + @Inject() + private customersApplication: CustomersApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IItemsFilter) { + const parsedQuery = { + sortOrder: 'DESC', + columnSortBy: 'created_at', + page: 1, + ...query, + pageSize: 12, + } as IItemsFilter; + + return this.customersApplication + .getCustomers(tenantId, parsedQuery) + .then((output) => output.customers); + } +} diff --git a/packages/server/src/services/Contacts/Vendors/VendorsExportable.ts b/packages/server/src/services/Contacts/Vendors/VendorsExportable.ts new file mode 100644 index 000000000..c11f50050 --- /dev/null +++ b/packages/server/src/services/Contacts/Vendors/VendorsExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { IItemsFilter } from '@/interfaces'; +import { Exportable } from '@/services/Export/Exportable'; +import { VendorsApplication } from './VendorsApplication'; + +@Service() +export class VendorsExportable extends Exportable { + @Inject() + private vendorsApplication: VendorsApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IItemsFilter) { + const parsedQuery = { + sortOrder: 'DESC', + columnSortBy: 'created_at', + page: 1, + ...query, + pageSize: 12, + } as IItemsFilter; + + return this.vendorsApplication + .getVendors(tenantId, parsedQuery) + .then((output) => output.vendors); + } +} diff --git a/packages/server/src/services/CreditNotes/CreditNotesExportable.ts b/packages/server/src/services/CreditNotes/CreditNotesExportable.ts new file mode 100644 index 000000000..09dae2a74 --- /dev/null +++ b/packages/server/src/services/CreditNotes/CreditNotesExportable.ts @@ -0,0 +1,30 @@ +import { Inject, Service } from 'typedi'; +import { ICreditNotesQueryDTO } from '@/interfaces'; +import { Exportable } from '@/services/Export/Exportable'; +import ListCreditNotes from './ListCreditNotes'; + +@Service() +export class CreditNotesExportable extends Exportable { + @Inject() + private getCreditNotes: ListCreditNotes; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId - + * @param {IVendorCreditsQueryDTO} query - + * @returns {} + */ + public exportable(tenantId: number, query: ICreditNotesQueryDTO) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 12000, + } as ICreditNotesQueryDTO; + + return this.getCreditNotes + .getCreditNotesList(tenantId, parsedQuery) + .then((output) => output.creditNotes); + } +} diff --git a/packages/server/src/services/CreditNotes/ListCreditNotes.ts b/packages/server/src/services/CreditNotes/ListCreditNotes.ts index 498d3d74d..11ec2f7fa 100644 --- a/packages/server/src/services/CreditNotes/ListCreditNotes.ts +++ b/packages/server/src/services/CreditNotes/ListCreditNotes.ts @@ -45,7 +45,7 @@ export default class ListCreditNotes extends BaseCreditNotes { ); const { results, pagination } = await CreditNote.query() .onBuild((builder) => { - builder.withGraphFetched('entries'); + builder.withGraphFetched('entries.item'); builder.withGraphFetched('customer'); dynamicFilter.buildQuery()(builder); }) diff --git a/packages/server/src/services/Expenses/ExpensesExportable.ts b/packages/server/src/services/Expenses/ExpensesExportable.ts new file mode 100644 index 000000000..51362912f --- /dev/null +++ b/packages/server/src/services/Expenses/ExpensesExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { Exportable } from '../Export/Exportable'; +import { IExpensesFilter } from '@/interfaces'; +import { ExpensesApplication } from './ExpensesApplication'; + +@Service() +export class ExpensesExportable extends Exportable { + @Inject() + private expensesApplication: ExpensesApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IExpensesFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 12000, + } as IExpensesFilter; + + return this.expensesApplication + .getExpenses(tenantId, parsedQuery) + .then((output) => output.expenses); + } +} diff --git a/packages/server/src/services/Export/ExportApplication.ts b/packages/server/src/services/Export/ExportApplication.ts new file mode 100644 index 000000000..44a7dc73f --- /dev/null +++ b/packages/server/src/services/Export/ExportApplication.ts @@ -0,0 +1,17 @@ +import { Inject, Service } from 'typedi'; +import { ExportResourceService } from './ExportService'; + +@Service() +export class ExportApplication { + @Inject() + private exportResource: ExportResourceService; + + /** + * Exports the given resource to csv, xlsx or pdf format. + * @param {string} reosurce + * @param {string} format + */ + public export(tenantId: number, resource: string, format: string) { + return this.exportResource.export(tenantId, resource, format); + } +} diff --git a/packages/server/src/services/Export/ExportRegistery.ts b/packages/server/src/services/Export/ExportRegistery.ts new file mode 100644 index 000000000..33271f4ec --- /dev/null +++ b/packages/server/src/services/Export/ExportRegistery.ts @@ -0,0 +1,49 @@ +import { camelCase, upperFirst } from 'lodash'; +import { Exportable } from './Exportable'; + +export class ExportableRegistry { + private static instance: ExportableRegistry; + private exportables: Record; + + /** + * Constructor method. + */ + constructor() { + this.exportables = {}; + } + + /** + * Gets singleton instance of registry. + * @returns {ExportableRegistry} + */ + public static getInstance(): ExportableRegistry { + if (!ExportableRegistry.instance) { + ExportableRegistry.instance = new ExportableRegistry(); + } + return ExportableRegistry.instance; + } + + /** + * Registers the given importable service. + * @param {string} resource + * @param {Exportable} importable + */ + public registerExportable(resource: string, importable: Exportable): void { + const _resource = this.sanitizeResourceName(resource); + this.exportables[_resource] = importable; + } + + /** + * Retrieves the importable service instance of the given resource name. + * @param {string} name + * @returns {Exportable} + */ + public getExportable(name: string): Exportable { + const _name = this.sanitizeResourceName(name); + return this.exportables[_name]; + } + + private sanitizeResourceName(resource: string) { + return upperFirst(camelCase(resource)); + } +} diff --git a/packages/server/src/services/Export/ExportResources.ts b/packages/server/src/services/Export/ExportResources.ts new file mode 100644 index 000000000..b252bfab1 --- /dev/null +++ b/packages/server/src/services/Export/ExportResources.ts @@ -0,0 +1,72 @@ +import Container, { Service } from 'typedi'; +import { AccountsExportable } from '../Accounts/AccountsExportable'; +import { ExportableRegistry } from './ExportRegistery'; +import { ItemsExportable } from '../Items/ItemsExportable'; +import { CustomersExportable } from '../Contacts/Customers/CustomersExportable'; +import { VendorsExportable } from '../Contacts/Vendors/VendorsExportable'; +import { ExpensesExportable } from '../Expenses/ExpensesExportable'; +import { SaleInvoicesExportable } from '../Sales/Invoices/SaleInvoicesExportable'; +import { SaleEstimatesExportable } from '../Sales/Estimates/SaleEstimatesExportable'; +import { SaleReceiptsExportable } from '../Sales/Receipts/SaleReceiptsExportable'; +import { BillsExportable } from '../Purchases/Bills/BillsExportable'; +import { PaymentsReceivedExportable } from '../Sales/PaymentReceives/PaymentsReceivedExportable'; +import { BillPaymentExportable } from '../Purchases/BillPayments/BillPaymentExportable'; +import { ManualJournalsExportable } from '../ManualJournals/ManualJournalExportable'; +import { CreditNotesExportable } from '../CreditNotes/CreditNotesExportable'; +import { VendorCreditsExportable } from '../Purchases/VendorCredits/VendorCreditsExportable'; +import { ItemCategoriesExportable } from '../ItemCategories/ItemCategoriesExportable'; + +@Service() +export class ExportableResources { + private static registry: ExportableRegistry; + + /** + * Consttuctor method. + */ + constructor() { + this.boot(); + } + + /** + * Importable instances. + */ + private importables = [ + { resource: 'Account', exportable: AccountsExportable }, + { resource: 'Item', exportable: ItemsExportable }, + { resource: 'ItemCategory', exportable: ItemCategoriesExportable }, + { resource: 'Customer', exportable: CustomersExportable }, + { resource: 'Vendor', exportable: VendorsExportable }, + { resource: 'Expense', exportable: ExpensesExportable }, + { resource: 'SaleInvoice', exportable: SaleInvoicesExportable }, + { resource: 'SaleEstimate', exportable: SaleEstimatesExportable }, + { resource: 'SaleReceipt', exportable: SaleReceiptsExportable }, + { resource: 'Bill', exportable: BillsExportable }, + { resource: 'PaymentReceive', exportable: PaymentsReceivedExportable }, + { resource: 'BillPayment', exportable: BillPaymentExportable }, + { resource: 'ManualJournal', exportable: ManualJournalsExportable }, + { resource: 'CreditNote', exportable: CreditNotesExportable }, + { resource: 'VendorCredit', exportable: VendorCreditsExportable }, + ]; + + /** + * + */ + public get registry() { + return ExportableResources.registry; + } + + /** + * Boots all the registered importables. + */ + public boot() { + if (!ExportableResources.registry) { + const instance = ExportableRegistry.getInstance(); + + this.importables.forEach((importable) => { + const importableInstance = Container.get(importable.exportable); + instance.registerExportable(importable.resource, importableInstance); + }); + ExportableResources.registry = instance; + } + } +} diff --git a/packages/server/src/services/Export/ExportService.ts b/packages/server/src/services/Export/ExportService.ts new file mode 100644 index 000000000..c9c3dc432 --- /dev/null +++ b/packages/server/src/services/Export/ExportService.ts @@ -0,0 +1,161 @@ +import { Inject, Service } from 'typedi'; +import xlsx from 'xlsx'; +import * as R from 'ramda'; +import { get } from 'lodash'; +import { sanitizeResourceName } from '../Import/_utils'; +import ResourceService from '../Resource/ResourceService'; +import { ExportableResources } from './ExportResources'; +import { ServiceError } from '@/exceptions'; +import { Errors } from './common'; +import { IModelMeta, IModelMetaColumn } from '@/interfaces'; +import { flatDataCollections, getDataAccessor } from './utils'; + +@Service() +export class ExportResourceService { + @Inject() + private resourceService: ResourceService; + + @Inject() + private exportableResources: ExportableResources; + + /** + * 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. + */ + public async export(tenantId: number, resourceName: string, format: string = 'csv') { + const resource = sanitizeResourceName(resourceName); + const resourceMeta = this.getResourceMeta(tenantId, resource); + + this.validateResourceMeta(resourceMeta); + + 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); + } + + /** + * Retrieves metadata for a specific resource. + * @param {number} tenantId - The tenant identifier. + * @param {string} resource - The name of the resource. + * @returns The metadata of the resource. + */ + private getResourceMeta(tenantId: number, resource: string) { + return this.resourceService.getResourceMeta(tenantId, resource); + } + + /** + * Validates if the resource metadata is exportable. + * @param {any} resourceMeta - The metadata of the resource. + * @throws {ServiceError} If the resource is not exportable or lacks columns. + */ + private validateResourceMeta(resourceMeta: any) { + if (!resourceMeta.exportable || !resourceMeta.columns) { + throw new ServiceError(Errors.RESOURCE_NOT_EXPORTABLE); + } + } + + /** + * Transforms the exported data based on the resource metadata. + * If the resource metadata specifies a flattening attribute (`exportFlattenOn`), + * the data will be flattened based on this attribute using the `flatDataCollections` utility function. + * + * @param {number} tenantId - The tenant identifier. + * @param {string} resource - The name of the resource. + * @param {Array>} data - The original data to be transformed. + * @returns {Array>} - The transformed data. + */ + private transformExportedData( + tenantId: number, + resource: string, + data: Array> + ): Array> { + const resourceMeta = this.getResourceMeta(tenantId, resource); + + return R.when>, Array>>( + R.always(Boolean(resourceMeta.exportFlattenOn)), + (data) => flatDataCollections(data, resourceMeta.exportFlattenOn), + data + ); + } + /** + * Fetches exportable data for a given resource. + * @param {number} tenantId - The tenant identifier. + * @param {string} resource - The name of the resource. + * @returns A promise that resolves to the exportable data. + */ + private async getExportableData(tenantId: number, resource: string) { + const exportable = + this.exportableResources.registry.getExportable(resource); + return exportable.exportable(tenantId, {}); + } + + /** + * Extracts columns that are marked as exportable from the resource metadata. + * @param {IModelMeta} resourceMeta - The metadata of the resource. + * @returns An array of exportable columns. + */ + private getExportableColumns(resourceMeta: IModelMeta) { + const processColumns = ( + columns: { [key: string]: IModelMetaColumn }, + parent = '' + ) => { + return Object.entries(columns) + .filter(([_, value]) => value.exportable !== 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. + * @param {any[]} exportableColumns - The columns to be included in the workbook. + * @returns The created workbook. + */ + private createWorkbook(data: any[], exportableColumns: any[]) { + const workbook = xlsx.utils.book_new(); + 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); + xlsx.utils.book_append_sheet(workbook, worksheet, 'Exported Data'); + + return workbook; + } + + /** + * Exports the workbook in the specified format. + * @param {any} workbook - The workbook to be exported. + * @param {string} format - The format to export the workbook in. + * @returns The exported workbook data. + */ + private exportWorkbook(workbook: any, format: string) { + if (format.toLowerCase() === 'csv') { + return xlsx.write(workbook, { type: 'buffer', bookType: 'csv' }); + } else if (format.toLowerCase() === 'xlsx') { + return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' }); + } + } +} diff --git a/packages/server/src/services/Export/Exportable.ts b/packages/server/src/services/Export/Exportable.ts new file mode 100644 index 000000000..0e8801678 --- /dev/null +++ b/packages/server/src/services/Export/Exportable.ts @@ -0,0 +1,22 @@ +export class Exportable { + /** + * + * @param tenantId + * @returns + */ + public async exportable( + tenantId: number, + query: Record + ): Promise>> { + return []; + } + + /** + * + * @param data + * @returns + */ + public transform(data: Record) { + return data; + } +} diff --git a/packages/server/src/services/Export/common.ts b/packages/server/src/services/Export/common.ts new file mode 100644 index 000000000..5895e3367 --- /dev/null +++ b/packages/server/src/services/Export/common.ts @@ -0,0 +1,3 @@ +export enum Errors { + RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE', +} diff --git a/packages/server/src/services/Export/utils.ts b/packages/server/src/services/Export/utils.ts new file mode 100644 index 000000000..e1436d8ab --- /dev/null +++ b/packages/server/src/services/Export/utils.ts @@ -0,0 +1,27 @@ +import { flatMap } from 'lodash'; +/** + * Flattens the data based on a specified attribute. + * @param data - The data to be flattened. + * @param flattenAttr - The attribute to be flattened. + * @returns - The flattened data. + */ +export const flatDataCollections = ( + data: Record, + flattenAttr: string +): Record[] => { + return flatMap(data, (item) => + item[flattenAttr].map((entry) => ({ + ...item, + [flattenAttr]: entry, + })) + ); +}; + +/** + * Gets the data accessor for a given column. + * @param col - The column to get the data accessor for. + * @returns - The data accessor. + */ +export const getDataAccessor = (col: any) => { + return col.group ? `${col.group}.${col.accessor}` : col.accessor; +}; diff --git a/packages/server/src/services/ItemCategories/ItemCategoriesExportable.ts b/packages/server/src/services/ItemCategories/ItemCategoriesExportable.ts new file mode 100644 index 000000000..0cc8142ce --- /dev/null +++ b/packages/server/src/services/ItemCategories/ItemCategoriesExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { Exportable } from '../Export/Exportable'; +import { IAccountsFilter, IAccountsStructureType } from '@/interfaces'; +import ItemCategoriesService from './ItemCategoriesService'; + +@Service() +export class ItemCategoriesExportable extends Exportable { + @Inject() + private itemCategoriesApplication: ItemCategoriesService; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IAccountsFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + inactiveMode: false, + ...query, + structure: IAccountsStructureType.Flat, + } as IAccountsFilter; + + return this.itemCategoriesApplication + .getItemCategoriesList(tenantId, parsedQuery, {}) + .then((output) => output.itemCategories); + } +} diff --git a/packages/server/src/services/Items/ItemsExportable.ts b/packages/server/src/services/Items/ItemsExportable.ts new file mode 100644 index 000000000..223b403d9 --- /dev/null +++ b/packages/server/src/services/Items/ItemsExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { Exportable } from '../Export/Exportable'; +import { IItemsFilter } from '@/interfaces'; +import { ItemsApplication } from './ItemsApplication'; + +@Service() +export class ItemsExportable extends Exportable { + @Inject() + private itemsApplication: ItemsApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IItemsFilter) { + const parsedQuery = { + sortOrder: 'DESC', + columnSortBy: 'created_at', + page: 1, + ...query, + pageSize: 12, + } as IItemsFilter; + + return this.itemsApplication + .getItems(tenantId, parsedQuery) + .then((output) => output.items); + } +} diff --git a/packages/server/src/services/ManualJournals/GetManualJournals.ts b/packages/server/src/services/ManualJournals/GetManualJournals.ts index b6bca8848..d4dd35f4d 100644 --- a/packages/server/src/services/ManualJournals/GetManualJournals.ts +++ b/packages/server/src/services/ManualJournals/GetManualJournals.ts @@ -39,7 +39,7 @@ export class GetManualJournals { tenantId: number, filterDTO: IManualJournalsFilter ): Promise<{ - manualJournals: IManualJournal; + manualJournals: IManualJournal[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> => { diff --git a/packages/server/src/services/ManualJournals/ManualJournalExportable.ts b/packages/server/src/services/ManualJournals/ManualJournalExportable.ts new file mode 100644 index 000000000..10d16e992 --- /dev/null +++ b/packages/server/src/services/ManualJournals/ManualJournalExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { IManualJournalsFilter } from '@/interfaces'; +import { Exportable } from '../Export/Exportable'; +import { ManualJournalsApplication } from './ManualJournalsApplication'; + +@Service() +export class ManualJournalsExportable extends Exportable { + @Inject() + private manualJournalsApplication: ManualJournalsApplication; + + /** + * Retrieves the manual journals data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IManualJournalsFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 12000, + } as IManualJournalsFilter; + + return this.manualJournalsApplication + .getManualJournals(tenantId, parsedQuery) + .then((output) => output.manualJournals); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentExportable.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentExportable.ts new file mode 100644 index 000000000..8c2a89b48 --- /dev/null +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentExportable.ts @@ -0,0 +1,28 @@ +import { Inject, Service } from 'typedi'; +import { Exportable } from '@/services/Export/Exportable'; +import { BillPaymentsApplication } from './BillPaymentsApplication'; + +@Service() +export class BillPaymentExportable extends Exportable { + @Inject() + private billPaymentsApplication: BillPaymentsApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: any) { + const parsedQuery = { + page: 1, + pageSize: 12, + ...query, + sortOrder: 'desc', + columnSortBy: 'created_at', + } as any; + + return this.billPaymentsApplication + .getBillPayments(tenantId, parsedQuery) + .then((output) => output.billPayments); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts b/packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts index 2c9fda01f..9c4464403 100644 --- a/packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts +++ b/packages/server/src/services/Purchases/BillPayments/GetBillPayments.ts @@ -31,7 +31,7 @@ export class GetBillPayments { tenantId: number, filterDTO: IBillPaymentsFilter ): Promise<{ - billPayments: IBillPayment; + billPayments: IBillPayment[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { diff --git a/packages/server/src/services/Purchases/Bills/BillsApplication.ts b/packages/server/src/services/Purchases/Bills/BillsApplication.ts index 1ffdd5aa4..2593b03b2 100644 --- a/packages/server/src/services/Purchases/Bills/BillsApplication.ts +++ b/packages/server/src/services/Purchases/Bills/BillsApplication.ts @@ -99,7 +99,7 @@ export class BillsApplication { tenantId: number, filterDTO: IBillsFilter ): Promise<{ - bills: IBill; + bills: IBill[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { diff --git a/packages/server/src/services/Purchases/Bills/BillsExportable.ts b/packages/server/src/services/Purchases/Bills/BillsExportable.ts new file mode 100644 index 000000000..a45783643 --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/BillsExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { IBillsFilter } from '@/interfaces'; +import { Exportable } from '@/services/Export/Exportable'; +import { BillsApplication } from './BillsApplication'; + +@Service() +export class BillsExportable extends Exportable { + @Inject() + private billsApplication: BillsApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: IBillsFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 12000, + } as IBillsFilter; + + return this.billsApplication + .getBills(tenantId, parsedQuery) + .then((output) => output.bills); + } +} diff --git a/packages/server/src/services/Purchases/Bills/GetBills.ts b/packages/server/src/services/Purchases/Bills/GetBills.ts index 1ea19797d..73abc55da 100644 --- a/packages/server/src/services/Purchases/Bills/GetBills.ts +++ b/packages/server/src/services/Purchases/Bills/GetBills.ts @@ -49,6 +49,7 @@ export class GetBills { const { results, pagination } = await Bill.query() .onBuild((builder) => { builder.withGraphFetched('vendor'); + builder.withGraphFetched('entries.item'); dynamicFilter.buildQuery()(builder); }) .pagination(filter.page - 1, filter.pageSize); diff --git a/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts b/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts index be1431ac2..282b6f08e 100644 --- a/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts +++ b/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts @@ -14,6 +14,7 @@ export class VendorCreditTransformer extends Transformer { 'formattedSubtotal', 'formattedVendorCreditDate', 'formattedCreditsRemaining', + 'formattedInvoicedAmount', 'entries', ]; }; @@ -58,6 +59,17 @@ export class VendorCreditTransformer extends Transformer { }); }; + /** + * Retrieves the formatted invoiced amount. + * @param credit + * @returns {string} + */ + protected formattedInvoicedAmount = (credit) => { + return formatNumber(credit.invoicedAmount, { + currencyCode: credit.currencyCode, + }); + }; + /** * Retrieves the entries of the bill. * @param {IVendorCredit} vendorCredit diff --git a/packages/server/src/services/Purchases/VendorCredits/VendorCreditsExportable.ts b/packages/server/src/services/Purchases/VendorCredits/VendorCreditsExportable.ts new file mode 100644 index 000000000..4e0963165 --- /dev/null +++ b/packages/server/src/services/Purchases/VendorCredits/VendorCreditsExportable.ts @@ -0,0 +1,30 @@ +import { Inject, Service } from 'typedi'; +import { IVendorCreditsQueryDTO } from '@/interfaces'; +import ListVendorCredits from './ListVendorCredits'; +import { Exportable } from '@/services/Export/Exportable'; + +@Service() +export class VendorCreditsExportable extends Exportable { + @Inject() + private getVendorCredits: ListVendorCredits; + + /** + * Retrieves the vendor credits data to exportable sheet. + * @param {number} tenantId - + * @param {IVendorCreditsQueryDTO} query - + * @returns {} + */ + public exportable(tenantId: number, query: IVendorCreditsQueryDTO) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 12000, + } as IVendorCreditsQueryDTO; + + return this.getVendorCredits + .getVendorCredits(tenantId, parsedQuery) + .then((output) => output.vendorCredits); + } +} diff --git a/packages/server/src/services/Resource/ResourceService.ts b/packages/server/src/services/Resource/ResourceService.ts index 653599d05..3e0ffbafe 100644 --- a/packages/server/src/services/Resource/ResourceService.ts +++ b/packages/server/src/services/Resource/ResourceService.ts @@ -105,7 +105,11 @@ export default class ResourceService { const $enumerationType = (field) => field.fieldType === 'enumeration' ? field : undefined; - const $hasFields = (field) => 'undefined' !== typeof field.fields ? field : undefined; + const $hasFields = (field) => + 'undefined' !== typeof field.fields ? field : undefined; + + const $hasColumns = (column) => + 'undefined' !== typeof column.columns ? column : undefined; const naviagations = [ ['fields', qim.$each, 'name'], @@ -113,6 +117,8 @@ export default class ResourceService { ['fields2', qim.$each, 'name'], ['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'], ['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'], + ['columns', qim.$each, 'name'], + ['columns', qim.$each, $hasColumns, 'columns', qim.$each, 'name'], ]; return this.i18nService.i18nApply(naviagations, meta, tenantId); } diff --git a/packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts b/packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts index e0b53adb3..bba1db943 100644 --- a/packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts +++ b/packages/server/src/services/Sales/Estimates/GetSaleEstimates.ts @@ -51,6 +51,7 @@ export class GetSaleEstimates { .onBuild((builder) => { builder.withGraphFetched('customer'); builder.withGraphFetched('entries'); + builder.withGraphFetched('entries.item'); dynamicFilter.buildQuery()(builder); }) .pagination(filter.page - 1, filter.pageSize); diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesExportable.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesExportable.ts new file mode 100644 index 000000000..455805df9 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { ISalesInvoicesFilter } from '@/interfaces'; +import { Exportable } from '@/services/Export/Exportable'; +import { SaleEstimatesApplication } from './SaleEstimatesApplication'; + +@Service() +export class SaleEstimatesExportable extends Exportable { + @Inject() + private saleEstimatesApplication: SaleEstimatesApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: ISalesInvoicesFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 12000, + } as ISalesInvoicesFilter; + + return this.saleEstimatesApplication + .getSaleEstimates(tenantId, parsedQuery) + .then((output) => output.salesEstimates); + } +} diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts index b1d9b93db..569aceccb 100644 --- a/packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoices.ts @@ -49,7 +49,7 @@ export class GetSaleInvoices { ); const { results, pagination } = await SaleInvoice.query() .onBuild((builder) => { - builder.withGraphFetched('entries'); + builder.withGraphFetched('entries.item'); builder.withGraphFetched('customer'); dynamicFilter.buildQuery()(builder); }) diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesExportable.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesExportable.ts new file mode 100644 index 000000000..e806b0939 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { ISalesInvoicesFilter } from '@/interfaces'; +import { SaleInvoiceApplication } from './SaleInvoicesApplication'; +import { Exportable } from '@/services/Export/Exportable'; + +@Service() +export class SaleInvoicesExportable extends Exportable { + @Inject() + private saleInvoicesApplication: SaleInvoiceApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: ISalesInvoicesFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 120000, + } as ISalesInvoicesFilter; + + return this.saleInvoicesApplication + .getSaleInvoices(tenantId, parsedQuery) + .then((output) => output.salesInvoices); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentsReceivedExportable.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentsReceivedExportable.ts new file mode 100644 index 000000000..932c0f5f3 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentsReceivedExportable.ts @@ -0,0 +1,30 @@ +import { Inject, Service } from 'typedi'; +import { IAccountsStructureType, IPaymentReceivesFilter } from '@/interfaces'; +import { Exportable } from '@/services/Export/Exportable'; +import { PaymentReceivesApplication } from './PaymentReceivesApplication'; + +@Service() +export class PaymentsReceivedExportable extends Exportable { + @Inject() + private paymentReceivedApp: PaymentReceivesApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @param {IPaymentReceivesFilter} query - + * @returns + */ + public exportable(tenantId: number, query: IPaymentReceivesFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + inactiveMode: false, + ...query, + structure: IAccountsStructureType.Flat, + } as IPaymentReceivesFilter; + + return this.paymentReceivedApp + .getPaymentReceives(tenantId, parsedQuery) + .then((output) => output.paymentReceives); + } +} diff --git a/packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts b/packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts index 24150e349..1916c5e75 100644 --- a/packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts +++ b/packages/server/src/services/Sales/Receipts/GetSaleReceipts.ts @@ -11,6 +11,9 @@ import { SaleReceiptTransformer } from './SaleReceiptTransformer'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +interface GetSaleReceiptsSettings { + fetchEntriesGraph?: boolean; +} @Service() export class GetSaleReceipts { @Inject() @@ -50,7 +53,7 @@ export class GetSaleReceipts { .onBuild((builder) => { builder.withGraphFetched('depositAccount'); builder.withGraphFetched('customer'); - builder.withGraphFetched('entries'); + builder.withGraphFetched('entries.item'); dynamicFilter.buildQuery()(builder); }) diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsExportable.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsExportable.ts new file mode 100644 index 000000000..199c2e5a8 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsExportable.ts @@ -0,0 +1,29 @@ +import { Inject, Service } from 'typedi'; +import { ISalesReceiptsFilter } from '@/interfaces'; +import { Exportable } from '@/services/Export/Exportable'; +import { SaleReceiptApplication } from './SaleReceiptApplication'; + +@Service() +export class SaleReceiptsExportable extends Exportable { + @Inject() + private saleReceiptsApp: SaleReceiptApplication; + + /** + * Retrieves the accounts data to exportable sheet. + * @param {number} tenantId + * @returns + */ + public exportable(tenantId: number, query: ISalesReceiptsFilter) { + const parsedQuery = { + sortOrder: 'desc', + columnSortBy: 'created_at', + ...query, + page: 1, + pageSize: 12, + } as ISalesReceiptsFilter; + + return this.saleReceiptsApp + .getSaleReceipts(tenantId, parsedQuery) + .then((output) => output.data); + } +} diff --git a/packages/webapp/src/components/DialogsContainer.tsx b/packages/webapp/src/components/DialogsContainer.tsx index fc1195545..02f18d071 100644 --- a/packages/webapp/src/components/DialogsContainer.tsx +++ b/packages/webapp/src/components/DialogsContainer.tsx @@ -51,6 +51,7 @@ import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/ import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog'; import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog'; import { ConnectBankDialog } from '@/containers/CashFlow/ConnectBankDialog'; +import { ExportDialog } from '@/containers/Dialogs/ExportDialog'; /** * Dialogs container. @@ -148,6 +149,8 @@ export default function DialogsContainer() { + + ); } diff --git a/packages/webapp/src/constants/dialogs.ts b/packages/webapp/src/constants/dialogs.ts index 34d30bd46..cd425ce58 100644 --- a/packages/webapp/src/constants/dialogs.ts +++ b/packages/webapp/src/constants/dialogs.ts @@ -73,5 +73,6 @@ export enum DialogsName { CustomerTransactionsPdfPreview = 'CustomerTransactionsPdfPreview', VendorTransactionsPdfPreview = 'VendorTransactionsPdfPreview', GeneralLedgerPdfPreview = 'GeneralLedgerPdfPreview', - SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview' + SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview', + Export = 'Export', } diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx index 676322da1..c6a8c447a 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx @@ -18,7 +18,7 @@ import { Can, If, DashboardActionViewsList, - DashboardActionsBar + DashboardActionsBar, } from '@/components'; import { useRefreshJournals } from '@/hooks/query/manualJournals'; import { useManualJournalsContext } from './ManualJournalsListProvider'; @@ -31,6 +31,7 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import { compose } from '@/utils'; +import { DialogsName } from '@/constants/dialogs'; /** * Manual journal actions bar. @@ -47,6 +48,9 @@ function ManualJournalActionsBar({ // #withSettingsActions addSetting, + + // #withDialogActions + openDialog }) { // History context. const history = useHistory(); @@ -75,13 +79,18 @@ function ManualJournalActionsBar({ // Handle import button click. const handleImportBtnClick = () => { history.push('/manual-journals/import'); - } + }; // Handle table row size change. const handleTableRowSizeChange = (size) => { addSetting('manualJournals', 'tableSize', size); }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'manual_journal' }); + }; + return ( @@ -140,6 +149,7 @@ function ManualJournalActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> { history.push('/accounts/import'); }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'account' }); + }; return ( @@ -182,17 +186,18 @@ function AccountsActionsBar({ icon={} text={} /> - + + + + + ); +} + +export const ExportDialogFormContent = compose(withDialogActions)( + ExportDialogFormContentRoot, +); diff --git a/packages/webapp/src/containers/Dialogs/ExportDialog/constants.ts b/packages/webapp/src/containers/Dialogs/ExportDialog/constants.ts new file mode 100644 index 000000000..b971a8433 --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/ExportDialog/constants.ts @@ -0,0 +1,17 @@ +export const ExportResources = [ + { value: 'account', text: 'Accounts' }, + { value: 'item', text: 'Items' }, + { value: 'item_category', text: 'Item Categories' }, + { value: 'customer', text: 'Customers' }, + { value: 'vendor', text: 'Vendors' }, + { value: 'manual_journal', text: 'Manual Journal' }, + { value: 'expense', text: 'Expenses' }, + { value: 'sale_invoice', text: 'Invoices' }, + { value: 'sale_estimate', text: ' Estimates' }, + { value: 'sale_receipt', text: 'Receipts' }, + { value: 'payment_receive', text: 'Payments Received' }, + { value: 'credit_note', text: 'Credit Notes' }, + { value: 'bill', text: 'Bills' }, + { value: 'bill_payment', text: 'Bill Payments' }, + { value: 'vendor_credit', text: 'Vendor Credits' }, +]; diff --git a/packages/webapp/src/containers/Dialogs/ExportDialog/index.tsx b/packages/webapp/src/containers/Dialogs/ExportDialog/index.tsx new file mode 100644 index 000000000..2ca7e562a --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/ExportDialog/index.tsx @@ -0,0 +1,31 @@ +// @ts-nocheck +import React, { lazy } from 'react'; +import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import { compose } from '@/utils'; + +const ExportDialogContent = lazy(() => import('./ExportDialogContent')); + +// User form dialog. +function ExportDialogRoot({ dialogName, payload, isOpen }) { + const { resource = null, format = null } = payload; + + return ( + + + + + + ); +} + +export const ExportDialog = compose(withDialogRedux())(ExportDialogRoot); diff --git a/packages/webapp/src/containers/Dialogs/ExportDialog/type.ts b/packages/webapp/src/containers/Dialogs/ExportDialog/type.ts new file mode 100644 index 000000000..448c30cbc --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/ExportDialog/type.ts @@ -0,0 +1,6 @@ + + +export interface ExportFormInitialValues { + resource?: string; + format?: string; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx index e5c7368e4..1a1fb0209 100644 --- a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx +++ b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx @@ -33,6 +33,7 @@ import withDialogActions from '@/containers/Dialog/withDialogActions'; import withSettings from '@/containers/Settings/withSettings'; import { compose } from '@/utils'; +import { DialogsName } from '@/constants/dialogs'; /** * Expenses actions bar. @@ -49,6 +50,9 @@ function ExpensesActionsBar({ // #withSettingsActions addSetting, + + // #withDialogActions + openDialog, }) { // History context. const history = useHistory(); @@ -63,7 +67,6 @@ function ExpensesActionsBar({ const onClickNewExpense = () => { history.push('/expenses/new'); }; - // Handle delete button click. const handleBulkDelete = () => {}; @@ -73,21 +76,23 @@ function ExpensesActionsBar({ viewSlug: view ? view.slug : null, }); }; - // Handle click a refresh const handleRefreshBtnClick = () => { refresh(); }; - // Handle the import button click. const handleImportBtnClick = () => { history.push('/expenses/import'); - } - + }; // Handle table row size change. const handleTableRowSizeChange = (size) => { addSetting('expenses', 'tableSize', size); }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'expense' }); + }; + return ( @@ -146,6 +151,7 @@ function ExpensesActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> { + openDialog(DialogsName.Export, { resource: 'item' }); + } + return ( @@ -154,6 +164,7 @@ function ItemsActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> { + openDialog(DialogsName.Export, { resource: 'item_category' }); + }; return ( @@ -105,6 +110,7 @@ function ItemsCategoryActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx index da5415e45..3d0fbb2ff 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx @@ -32,6 +32,8 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions'; import { useBillsListContext } from './BillsListProvider'; import { useRefreshBills } from '@/hooks/query/bills'; import { compose } from '@/utils'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { DialogsName } from '@/constants/dialogs'; /** * Bills actions bar. @@ -48,6 +50,9 @@ function BillActionsBar({ // #withSettingsActions addSetting, + + // #withDialogActions + openDialog, }) { const history = useHistory(); @@ -81,7 +86,12 @@ function BillActionsBar({ // Handle the import button click. const handleImportBtnClick = () => { history.push('/bills/import'); - } + }; + + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'bill' }); + }; return ( @@ -141,6 +151,7 @@ function BillActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> @@ -170,4 +181,5 @@ export default compose( withSettings(({ billsettings }) => ({ billsTableSize: billsettings?.tableSize, })), + withDialogActions, )(BillActionsBar); diff --git a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx index e4a7b1ff9..7ba8e7531 100644 --- a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx +++ b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx @@ -22,14 +22,16 @@ import { import { useVendorsCreditNoteListContext } from './VendorsCreditNoteListProvider'; import { VendorCreditAction, AbilitySubject } from '@/constants/abilityOption'; + +import withVendorsCreditNotesActions from './withVendorsCreditNotesActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withVendorsCreditNotes from './withVendorsCreditNotes'; -import withVendorsCreditNotesActions from './withVendorsCreditNotesActions'; - +import withDialogActions from '@/containers/Dialog/withDialogActions'; import withVendorActions from './withVendorActions'; import { compose } from '@/utils'; +import { DialogsName } from '@/constants/dialogs'; /** * Vendors Credit note table actions bar. @@ -48,6 +50,9 @@ function VendorsCreditNoteActionsBar({ // #withSettingsActions addSetting, + + // #withDialogActions + openDialog, }) { const history = useHistory(); @@ -77,8 +82,13 @@ function VendorsCreditNoteActionsBar({ // Handle import button click. const handleImportBtnClick = () => { - history.push('/vendor-credits/import') - } + history.push('/vendor-credits/import'); + }; + + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'vendor_credit' }); + }; return ( @@ -128,6 +138,7 @@ function VendorsCreditNoteActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> ({ creditNoteTableSize: vendorsCreditNoteSetting?.tableSize, })), + withDialogActions, )(VendorsCreditNoteActionsBar); diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.tsx index 1d7e3d67d..f269f63ed 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.tsx @@ -33,6 +33,8 @@ import { useRefreshPaymentMades } from '@/hooks/query/paymentMades'; import { PaymentMadeAction, AbilitySubject } from '@/constants/abilityOption'; import { compose } from '@/utils'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { DialogsName } from '@/constants/dialogs'; /** * Payment made actions bar. @@ -47,6 +49,9 @@ function PaymentMadeActionsBar({ // #withSettings paymentMadesTableSize, + // #withDialogActions + openDialog, + // #withSettingsActions addSetting, }) { @@ -81,7 +86,12 @@ function PaymentMadeActionsBar({ // Handle the import button click. const handleImportBtnClick = () => { history.push('/payment-mades/import'); - } + }; + + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'bill_payment' }); + }; return ( @@ -139,6 +149,7 @@ function PaymentMadeActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> @@ -168,4 +179,5 @@ export default compose( withSettings(({ billPaymentSettings }) => ({ paymentMadesTableSize: billPaymentSettings?.tableSize, })), + withDialogActions, )(PaymentMadeActionsBar); diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx index 7c8294858..7e55e6a13 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx @@ -25,8 +25,10 @@ import withCreditNotes from './withCreditNotes'; import withCreditNotesActions from './withCreditNotesActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; import { compose } from '@/utils'; +import { DialogsName } from '@/constants/dialogs'; /** * Credit note table actions bar. @@ -43,6 +45,9 @@ function CreditNotesActionsBar({ // #withSettingsActions addSetting, + + // #withDialogActions + openDialog, }) { const history = useHistory(); @@ -74,6 +79,11 @@ function CreditNotesActionsBar({ history.push('/credit-notes/import'); }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'credit_note' }); + }; + return ( @@ -122,6 +132,7 @@ function CreditNotesActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> ({ creditNoteTableSize: creditNoteSettings?.tableSize, })), + withDialogActions, )(CreditNotesActionsBar); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx index ff4f5d6bd..2b374d2db 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.tsx @@ -31,6 +31,8 @@ import { useEstimatesListContext } from './EstimatesListProvider'; import { useRefreshEstimates } from '@/hooks/query/estimates'; import { SaleEstimateAction, AbilitySubject } from '@/constants/abilityOption'; import { compose } from '@/utils'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { DialogsName } from '@/constants/dialogs'; /** * Estimates list actions bar. @@ -45,6 +47,9 @@ function EstimateActionsBar({ // #withSettings estimatesTableSize, + // #withDialogActions + openDialog, + // #withSettingsActions addSetting, }) { @@ -80,7 +85,11 @@ function EstimateActionsBar({ // Handle the import button click. const handleImportBtnClick = () => { history.push('/estimates/import'); - } + }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'sale_estimate' }); + }; return ( @@ -141,6 +150,7 @@ function EstimateActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> ({ estimatesTableSize: estimatesSettings?.tableSize, })), + withDialogActions )(EstimateActionsBar); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.tsx index cdfa295a3..8f4a4d686 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.tsx @@ -29,6 +29,8 @@ import withInvoiceActions from './withInvoiceActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import { compose } from '@/utils'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { DialogsName } from '@/constants/dialogs'; /** * Invoices table actions bar. @@ -45,6 +47,9 @@ function InvoiceActionsBar({ // #withSettingsActions addSetting, + + // #withDialogsActions + openDialog }) { const history = useHistory(); @@ -79,6 +84,11 @@ function InvoiceActionsBar({ history.push('/invoices/import'); }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'sale_invoice' }); + }; + return ( @@ -135,6 +145,7 @@ function InvoiceActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> ({ invoicesTableSize: invoiceSettings?.tableSize, })), + withDialogActions, )(InvoiceActionsBar); diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.tsx index c1d510c1d..42bf63666 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.tsx @@ -26,6 +26,7 @@ import withPaymentReceives from './withPaymentReceives'; import withPaymentReceivesActions from './withPaymentReceivesActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; import { PaymentReceiveAction, AbilitySubject, @@ -33,6 +34,7 @@ import { import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider'; import { useRefreshPaymentReceive } from '@/hooks/query/paymentReceives'; import { compose } from '@/utils'; +import { DialogsName } from '@/constants/dialogs'; /** * Payment receives actions bar. @@ -49,6 +51,9 @@ function PaymentReceiveActionsBar({ // #withSettingsActions addSetting, + + // #withDialogActions + openDialog, }) { // History context. const history = useHistory(); @@ -82,6 +87,10 @@ function PaymentReceiveActionsBar({ const handleImportBtnClick = () => { history.push('/payment-receives/import'); }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'payment_receive' }); + }; return ( @@ -139,6 +148,7 @@ function PaymentReceiveActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> @@ -169,4 +179,5 @@ export default compose( withSettings(({ paymentReceiveSettings }) => ({ paymentReceivesTableSize: paymentReceiveSettings?.tableSize, })), + withDialogActions, )(PaymentReceiveActionsBar); diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.tsx index f27a505c9..f4ecfe39f 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.tsx @@ -35,6 +35,8 @@ import { useRefreshReceipts } from '@/hooks/query/receipts'; import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption'; import { compose } from '@/utils'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { DialogsName } from '@/constants/dialogs'; /** * Receipts actions bar. @@ -49,6 +51,9 @@ function ReceiptActionsBar({ // #withSettings receiptsTableSize, + // #withDialogActions + openDialog, + // #withSettingsActions addSetting, }) { @@ -86,6 +91,11 @@ function ReceiptActionsBar({ history.push('/receipts/import'); }; + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'sale_receipt' }); + }; + return ( @@ -145,6 +155,7 @@ function ReceiptActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> ({ receiptsTableSize: receiptSettings?.tableSize, })), + withDialogActions, )(ReceiptActionsBar); diff --git a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx index bd54a6b16..86c11ea8a 100644 --- a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx +++ b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx @@ -31,8 +31,10 @@ import withVendors from './withVendors'; import withVendorsActions from './withVendorsActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; import { compose } from '@/utils'; +import { DialogsName } from '@/constants/dialogs'; /** * Vendors actions bar. @@ -50,6 +52,9 @@ function VendorActionsBar({ // #withSettingsActions addSetting, + + // #withDialogActions + openDialog, }) { const history = useHistory(); @@ -83,10 +88,17 @@ function VendorActionsBar({ const handleTableRowSizeChange = (size) => { addSetting('vendors', 'tableSize', size); }; + // Handle import button success. const handleImportBtnSuccess = () => { history.push('/vendors/import'); }; + + // Handle the export button click. + const handleExportBtnClick = () => { + openDialog(DialogsName.Export, { resource: 'vendor' }); + }; + return ( @@ -138,6 +150,7 @@ function VendorActionsBar({ className={Classes.MINIMAL} icon={} text={} + onClick={handleExportBtnClick} /> ({ vendorsTableSize: vendorsSettings?.tableSize, })), + withDialogActions, )(VendorActionsBar); diff --git a/packages/webapp/src/hooks/query/FinancialReports/use-export.ts b/packages/webapp/src/hooks/query/FinancialReports/use-export.ts new file mode 100644 index 000000000..63e1e5d74 --- /dev/null +++ b/packages/webapp/src/hooks/query/FinancialReports/use-export.ts @@ -0,0 +1,38 @@ +// @ts-nocheck +import { downloadFile } from '@/hooks/useDownloadFile'; +import useApiRequest from '@/hooks/useRequest'; +import { AxiosError } from 'axios'; +import { useMutation } from 'react-query'; + +interface ResourceExportValues { + resource: string; + format: string; +} +/** + * Initiates a download of the balance sheet in XLSX format. + * @param {Object} query - The query parameters for the request. + * @param {Object} args - Additional configurations for the download. + * @returns {Function} A function to trigger the file download. + */ +export const useResourceExport = () => { + const apiRequest = useApiRequest(); + + return useMutation((data: ResourceExportValues) => { + return apiRequest + .get('/export', { + responseType: 'blob', + headers: { + accept: + data.format === 'xlsx' ? 'application/xlsx' : 'application/csv', + }, + params: { + resource: data.resource, + format: data.format, + }, + }) + .then((res) => { + downloadFile(res.data, `${data.resource}.${data.format}`); + return res; + }); + }); +};