mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 15:50:32 +00:00
feat: aggregate rows on import feature
This commit is contained in:
@@ -355,11 +355,13 @@
|
|||||||
"expense.field.status.published": "Published",
|
"expense.field.status.published": "Published",
|
||||||
"expense.field.created_at": "Created at",
|
"expense.field.created_at": "Created at",
|
||||||
"manual_journal.field.date": "Date",
|
"manual_journal.field.date": "Date",
|
||||||
"manual_journal.field.journal_number": "Journal number",
|
"manual_journal.field.journal_number": "Journal No.",
|
||||||
"manual_journal.field.reference": "Reference No.",
|
"manual_journal.field.reference": "Reference No.",
|
||||||
"manual_journal.field.journal_type": "Journal type",
|
"manual_journal.field.journal_type": "Journal Type",
|
||||||
"manual_journal.field.amount": "Amount",
|
"manual_journal.field.amount": "Amount",
|
||||||
"manual_journal.field.description": "Description",
|
"manual_journal.field.description": "Description",
|
||||||
|
"manual_journal.field.currency": "Currency",
|
||||||
|
"manual_journal.field.exchange_rate": "Exchange Rate",
|
||||||
"manual_journal.field.status": "Status",
|
"manual_journal.field.status": "Status",
|
||||||
"manual_journal.field.created_at": "Created at",
|
"manual_journal.field.created_at": "Created at",
|
||||||
"receipt.field.amount": "Amount",
|
"receipt.field.amount": "Amount",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export class ImportController extends BaseController {
|
|||||||
[
|
[
|
||||||
param('import_id').exists().isString(),
|
param('import_id').exists().isString(),
|
||||||
body('mapping').exists().isArray({ min: 1 }),
|
body('mapping').exists().isArray({ min: 1 }),
|
||||||
|
body('mapping.*.group').optional(),
|
||||||
body('mapping.*.from').exists(),
|
body('mapping.*.from').exists(),
|
||||||
body('mapping.*.to').exists(),
|
body('mapping.*.to').exists(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export type IModelMetaField = IModelMetaFieldCommon &
|
|||||||
| IModelMetaFieldUrl
|
| IModelMetaFieldUrl
|
||||||
| IModelMetaEnumerationField
|
| IModelMetaEnumerationField
|
||||||
| IModelMetaRelationField
|
| IModelMetaRelationField
|
||||||
|
| IModelMetaCollectionField
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface IModelMetaEnumerationOption {
|
export interface IModelMetaEnumerationOption {
|
||||||
@@ -92,12 +93,71 @@ export interface IModelMetaRelationEnumerationField {
|
|||||||
relationEntityKey: string;
|
relationEntityKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaFieldWithFields {
|
||||||
|
fields: IModelMetaFieldCommon2 &
|
||||||
|
(
|
||||||
|
| IModelMetaFieldText
|
||||||
|
| IModelMetaFieldNumber
|
||||||
|
| IModelMetaFieldBoolean
|
||||||
|
| IModelMetaFieldDate
|
||||||
|
| IModelMetaFieldUrl
|
||||||
|
| IModelMetaEnumerationField
|
||||||
|
| IModelMetaRelationField
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IModelMetaCollectionObjectField extends IModelMetaFieldWithFields {
|
||||||
|
collectionOf: 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaCollectionFieldCommon {
|
||||||
|
fieldType: 'collection';
|
||||||
|
collectionMinLength?: number;
|
||||||
|
collectionMaxLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
|
||||||
|
IModelMetaCollectionObjectField;
|
||||||
|
|
||||||
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
|
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
|
||||||
IModelMetaRelationEnumerationField;
|
IModelMetaRelationEnumerationField;
|
||||||
|
|
||||||
export interface IModelMeta {
|
export interface IModelMeta {
|
||||||
defaultFilterField: string;
|
defaultFilterField: string;
|
||||||
defaultSort: IModelMetaDefaultSort;
|
defaultSort: IModelMetaDefaultSort;
|
||||||
|
|
||||||
importable?: boolean;
|
importable?: boolean;
|
||||||
|
|
||||||
|
importAggregator?: string;
|
||||||
|
importAggregateOn?: string;
|
||||||
|
importAggregateBy?: string;
|
||||||
|
|
||||||
fields: { [key: string]: IModelMetaField };
|
fields: { [key: string]: IModelMetaField };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
export interface IModelMetaFieldCommon2 {
|
||||||
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
|
importHint?: string;
|
||||||
|
order?: number;
|
||||||
|
unique?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaRelationField2 {
|
||||||
|
fieldType: 'relation';
|
||||||
|
relationModel: string;
|
||||||
|
importableRelationLabel: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IModelMetaField2 = IModelMetaFieldCommon2 &
|
||||||
|
(
|
||||||
|
| IModelMetaFieldText
|
||||||
|
| IModelMetaFieldNumber
|
||||||
|
| IModelMetaFieldBoolean
|
||||||
|
| IModelMetaFieldDate
|
||||||
|
| IModelMetaFieldUrl
|
||||||
|
| IModelMetaEnumerationField
|
||||||
|
| IModelMetaRelationField2
|
||||||
|
| IModelMetaCollectionField
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
defaultFilterField: 'vendor',
|
defaultFilterField: 'vendor',
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
sortField: 'bill_date',
|
sortField: 'bill_date',
|
||||||
},
|
},
|
||||||
|
importable: true,
|
||||||
|
importAggregator: 'group',
|
||||||
|
importAggregateOn: 'entries',
|
||||||
|
importAggregateBy: 'billNumber',
|
||||||
fields: {
|
fields: {
|
||||||
vendor: {
|
vendor: {
|
||||||
name: 'bill.field.vendor',
|
name: 'bill.field.vendor',
|
||||||
@@ -77,6 +80,76 @@ export default {
|
|||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fields2: {
|
||||||
|
billNumber: {
|
||||||
|
name: 'Bill No.',
|
||||||
|
fieldType: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
referenceNo: {
|
||||||
|
name: 'Reference No.',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
billDate: {
|
||||||
|
name: 'Date',
|
||||||
|
fieldType: 'date',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dueDate: {
|
||||||
|
name: 'Due Date',
|
||||||
|
fieldType: 'date',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
vendorId: {
|
||||||
|
name: 'Vendor',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Contact',
|
||||||
|
relationImportMatch: 'displayName',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
exchangeRate: {
|
||||||
|
name: 'Exchange Rate',
|
||||||
|
fieldType: 'number',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
name: 'Note',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
name: 'Open',
|
||||||
|
fieldType: 'boolean',
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
name: 'Entries',
|
||||||
|
fieldType: 'collection',
|
||||||
|
collectionOf: 'object',
|
||||||
|
collectionMinLength: 1,
|
||||||
|
required: true,
|
||||||
|
fields: {
|
||||||
|
itemId: {
|
||||||
|
name: 'Item',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Item',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
rate: {
|
||||||
|
name: 'Rate',
|
||||||
|
fieldType: 'number',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
name: 'Quantity',
|
||||||
|
fieldType: 'number',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: 'Description',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,54 +4,193 @@ export default {
|
|||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
sortField: 'name',
|
sortField: 'name',
|
||||||
},
|
},
|
||||||
|
importable: true,
|
||||||
|
importAggregator: 'group',
|
||||||
|
importAggregateOn: 'entries',
|
||||||
|
importAggregateBy: 'journalNumber',
|
||||||
fields: {
|
fields: {
|
||||||
'date': {
|
date: {
|
||||||
name: 'manual_journal.field.date',
|
name: 'manual_journal.field.date',
|
||||||
column: 'date',
|
column: 'date',
|
||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
'journal_number': {
|
journalNumber: {
|
||||||
name: 'manual_journal.field.journal_number',
|
name: 'manual_journal.field.journal_number',
|
||||||
column: 'journal_number',
|
column: 'journal_number',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
'reference': {
|
reference: {
|
||||||
name: 'manual_journal.field.reference',
|
name: 'manual_journal.field.reference',
|
||||||
column: 'reference',
|
column: 'reference',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
'journal_type': {
|
journalType: {
|
||||||
name: 'manual_journal.field.journal_type',
|
name: 'manual_journal.field.journal_type',
|
||||||
column: 'journal_type',
|
column: 'journal_type',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
'amount': {
|
amount: {
|
||||||
name: 'manual_journal.field.amount',
|
name: 'manual_journal.field.amount',
|
||||||
column: 'amount',
|
column: 'amount',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
},
|
},
|
||||||
'description': {
|
description: {
|
||||||
name: 'manual_journal.field.description',
|
name: 'manual_journal.field.description',
|
||||||
column: 'description',
|
column: 'description',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
'status': {
|
entries: {
|
||||||
|
name: 'Entries',
|
||||||
|
fieldType: 'collection',
|
||||||
|
collectionOf: 'object',
|
||||||
|
collectionMinLength: 2,
|
||||||
|
required: true,
|
||||||
|
importable: true,
|
||||||
|
filterable: false,
|
||||||
|
fields: {
|
||||||
|
credit: {
|
||||||
|
name: 'Credit',
|
||||||
|
fieldType: 'number',
|
||||||
|
importable: true,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
debit: {
|
||||||
|
name: 'Debit',
|
||||||
|
fieldType: 'number',
|
||||||
|
importable: true,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
accountId: {
|
||||||
|
name: 'Account',
|
||||||
|
fieldType: 'relation',
|
||||||
|
|
||||||
|
relationKey: 'account',
|
||||||
|
relationModel: 'Account',
|
||||||
|
|
||||||
|
importable: true,
|
||||||
|
required: true,
|
||||||
|
importableRelationLabel: ['name', 'code'],
|
||||||
|
},
|
||||||
|
contactId: {
|
||||||
|
name: 'Contact',
|
||||||
|
fieldType: 'relation',
|
||||||
|
|
||||||
|
relationKey: 'contact',
|
||||||
|
relationModel: 'Contact',
|
||||||
|
|
||||||
|
required: false,
|
||||||
|
|
||||||
|
importable: true,
|
||||||
|
importableRelationLabel: 'displayName',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
name: 'Note',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publish: {
|
||||||
|
name: 'Publish',
|
||||||
|
fieldType: 'boolean',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
name: 'manual_journal.field.status',
|
name: 'manual_journal.field.status',
|
||||||
column: 'status',
|
column: 'status',
|
||||||
fieldType: 'enumeration',
|
fieldType: 'enumeration',
|
||||||
options: [
|
options: [
|
||||||
{ key: 'draft', label: 'Draft' },
|
{ key: 'draft', label: 'Draft' },
|
||||||
{ key: 'published', label: 'published' }
|
{ key: 'published', label: 'published' },
|
||||||
],
|
],
|
||||||
filterCustomQuery: StatusFieldFilterQuery,
|
filterCustomQuery: StatusFieldFilterQuery,
|
||||||
sortCustomQuery: StatusFieldSortQuery,
|
sortCustomQuery: StatusFieldSortQuery,
|
||||||
},
|
},
|
||||||
'created_at': {
|
createdAt: {
|
||||||
name: 'manual_journal.field.created_at',
|
name: 'manual_journal.field.created_at',
|
||||||
column: 'created_at',
|
column: 'created_at',
|
||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fields2: {
|
||||||
|
date: {
|
||||||
|
name: 'manual_journal.field.date',
|
||||||
|
fieldType: 'date',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
journalNumber: {
|
||||||
|
name: 'manual_journal.field.journal_number',
|
||||||
|
fieldType: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
name: 'manual_journal.field.reference',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
journalType: {
|
||||||
|
name: 'manual_journal.field.journal_type',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
currencyCode: {
|
||||||
|
name: 'manual_journal.field.currency',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
exchange_rate: {
|
||||||
|
name: 'manual_journal.field.exchange_rate',
|
||||||
|
fieldType: 'number',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: 'manual_journal.field.description',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
name: 'Entries',
|
||||||
|
fieldType: 'collection',
|
||||||
|
collectionOf: 'object',
|
||||||
|
collectionMinLength: 2,
|
||||||
|
required: true,
|
||||||
|
fields: {
|
||||||
|
credit: {
|
||||||
|
name: 'Credit',
|
||||||
|
fieldType: 'number',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
debit: {
|
||||||
|
name: 'Debit',
|
||||||
|
fieldType: 'number',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
accountId: {
|
||||||
|
name: 'Account',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Account',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
name: 'Contact',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Contact',
|
||||||
|
relationImportMatch: 'displayName',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
name: 'Note',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publish: {
|
||||||
|
name: 'Publish',
|
||||||
|
fieldType: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +203,6 @@ function StatusFieldSortQuery(query, role) {
|
|||||||
/**
|
/**
|
||||||
* Status field filter custom query.
|
* Status field filter custom query.
|
||||||
*/
|
*/
|
||||||
function StatusFieldFilterQuery(query, role) {
|
function StatusFieldFilterQuery(query, role) {
|
||||||
query.modify('filterByStatus', role.value);
|
query.modify('filterByStatus', role.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class ImportFileCommon {
|
|||||||
parsedData: Record<string, any>[],
|
parsedData: Record<string, any>[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
||||||
const importableFields = this.resource.getResourceImportableFields(
|
const resourceFields = this.resource.getResourceFields2(
|
||||||
tenantId,
|
tenantId,
|
||||||
importFile.resource
|
importFile.resource
|
||||||
);
|
);
|
||||||
@@ -90,7 +90,7 @@ export class ImportFileCommon {
|
|||||||
};
|
};
|
||||||
const transformedDTO = importable.transform(objectDTO, context);
|
const transformedDTO = importable.transform(objectDTO, context);
|
||||||
const rowNumber = index + 1;
|
const rowNumber = index + 1;
|
||||||
const uniqueValue = getUniqueImportableValue(importableFields, objectDTO);
|
const uniqueValue = getUniqueImportableValue(resourceFields, objectDTO);
|
||||||
const errorContext = {
|
const errorContext = {
|
||||||
rowNumber,
|
rowNumber,
|
||||||
uniqueValue,
|
uniqueValue,
|
||||||
@@ -98,7 +98,7 @@ export class ImportFileCommon {
|
|||||||
try {
|
try {
|
||||||
// Validate the DTO object before passing it to the service layer.
|
// Validate the DTO object before passing it to the service layer.
|
||||||
await this.importFileValidator.validateData(
|
await this.importFileValidator.validateData(
|
||||||
importableFields,
|
resourceFields,
|
||||||
transformedDTO
|
transformedDTO
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,63 +1,91 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
|
||||||
import bluebird from 'bluebird';
|
import bluebird from 'bluebird';
|
||||||
import { isUndefined, get, chain, toArray, pickBy, castArray } from 'lodash';
|
import { isUndefined, pickBy, set } from 'lodash';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
|
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { trimObject, parseBoolean } from './_utils';
|
import {
|
||||||
import { Account, Item } from '@/models';
|
valueParser,
|
||||||
|
parseKey,
|
||||||
|
getFieldKey,
|
||||||
|
aggregate,
|
||||||
|
sanitizeSheetData,
|
||||||
|
} from './_utils';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import { multiNumberParse } from '@/utils/multi-number-parse';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
|
||||||
const CurrencyParsingDTOs = 10;
|
const CurrencyParsingDTOs = 10;
|
||||||
|
|
||||||
|
const getMapToPath = (to: string, group = '') =>
|
||||||
|
group ? `${group}.${to}` : to;
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileDataTransformer {
|
export class ImportFileDataTransformer {
|
||||||
@Inject()
|
@Inject()
|
||||||
private resource: ResourceService;
|
private resource: ResourceService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the given sheet data before passing to the service layer.
|
* Parses the given sheet data before passing to the service layer.
|
||||||
* based on the mapped fields and the each field type .
|
* based on the mapped fields and the each field type.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {}
|
* @param {}
|
||||||
*/
|
*/
|
||||||
public async parseSheetData(
|
public async parseSheetData(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
importFile: any,
|
importFile: any,
|
||||||
importableFields: any,
|
importableFields: ResourceMetaFieldsMap,
|
||||||
data: Record<string, unknown>[],
|
data: Record<string, unknown>[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
): Promise<Record<string, any>[]> {
|
||||||
// Sanitize the sheet data.
|
// Sanitize the sheet data.
|
||||||
const sanitizedData = this.sanitizeSheetData(data);
|
const sanitizedData = sanitizeSheetData(data);
|
||||||
|
|
||||||
// Map the sheet columns key with the given map.
|
// Map the sheet columns key with the given map.
|
||||||
const mappedDTOs = this.mapSheetColumns(
|
const mappedDTOs = this.mapSheetColumns(
|
||||||
sanitizedData,
|
sanitizedData,
|
||||||
importFile.mappingParsed
|
importFile.mappingParsed
|
||||||
);
|
);
|
||||||
const resourceModel = this.resource.getResourceModel(
|
|
||||||
tenantId,
|
|
||||||
importFile.resource
|
|
||||||
);
|
|
||||||
// Parse the mapped sheet values.
|
// Parse the mapped sheet values.
|
||||||
return this.parseExcelValues(
|
const parsedValues = await this.parseExcelValues(
|
||||||
|
tenantId,
|
||||||
importableFields,
|
importableFields,
|
||||||
mappedDTOs,
|
mappedDTOs,
|
||||||
resourceModel,
|
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
const aggregateValues = this.aggregateParsedValues(
|
||||||
|
tenantId,
|
||||||
|
importFile.resource,
|
||||||
|
parsedValues
|
||||||
|
);
|
||||||
|
return aggregateValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes the data in the imported sheet by trimming object keys.
|
* Aggregates parsed data based on resource metadata configuration.
|
||||||
* @param json - The JSON data representing the imported sheet.
|
* @param {number} tenantId
|
||||||
* @returns {string[][]} - The sanitized data with trimmed object keys.
|
* @param {string} resourceName
|
||||||
|
* @param {Record<string, any>} parsedData
|
||||||
|
* @returns {Record<string, any>[]}
|
||||||
*/
|
*/
|
||||||
public sanitizeSheetData(json) {
|
public aggregateParsedValues = (
|
||||||
return R.compose(R.map(trimObject))(json);
|
tenantId: number,
|
||||||
}
|
resourceName: string,
|
||||||
|
parsedData: Record<string, any>[]
|
||||||
|
): Record<string, any>[] => {
|
||||||
|
let _value = parsedData;
|
||||||
|
const meta = this.resource.getResourceMeta(tenantId, resourceName);
|
||||||
|
|
||||||
|
if (meta.importAggregator === 'group') {
|
||||||
|
_value = aggregate(
|
||||||
|
_value,
|
||||||
|
meta.importAggregateBy,
|
||||||
|
meta.importAggregateOn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _value;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
* Maps the columns of the imported data based on the provided mapping attributes.
|
||||||
@@ -74,7 +102,8 @@ export class ImportFileDataTransformer {
|
|||||||
map
|
map
|
||||||
.filter((mapping) => !isUndefined(item[mapping.from]))
|
.filter((mapping) => !isUndefined(item[mapping.from]))
|
||||||
.forEach((mapping) => {
|
.forEach((mapping) => {
|
||||||
newItem[mapping.to] = item[mapping.from];
|
const toPath = getMapToPath(mapping.to, mapping.group);
|
||||||
|
newItem[toPath] = item[mapping.from];
|
||||||
});
|
});
|
||||||
return newItem;
|
return newItem;
|
||||||
});
|
});
|
||||||
@@ -87,78 +116,32 @@ export class ImportFileDataTransformer {
|
|||||||
* @returns {Record<string, any>}
|
* @returns {Record<string, any>}
|
||||||
*/
|
*/
|
||||||
public async parseExcelValues(
|
public async parseExcelValues(
|
||||||
|
tenantId: number,
|
||||||
fields: ResourceMetaFieldsMap,
|
fields: ResourceMetaFieldsMap,
|
||||||
valueDTOs: Record<string, any>[],
|
valueDTOs: Record<string, any>[],
|
||||||
resourceModel: any,
|
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>[]> {
|
||||||
// Prases the given object value based on the field key type.
|
const tenantModels = this.tenancy.models(tenantId);
|
||||||
const parser = async (value, key) => {
|
const _valueParser = valueParser(fields, tenantModels, trx);
|
||||||
let _value = value;
|
const _keyParser = parseKey(fields);
|
||||||
const field = fields[key];
|
|
||||||
|
|
||||||
// Parses the boolean value.
|
|
||||||
if (fields[key].fieldType === 'boolean') {
|
|
||||||
_value = parseBoolean(value);
|
|
||||||
|
|
||||||
// Parses the enumeration value.
|
|
||||||
} else if (field.fieldType === 'enumeration') {
|
|
||||||
const field = fields[key];
|
|
||||||
const option = get(field, 'options', []).find(
|
|
||||||
(option) => option.label === value
|
|
||||||
);
|
|
||||||
_value = get(option, 'key');
|
|
||||||
// Parses the numeric value.
|
|
||||||
} else if (fields[key].fieldType === 'number') {
|
|
||||||
_value = multiNumberParse(value);
|
|
||||||
// Parses the relation value.
|
|
||||||
} else if (field.fieldType === 'relation') {
|
|
||||||
const relationModel = resourceModel.relationMappings[field.relationKey];
|
|
||||||
const RelationModel = relationModel?.modelClass;
|
|
||||||
|
|
||||||
if (!relationModel || !RelationModel) {
|
|
||||||
throw new Error(`The relation model of ${key} field is not exist.`);
|
|
||||||
}
|
|
||||||
const relationQuery = RelationModel.query(trx);
|
|
||||||
const relationKeys = field?.importableRelationLabel
|
|
||||||
? castArray(field?.importableRelationLabel)
|
|
||||||
: castArray(field?.relationEntityLabel);
|
|
||||||
|
|
||||||
relationQuery.where(function () {
|
|
||||||
relationKeys.forEach((relationKey: string) => {
|
|
||||||
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const result = await relationQuery.first();
|
|
||||||
_value = get(result, 'id');
|
|
||||||
}
|
|
||||||
return _value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseKey = (key: string) => {
|
|
||||||
const field = fields[key];
|
|
||||||
let _objectTransferObjectKey = key;
|
|
||||||
|
|
||||||
if (field.fieldType === 'relation') {
|
|
||||||
_objectTransferObjectKey = `${key}Id`;
|
|
||||||
}
|
|
||||||
return _objectTransferObjectKey;
|
|
||||||
};
|
|
||||||
const parseAsync = async (valueDTO) => {
|
const parseAsync = async (valueDTO) => {
|
||||||
// Remove the undefined fields.
|
// Clean up the undefined keys that not exist in resource fields.
|
||||||
const _valueDTO = pickBy(
|
const _valueDTO = pickBy(
|
||||||
valueDTO,
|
valueDTO,
|
||||||
(value, key) => !isUndefined(fields[key])
|
(value, key) => !isUndefined(fields[getFieldKey(key)])
|
||||||
);
|
);
|
||||||
|
// Keys of mapped values. key structure: `group.key` or `key`.
|
||||||
const keys = Object.keys(_valueDTO);
|
const keys = Object.keys(_valueDTO);
|
||||||
|
|
||||||
// Map the object values.
|
// Map the object values.
|
||||||
return bluebird.reduce(
|
return bluebird.reduce(
|
||||||
keys,
|
keys,
|
||||||
async (acc, key) => {
|
async (acc, key) => {
|
||||||
const parsedValue = await parser(_valueDTO[key], key);
|
const parsedValue = await _valueParser(_valueDTO[key], key);
|
||||||
const parsedKey = await parseKey(key);
|
const parsedKey = await _keyParser(key);
|
||||||
acc[parsedKey] = parsedValue;
|
|
||||||
|
set(acc, parsedKey, parsedValue);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fromPairs } from 'lodash';
|
import { fromPairs, isUndefined } from 'lodash';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import {
|
import {
|
||||||
@@ -69,7 +69,7 @@ export class ImportFileMapping {
|
|||||||
importFile: any,
|
importFile: any,
|
||||||
maps: ImportMappingAttr[]
|
maps: ImportMappingAttr[]
|
||||||
) {
|
) {
|
||||||
const fields = this.resource.getResourceImportableFields(
|
const fields = this.resource.getResourceFields2(
|
||||||
tenantId,
|
tenantId,
|
||||||
importFile.resource
|
importFile.resource
|
||||||
);
|
);
|
||||||
@@ -78,11 +78,20 @@ export class ImportFileMapping {
|
|||||||
);
|
);
|
||||||
const invalid = [];
|
const invalid = [];
|
||||||
|
|
||||||
|
// is not empty, is not undefined or map.group
|
||||||
maps.forEach((map) => {
|
maps.forEach((map) => {
|
||||||
if (
|
let _invalid = true;
|
||||||
'undefined' === typeof fields[map.to] ||
|
|
||||||
'undefined' === typeof columnsMap[map.from]
|
if (!map.group && fields[map.to]) {
|
||||||
) {
|
_invalid = false;
|
||||||
|
}
|
||||||
|
if (map.group && fields[map.group] && fields[map.group]?.fields[map.to]) {
|
||||||
|
_invalid = false;
|
||||||
|
}
|
||||||
|
if (columnsMap[map.from]) {
|
||||||
|
_invalid = false;
|
||||||
|
}
|
||||||
|
if (_invalid) {
|
||||||
invalid.push(map);
|
invalid.push(map);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -105,10 +114,14 @@ export class ImportFileMapping {
|
|||||||
} else {
|
} else {
|
||||||
fromMap[map.from] = true;
|
fromMap[map.from] = true;
|
||||||
}
|
}
|
||||||
if (toMap[map.to]) {
|
const toPath = !isUndefined(map?.group)
|
||||||
|
? `${map.group}.${map.to}`
|
||||||
|
: map.to;
|
||||||
|
|
||||||
|
if (toMap[toPath]) {
|
||||||
throw new ServiceError(ERRORS.DUPLICATED_TO_MAP_ATTR);
|
throw new ServiceError(ERRORS.DUPLICATED_TO_MAP_ATTR);
|
||||||
} else {
|
} else {
|
||||||
toMap[map.to] = true;
|
toMap[toPath] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,6 +141,7 @@ export class ImportFileMapping {
|
|||||||
tenantId,
|
tenantId,
|
||||||
resource
|
resource
|
||||||
);
|
);
|
||||||
|
// @todo Validate date type of the nested fields.
|
||||||
maps.forEach((map) => {
|
maps.forEach((map) => {
|
||||||
if (
|
if (
|
||||||
typeof fields[map.to] !== 'undefined' &&
|
typeof fields[map.to] !== 'undefined' &&
|
||||||
|
|||||||
@@ -53,11 +53,10 @@ export class ImportFileProcess {
|
|||||||
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
||||||
const header = getSheetColumns(sheetData);
|
const header = getSheetColumns(sheetData);
|
||||||
|
|
||||||
const importableFields = this.resource.getResourceImportableFields(
|
const resourceFields = this.resource.getResourceFields2(
|
||||||
tenantId,
|
tenantId,
|
||||||
importFile.resource
|
importFile.resource
|
||||||
);
|
);
|
||||||
|
|
||||||
// Runs the importing operation with ability to return errors that will happen.
|
// Runs the importing operation with ability to return errors that will happen.
|
||||||
const [successedImport, failedImport, allData] =
|
const [successedImport, failedImport, allData] =
|
||||||
await this.uow.withTransaction(
|
await this.uow.withTransaction(
|
||||||
@@ -67,7 +66,7 @@ export class ImportFileProcess {
|
|||||||
const parsedData = await this.importParser.parseSheetData(
|
const parsedData = await this.importParser.parseSheetData(
|
||||||
tenantId,
|
tenantId,
|
||||||
importFile,
|
importFile,
|
||||||
importableFields,
|
resourceFields,
|
||||||
sheetData,
|
sheetData,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { sanitizeResourceName, validateSheetEmpty } from './_utils';
|
import {
|
||||||
|
getResourceColumns,
|
||||||
|
sanitizeResourceName,
|
||||||
|
validateSheetEmpty,
|
||||||
|
} from './_utils';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import { IModelMetaField } from '@/interfaces';
|
|
||||||
import { ImportFileCommon } from './ImportFileCommon';
|
import { ImportFileCommon } from './ImportFileCommon';
|
||||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||||
import { ImportFileUploadPOJO } from './interfaces';
|
import { ImportFileUploadPOJO } from './interfaces';
|
||||||
@@ -77,11 +80,11 @@ export class ImportFileUploadService {
|
|||||||
columns: coumnsStringified,
|
columns: coumnsStringified,
|
||||||
params: paramsStringified,
|
params: paramsStringified,
|
||||||
});
|
});
|
||||||
const resourceColumnsMap = this.resourceService.getResourceImportableFields(
|
const resourceColumnsMap = this.resourceService.getResourceFields2(
|
||||||
tenantId,
|
tenantId,
|
||||||
resource
|
resource
|
||||||
);
|
);
|
||||||
const resourceColumns = this.getResourceColumns(resourceColumnsMap);
|
const resourceColumns = getResourceColumns(resourceColumnsMap);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
import: {
|
import: {
|
||||||
@@ -92,23 +95,4 @@ export class ImportFileUploadService {
|
|||||||
resourceColumns,
|
resourceColumns,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceColumns(resourceColumns: { [key: string]: IModelMetaField }) {
|
|
||||||
return Object.entries(resourceColumns)
|
|
||||||
.map(
|
|
||||||
([key, { name, importHint, required, order }]: [
|
|
||||||
string,
|
|
||||||
IModelMetaField
|
|
||||||
]) => ({
|
|
||||||
key,
|
|
||||||
name,
|
|
||||||
required,
|
|
||||||
hint: importHint,
|
|
||||||
order,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.sort((a, b) =>
|
|
||||||
a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export class ImportableRegistry {
|
|||||||
private static instance: ImportableRegistry;
|
private static instance: ImportableRegistry;
|
||||||
private importables: Record<string, Importable>;
|
private importables: Record<string, Importable>;
|
||||||
|
|
||||||
private constructor() {
|
constructor() {
|
||||||
this.importables = {};
|
this.importables = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
|
|||||||
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
|
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
|
||||||
import { ItemsImportable } from '../Items/ItemsImportable';
|
import { ItemsImportable } from '../Items/ItemsImportable';
|
||||||
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
|
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
|
||||||
|
import { ManualJournalImportable } from '../ManualJournals/ManualJournalsImport';
|
||||||
|
import { BillsImportable } from '../Purchases/Bills/BillsImportable';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportableResources {
|
export class ImportableResources {
|
||||||
@@ -28,6 +30,8 @@ export class ImportableResources {
|
|||||||
{ resource: 'Vendor', importable: VendorsImportable },
|
{ resource: 'Vendor', importable: VendorsImportable },
|
||||||
{ resource: 'Item', importable: ItemsImportable },
|
{ resource: 'Item', importable: ItemsImportable },
|
||||||
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
|
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
|
||||||
|
{ resource: 'ManualJournal', importable: ManualJournalImportable },
|
||||||
|
{ resource: 'Bill', importable: BillsImportable },
|
||||||
];
|
];
|
||||||
|
|
||||||
public get registry() {
|
public get registry() {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Knex } from 'knex';
|
||||||
import {
|
import {
|
||||||
defaultTo,
|
defaultTo,
|
||||||
upperFirst,
|
upperFirst,
|
||||||
@@ -8,11 +10,17 @@ import {
|
|||||||
isUndefined,
|
isUndefined,
|
||||||
pickBy,
|
pickBy,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
|
castArray,
|
||||||
|
get,
|
||||||
|
head,
|
||||||
|
split,
|
||||||
|
last,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { ResourceMetaFieldsMap } from './interfaces';
|
import { ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { multiNumberParse } from '@/utils/multi-number-parse';
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
|
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
|
||||||
@@ -40,6 +48,7 @@ export function trimObject(obj) {
|
|||||||
|
|
||||||
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
||||||
const yupSchema = {};
|
const yupSchema = {};
|
||||||
|
|
||||||
Object.keys(fields).forEach((fieldName: string) => {
|
Object.keys(fields).forEach((fieldName: string) => {
|
||||||
const field = fields[fieldName] as IModelMetaField;
|
const field = fields[fieldName] as IModelMetaField;
|
||||||
let fieldSchema;
|
let fieldSchema;
|
||||||
@@ -89,6 +98,17 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
|||||||
);
|
);
|
||||||
} else if (field.fieldType === 'url') {
|
} else if (field.fieldType === 'url') {
|
||||||
fieldSchema = fieldSchema.url();
|
fieldSchema = fieldSchema.url();
|
||||||
|
} else if (field.fieldType === 'collection') {
|
||||||
|
const nestedFieldShema = convertFieldsToYupValidation(field.fields);
|
||||||
|
fieldSchema = Yup.array().label(field.name);
|
||||||
|
|
||||||
|
if (!isUndefined(field.collectionMaxLength)) {
|
||||||
|
fieldSchema = fieldSchema.max(field.collectionMaxLength);
|
||||||
|
}
|
||||||
|
if (!isUndefined(field.collectionMinLength)) {
|
||||||
|
fieldSchema = fieldSchema.min(field.collectionMinLength);
|
||||||
|
}
|
||||||
|
fieldSchema = fieldSchema.of(nestedFieldShema);
|
||||||
}
|
}
|
||||||
if (field.required) {
|
if (field.required) {
|
||||||
fieldSchema = fieldSchema.required();
|
fieldSchema = fieldSchema.required();
|
||||||
@@ -103,9 +123,9 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
|||||||
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
|
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
|
||||||
let _key = fieldName;
|
let _key = fieldName;
|
||||||
|
|
||||||
if (field.fieldType === 'relation') {
|
// if (field.fieldType === 'relation') {
|
||||||
_key = `${fieldName}Id`;
|
// _key = `${fieldName}Id`;
|
||||||
}
|
// }
|
||||||
if (field.dataTransferObjectKey) {
|
if (field.dataTransferObjectKey) {
|
||||||
_key = field.dataTransferObjectKey;
|
_key = field.dataTransferObjectKey;
|
||||||
}
|
}
|
||||||
@@ -134,7 +154,7 @@ export const getSheetColumns = (sheetData: unknown[]) => {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const getUniqueImportableValue = (
|
export const getUniqueImportableValue = (
|
||||||
importableFields: { [key: string]: IModelMetaField },
|
importableFields: { [key: string]: IModelMetaField2 },
|
||||||
objectDTO: Record<string, any>
|
objectDTO: Record<string, any>
|
||||||
) => {
|
) => {
|
||||||
const uniqueImportableValue = pickBy(
|
const uniqueImportableValue = pickBy(
|
||||||
@@ -155,15 +175,15 @@ export const validateSheetEmpty = (sheetData: Array<any>) => {
|
|||||||
if (isEmpty(sheetData)) {
|
if (isEmpty(sheetData)) {
|
||||||
throw new ServiceError(ERRORS.IMPORTED_SHEET_EMPTY);
|
throw new ServiceError(ERRORS.IMPORTED_SHEET_EMPTY);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const booleanValuesRepresentingTrue: string[] = ['true', 'yes', 'y', 't', '1'];
|
const booleanValuesRepresentingTrue: string[] = ['true', 'yes', 'y', 't', '1'];
|
||||||
const booleanValuesRepresentingFalse: string[] = ['false', 'no', 'n', 'f', '0'];
|
const booleanValuesRepresentingFalse: string[] = ['false', 'no', 'n', 'f', '0'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the given string value to boolean.
|
* Parses the given string value to boolean.
|
||||||
* @param {string} value
|
* @param {string} value
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
export const parseBoolean = (value: string): boolean | null => {
|
export const parseBoolean = (value: string): boolean | null => {
|
||||||
const normalizeValue = (value: string): string =>
|
const normalizeValue = (value: string): string =>
|
||||||
@@ -182,3 +202,204 @@ export const parseBoolean = (value: string): boolean | null => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const transformInputToGroupedFields = (input) => {
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
// Group for non-nested fields
|
||||||
|
const mainGroup = {
|
||||||
|
groupLabel: '',
|
||||||
|
groupKey: '',
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
|
input.forEach((item) => {
|
||||||
|
if (!item.fields) {
|
||||||
|
// If the item does not have nested fields, add it to the main group
|
||||||
|
mainGroup.fields.push(item);
|
||||||
|
} else {
|
||||||
|
// If the item has nested fields, create a new group for these fields
|
||||||
|
output.push({
|
||||||
|
groupLabel: item.name,
|
||||||
|
groupKey: item.key,
|
||||||
|
fields: item.fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Add the main group to the output if it contains any fields
|
||||||
|
if (mainGroup.fields.length > 0) {
|
||||||
|
output.unshift(mainGroup); // Add the main group at the beginning
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResourceColumns = (resourceColumns: {
|
||||||
|
[key: string]: IModelMetaField2;
|
||||||
|
}) => {
|
||||||
|
const mapColumn =
|
||||||
|
(group: string) =>
|
||||||
|
([fieldKey, { name, importHint, required, order, ...field }]: [
|
||||||
|
string,
|
||||||
|
IModelMetaField2
|
||||||
|
]) => {
|
||||||
|
const extra: Record<string, any> = {};
|
||||||
|
const key = fieldKey;
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
extra.group = group;
|
||||||
|
}
|
||||||
|
if (field.fieldType === 'collection') {
|
||||||
|
extra.fields = mapColumns(field.fields, key);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
name,
|
||||||
|
required,
|
||||||
|
hint: importHint,
|
||||||
|
order,
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const sortColumn = (a, b) =>
|
||||||
|
a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0;
|
||||||
|
|
||||||
|
const mapColumns = (columns, parentKey = '') =>
|
||||||
|
Object.entries(columns).map(mapColumn(parentKey)).sort(sortColumn);
|
||||||
|
|
||||||
|
return R.compose(transformInputToGroupedFields, mapColumns)(resourceColumns);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prases the given object value based on the field key type.
|
||||||
|
export const valueParser =
|
||||||
|
(fields: ResourceMetaFieldsMap, tenantModels: any, trx?: Knex.Transaction) =>
|
||||||
|
async (value: any, key: string, group = '') => {
|
||||||
|
let _value = value;
|
||||||
|
|
||||||
|
const fieldKey = key.includes('.') ? key.split('.')[0] : key;
|
||||||
|
const field = group ? fields[group]?.fields[fieldKey] : fields[fieldKey];
|
||||||
|
|
||||||
|
// Parses the boolean value.
|
||||||
|
if (field.fieldType === 'boolean') {
|
||||||
|
_value = parseBoolean(value);
|
||||||
|
|
||||||
|
// Parses the enumeration value.
|
||||||
|
} else if (field.fieldType === 'enumeration') {
|
||||||
|
const option = get(field, 'options', []).find(
|
||||||
|
(option) => option.label === value
|
||||||
|
);
|
||||||
|
_value = get(option, 'key');
|
||||||
|
// Parses the numeric value.
|
||||||
|
} else if (field.fieldType === 'number') {
|
||||||
|
_value = multiNumberParse(value);
|
||||||
|
// Parses the relation value.
|
||||||
|
} else if (field.fieldType === 'relation') {
|
||||||
|
const RelationModel = tenantModels[field.relationModel];
|
||||||
|
|
||||||
|
if (!RelationModel) {
|
||||||
|
throw new Error(`The relation model of ${key} field is not exist.`);
|
||||||
|
}
|
||||||
|
const relationQuery = RelationModel.query(trx);
|
||||||
|
const relationKeys = castArray(field?.relationImportMatch);
|
||||||
|
|
||||||
|
relationQuery.where(function () {
|
||||||
|
relationKeys.forEach((relationKey: string) => {
|
||||||
|
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const result = await relationQuery.first();
|
||||||
|
_value = get(result, 'id');
|
||||||
|
} else if (field.fieldType === 'collection') {
|
||||||
|
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
|
||||||
|
const _valueParser = valueParser(fields, tenantModels);
|
||||||
|
_value = await _valueParser(value, ObjectFieldKey, fieldKey);
|
||||||
|
}
|
||||||
|
return _value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseKey = R.curry(
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {{ [key: string]: IModelMetaField2 }} fields
|
||||||
|
* @param {string} key - Mapped key path. formats: `group.key` or `key`.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
(fields: { [key: string]: IModelMetaField2 }, key: string) => {
|
||||||
|
const fieldKey = getFieldKey(key);
|
||||||
|
const field = fields[fieldKey];
|
||||||
|
let _key = key;
|
||||||
|
|
||||||
|
if (field.fieldType === 'collection') {
|
||||||
|
if (field.collectionOf === 'object') {
|
||||||
|
const nestedFieldKey = last(key.split('.'));
|
||||||
|
_key = `${fieldKey}[0].${nestedFieldKey}`;
|
||||||
|
} else if (
|
||||||
|
field.collectionOf === 'string' ||
|
||||||
|
field.collectionOf ||
|
||||||
|
'numberic'
|
||||||
|
) {
|
||||||
|
_key = `${fieldKey}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(_key);
|
||||||
|
return _key;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getFieldKey = (input: string) => {
|
||||||
|
const keys = split(input, '.');
|
||||||
|
const firstKey = head(keys).split('[')[0]; // Split by "[" in case of array notation
|
||||||
|
return firstKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
{ * Aggregates the input array of objects based on a comparator attribute and groups the entries.
|
||||||
|
* This function is useful for combining multiple entries into a single entry based on a specific attribute,
|
||||||
|
* while aggregating other attributes into an array.}
|
||||||
|
*
|
||||||
|
* @param {Array} input - The array of objects to be aggregated.
|
||||||
|
* @param {string} comparatorAttr - The attribute of the objects used for comparison to aggregate.
|
||||||
|
* @param {string} groupOn - The attribute of the objects where the grouped entries will be pushed.
|
||||||
|
* @returns {Array} - The aggregated array of objects.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Example input:
|
||||||
|
* const input = [
|
||||||
|
* { id: 1, name: 'John', entries: ['entry1'] },
|
||||||
|
* { id: 2, name: 'Jane', entries: ['entry2'] },
|
||||||
|
* { id: 1, name: 'John', entries: ['entry3'] },
|
||||||
|
* ];
|
||||||
|
* const comparatorAttr = 'id';
|
||||||
|
* const groupOn = 'entries';
|
||||||
|
*
|
||||||
|
* // Example output:
|
||||||
|
* const output = [
|
||||||
|
* { id: 1, name: 'John', entries: ['entry1', 'entry3'] },
|
||||||
|
* { id: 2, name: 'Jane', entries: ['entry2'] },
|
||||||
|
* ];
|
||||||
|
*/
|
||||||
|
export function aggregate(
|
||||||
|
input: Array<any>,
|
||||||
|
comparatorAttr: string,
|
||||||
|
groupOn: string
|
||||||
|
): Array<Record<string, any>> {
|
||||||
|
return input.reduce((acc, curr) => {
|
||||||
|
const existingEntry = acc.find(
|
||||||
|
(entry) => entry[comparatorAttr] === curr[comparatorAttr]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingEntry) {
|
||||||
|
existingEntry[groupOn].push(...curr.entries);
|
||||||
|
} else {
|
||||||
|
acc.push({ ...curr });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes the data in the imported sheet by trimming object keys.
|
||||||
|
* @param json - The JSON data representing the imported sheet.
|
||||||
|
* @returns {string[][]} - The sanitized data with trimmed object keys.
|
||||||
|
*/
|
||||||
|
export const sanitizeSheetData = (json) => {
|
||||||
|
return R.compose(R.map(trimObject))(json);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
|
||||||
import Import from '@/models/Import';
|
import Import from '@/models/Import';
|
||||||
|
|
||||||
export interface ImportMappingAttr {
|
export interface ImportMappingAttr {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
group?: string;
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export interface ImportValidationError {
|
|||||||
constraints: Record<string, string>;
|
constraints: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField };
|
export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField2 };
|
||||||
|
|
||||||
export interface ImportInsertError {
|
export interface ImportInsertError {
|
||||||
rowNumber: number;
|
rowNumber: number;
|
||||||
@@ -61,16 +62,15 @@ export interface ImportOperError {
|
|||||||
error: ImportInsertError[];
|
error: ImportInsertError[];
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportableContext {
|
export interface ImportableContext {
|
||||||
import: Import,
|
import: Import;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const ImportDateFormats = [
|
export const ImportDateFormats = [
|
||||||
'yyyy-MM-dd',
|
'yyyy-MM-dd',
|
||||||
'dd.MM.yy',
|
'dd.MM.yy',
|
||||||
'MM/dd/yy',
|
'MM/dd/yy',
|
||||||
'dd/MMM/yyyy'
|
'dd/MMM/yyyy',
|
||||||
]
|
];
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export class ImportDeleteExpiredFilesJobs {}
|
||||||
@@ -73,9 +73,7 @@ export class CreateManualJournalService {
|
|||||||
return R.compose(
|
return R.compose(
|
||||||
// Omits the `branchId` from entries if multiply branches feature not active.
|
// Omits the `branchId` from entries if multiply branches feature not active.
|
||||||
this.branchesDTOTransformer.transformDTO(tenantId)
|
this.branchesDTOTransformer.transformDTO(tenantId)
|
||||||
)(
|
)(initialDTO);
|
||||||
initialDTO
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,7 +131,8 @@ export class CreateManualJournalService {
|
|||||||
public makeJournalEntries = async (
|
public makeJournalEntries = async (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
manualJournalDTO: IManualJournalDTO,
|
manualJournalDTO: IManualJournalDTO,
|
||||||
authorizedUser: ISystemUser
|
authorizedUser: ISystemUser,
|
||||||
|
trx?: Knex.Transaction
|
||||||
): Promise<{ manualJournal: IManualJournal }> => {
|
): Promise<{ manualJournal: IManualJournal }> => {
|
||||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
@@ -156,27 +155,31 @@ export class CreateManualJournalService {
|
|||||||
);
|
);
|
||||||
// Creates a manual journal transactions with associated transactions
|
// Creates a manual journal transactions with associated transactions
|
||||||
// under unit-of-work envirement.
|
// under unit-of-work envirement.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(
|
||||||
// Triggers `onManualJournalCreating` event.
|
tenantId,
|
||||||
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
|
async (trx: Knex.Transaction) => {
|
||||||
tenantId,
|
// Triggers `onManualJournalCreating` event.
|
||||||
manualJournalDTO,
|
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
|
||||||
trx,
|
tenantId,
|
||||||
} as IManualJournalCreatingPayload);
|
manualJournalDTO,
|
||||||
|
trx,
|
||||||
|
} as IManualJournalCreatingPayload);
|
||||||
|
|
||||||
// Upsert the manual journal object.
|
// Upsert the manual journal object.
|
||||||
const manualJournal = await ManualJournal.query(trx).upsertGraph({
|
const manualJournal = await ManualJournal.query(trx).upsertGraph({
|
||||||
...manualJournalObj,
|
...manualJournalObj,
|
||||||
});
|
});
|
||||||
// Triggers `onManualJournalCreated` event.
|
// Triggers `onManualJournalCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
|
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
|
||||||
tenantId,
|
tenantId,
|
||||||
manualJournal,
|
manualJournal,
|
||||||
manualJournalId: manualJournal.id,
|
manualJournalId: manualJournal.id,
|
||||||
trx,
|
trx,
|
||||||
} as IManualJournalEventCreatedPayload);
|
} as IManualJournalEventCreatedPayload);
|
||||||
|
|
||||||
return { manualJournal };
|
return { manualJournal };
|
||||||
});
|
},
|
||||||
|
trx
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Inject } from 'typedi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { Importable } from '../Import/Importable';
|
||||||
|
import { CreateManualJournalService } from './CreateManualJournal';
|
||||||
|
import { IManualJournalDTO } from '@/interfaces';
|
||||||
|
import { ImportableContext } from '../Import/interfaces';
|
||||||
|
import { ManualJournalsSampleData } from './constants';
|
||||||
|
|
||||||
|
export class ManualJournalImportable extends Importable {
|
||||||
|
@Inject()
|
||||||
|
private createManualJournalService: CreateManualJournalService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importing to account service.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IAccountCreateDTO} createAccountDTO
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public importable(
|
||||||
|
tenantId: number,
|
||||||
|
createJournalDTO: IManualJournalDTO,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
) {
|
||||||
|
return this.createManualJournalService.makeJournalEntries(
|
||||||
|
tenantId,
|
||||||
|
createJournalDTO,
|
||||||
|
{},
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the DTO before passing it to importable and validation.
|
||||||
|
* @param {Record<string, any>} createDTO
|
||||||
|
* @param {ImportableContext} context
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public transform(createDTO: Record<string, any>, context: ImportableContext) {
|
||||||
|
return createDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params validation schema.
|
||||||
|
* @returns {ValidationSchema[]}
|
||||||
|
*/
|
||||||
|
public paramsValidationSchema() {
|
||||||
|
return Yup.object().shape({
|
||||||
|
autoIncrement: Yup.boolean(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sample data of manual journals that used to download sample sheet.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public sampleData(): Record<string, any>[] {
|
||||||
|
return ManualJournalsSampleData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,3 +29,36 @@ export const CONTACTS_CONFIG = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_VIEWS = [];
|
export const DEFAULT_VIEWS = [];
|
||||||
|
|
||||||
|
export const ManualJournalsSampleData = [
|
||||||
|
{
|
||||||
|
Date: '2024-02-02',
|
||||||
|
'Journal No': 'J-100022',
|
||||||
|
'Reference No.': 'REF-10000',
|
||||||
|
'Currency Code': '',
|
||||||
|
'Exchange Rate': '',
|
||||||
|
'Journal Type': '',
|
||||||
|
Description: 'Animi quasi qui itaque aut possimus illum est magnam enim.',
|
||||||
|
Credit: 1000,
|
||||||
|
Debit: 0,
|
||||||
|
Note: 'Qui reprehenderit voluptate.',
|
||||||
|
Account: 'Bank Account',
|
||||||
|
Contact: '',
|
||||||
|
Publish: 'T',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Date: '2024-02-02',
|
||||||
|
'Journal No': 'J-100022',
|
||||||
|
'Reference No.': 'REF-10000',
|
||||||
|
'Currency Code': '',
|
||||||
|
'Exchange Rate': '',
|
||||||
|
'Journal Type': '',
|
||||||
|
Description: 'In assumenda dicta autem non est corrupti non et.',
|
||||||
|
Credit: 0,
|
||||||
|
Debit: 1000,
|
||||||
|
Note: 'Omnis tempora qui fugiat neque dolor voluptatem aut repudiandae nihil.',
|
||||||
|
Account: 'Bank Account',
|
||||||
|
Contact: '',
|
||||||
|
Publish: 'T',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Importable } from '@/services/Import/Importable';
|
||||||
|
import { CreateBill } from './CreateBill';
|
||||||
|
import { IBillDTO } from '@/interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class BillsImportable extends Importable {
|
||||||
|
@Inject()
|
||||||
|
private createBillService: CreateBill;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importing to account service.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IAccountCreateDTO} createAccountDTO
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public importable(
|
||||||
|
tenantId: number,
|
||||||
|
createAccountDTO: IBillDTO,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
) {
|
||||||
|
console.log(JSON.stringify(createAccountDTO));
|
||||||
|
return this.createBillService.createBill(
|
||||||
|
tenantId,
|
||||||
|
createAccountDTO,
|
||||||
|
{},
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concurrrency controlling of the importing process.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public get concurrency() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sample data that used to download accounts sample sheet.
|
||||||
|
*/
|
||||||
|
public sampleData(): any[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,8 @@ export class CreateBill {
|
|||||||
public async createBill(
|
public async createBill(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
billDTO: IBillDTO,
|
billDTO: IBillDTO,
|
||||||
authorizedUser: ISystemUser
|
authorizedUser: ISystemUser,
|
||||||
|
trx?: Knex.Transaction
|
||||||
): Promise<IBill> {
|
): Promise<IBill> {
|
||||||
const { Bill, Contact } = this.tenancy.models(tenantId);
|
const { Bill, Contact } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
@@ -91,26 +92,30 @@ export class CreateBill {
|
|||||||
authorizedUser
|
authorizedUser
|
||||||
);
|
);
|
||||||
// Write new bill transaction with associated transactions under UOW env.
|
// Write new bill transaction with associated transactions under UOW env.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(
|
||||||
// Triggers `onBillCreating` event.
|
tenantId,
|
||||||
await this.eventPublisher.emitAsync(events.bill.onCreating, {
|
async (trx: Knex.Transaction) => {
|
||||||
trx,
|
// Triggers `onBillCreating` event.
|
||||||
billDTO,
|
await this.eventPublisher.emitAsync(events.bill.onCreating, {
|
||||||
tenantId,
|
trx,
|
||||||
} as IBillCreatingPayload);
|
billDTO,
|
||||||
|
tenantId,
|
||||||
|
} as IBillCreatingPayload);
|
||||||
|
|
||||||
// Inserts the bill graph object to the storage.
|
// Inserts the bill graph object to the storage.
|
||||||
const bill = await Bill.query(trx).upsertGraph(billObj);
|
const bill = await Bill.query(trx).upsertGraph(billObj);
|
||||||
|
|
||||||
// Triggers `onBillCreated` event.
|
// Triggers `onBillCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.bill.onCreated, {
|
await this.eventPublisher.emitAsync(events.bill.onCreated, {
|
||||||
tenantId,
|
tenantId,
|
||||||
bill,
|
bill,
|
||||||
billId: bill.id,
|
billId: bill.id,
|
||||||
trx,
|
trx,
|
||||||
} as IBillCreatedPayload);
|
} as IBillCreatedPayload);
|
||||||
|
|
||||||
return bill;
|
return bill;
|
||||||
});
|
},
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
|
|||||||
import { camelCase, upperFirst, pickBy } from 'lodash';
|
import { camelCase, upperFirst, pickBy } from 'lodash';
|
||||||
import * as qim from 'qim';
|
import * as qim from 'qim';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { IModelMeta, IModelMetaField } from '@/interfaces';
|
import { IModelMeta, IModelMetaField, IModelMetaField2 } from '@/interfaces';
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import I18nService from '@/services/I18n/I18nService';
|
import I18nService from '@/services/I18n/I18nService';
|
||||||
@@ -74,11 +74,20 @@ export default class ResourceService {
|
|||||||
return meta.fields;
|
return meta.fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getResourceFields2(
|
||||||
|
tenantId: number,
|
||||||
|
modelName: string
|
||||||
|
): { [key: string]: IModelMetaField2 } {
|
||||||
|
const meta = this.getResourceMeta(tenantId, modelName);
|
||||||
|
|
||||||
|
return meta.fields2;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {string} modelName
|
* @param {string} modelName
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public getResourceImportableFields(
|
public getResourceImportableFields(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
@@ -98,7 +107,9 @@ export default class ResourceService {
|
|||||||
|
|
||||||
const naviagations = [
|
const naviagations = [
|
||||||
['fields', qim.$each, 'name'],
|
['fields', qim.$each, 'name'],
|
||||||
|
['fields2', qim.$each, 'name'],
|
||||||
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
|
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
|
||||||
|
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
|
||||||
];
|
];
|
||||||
return this.i18nService.i18nApply(naviagations, meta, tenantId);
|
return this.i18nService.i18nApply(naviagations, meta, tenantId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ function ManualJournalActionsBar({
|
|||||||
const handleRefreshBtnClick = () => {
|
const handleRefreshBtnClick = () => {
|
||||||
refresh();
|
refresh();
|
||||||
};
|
};
|
||||||
|
// Handle import button click.
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push('/manual-journals/import');
|
||||||
|
}
|
||||||
|
|
||||||
// Handle table row size change.
|
// Handle table row size change.
|
||||||
const handleTableRowSizeChange = (size) => {
|
const handleTableRowSizeChange = (size) => {
|
||||||
@@ -130,6 +134,7 @@ function ManualJournalActionsBar({
|
|||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
|
onClick={handleImportBtnClick}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '../Import/ImportView';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function ManualJournalsImport() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
history.push('/manual-journals');
|
||||||
|
};
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push('/manual-journals');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-manual-journals'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'manual-journals'}
|
||||||
|
onCancelClick={handleCancelBtnClick}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
import { Button, Intent, Position } from '@blueprintjs/core';
|
import { Button, Intent, Position } from '@blueprintjs/core';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { FSelect, Group, Hint } from '@/components';
|
import { Box, FSelect, Group, Hint } from '@/components';
|
||||||
import { ImportFileMappingForm } from './ImportFileMappingForm';
|
import { ImportFileMappingForm } from './ImportFileMappingForm';
|
||||||
import { EntityColumn, useImportFileContext } from './ImportFileProvider';
|
import { EntityColumnField, useImportFileContext } from './ImportFileProvider';
|
||||||
import { CLASSES } from '@/constants';
|
import { CLASSES } from '@/constants';
|
||||||
import { ImportFileContainer } from './ImportFileContainer';
|
import { ImportFileContainer } from './ImportFileContainer';
|
||||||
import { ImportStepperStep } from './_types';
|
import { ImportStepperStep } from './_types';
|
||||||
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
|
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
|
||||||
import styles from './ImportFileMapping.module.scss';
|
import styles from './ImportFileMapping.module.scss';
|
||||||
|
import { getFieldKey } from './_utils';
|
||||||
|
|
||||||
export function ImportFileMapping() {
|
export function ImportFileMapping() {
|
||||||
const { importId } = useImportFileContext();
|
const { importId, entityColumns } = useImportFileContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportFileMapBootProvider importId={importId}>
|
<ImportFileMapBootProvider importId={importId}>
|
||||||
@@ -23,56 +24,98 @@ export function ImportFileMapping() {
|
|||||||
Bigcapital fields.
|
Bigcapital fields.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table className={clsx('bp4-html-table', styles.table)}>
|
{entityColumns.map((entityColumn, index) => (
|
||||||
<thead>
|
<ImportFileMappingGroup
|
||||||
<tr>
|
groupKey={entityColumn.groupKey}
|
||||||
<th className={styles.label}>Bigcapital Fields</th>
|
groupName={entityColumn.groupName}
|
||||||
<th className={styles.field}>Sheet Column Headers</th>
|
fields={entityColumn.fields}
|
||||||
</tr>
|
/>
|
||||||
</thead>
|
))}
|
||||||
<tbody>
|
|
||||||
<ImportFileMappingFields />
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ImportFileContainer>
|
</ImportFileContainer>
|
||||||
|
|
||||||
<ImportFileMappingFloatingActions />
|
<ImportFileMappingFloatingActions />
|
||||||
</ImportFileMappingForm>
|
</ImportFileMappingForm>
|
||||||
</ImportFileMapBootProvider>
|
</ImportFileMapBootProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImportFileMappingFields() {
|
interface ImportFileMappingGroupProps {
|
||||||
const { entityColumns, sheetColumns } = useImportFileContext();
|
groupKey: string;
|
||||||
|
groupName: string;
|
||||||
|
fields: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping fields group
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
function ImportFileMappingGroup({
|
||||||
|
groupKey,
|
||||||
|
groupName,
|
||||||
|
fields,
|
||||||
|
}: ImportFileMappingGroupProps) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{groupName && <h3>{groupName}</h3>}
|
||||||
|
|
||||||
|
<table className={clsx('bp4-html-table', styles.table)}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={styles.label}>Bigcapital Fields</th>
|
||||||
|
<th className={styles.field}>Sheet Column Headers</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<ImportFileMappingFields fields={fields} />
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportFileMappingFieldsProps {
|
||||||
|
fields: EntityColumnField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import mapping fields.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
|
||||||
|
const { sheetColumns } = useImportFileContext();
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => sheetColumns.map((column) => ({ value: column, text: column })),
|
() => sheetColumns.map((column) => ({ value: column, text: column })),
|
||||||
[sheetColumns],
|
[sheetColumns],
|
||||||
);
|
);
|
||||||
const columnMapper = (column: EntityColumn, index: number) => (
|
const columnMapper = useCallback(
|
||||||
<tr key={index}>
|
(column: EntityColumnField, index: number) => (
|
||||||
<td className={styles.label}>
|
<tr key={index}>
|
||||||
{column.name}{' '}
|
<td className={styles.label}>
|
||||||
{column.required && <span className={styles.requiredSign}>*</span>}
|
{column.name}{' '}
|
||||||
</td>
|
{column.required && <span className={styles.requiredSign}>*</span>}
|
||||||
<td className={styles.field}>
|
</td>
|
||||||
<Group spacing={4}>
|
<td className={styles.field}>
|
||||||
<FSelect
|
<Group spacing={4}>
|
||||||
name={column.key}
|
<FSelect
|
||||||
items={items}
|
name={getFieldKey(column.key, column.group)}
|
||||||
popoverProps={{ minimal: true }}
|
items={items}
|
||||||
minimal={true}
|
popoverProps={{ minimal: true }}
|
||||||
fill={true}
|
minimal={true}
|
||||||
/>
|
fill={true}
|
||||||
{column.hint && (
|
/>
|
||||||
<Hint content={column.hint} position={Position.BOTTOM} />
|
{column.hint && (
|
||||||
)}
|
<Hint content={column.hint} position={Position.BOTTOM} />
|
||||||
</Group>
|
)}
|
||||||
</td>
|
</Group>
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
),
|
||||||
|
[items],
|
||||||
|
);
|
||||||
|
const columns = useMemo(
|
||||||
|
() => fields.map(columnMapper),
|
||||||
|
[columnMapper, fields],
|
||||||
);
|
);
|
||||||
const columns = entityColumns.map(columnMapper);
|
|
||||||
|
|
||||||
return <>{columns}</>;
|
return <>{columns}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,12 @@ import { Intent } from '@blueprintjs/core';
|
|||||||
import { useImportFileMapping } from '@/hooks/query/import';
|
import { useImportFileMapping } from '@/hooks/query/import';
|
||||||
import { Form, Formik, FormikHelpers } from 'formik';
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
import { useImportFileContext } from './ImportFileProvider';
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { isEmpty, lowerCase } from 'lodash';
|
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
|
import { ImportFileMappingFormProps } from './_types';
|
||||||
import { transformToForm } from '@/utils';
|
import {
|
||||||
|
transformValueToReq,
|
||||||
interface ImportFileMappingFormProps {
|
useImportFileMappingInitialValues,
|
||||||
children: React.ReactNode;
|
} from './_utils';
|
||||||
}
|
|
||||||
|
|
||||||
type ImportFileMappingFormValues = Record<string, string | null>;
|
|
||||||
|
|
||||||
export function ImportFileMappingForm({
|
export function ImportFileMappingForm({
|
||||||
children,
|
children,
|
||||||
@@ -52,50 +47,3 @@ export function ImportFileMappingForm({
|
|||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformValueToReq = (value: ImportFileMappingFormValues) => {
|
|
||||||
const mapping = Object.keys(value)
|
|
||||||
.filter((key) => !isEmpty(value[key]))
|
|
||||||
.map((key) => ({ from: value[key], to: key }));
|
|
||||||
return { mapping };
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformResToFormValues = (value: { from: string; to: string }[]) => {
|
|
||||||
return value?.reduce((acc, map) => {
|
|
||||||
acc[map.to] = map.from;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const useImportFileMappingInitialValues = () => {
|
|
||||||
const { importFile } = useImportFileMapBootContext();
|
|
||||||
const { entityColumns, sheetColumns } = useImportFileContext();
|
|
||||||
|
|
||||||
const initialResValues = useMemo(
|
|
||||||
() => transformResToFormValues(importFile?.map || []),
|
|
||||||
[importFile?.map],
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialValues = useMemo(
|
|
||||||
() =>
|
|
||||||
entityColumns.reduce((acc, { key, name }) => {
|
|
||||||
const _name = lowerCase(name);
|
|
||||||
const _matched = sheetColumns.find(
|
|
||||||
(column) => lowerCase(column) === _name,
|
|
||||||
);
|
|
||||||
// Match the default column name the same field name
|
|
||||||
// if matched one of sheet columns has the same field name.
|
|
||||||
acc[key] = _matched ? _matched : '';
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
[entityColumns, sheetColumns],
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
...transformToForm(initialResValues, initialValues),
|
|
||||||
...initialValues,
|
|
||||||
}),
|
|
||||||
[initialValues, initialResValues],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -7,12 +7,19 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
export type EntityColumn = {
|
export type EntityColumnField = {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
|
group?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface EntityColumn {
|
||||||
|
groupKey: string;
|
||||||
|
groupName: string;
|
||||||
|
fields: EntityColumnField[];
|
||||||
|
}
|
||||||
export type SheetColumn = string;
|
export type SheetColumn = string;
|
||||||
export type SheetMap = { from: string; to: string };
|
export type SheetMap = { from: string; to: string };
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as Yup from 'yup';
|
|||||||
import { useImportFileContext } from './ImportFileProvider';
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
import { ImportAlert, ImportStepperStep } from './_types';
|
import { ImportAlert, ImportStepperStep } from './_types';
|
||||||
import { useAlertsManager } from './AlertsManager';
|
import { useAlertsManager } from './AlertsManager';
|
||||||
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
file: null,
|
file: null,
|
||||||
@@ -55,9 +56,11 @@ export function ImportFileUploadForm({
|
|||||||
|
|
||||||
uploadImportFile(formData)
|
uploadImportFile(formData)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
setImportId(data.import.import_id);
|
const _data = transformToCamelCase(data);
|
||||||
setSheetColumns(data.sheet_columns);
|
|
||||||
setEntityColumns(data.resource_columns);
|
setImportId(_data.import.importId);
|
||||||
|
setSheetColumns(_data.sheetColumns);
|
||||||
|
setEntityColumns(_data.resourceColumns);
|
||||||
setStep(ImportStepperStep.Mapping);
|
setStep(ImportStepperStep.Mapping);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,5 +5,11 @@ export enum ImportStepperStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ImportAlert {
|
export enum ImportAlert {
|
||||||
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY'
|
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImportFileMappingFormProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImportFileMappingFormValues = Record<string, string | null>;
|
||||||
|
|||||||
87
packages/webapp/src/containers/Import/_utils.ts
Normal file
87
packages/webapp/src/containers/Import/_utils.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { chain, isEmpty, lowerCase, head, last, set } from 'lodash';
|
||||||
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
|
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
|
||||||
|
import { deepdash, transformToForm } from '@/utils';
|
||||||
|
import { ImportFileMappingFormValues } from './_types';
|
||||||
|
|
||||||
|
export const getFieldKey = (key: string, group = '') => {
|
||||||
|
return group ? `${group}.${key}` : key;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImportFileMappingRes = { from: string; to: string; group: string }[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the mapping form values to request.
|
||||||
|
* @param {ImportFileMappingFormValues} value
|
||||||
|
* @returns {ImportFileMappingRes[]}
|
||||||
|
*/
|
||||||
|
export const transformValueToReq = (
|
||||||
|
value: ImportFileMappingFormValues,
|
||||||
|
): { mapping: ImportFileMappingRes[] } => {
|
||||||
|
const mapping = chain(value)
|
||||||
|
.thru(deepdash.index)
|
||||||
|
.pickBy((_value, key) => !isEmpty(_.get(value, key)))
|
||||||
|
.map((from, key) => ({
|
||||||
|
from,
|
||||||
|
to: key.includes('.') ? last(key.split('.')) : key,
|
||||||
|
group: key.includes('.') ? head(key.split('.')) : '',
|
||||||
|
}))
|
||||||
|
.value();
|
||||||
|
|
||||||
|
return { mapping };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const transformResToFormValues = (
|
||||||
|
value: { from: string; to: string }[],
|
||||||
|
) => {
|
||||||
|
return value?.reduce((acc, map) => {
|
||||||
|
acc[map.to] = map.from;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the initial values of mapping form.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
export const useImportFileMappingInitialValues = () => {
|
||||||
|
const { importFile } = useImportFileMapBootContext();
|
||||||
|
const { entityColumns, sheetColumns } = useImportFileContext();
|
||||||
|
|
||||||
|
const initialResValues = useMemo(
|
||||||
|
() => transformResToFormValues(importFile?.map || []),
|
||||||
|
[importFile?.map],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialValues = useMemo(
|
||||||
|
() =>
|
||||||
|
entityColumns.reduce((acc, { fields, groupKey }) => {
|
||||||
|
fields.forEach(({ key, name }) => {
|
||||||
|
const _name = lowerCase(name);
|
||||||
|
const _matched = sheetColumns.find(
|
||||||
|
(column) => lowerCase(column) === _name,
|
||||||
|
);
|
||||||
|
const _key = groupKey ? `${groupKey}.${key}` : key;
|
||||||
|
const _value = _matched ? _matched : '';
|
||||||
|
|
||||||
|
set(acc, _key, _value);
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
[entityColumns, sheetColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo<Record<string, any>>(
|
||||||
|
() => ({
|
||||||
|
...transformToForm(initialResValues, initialValues),
|
||||||
|
...initialValues,
|
||||||
|
}),
|
||||||
|
[initialValues, initialResValues],
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '@/containers/Import';
|
||||||
|
|
||||||
|
export default function BillsImport() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
history.push('/bills');
|
||||||
|
};
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push('/bills');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-bills'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'bills'}
|
||||||
|
onCancelClick={handleCancelBtnClick}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -78,6 +78,11 @@ function BillActionsBar({
|
|||||||
addSetting('bills', 'tableSize', size);
|
addSetting('bills', 'tableSize', size);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle the import button click.
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push('/bills/import');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -130,6 +135,7 @@ function BillActionsBar({
|
|||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon={'file-import-16'} />}
|
icon={<Icon icon={'file-import-16'} />}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
|
onClick={handleImportBtnClick}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ export const getDashboardRoutes = () => [
|
|||||||
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `/manual-journals/import`,
|
||||||
|
component: lazy(
|
||||||
|
() => import('@/containers/Accounting/ManualJournalsImport'),
|
||||||
|
),
|
||||||
|
breadcrumb: intl.get('edit'),
|
||||||
|
pageTitle: 'Manual Journals Import',
|
||||||
|
sidebarExpand: false,
|
||||||
|
backLink: true,
|
||||||
|
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
||||||
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `/manual-journals`,
|
path: `/manual-journals`,
|
||||||
component: lazy(
|
component: lazy(
|
||||||
@@ -893,6 +905,17 @@ export const getDashboardRoutes = () => [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Bills
|
// Bills
|
||||||
|
{
|
||||||
|
path: `/bills/import`,
|
||||||
|
component: lazy(() => import('@/containers/Purchases/Bills/BillImport')),
|
||||||
|
name: 'bill-edit',
|
||||||
|
// breadcrumb: intl.get('edit'),
|
||||||
|
pageTitle: 'Bills Import',
|
||||||
|
sidebarExpand: false,
|
||||||
|
backLink: true,
|
||||||
|
defaultSearchResource: RESOURCES_TYPES.BILL,
|
||||||
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `/bills/:id/edit`,
|
path: `/bills/:id/edit`,
|
||||||
component: lazy(
|
component: lazy(
|
||||||
|
|||||||
Reference in New Issue
Block a user