Merge pull request #430 from bigcapitalhq/export-resources

feat: Export resource data to csv, xlsx
This commit is contained in:
Ahmed Bouhuolia
2024-05-02 16:20:20 +02:00
committed by GitHub
80 changed files with 2630 additions and 51 deletions

View File

@@ -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",

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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);

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
})

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,49 @@
import { camelCase, upperFirst } from 'lodash';
import { Exportable } from './Exportable';
export class ExportableRegistry {
private static instance: ExportableRegistry;
private exportables: Record<string, Exportable>;
/**
* 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));
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<Record<string, any>>} data - The original data to be transformed.
* @returns {Array<Record<string, any>>} - The transformed data.
*/
private transformExportedData(
tenantId: number,
resource: string,
data: Array<Record<string, any>>
): Array<Record<string, any>> {
const resourceMeta = this.getResourceMeta(tenantId, resource);
return R.when<Array<Record<string, any>>, Array<Record<string, any>>>(
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' });
}
}
}

View File

@@ -0,0 +1,22 @@
export class Exportable {
/**
*
* @param tenantId
* @returns
*/
public async exportable(
tenantId: number,
query: Record<string, any>
): Promise<Array<Record<string, any>>> {
return [];
}
/**
*
* @param data
* @returns
*/
public transform(data: Record<string, any>) {
return data;
}
}

View File

@@ -0,0 +1,3 @@
export enum Errors {
RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE',
}

View File

@@ -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<string, any>,
flattenAttr: string
): Record<string, any>[] => {
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;
};

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -39,7 +39,7 @@ export class GetManualJournals {
tenantId: number,
filterDTO: IManualJournalsFilter
): Promise<{
manualJournals: IManualJournal;
manualJournals: IManualJournal[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> => {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -31,7 +31,7 @@ export class GetBillPayments {
tenantId: number,
filterDTO: IBillPaymentsFilter
): Promise<{
billPayments: IBillPayment;
billPayments: IBillPayment[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {

View File

@@ -99,7 +99,7 @@ export class BillsApplication {
tenantId: number,
filterDTO: IBillsFilter
): Promise<{
bills: IBill;
bills: IBill[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
})

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
})

View File

@@ -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);
}
}

View File

@@ -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() {
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ConnectBankDialog dialogName={DialogsName.ConnectBankCreditCard} />
<ExportDialog dialogName={DialogsName.Export} />
</div>
);
}

View File

@@ -73,5 +73,6 @@ export enum DialogsName {
CustomerTransactionsPdfPreview = 'CustomerTransactionsPdfPreview',
VendorTransactionsPdfPreview = 'VendorTransactionsPdfPreview',
GeneralLedgerPdfPreview = 'GeneralLedgerPdfPreview',
SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview'
SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview',
Export = 'Export',
}

View File

