feat: flatten the nested columns of exported data

This commit is contained in:
Ahmed Bouhuolia
2024-05-02 15:38:57 +02:00
parent 55aab76c9b
commit 83a5010dc5
21 changed files with 392 additions and 97 deletions

View File

@@ -432,6 +432,7 @@
"vendor.field.created_at": "Created at", "vendor.field.created_at": "Created at",
"vendor.field.balance": "Balance", "vendor.field.balance": "Balance",
"vendor.field.status": "Status", "vendor.field.status": "Status",
"vendor.field.note": "Note",
"vendor.field.currency": "Currency", "vendor.field.currency": "Currency",
"vendor.field.status.active": "Active", "vendor.field.status.active": "Active",
"vendor.field.status.inactive": "Inactive", "vendor.field.status.inactive": "Inactive",

View File

@@ -126,9 +126,10 @@ export interface IModelMeta {
defaultFilterField: string; defaultFilterField: string;
defaultSort: IModelMetaDefaultSort; defaultSort: IModelMetaDefaultSort;
importable?: boolean;
exportable?: boolean; exportable?: boolean;
exportFlattenOn?: string;
importable?: boolean;
importAggregator?: string; importAggregator?: string;
importAggregateOn?: string; importAggregateOn?: string;
importAggregateBy?: string; importAggregateBy?: string;
@@ -174,4 +175,11 @@ interface IModelMetaColumnText {
type: 'text;'; type: 'text;';
} }
export type IModelMetaColumn = ImodelMetaColumnMeta & IModelMetaColumnText; interface IModelMetaColumnCollection {
type: 'collection';
collectionOf: 'object';
columns: { [key: string]: ImodelMetaColumnMeta & IModelMetaColumnText };
}
export type IModelMetaColumn = ImodelMetaColumnMeta &
(IModelMetaColumnText | IModelMetaColumnCollection);

View File