@@ -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 (
<DashboardActionsBar>
<NavbarGroup>
@@ -140,6 +149,7 @@ function ManualJournalActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton

View File

@@ -118,6 +118,10 @@ function AccountsActionsBar({
const handleImportBtnClick = () => {
history.push('/accounts/import');
};
// Handle the export button click.
const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'account' });
};
return (
<DashboardActionsBar>
@@ -182,17 +186,18 @@ function AccountsActionsBar({
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={accountsTableSize}

View File

@@ -92,13 +92,13 @@ function CashFlowAccountsActionsBar({
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<NavbarDivider />
<Can I={CashflowAction.Edit} a={AbilitySubject.Cashflow}>
@@ -117,7 +117,7 @@ function CashFlowAccountsActionsBar({
text={'Connect to Bank / Credit Card'}
onClick={handleConnectToBank}
/>
<NavbarDivider />
<NavbarDivider />
</FeatureCan>
<Button
className={Classes.MINIMAL}

View File

@@ -31,9 +31,11 @@ import withCustomersActions from './withCustomersActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withSettings from '@/containers/Settings/withSettings';
import { CustomerAction, AbilitySubject } from '@/constants/abilityOption';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { CustomerAction, AbilitySubject } from '@/constants/abilityOption';
import { compose } from '@/utils';
import { DialogsName } from '@/constants/dialogs';
/**
* Customers actions bar.
@@ -55,6 +57,9 @@ function CustomerActionsBar({
// #withSettingsActions
addSetting,
// #withDialogActions
openDialog,
}) {
// History context.
const history = useHistory();
@@ -100,6 +105,11 @@ function CustomerActionsBar({
history.push('/customers/import');
};
// Handle the export button click.
const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'customer' });
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -154,6 +164,7 @@ function CustomerActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -192,4 +203,5 @@ export default compose(
customersTableSize: customersSettings?.tableSize,
})),
withAlertActions,
withDialogActions,
)(CustomerActionsBar);

View File

@@ -0,0 +1,18 @@
.root{
padding: 20px;
}
.footer{
margin-top: 2rem;
}
.resourceFormGroup{
max-width: 280px;
}
.paragraph{
color: #5F6B7C;
margin-bottom: 1.2rem;
}

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
import { ExportDialogForm } from './ExportDialogForm';
import { ExportFormInitialValues } from './type';
interface ExportDialogContentProps {
initialValues?: ExportFormInitialValues;
}
/**
* Account dialog content.
*/
export default function ExportDialogContent({
initialValues,
}: ExportDialogContentProps) {
return <ExportDialogForm initialValues={initialValues} />;
}

View File

@@ -0,0 +1,9 @@
// @ts-nocheck
import * as Yup from 'yup';
const Schema = Yup.object().shape({
resource: Yup.string().required().label('Resource'),
format: Yup.string().required().label('Format'),
});
export const ExportDialogFormSchema = Schema;

View File

@@ -0,0 +1,78 @@
// @ts-nocheck
import { Formik } from 'formik';
import { compose, transformToForm } from '@/utils';
import { ExportDialogFormSchema } from './ExportDialogForm.schema';
import { ExportDialogFormContent } from './ExportDialogFormContent';
import { useResourceExport } from '@/hooks/query/FinancialReports/use-export';
import { ExportFormInitialValues } from './type';
import { AppToaster } from '@/components';
import { Intent } from '@blueprintjs/core';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
// Default initial form values.
const defaultInitialValues = {
resource: '',
format: 'csv',
};
interface ExportDialogFormProps {
initialValues?: ExportFormInitialValues;
}
/**
* Account form dialog content.
*/
function ExportDialogFormRoot({
// #ownProps
initialValues,
// #withDialogActions
closeDialog,
}: ExportDialogFormProps) {
const { mutateAsync: mutateExport } = useResourceExport();
// Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
const { resource, format } = values;
mutateExport({ resource, format })
.then(() => {
setSubmitting(false);
closeDialog(DialogsName.Export);
})
.catch(() => {
setSubmitting(false);
AppToaster.show({
intent: Intent.DANGER,
message: 'Something went wrong!',
});
});
};
// Form initial values in create and edit mode.
const initialFormValues = {
...defaultInitialValues,
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(initialValues, defaultInitialValues),
};
return (
<Formik
validationSchema={ExportDialogFormSchema}
initialValues={initialFormValues}
onSubmit={handleFormSubmit}
>
<ExportDialogFormContent />
</Formik>
);
}
export const ExportDialogForm =
compose(withDialogActions)(ExportDialogFormRoot);

View File

@@ -0,0 +1,63 @@
// @ts-nocheck
import { FFormGroup, FRadioGroup, FSelect, Group } from '@/components';
import { Button, Intent, Radio } from '@blueprintjs/core';
import { Form, useFormikContext } from 'formik';
import { ExportResources } from './constants';
import styles from './ExportDialogContent.module.scss';
import { compose } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
function ExportDialogFormContentRoot({
// #withDialogActions
closeDialog,
}) {
const { isSubmitting } = useFormikContext();
const handleCancelBtnClick = () => {
closeDialog(DialogsName.Export);
};
return (
<Form>
<div className={styles.root}>
<p className={styles.paragraph}>
You can export data from Bigcapital in CSV or XLSX format
</p>
<FFormGroup
name={'resource'}
label={'Select Resource'}
className={styles.resourceFormGroup}
>
<FSelect
name={'resource'}
items={ExportResources}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
<FRadioGroup label={'Export As'} name={'format'}>
<Radio value={'xlsx'}>XLSX (Microsoft Excel)</Radio>
<Radio value={'csv'}>CSV (Comma Seperated Value)</Radio>
</FRadioGroup>
<Group position={'right'} spacing={10} className={styles.footer}>
<Button intent={Intent.NONE} onClick={handleCancelBtnClick}>
Cancel
</Button>
<Button
type={'submit'}
intent={Intent.PRIMARY}
loading={isSubmitting}
>
Export
</Button>
</Group>
</div>
</Form>
);
}
export const ExportDialogFormContent = compose(withDialogActions)(
ExportDialogFormContentRoot,
);

View File

@@ -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' },
];

View File

@@ -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 (
<Dialog
name={dialogName}
title={'Export Data'}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
>
<DialogSuspense>
<ExportDialogContent
dialogName={dialogName}
initialValues={{ resource, format }}
/>
</DialogSuspense>
</Dialog>
);
}
export const ExportDialog = compose(withDialogRedux())(ExportDialogRoot);

View File

@@ -0,0 +1,6 @@
export interface ExportFormInitialValues {
resource?: string;
format?: string;
}

View File

@@ -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 (
<DashboardActionsBar>
<NavbarGroup>
@@ -146,6 +151,7 @@ function ExpensesActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton

View File

@@ -33,7 +33,9 @@ import withItemsActions from './withItemsActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '../Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils';
/**
@@ -56,6 +58,9 @@ function ItemsActionsBar({
// #withSettingsActions
addSetting,
// #withDialogActions
openDialog
}) {
// Items list context.
const { itemsViews, fields } = useItemsListContext();
@@ -99,6 +104,11 @@ function ItemsActionsBar({
history.push('/items/import');
};
// Handle the export button click.
const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'item' });
}
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -154,6 +164,7 @@ function ItemsActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -193,4 +204,5 @@ export default compose(
})),
withItemsActions,
withAlertActions,
withDialogActions
)(ItemsActionsBar);

View File

@@ -23,6 +23,7 @@ import withAlertActions from '@/containers/Alert/withAlertActions';
import { compose } from '@/utils';
import { useItemsCategoriesContext } from './ItemsCategoriesProvider';
import { useHistory } from 'react-router-dom';
import { DialogsName } from '@/constants/dialogs';
/**
* Items categories actions bar.
@@ -58,6 +59,10 @@ function ItemsCategoryActionsBar({
itemCategoriesIds: itemCategoriesSelectedRows,
});
};
// Handle the export button click.
const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'item_category' });
};
return (
<DashboardActionsBar>
@@ -105,6 +110,7 @@ function ItemsCategoryActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>

View File

@@ -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 (
<DashboardActionsBar>
@@ -141,6 +151,7 @@ function BillActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
@@ -170,4 +181,5 @@ export default compose(
withSettings(({ billsettings }) => ({
billsTableSize: billsettings?.tableSize,
})),
withDialogActions,
)(BillActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
@@ -128,6 +138,7 @@ function VendorsCreditNoteActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -157,4 +168,5 @@ export default compose(
withSettings(({ vendorsCreditNoteSetting }) => ({
creditNoteTableSize: vendorsCreditNoteSetting?.tableSize,
})),
withDialogActions,
)(VendorsCreditNoteActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
@@ -139,6 +149,7 @@ function PaymentMadeActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
@@ -168,4 +179,5 @@ export default compose(
withSettings(({ billPaymentSettings }) => ({
paymentMadesTableSize: billPaymentSettings?.tableSize,
})),
withDialogActions,
)(PaymentMadeActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
<NavbarGroup>
@@ -122,6 +132,7 @@ function CreditNotesActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -150,4 +161,5 @@ export default compose(
withSettings(({ creditNoteSettings }) => ({
creditNoteTableSize: creditNoteSettings?.tableSize,
})),
withDialogActions,
)(CreditNotesActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
@@ -141,6 +150,7 @@ function EstimateActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -170,4 +180,5 @@ export default compose(
withSettings(({ estimatesSettings }) => ({
estimatesTableSize: estimatesSettings?.tableSize,
})),
withDialogActions
)(EstimateActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
<NavbarGroup>
@@ -135,6 +145,7 @@ function InvoiceActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -163,4 +174,5 @@ export default compose(
withSettings(({ invoiceSettings }) => ({
invoicesTableSize: invoiceSettings?.tableSize,
})),
withDialogActions,
)(InvoiceActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
@@ -139,6 +148,7 @@ function PaymentReceiveActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
@@ -169,4 +179,5 @@ export default compose(
withSettings(({ paymentReceiveSettings }) => ({
paymentReceivesTableSize: paymentReceiveSettings?.tableSize,
})),
withDialogActions,
)(PaymentReceiveActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
<NavbarGroup>
@@ -145,6 +155,7 @@ function ReceiptActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -173,4 +184,5 @@ export default compose(
withSettings(({ receiptSettings }) => ({
receiptsTableSize: receiptSettings?.tableSize,
})),
withDialogActions,
)(ReceiptActionsBar);

View File

@@ -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 (
<DashboardActionsBar>
<NavbarGroup>
@@ -138,6 +150,7 @@ function VendorActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
@@ -175,4 +188,5 @@ export default compose(
withSettings(({ vendorsSettings }) => ({
vendorsTableSize: vendorsSettings?.tableSize,
})),
withDialogActions,
)(VendorActionsBar);

View File

@@ -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<void, AxiosError, any>((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;
});
});
};