@@ -5,6 +5,7 @@ export default {
sortField: 'bill_date', sortField: 'bill_date',
}, },
importable: true, importable: true,
exportFlattenOn: 'entries',
exportable: true, exportable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
@@ -121,7 +122,7 @@ export default {
}, },
paidAmount: { paidAmount: {
name: 'Paid Amount', name: 'Paid Amount',
accessor: 'formattedPaymentAmount' accessor: 'formattedPaymentAmount',
}, },
note: { note: {
name: 'Note', name: 'Note',
@@ -131,6 +132,33 @@ export default {
name: 'Open', name: 'Open',
type: 'boolean', 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: { fields2: {
billNumber: { billNumber: {

View File

@@ -13,10 +13,13 @@ export default {
sortField: 'name', sortField: 'name',
}, },
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importable: true, importable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'creditNoteNumber', importAggregateBy: 'creditNoteNumber',
fields: { fields: {
customer: { customer: {
name: 'credit_note.field.customer', name: 'credit_note.field.customer',
@@ -116,6 +119,32 @@ export default {
name: 'Open', name: 'Open',
type: 'boolean', 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: { fields2: {
customerId: { customerId: {

View File

@@ -95,157 +95,132 @@ export default {
firstName: { firstName: {
name: 'vendor.field.first_name', name: 'vendor.field.first_name',
type: 'text', type: 'text',
exportable: true,
}, },
lastName: { lastName: {
name: 'vendor.field.last_name', name: 'vendor.field.last_name',
type: 'text', type: 'text',
exportable: true,
}, },
displayName: { displayName: {
name: 'vendor.field.display_name', name: 'vendor.field.display_name',
type: 'text', type: 'text',
exportable: true,
}, },
email: { email: {
name: 'vendor.field.email', name: 'vendor.field.email',
type: 'text', type: 'text',
exportable: true,
}, },
workPhone: { workPhone: {
name: 'vendor.field.work_phone', name: 'vendor.field.work_phone',
type: 'text', type: 'text',
exportable: true,
}, },
personalPhone: { personalPhone: {
name: 'vendor.field.personal_pone', name: 'vendor.field.personal_phone',
type: 'text', type: 'text',
exportable: true,
}, },
companyName: { companyName: {
name: 'vendor.field.company_name', name: 'vendor.field.company_name',
type: 'text', type: 'text',
exportable: true,
}, },
website: { website: {
name: 'vendor.field.website', name: 'vendor.field.website',
type: 'text', type: 'text',
exportable: true,
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
exportable: true,
}, },
balance: { balance: {
name: 'vendor.field.balance', name: 'vendor.field.balance',
type: 'number', type: 'number',
exportable: true,
}, },
openingBalance: { openingBalance: {
name: 'vendor.field.opening_balance', name: 'vendor.field.opening_balance',
type: 'number', type: 'number',
exportable: true,
}, },
openingBalanceAt: { openingBalanceAt: {
name: 'vendor.field.opening_balance_at', name: 'vendor.field.opening_balance_at',
type: 'date', type: 'date',
exportable: true,
}, },
currencyCode: { currencyCode: {
name: 'vendor.field.currency', name: 'vendor.field.currency',
type: 'text', type: 'text',
exportable: true,
}, },
status: { status: {
name: 'vendor.field.status', name: 'vendor.field.status',
exportable: true, },
note: {
name: 'vendor.field.note',
}, },
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
name: 'Billing Address 1', name: 'Billing Address 1',
column: 'billing_address1', column: 'billing_address1',
type: 'text', type: 'text',
exportable: true,
}, },
billingAddress2: { billingAddress2: {
name: 'Billing Address 2', name: 'Billing Address 2',
column: 'billing_address2', column: 'billing_address2',
type: 'text', type: 'text',
exportable: true,
}, },
billingAddressCity: { billingAddressCity: {
name: 'Billing Address City', name: 'Billing Address City',
column: 'billing_address_city', column: 'billing_address_city',
type: 'text', type: 'text',
exportable: true,
}, },
billingAddressCountry: { billingAddressCountry: {
name: 'Billing Address Country', name: 'Billing Address Country',
column: 'billing_address_country', column: 'billing_address_country',
type: 'text', type: 'text',
exportable: true,
}, },
billingAddressPostcode: { billingAddressPostcode: {
name: 'Billing Address Postcode', name: 'Billing Address Postcode',
column: 'billing_address_postcode', column: 'billing_address_postcode',
type: 'text', type: 'text',
exportable: true,
}, },
billingAddressState: { billingAddressState: {
name: 'Billing Address State', name: 'Billing Address State',
column: 'billing_address_state', column: 'billing_address_state',
type: 'text', type: 'text',
exportable: true,
}, },
billingAddressPhone: { billingAddressPhone: {
name: 'Billing Address Phone', name: 'Billing Address Phone',
column: 'billing_address_phone', column: 'billing_address_phone',
type: 'text', type: 'text',
exportable: true,
}, },
// Shipping Address // Shipping Address
shippingAddress1: { shippingAddress1: {
name: 'Shipping Address 1', name: 'Shipping Address 1',
column: 'shipping_address1', column: 'shipping_address1',
type: 'text', type: 'text',
exportable: true,
}, },
shippingAddress2: { shippingAddress2: {
name: 'Shipping Address 2', name: 'Shipping Address 2',
column: 'shipping_address2', column: 'shipping_address2',
type: 'text', type: 'text',
exportable: true,
}, },
shippingAddressCity: { shippingAddressCity: {
name: 'Shipping Address City', name: 'Shipping Address City',
column: 'shipping_address_city', column: 'shipping_address_city',
type: 'text', type: 'text',
exportable: true,
}, },
shippingAddressCountry: { shippingAddressCountry: {
name: 'Shipping Address Country', name: 'Shipping Address Country',
column: 'shipping_address_country', column: 'shipping_address_country',
type: 'text', type: 'text',
exportable: true,
}, },
shippingAddressPostcode: { shippingAddressPostcode: {
name: 'Shipping Address Postcode', name: 'Shipping Address Postcode',
column: 'shipping_address_postcode', column: 'shipping_address_postcode',
type: 'text', type: 'text',
exportable: true,
}, },
shippingAddressPhone: { shippingAddressPhone: {
name: 'Shipping Address Phone', name: 'Shipping Address Phone',
column: 'shipping_address_phone', column: 'shipping_address_phone',
type: 'text', type: 'text',
exportable: true,
}, },
shippingAddressState: { shippingAddressState: {
name: 'Shipping Address State', name: 'Shipping Address State',
column: 'shipping_address_state', column: 'shipping_address_state',
type: 'text', type: 'text',
exportable: true, },
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
}, },
}, },
fields2: { fields2: {

View File

@@ -8,6 +8,7 @@ export default {
sortField: 'name', sortField: 'name',
}, },
importable: true, importable: true,
exportFlattenOn: 'categories',
exportable: true, exportable: true,
fields: { fields: {
payment_date: { payment_date: {
@@ -66,42 +67,50 @@ export default {
paymentReceive: { paymentReceive: {
name: 'expense.field.payment_account', name: 'expense.field.payment_account',
type: 'text', type: 'text',
exportable: true, accessor: 'paymentAccount.name'
}, },
referenceNo: { referenceNo: {
name: 'expense.field.reference_no', name: 'expense.field.reference_no',
type: 'text', type: 'text',
exportable: true,
}, },
paymentDate: { paymentDate: {
name: 'expense.field.payment_date', name: 'expense.field.payment_date',
type: 'date', type: 'date',
exportable: true,
}, },
currencyCode: { currencyCode: {
name: 'expense.field.currency_code', name: 'expense.field.currency_code',
type: 'text', type: 'text',
exportable: true,
}, },
exchangeRate: { exchangeRate: {
name: 'expense.field.exchange_rate', name: 'expense.field.exchange_rate',
type: 'number', type: 'number',
exportable: true,
}, },
description: { description: {
name: 'expense.field.description', name: 'expense.field.description',
type: 'text', type: 'text',
exportable: true,
}, },
categories: { categories: {
name: 'expense.field.categories', name: 'expense.field.categories',
type: 'collection', type: 'collection',
exportable: true, 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: { publish: {
name: 'expense.field.publish', name: 'expense.field.publish',
type: 'boolean', type: 'boolean',
exportable: true,
}, },
}, },
fields2: { fields2: {

View File

@@ -5,6 +5,8 @@ export default {
sortField: 'name', sortField: 'name',
}, },
importable: true, importable: true,
exportFlattenOn: 'entries',
exportable: true, exportable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
@@ -61,42 +63,70 @@ export default {
date: { date: {
name: 'manual_journal.field.date', name: 'manual_journal.field.date',
type: 'date', type: 'date',
exportable: true,
}, },
journalNumber: { journalNumber: {
name: 'manual_journal.field.journal_number', name: 'manual_journal.field.journal_number',
type: 'text', type: 'text',
exportable: true,
}, },
reference: { reference: {
name: 'manual_journal.field.reference', name: 'manual_journal.field.reference',
type: 'text', type: 'text',
exportable: true,
}, },
journalType: { journalType: {
name: 'manual_journal.field.journal_type', name: 'manual_journal.field.journal_type',
type: 'text', type: 'text',
exportable: true, },
amount: {
name: 'Amount',
accessor: 'formattedAmount',
}, },
currencyCode: { currencyCode: {
name: 'manual_journal.field.currency', name: 'manual_journal.field.currency',
type: 'text', type: 'text',
exportable: true,
}, },
exchange_rate: { exchangeRate: {
name: 'manual_journal.field.exchange_rate', name: 'manual_journal.field.exchange_rate',
type: 'number', type: 'number',
exportable: true,
}, },
description: { description: {
name: 'manual_journal.field.description', name: 'manual_journal.field.description',
type: 'text', type: 'text',
exportable: true,
}, },
publish: { entries: {
name: 'Publish', name: 'Entries',
type: 'boolean', type: 'collection',
exportable: true, 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: { fields2: {

View File

@@ -5,6 +5,8 @@ export default {
sortField: 'estimate_date', sortField: 'estimate_date',
}, },
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importable: true, importable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
@@ -131,6 +133,33 @@ export default {
type: 'boolean', type: 'boolean',
exportable: true, 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: { fields2: {
customerId: { customerId: {
@@ -191,7 +220,7 @@ export default {
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: "Matches the item name or code." importHint: 'Matches the item name or code.',
}, },
rate: { rate: {
name: 'invoice.field.rate', name: 'invoice.field.rate',

View File

@@ -5,6 +5,8 @@ export default {
sortField: 'created_at', sortField: 'created_at',
}, },
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importable: true, importable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
@@ -125,7 +127,7 @@ export default {
}, },
paidAmount: { paidAmount: {
name: 'Paid Amount', name: 'Paid Amount',
accessor: 'paymentAmountFormatted' accessor: 'paymentAmountFormatted',
}, },
dueAmount: { dueAmount: {
name: 'Due Amount', name: 'Due Amount',
@@ -143,6 +145,33 @@ export default {
name: 'invoice.field.delivered', name: 'invoice.field.delivered',
type: 'boolean', 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: { fields2: {
invoiceDate: { invoiceDate: {

View File

@@ -5,6 +5,8 @@ export default {
sortField: 'created_at', sortField: 'created_at',
}, },
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importable: true, importable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
@@ -117,10 +119,6 @@ export default {
name: 'receipt.field.statement', name: 'receipt.field.statement',
type: 'text', type: 'text',
}, },
createdAt: {
name: 'receipt.field.created_at',
type: 'date',
},
status: { status: {
name: 'receipt.field.status', name: 'receipt.field.status',
type: 'enumeration', type: 'enumeration',
@@ -130,6 +128,37 @@ export default {
], ],
exportable: true, 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: { fields2: {
receiptDate: { receiptDate: {

View File

@@ -95,71 +95,57 @@ export default {
firstName: { firstName: {
name: 'vendor.field.first_name', name: 'vendor.field.first_name',
type: 'text', type: 'text',
exportable: true,
}, },
lastName: { lastName: {
name: 'vendor.field.last_name', name: 'vendor.field.last_name',
type: 'text', type: 'text',
exportable: true,
}, },
displayName: { displayName: {
name: 'vendor.field.display_name', name: 'vendor.field.display_name',
type: 'text', type: 'text',
exportable: true,
}, },
email: { email: {
name: 'vendor.field.email', name: 'vendor.field.email',
type: 'text', type: 'text',
exportable: true,
}, },
workPhone: { workPhone: {
name: 'vendor.field.work_phone', name: 'vendor.field.work_phone',
type: 'text', type: 'text',
exportable: true,
}, },
personalPhone: { personalPhone: {
name: 'vendor.field.personal_phone', name: 'vendor.field.personal_phone',
type: 'text', type: 'text',
exportable: true,
}, },
companyName: { companyName: {
name: 'vendor.field.company_name', name: 'vendor.field.company_name',
type: 'text', type: 'text',
exportable: true,
}, },
website: { website: {
name: 'vendor.field.website', name: 'vendor.field.website',
type: 'text', type: 'text',
exportable: true,
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
exportable: true,
}, },
balance: { balance: {
name: 'vendor.field.balance', name: 'vendor.field.balance',
type: 'number', type: 'number',
exportable: true,
}, },
openingBalance: { openingBalance: {
name: 'vendor.field.opening_balance', name: 'vendor.field.opening_balance',
type: 'number', type: 'number',
exportable: true,
}, },
openingBalanceAt: { openingBalanceAt: {
name: 'vendor.field.opening_balance_at', name: 'vendor.field.opening_balance_at',
type: 'date', type: 'date',
exportable: true,
}, },
currencyCode: { currencyCode: {
name: 'vendor.field.currency', name: 'vendor.field.currency',
type: 'text', type: 'text',
exportable: true,
}, },
status: { status: {
name: 'vendor.field.status', name: 'vendor.field.status',
exportable: true, },
note: {
name: 'vendor.field.note',
type: 'text',
}, },
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
@@ -246,7 +232,12 @@ export default {
column: 'shipping_address_phone', column: 'shipping_address_phone',
type: 'text', type: 'text',
exportable: true, exportable: true,
} },
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
exportable: true,
},
}, },
fields2: { fields2: {
firstName: { firstName: {

View File

@@ -13,10 +13,13 @@ export default {
sortField: 'name', sortField: 'name',
}, },
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importable: true, importable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'vendorCreditNumber', importAggregateBy: 'vendorCreditNumber',
fields: { fields: {
vendor: { vendor: {
name: 'vendor_credit.field.vendor', name: 'vendor_credit.field.vendor',
@@ -99,6 +102,22 @@ export default {
name: 'Vendor Credit Date', name: 'Vendor Credit Date',
type: '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: { note: {
name: 'Note', name: 'Note',
type: 'text', type: 'text',
@@ -107,6 +126,32 @@ export default {
name: 'Open', name: 'Open',
type: 'boolean', 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: { fields2: {
vendorId: { vendorId: {

View File

@@ -45,7 +45,7 @@ export default class ListCreditNotes extends BaseCreditNotes {
); );
const { results, pagination } = await CreditNote.query() const { results, pagination } = await CreditNote.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('entries'); builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })

View File

@@ -1,12 +1,14 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import xlsx from 'xlsx'; import xlsx from 'xlsx';
import * as R from 'ramda';
import { get } from 'lodash'; import { get } from 'lodash';
import { sanitizeResourceName } from '../Import/_utils'; import { sanitizeResourceName } from '../Import/_utils';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import { ExportableResources } from './ExportResources'; import { ExportableResources } from './ExportResources';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { Errors } from './common'; import { Errors } from './common';
import { IModelMeta } from '@/interfaces'; import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils';
@Service() @Service()
export class ExportResourceService { export class ExportResourceService {
@@ -22,16 +24,16 @@ export class ExportResourceService {
* @param {string} resourceName - Resource name. * @param {string} resourceName - Resource name.
* @param {string} format - File format. * @param {string} format - File format.
*/ */
async export(tenantId: number, resourceName: string, format: string = 'csv') { public async export(tenantId: number, resourceName: string, format: string = 'csv') {
const resource = sanitizeResourceName(resourceName); const resource = sanitizeResourceName(resourceName);
const resourceMeta = this.getResourceMeta(tenantId, resource); const resourceMeta = this.getResourceMeta(tenantId, resource);
this.validateResourceMeta(resourceMeta); this.validateResourceMeta(resourceMeta);
const data = await this.getExportableData(tenantId, resource); const data = await this.getExportableData(tenantId, resource);
const transformed = this.transformExportedData(tenantId, resource, data);
const exportableColumns = this.getExportableColumns(resourceMeta); const exportableColumns = this.getExportableColumns(resourceMeta);
const workbook = this.createWorkbook(transformed, exportableColumns);
const workbook = this.createWorkbook(data, exportableColumns);
return this.exportWorkbook(workbook, format); return this.exportWorkbook(workbook, format);
} }
@@ -57,6 +59,29 @@ export class ExportResourceService {
} }
} }
/**
* 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. * Fetches exportable data for a given resource.
* @param {number} tenantId - The tenant identifier. * @param {number} tenantId - The tenant identifier.
@@ -75,13 +100,29 @@ export class ExportResourceService {
* @returns An array of exportable columns. * @returns An array of exportable columns.
*/ */
private getExportableColumns(resourceMeta: IModelMeta) { private getExportableColumns(resourceMeta: IModelMeta) {
return Object.entries(resourceMeta.columns) const processColumns = (
.filter(([_, value]) => value.exportable !== false) columns: { [key: string]: IModelMetaColumn },
.map(([key, value]) => ({ parent = ''
name: value.name, ) => {
type: value.type, return Object.entries(columns)
accessor: value.accessor || key, .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);
} }
/** /**
@@ -93,12 +134,14 @@ export class ExportResourceService {
private createWorkbook(data: any[], exportableColumns: any[]) { private createWorkbook(data: any[], exportableColumns: any[]) {
const workbook = xlsx.utils.book_new(); const workbook = xlsx.utils.book_new();
const worksheetData = data.map((item) => const worksheetData = data.map((item) =>
exportableColumns.map((col) => get(item, col.accessor)) exportableColumns.map((col) => get(item, getDataAccessor(col)))
); );
worksheetData.unshift(exportableColumns.map((col) => col.name)); worksheetData.unshift(exportableColumns.map((col) => col.name));
const worksheet = xlsx.utils.aoa_to_sheet(worksheetData); const worksheet = xlsx.utils.aoa_to_sheet(worksheetData);
xlsx.utils.book_append_sheet(workbook, worksheet, 'Exported Data'); xlsx.utils.book_append_sheet(workbook, worksheet, 'Exported Data');
return workbook; return workbook;
} }

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

@@ -49,6 +49,7 @@ export class GetBills {
const { results, pagination } = await Bill.query() const { results, pagination } = await Bill.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('vendor'); builder.withGraphFetched('vendor');
builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -14,6 +14,7 @@ export class VendorCreditTransformer extends Transformer {
'formattedSubtotal', 'formattedSubtotal',
'formattedVendorCreditDate', 'formattedVendorCreditDate',
'formattedCreditsRemaining', 'formattedCreditsRemaining',
'formattedInvoicedAmount',
'entries', '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. * Retrieves the entries of the bill.
* @param {IVendorCredit} vendorCredit * @param {IVendorCredit} vendorCredit

View File

@@ -105,7 +105,11 @@ export default class ResourceService {
const $enumerationType = (field) => const $enumerationType = (field) =>
field.fieldType === 'enumeration' ? field : undefined; 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 = [ const naviagations = [
['fields', qim.$each, 'name'], ['fields', qim.$each, 'name'],
@@ -114,6 +118,7 @@ export default class ResourceService {
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'], ['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'], ['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
['columns', qim.$each, 'name'], ['columns', qim.$each, 'name'],
['columns', qim.$each, $hasColumns, 'columns', qim.$each, 'name'],
]; ];
return this.i18nService.i18nApply(naviagations, meta, tenantId); return this.i18nService.i18nApply(naviagations, meta, tenantId);
} }

View File

@@ -51,6 +51,7 @@ export class GetSaleEstimates {
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
builder.withGraphFetched('entries'); builder.withGraphFetched('entries');
builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -49,7 +49,7 @@ export class GetSaleInvoices {
); );
const { results, pagination } = await SaleInvoice.query() const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('entries'); builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })

View File

@@ -11,6 +11,9 @@ import { SaleReceiptTransformer } from './SaleReceiptTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
interface GetSaleReceiptsSettings {
fetchEntriesGraph?: boolean;
}
@Service() @Service()
export class GetSaleReceipts { export class GetSaleReceipts {
@Inject() @Inject()
@@ -50,7 +53,7 @@ export class GetSaleReceipts {
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('depositAccount'); builder.withGraphFetched('depositAccount');
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
builder.withGraphFetched('entries'); builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })