diff --git a/packages/server/resources/locales/en.json b/packages/server/resources/locales/en.json
index 8e5ace3ee..a82ad052e 100644
--- a/packages/server/resources/locales/en.json
+++ b/packages/server/resources/locales/en.json
@@ -355,11 +355,13 @@
"expense.field.status.published": "Published",
"expense.field.created_at": "Created at",
"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.journal_type": "Journal type",
+ "manual_journal.field.journal_type": "Journal Type",
"manual_journal.field.amount": "Amount",
"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.created_at": "Created at",
"receipt.field.amount": "Amount",
diff --git a/packages/server/src/api/controllers/Import/ImportController.ts b/packages/server/src/api/controllers/Import/ImportController.ts
index 645531f28..7db23fd1d 100644
--- a/packages/server/src/api/controllers/Import/ImportController.ts
+++ b/packages/server/src/api/controllers/Import/ImportController.ts
@@ -37,6 +37,7 @@ export class ImportController extends BaseController {
[
param('import_id').exists().isString(),
body('mapping').exists().isArray({ min: 1 }),
+ body('mapping.*.group').optional(),
body('mapping.*.from').exists(),
body('mapping.*.to').exists(),
],
diff --git a/packages/server/src/interfaces/Model.ts b/packages/server/src/interfaces/Model.ts
index fdebe7d8c..87912bafe 100644
--- a/packages/server/src/interfaces/Model.ts
+++ b/packages/server/src/interfaces/Model.ts
@@ -69,6 +69,7 @@ export type IModelMetaField = IModelMetaFieldCommon &
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
+ | IModelMetaCollectionField
);
export interface IModelMetaEnumerationOption {
@@ -92,12 +93,71 @@ export interface IModelMetaRelationEnumerationField {
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 &
IModelMetaRelationEnumerationField;
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
+
importable?: boolean;
+
+ importAggregator?: string;
+ importAggregateOn?: string;
+ importAggregateBy?: string;
+
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
+ );
diff --git a/packages/server/src/models/Bill.Settings.ts b/packages/server/src/models/Bill.Settings.ts
index 166648a0c..962aafca5 100644
--- a/packages/server/src/models/Bill.Settings.ts
+++ b/packages/server/src/models/Bill.Settings.ts
@@ -1,10 +1,13 @@
-
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
+ importable: true,
+ importAggregator: 'group',
+ importAggregateOn: 'entries',
+ importAggregateBy: 'billNumber',
fields: {
vendor: {
name: 'bill.field.vendor',
@@ -77,6 +80,76 @@ export default {
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',
+ },
+ },
+ },
+ },
};
/**
diff --git a/packages/server/src/models/ManualJournal.Settings.ts b/packages/server/src/models/ManualJournal.Settings.ts
index bc6ae8d64..210f07682 100644
--- a/packages/server/src/models/ManualJournal.Settings.ts
+++ b/packages/server/src/models/ManualJournal.Settings.ts
@@ -4,54 +4,193 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
+ importable: true,
+ importAggregator: 'group',
+ importAggregateOn: 'entries',
+ importAggregateBy: 'journalNumber',
fields: {
- 'date': {
+ date: {
name: 'manual_journal.field.date',
column: 'date',
fieldType: 'date',
+ importable: true,
+ required: true,
},
- 'journal_number': {
+ journalNumber: {
name: 'manual_journal.field.journal_number',
column: 'journal_number',
fieldType: 'text',
+ importable: true,
+ required: true,
},
- 'reference': {
+ reference: {
name: 'manual_journal.field.reference',
column: 'reference',
fieldType: 'text',
+ importable: true,
},
- 'journal_type': {
+ journalType: {
name: 'manual_journal.field.journal_type',
column: 'journal_type',
fieldType: 'text',
},
- 'amount': {
+ amount: {
name: 'manual_journal.field.amount',
column: 'amount',
fieldType: 'number',
},
- 'description': {
+ description: {
name: 'manual_journal.field.description',
column: 'description',
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',
column: 'status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
- { key: 'published', label: 'published' }
+ { key: 'published', label: 'published' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
- 'created_at': {
+ createdAt: {
name: 'manual_journal.field.created_at',
column: 'created_at',
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.
*/
- function StatusFieldFilterQuery(query, role) {
+function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
diff --git a/packages/server/src/services/Import/ImportFileCommon.ts b/packages/server/src/services/Import/ImportFileCommon.ts
index 9562e083a..f41ec2b50 100644
--- a/packages/server/src/services/Import/ImportFileCommon.ts
+++ b/packages/server/src/services/Import/ImportFileCommon.ts
@@ -71,7 +71,7 @@ export class ImportFileCommon {
parsedData: Record[],
trx?: Knex.Transaction
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
- const importableFields = this.resource.getResourceImportableFields(
+ const resourceFields = this.resource.getResourceFields2(
tenantId,
importFile.resource
);
@@ -90,7 +90,7 @@ export class ImportFileCommon {
};
const transformedDTO = importable.transform(objectDTO, context);
const rowNumber = index + 1;
- const uniqueValue = getUniqueImportableValue(importableFields, objectDTO);
+ const uniqueValue = getUniqueImportableValue(resourceFields, objectDTO);
const errorContext = {
rowNumber,
uniqueValue,
@@ -98,7 +98,7 @@ export class ImportFileCommon {
try {
// Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData(
- importableFields,
+ resourceFields,
transformedDTO
);
try {
diff --git a/packages/server/src/services/Import/ImportFileDataTransformer.ts b/packages/server/src/services/Import/ImportFileDataTransformer.ts
index 4fb8cf300..8fd47b584 100644
--- a/packages/server/src/services/Import/ImportFileDataTransformer.ts
+++ b/packages/server/src/services/Import/ImportFileDataTransformer.ts
@@ -1,63 +1,91 @@
import { Inject, Service } from 'typedi';
-import * as R from 'ramda';
import bluebird from 'bluebird';
-import { isUndefined, get, chain, toArray, pickBy, castArray } from 'lodash';
+import { isUndefined, pickBy, set } from 'lodash';
import { Knex } from 'knex';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
-import { trimObject, parseBoolean } from './_utils';
-import { Account, Item } from '@/models';
+import {
+ valueParser,
+ parseKey,
+ getFieldKey,
+ aggregate,
+ sanitizeSheetData,
+} from './_utils';
import ResourceService from '../Resource/ResourceService';
-import { multiNumberParse } from '@/utils/multi-number-parse';
+import HasTenancyService from '../Tenancy/TenancyService';
const CurrencyParsingDTOs = 10;
+const getMapToPath = (to: string, group = '') =>
+ group ? `${group}.${to}` : to;
+
@Service()
export class ImportFileDataTransformer {
@Inject()
private resource: ResourceService;
+ @Inject()
+ private tenancy: HasTenancyService;
+
/**
* 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 {}
*/
public async parseSheetData(
tenantId: number,
importFile: any,
- importableFields: any,
+ importableFields: ResourceMetaFieldsMap,
data: Record[],
trx?: Knex.Transaction
- ) {
+ ): Promise[]> {
// Sanitize the sheet data.
- const sanitizedData = this.sanitizeSheetData(data);
+ const sanitizedData = sanitizeSheetData(data);
// Map the sheet columns key with the given map.
const mappedDTOs = this.mapSheetColumns(
sanitizedData,
importFile.mappingParsed
);
- const resourceModel = this.resource.getResourceModel(
- tenantId,
- importFile.resource
- );
// Parse the mapped sheet values.
- return this.parseExcelValues(
+ const parsedValues = await this.parseExcelValues(
+ tenantId,
importableFields,
mappedDTOs,
- resourceModel,
trx
);
+ const aggregateValues = this.aggregateParsedValues(
+ tenantId,
+ importFile.resource,
+ parsedValues
+ );
+ return aggregateValues;
}
/**
- * 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.
+ * Aggregates parsed data based on resource metadata configuration.
+ * @param {number} tenantId
+ * @param {string} resourceName
+ * @param {Record} parsedData
+ * @returns {Record[]}
*/
- public sanitizeSheetData(json) {
- return R.compose(R.map(trimObject))(json);
- }
+ public aggregateParsedValues = (
+ tenantId: number,
+ resourceName: string,
+ parsedData: Record[]
+ ): Record[] => {
+ 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.
@@ -74,7 +102,8 @@ export class ImportFileDataTransformer {
map
.filter((mapping) => !isUndefined(item[mapping.from]))
.forEach((mapping) => {
- newItem[mapping.to] = item[mapping.from];
+ const toPath = getMapToPath(mapping.to, mapping.group);
+ newItem[toPath] = item[mapping.from];
});
return newItem;
});
@@ -87,78 +116,32 @@ export class ImportFileDataTransformer {
* @returns {Record}
*/
public async parseExcelValues(
+ tenantId: number,
fields: ResourceMetaFieldsMap,
valueDTOs: Record[],
- resourceModel: any,
trx?: Knex.Transaction
- ): Promise> {
- // Prases the given object value based on the field key type.
- const parser = async (value, key) => {
- let _value = value;
- const field = fields[key];
+ ): Promise[]> {
+ const tenantModels = this.tenancy.models(tenantId);
+ const _valueParser = valueParser(fields, tenantModels, trx);
+ const _keyParser = parseKey(fields);
- // 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) => {
- // Remove the undefined fields.
+ // Clean up the undefined keys that not exist in resource fields.
const _valueDTO = pickBy(
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);
// Map the object values.
return bluebird.reduce(
keys,
async (acc, key) => {
- const parsedValue = await parser(_valueDTO[key], key);
- const parsedKey = await parseKey(key);
- acc[parsedKey] = parsedValue;
+ const parsedValue = await _valueParser(_valueDTO[key], key);
+ const parsedKey = await _keyParser(key);
+
+ set(acc, parsedKey, parsedValue);
return acc;
},
{}
diff --git a/packages/server/src/services/Import/ImportFileMapping.ts b/packages/server/src/services/Import/ImportFileMapping.ts
index 86b5e42cb..8d8f062ac 100644
--- a/packages/server/src/services/Import/ImportFileMapping.ts
+++ b/packages/server/src/services/Import/ImportFileMapping.ts
@@ -1,4 +1,4 @@
-import { fromPairs } from 'lodash';
+import { fromPairs, isUndefined } from 'lodash';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import {
@@ -69,7 +69,7 @@ export class ImportFileMapping {
importFile: any,
maps: ImportMappingAttr[]
) {
- const fields = this.resource.getResourceImportableFields(
+ const fields = this.resource.getResourceFields2(
tenantId,
importFile.resource
);
@@ -78,11 +78,20 @@ export class ImportFileMapping {
);
const invalid = [];
+ // is not empty, is not undefined or map.group
maps.forEach((map) => {
- if (
- 'undefined' === typeof fields[map.to] ||
- 'undefined' === typeof columnsMap[map.from]
- ) {
+ let _invalid = true;
+
+ 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);
}
});
@@ -105,10 +114,14 @@ export class ImportFileMapping {
} else {
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);
} else {
- toMap[map.to] = true;
+ toMap[toPath] = true;
}
});
}
@@ -128,6 +141,7 @@ export class ImportFileMapping {
tenantId,
resource
);
+ // @todo Validate date type of the nested fields.
maps.forEach((map) => {
if (
typeof fields[map.to] !== 'undefined' &&
diff --git a/packages/server/src/services/Import/ImportFileProcess.ts b/packages/server/src/services/Import/ImportFileProcess.ts
index 55726bc3c..3c57f2b69 100644
--- a/packages/server/src/services/Import/ImportFileProcess.ts
+++ b/packages/server/src/services/Import/ImportFileProcess.ts
@@ -53,11 +53,10 @@ export class ImportFileProcess {
const sheetData = this.importCommon.parseXlsxSheet(buffer);
const header = getSheetColumns(sheetData);
- const importableFields = this.resource.getResourceImportableFields(
+ const resourceFields = this.resource.getResourceFields2(
tenantId,
importFile.resource
);
-
// Runs the importing operation with ability to return errors that will happen.
const [successedImport, failedImport, allData] =
await this.uow.withTransaction(
@@ -67,7 +66,7 @@ export class ImportFileProcess {
const parsedData = await this.importParser.parseSheetData(
tenantId,
importFile,
- importableFields,
+ resourceFields,
sheetData,
trx
);
diff --git a/packages/server/src/services/Import/ImportFileUpload.ts b/packages/server/src/services/Import/ImportFileUpload.ts
index 048739e03..11dfc52c0 100644
--- a/packages/server/src/services/Import/ImportFileUpload.ts
+++ b/packages/server/src/services/Import/ImportFileUpload.ts
@@ -1,8 +1,11 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
-import { sanitizeResourceName, validateSheetEmpty } from './_utils';
+import {
+ getResourceColumns,
+ sanitizeResourceName,
+ validateSheetEmpty,
+} from './_utils';
import ResourceService from '../Resource/ResourceService';
-import { IModelMetaField } from '@/interfaces';
import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces';
@@ -77,11 +80,11 @@ export class ImportFileUploadService {
columns: coumnsStringified,
params: paramsStringified,
});
- const resourceColumnsMap = this.resourceService.getResourceImportableFields(
+ const resourceColumnsMap = this.resourceService.getResourceFields2(
tenantId,
resource
);
- const resourceColumns = this.getResourceColumns(resourceColumnsMap);
+ const resourceColumns = getResourceColumns(resourceColumnsMap);
return {
import: {
@@ -92,23 +95,4 @@ export class ImportFileUploadService {
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
- );
- }
}
diff --git a/packages/server/src/services/Import/ImportableRegistry.ts b/packages/server/src/services/Import/ImportableRegistry.ts
index c260c5bd5..9fe25ec43 100644
--- a/packages/server/src/services/Import/ImportableRegistry.ts
+++ b/packages/server/src/services/Import/ImportableRegistry.ts
@@ -5,7 +5,7 @@ export class ImportableRegistry {
private static instance: ImportableRegistry;
private importables: Record;
- private constructor() {
+ constructor() {
this.importables = {};
}
diff --git a/packages/server/src/services/Import/ImportableResources.ts b/packages/server/src/services/Import/ImportableResources.ts
index 96aee4290..1cffc1a8a 100644
--- a/packages/server/src/services/Import/ImportableResources.ts
+++ b/packages/server/src/services/Import/ImportableResources.ts
@@ -6,6 +6,8 @@ import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
import { ItemsImportable } from '../Items/ItemsImportable';
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
+import { ManualJournalImportable } from '../ManualJournals/ManualJournalsImport';
+import { BillsImportable } from '../Purchases/Bills/BillsImportable';
@Service()
export class ImportableResources {
@@ -28,6 +30,8 @@ export class ImportableResources {
{ resource: 'Vendor', importable: VendorsImportable },
{ resource: 'Item', importable: ItemsImportable },
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
+ { resource: 'ManualJournal', importable: ManualJournalImportable },
+ { resource: 'Bill', importable: BillsImportable },
];
public get registry() {
diff --git a/packages/server/src/services/Import/_utils.ts b/packages/server/src/services/Import/_utils.ts
index 95cfb1367..67fb7f5cc 100644
--- a/packages/server/src/services/Import/_utils.ts
+++ b/packages/server/src/services/Import/_utils.ts
@@ -1,5 +1,7 @@
import * as Yup from 'yup';
import moment from 'moment';
+import * as R from 'ramda';
+import { Knex } from 'knex';
import {
defaultTo,
upperFirst,
@@ -8,11 +10,17 @@ import {
isUndefined,
pickBy,
isEmpty,
+ castArray,
+ get,
+ head,
+ split,
+ last,
} from 'lodash';
import pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces';
-import { IModelMetaField } from '@/interfaces';
+import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import { ServiceError } from '@/exceptions';
+import { multiNumberParse } from '@/utils/multi-number-parse';
export const ERRORS = {
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
@@ -40,6 +48,7 @@ export function trimObject(obj) {
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
const yupSchema = {};
+
Object.keys(fields).forEach((fieldName: string) => {
const field = fields[fieldName] as IModelMetaField;
let fieldSchema;
@@ -89,6 +98,17 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
);
} else if (field.fieldType === '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) {
fieldSchema = fieldSchema.required();
@@ -103,9 +123,9 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
let _key = fieldName;
- if (field.fieldType === 'relation') {
- _key = `${fieldName}Id`;
- }
+ // if (field.fieldType === 'relation') {
+ // _key = `${fieldName}Id`;
+ // }
if (field.dataTransferObjectKey) {
_key = field.dataTransferObjectKey;
}
@@ -134,7 +154,7 @@ export const getSheetColumns = (sheetData: unknown[]) => {
* @returns {string}
*/
export const getUniqueImportableValue = (
- importableFields: { [key: string]: IModelMetaField },
+ importableFields: { [key: string]: IModelMetaField2 },
objectDTO: Record
) => {
const uniqueImportableValue = pickBy(
@@ -155,15 +175,15 @@ export const validateSheetEmpty = (sheetData: Array) => {
if (isEmpty(sheetData)) {
throw new ServiceError(ERRORS.IMPORTED_SHEET_EMPTY);
}
-}
+};
const booleanValuesRepresentingTrue: string[] = ['true', 'yes', 'y', 't', '1'];
const booleanValuesRepresentingFalse: string[] = ['false', 'no', 'n', 'f', '0'];
/**
* Parses the given string value to boolean.
- * @param {string} value
- * @returns {string|null}
+ * @param {string} value
+ * @returns {string|null}
*/
export const parseBoolean = (value: string): boolean | null => {
const normalizeValue = (value: string): string =>
@@ -182,3 +202,204 @@ export const parseBoolean = (value: string): boolean | 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 = {};
+ 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,
+ comparatorAttr: string,
+ groupOn: string
+): Array> {
+ 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);
+};
diff --git a/packages/server/src/services/Import/interfaces.ts b/packages/server/src/services/Import/interfaces.ts
index b3cb9d8e1..3da238e5f 100644
--- a/packages/server/src/services/Import/interfaces.ts
+++ b/packages/server/src/services/Import/interfaces.ts
@@ -1,9 +1,10 @@
-import { IModelMetaField } from '@/interfaces';
+import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import Import from '@/models/Import';
export interface ImportMappingAttr {
from: string;
to: string;
+ group?: string;
dateFormat?: string;
}
@@ -13,7 +14,7 @@ export interface ImportValidationError {
constraints: Record;
}
-export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField };
+export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField2 };
export interface ImportInsertError {
rowNumber: number;
@@ -61,16 +62,15 @@ export interface ImportOperError {
error: ImportInsertError[];
index: number;
}
-
+
export interface ImportableContext {
- import: Import,
+ import: Import;
rowIndex: number;
}
-
export const ImportDateFormats = [
'yyyy-MM-dd',
'dd.MM.yy',
'MM/dd/yy',
- 'dd/MMM/yyyy'
-]
+ 'dd/MMM/yyyy',
+];
diff --git a/packages/server/src/services/Import/jobs/ImportDeleteExpiredFilesJob.ts b/packages/server/src/services/Import/jobs/ImportDeleteExpiredFilesJob.ts
new file mode 100644
index 000000000..89a6f5b21
--- /dev/null
+++ b/packages/server/src/services/Import/jobs/ImportDeleteExpiredFilesJob.ts
@@ -0,0 +1 @@
+export class ImportDeleteExpiredFilesJobs {}
diff --git a/packages/server/src/services/ManualJournals/CreateManualJournal.ts b/packages/server/src/services/ManualJournals/CreateManualJournal.ts
index 4cd213947..7374fb358 100644
--- a/packages/server/src/services/ManualJournals/CreateManualJournal.ts
+++ b/packages/server/src/services/ManualJournals/CreateManualJournal.ts
@@ -73,9 +73,7 @@ export class CreateManualJournalService {
return R.compose(
// Omits the `branchId` from entries if multiply branches feature not active.
this.branchesDTOTransformer.transformDTO(tenantId)
- )(
- initialDTO
- );
+ )(initialDTO);
}
/**
@@ -133,7 +131,8 @@ export class CreateManualJournalService {
public makeJournalEntries = async (
tenantId: number,
manualJournalDTO: IManualJournalDTO,
- authorizedUser: ISystemUser
+ authorizedUser: ISystemUser,
+ trx?: Knex.Transaction
): Promise<{ manualJournal: IManualJournal }> => {
const { ManualJournal } = this.tenancy.models(tenantId);
@@ -156,27 +155,31 @@ export class CreateManualJournalService {
);
// Creates a manual journal transactions with associated transactions
// under unit-of-work envirement.
- return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
- // Triggers `onManualJournalCreating` event.
- await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
- tenantId,
- manualJournalDTO,
- trx,
- } as IManualJournalCreatingPayload);
+ return this.uow.withTransaction(
+ tenantId,
+ async (trx: Knex.Transaction) => {
+ // Triggers `onManualJournalCreating` event.
+ await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
+ tenantId,
+ manualJournalDTO,
+ trx,
+ } as IManualJournalCreatingPayload);
- // Upsert the manual journal object.
- const manualJournal = await ManualJournal.query(trx).upsertGraph({
- ...manualJournalObj,
- });
- // Triggers `onManualJournalCreated` event.
- await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
- tenantId,
- manualJournal,
- manualJournalId: manualJournal.id,
- trx,
- } as IManualJournalEventCreatedPayload);
+ // Upsert the manual journal object.
+ const manualJournal = await ManualJournal.query(trx).upsertGraph({
+ ...manualJournalObj,
+ });
+ // Triggers `onManualJournalCreated` event.
+ await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
+ tenantId,
+ manualJournal,
+ manualJournalId: manualJournal.id,
+ trx,
+ } as IManualJournalEventCreatedPayload);
- return { manualJournal };
- });
+ return { manualJournal };
+ },
+ trx
+ );
};
}
diff --git a/packages/server/src/services/ManualJournals/ManualJournalsImport.ts b/packages/server/src/services/ManualJournals/ManualJournalsImport.ts
new file mode 100644
index 000000000..1f3af74e9
--- /dev/null
+++ b/packages/server/src/services/ManualJournals/ManualJournalsImport.ts
@@ -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} createDTO
+ * @param {ImportableContext} context
+ * @returns {Record}
+ */
+ public transform(createDTO: Record, 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}
+ */
+ public sampleData(): Record[] {
+ return ManualJournalsSampleData;
+ }
+}
diff --git a/packages/server/src/services/ManualJournals/constants.ts b/packages/server/src/services/ManualJournals/constants.ts
index 6f6379be2..e72cc11cf 100644
--- a/packages/server/src/services/ManualJournals/constants.ts
+++ b/packages/server/src/services/ManualJournals/constants.ts
@@ -29,3 +29,36 @@ export const CONTACTS_CONFIG = [
];
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',
+ },
+];
diff --git a/packages/server/src/services/Purchases/Bills/BillsImportable.ts b/packages/server/src/services/Purchases/Bills/BillsImportable.ts
new file mode 100644
index 000000000..a67f93546
--- /dev/null
+++ b/packages/server/src/services/Purchases/Bills/BillsImportable.ts
@@ -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 [];
+ }
+}
diff --git a/packages/server/src/services/Purchases/Bills/CreateBill.ts b/packages/server/src/services/Purchases/Bills/CreateBill.ts
index 47dfadecc..98faea3c5 100644
--- a/packages/server/src/services/Purchases/Bills/CreateBill.ts
+++ b/packages/server/src/services/Purchases/Bills/CreateBill.ts
@@ -53,7 +53,8 @@ export class CreateBill {
public async createBill(
tenantId: number,
billDTO: IBillDTO,
- authorizedUser: ISystemUser
+ authorizedUser: ISystemUser,
+ trx?: Knex.Transaction
): Promise {
const { Bill, Contact } = this.tenancy.models(tenantId);
@@ -91,26 +92,30 @@ export class CreateBill {
authorizedUser
);
// Write new bill transaction with associated transactions under UOW env.
- return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
- // Triggers `onBillCreating` event.
- await this.eventPublisher.emitAsync(events.bill.onCreating, {
- trx,
- billDTO,
- tenantId,
- } as IBillCreatingPayload);
+ return this.uow.withTransaction(
+ tenantId,
+ async (trx: Knex.Transaction) => {
+ // Triggers `onBillCreating` event.
+ await this.eventPublisher.emitAsync(events.bill.onCreating, {
+ trx,
+ billDTO,
+ tenantId,
+ } as IBillCreatingPayload);
- // Inserts the bill graph object to the storage.
- const bill = await Bill.query(trx).upsertGraph(billObj);
+ // Inserts the bill graph object to the storage.
+ const bill = await Bill.query(trx).upsertGraph(billObj);
- // Triggers `onBillCreated` event.
- await this.eventPublisher.emitAsync(events.bill.onCreated, {
- tenantId,
- bill,
- billId: bill.id,
- trx,
- } as IBillCreatedPayload);
+ // Triggers `onBillCreated` event.
+ await this.eventPublisher.emitAsync(events.bill.onCreated, {
+ tenantId,
+ bill,
+ billId: bill.id,
+ trx,
+ } as IBillCreatedPayload);
- return bill;
- });
+ return bill;
+ },
+ trx
+ );
}
}
diff --git a/packages/server/src/services/Resource/ResourceService.ts b/packages/server/src/services/Resource/ResourceService.ts
index 529448f8d..edf97cd70 100644
--- a/packages/server/src/services/Resource/ResourceService.ts
+++ b/packages/server/src/services/Resource/ResourceService.ts
@@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
import { camelCase, upperFirst, pickBy } from 'lodash';
import * as qim from 'qim';
import pluralize from 'pluralize';
-import { IModelMeta, IModelMetaField } from '@/interfaces';
+import { IModelMeta, IModelMetaField, IModelMetaField2 } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import I18nService from '@/services/I18n/I18nService';
@@ -74,11 +74,20 @@ export default class ResourceService {
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 {string} modelName
- * @returns
+ *
+ * @param {number} tenantId
+ * @param {string} modelName
+ * @returns
*/
public getResourceImportableFields(
tenantId: number,
@@ -98,7 +107,9 @@ export default class ResourceService {
const naviagations = [
['fields', qim.$each, 'name'],
+ ['fields2', qim.$each, 'name'],
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
+ ['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
];
return this.i18nService.i18nApply(naviagations, meta, tenantId);
}
diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx
index e1a9bbac5..676322da1 100644
--- a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx
+++ b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.tsx
@@ -72,6 +72,10 @@ function ManualJournalActionsBar({
const handleRefreshBtnClick = () => {
refresh();
};
+ // Handle import button click.
+ const handleImportBtnClick = () => {
+ history.push('/manual-journals/import');
+ }
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
@@ -130,6 +134,7 @@ function ManualJournalActionsBar({
className={Classes.MINIMAL}
icon={}
text={}
+ onClick={handleImportBtnClick}
/>
-
-
-
- | Bigcapital Fields |
- Sheet Column Headers |
-
-
-
-
-
-
+ {entityColumns.map((entityColumn, index) => (
+
+ ))}
-
);
}
-function ImportFileMappingFields() {
- const { entityColumns, sheetColumns } = useImportFileContext();
+interface ImportFileMappingGroupProps {
+ groupKey: string;
+ groupName: string;
+ fields: any;
+}
+
+/**
+ * Mapping fields group
+ * @returns {React.ReactNode}
+ */
+function ImportFileMappingGroup({
+ groupKey,
+ groupName,
+ fields,
+}: ImportFileMappingGroupProps) {
+ return (
+
+ {groupName && {groupName}
}
+
+
+
+
+ | Bigcapital Fields |
+ Sheet Column Headers |
+
+
+
+
+
+
+
+ );
+}
+
+interface ImportFileMappingFieldsProps {
+ fields: EntityColumnField[];
+}
+
+/**
+ * Import mapping fields.
+ * @returns {React.ReactNode}
+ */
+function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
+ const { sheetColumns } = useImportFileContext();
const items = useMemo(
() => sheetColumns.map((column) => ({ value: column, text: column })),
[sheetColumns],
);
- const columnMapper = (column: EntityColumn, index: number) => (
-
- |
- {column.name}{' '}
- {column.required && *}
- |
-
-
-
- {column.hint && (
-
- )}
-
- |
-
+ const columnMapper = useCallback(
+ (column: EntityColumnField, index: number) => (
+
+ |
+ {column.name}{' '}
+ {column.required && *}
+ |
+
+
+
+ {column.hint && (
+
+ )}
+
+ |
+
+ ),
+ [items],
+ );
+ const columns = useMemo(
+ () => fields.map(columnMapper),
+ [columnMapper, fields],
);
- const columns = entityColumns.map(columnMapper);
-
return <>{columns}>;
}
diff --git a/packages/webapp/src/containers/Import/ImportFileMappingForm.tsx b/packages/webapp/src/containers/Import/ImportFileMappingForm.tsx
index 283e06e31..c02e2b3ae 100644
--- a/packages/webapp/src/containers/Import/ImportFileMappingForm.tsx
+++ b/packages/webapp/src/containers/Import/ImportFileMappingForm.tsx
@@ -3,17 +3,12 @@ import { Intent } from '@blueprintjs/core';
import { useImportFileMapping } from '@/hooks/query/import';
import { Form, Formik, FormikHelpers } from 'formik';
import { useImportFileContext } from './ImportFileProvider';
-import { useMemo } from 'react';
-import { isEmpty, lowerCase } from 'lodash';
import { AppToaster } from '@/components';
-import { useImportFileMapBootContext } from './ImportFileMappingBoot';
-import { transformToForm } from '@/utils';
-
-interface ImportFileMappingFormProps {
- children: React.ReactNode;
-}
-
-type ImportFileMappingFormValues = Record;
+import { ImportFileMappingFormProps } from './_types';
+import {
+ transformValueToReq,
+ useImportFileMappingInitialValues,
+} from './_utils';
export function ImportFileMappingForm({
children,
@@ -52,50 +47,3 @@ export function ImportFileMappingForm({
);
}
-
-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],
- );
-};
diff --git a/packages/webapp/src/containers/Import/ImportFileProvider.tsx b/packages/webapp/src/containers/Import/ImportFileProvider.tsx
index 3e7d0233f..14490a0db 100644
--- a/packages/webapp/src/containers/Import/ImportFileProvider.tsx
+++ b/packages/webapp/src/containers/Import/ImportFileProvider.tsx
@@ -7,12 +7,19 @@ import React, {
useState,
} from 'react';
-export type EntityColumn = {
+export type EntityColumnField = {
key: string;
name: string;
required?: boolean;
hint?: string;
+ group?: string;
};
+
+export interface EntityColumn {
+ groupKey: string;
+ groupName: string;
+ fields: EntityColumnField[];
+}
export type SheetColumn = string;
export type SheetMap = { from: string; to: string };
diff --git a/packages/webapp/src/containers/Import/ImportFileUploadForm.tsx b/packages/webapp/src/containers/Import/ImportFileUploadForm.tsx
index 73db99efc..483d1bcdd 100644
--- a/packages/webapp/src/containers/Import/ImportFileUploadForm.tsx
+++ b/packages/webapp/src/containers/Import/ImportFileUploadForm.tsx
@@ -7,6 +7,7 @@ import * as Yup from 'yup';
import { useImportFileContext } from './ImportFileProvider';
import { ImportAlert, ImportStepperStep } from './_types';
import { useAlertsManager } from './AlertsManager';
+import { transformToCamelCase } from '@/utils';
const initialValues = {
file: null,
@@ -55,9 +56,11 @@ export function ImportFileUploadForm({
uploadImportFile(formData)
.then(({ data }) => {
- setImportId(data.import.import_id);
- setSheetColumns(data.sheet_columns);
- setEntityColumns(data.resource_columns);
+ const _data = transformToCamelCase(data);
+
+ setImportId(_data.import.importId);
+ setSheetColumns(_data.sheetColumns);
+ setEntityColumns(_data.resourceColumns);
setStep(ImportStepperStep.Mapping);
setSubmitting(false);
})
diff --git a/packages/webapp/src/containers/Import/_types.ts b/packages/webapp/src/containers/Import/_types.ts
index 08df767d9..a5f2ca79f 100644
--- a/packages/webapp/src/containers/Import/_types.ts
+++ b/packages/webapp/src/containers/Import/_types.ts
@@ -5,5 +5,11 @@ export enum ImportStepperStep {
}
export enum ImportAlert {
- IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY'
-}
\ No newline at end of file
+ IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY',
+}
+
+export interface ImportFileMappingFormProps {
+ children: React.ReactNode;
+}
+
+export type ImportFileMappingFormValues = Record;
diff --git a/packages/webapp/src/containers/Import/_utils.ts b/packages/webapp/src/containers/Import/_utils.ts
new file mode 100644
index 000000000..667c25c1d
--- /dev/null
+++ b/packages/webapp/src/containers/Import/_utils.ts
@@ -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}
+ */
+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>(
+ () => ({
+ ...transformToForm(initialResValues, initialValues),
+ ...initialValues,
+ }),
+ [initialValues, initialResValues],
+ );
+};
diff --git a/packages/webapp/src/containers/Purchases/Bills/BillImport.tsx b/packages/webapp/src/containers/Purchases/Bills/BillImport.tsx
new file mode 100644
index 000000000..fa2924800
--- /dev/null
+++ b/packages/webapp/src/containers/Purchases/Bills/BillImport.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx
index fdbf3a8ef..da5415e45 100644
--- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx
+++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx
@@ -78,6 +78,11 @@ function BillActionsBar({
addSetting('bills', 'tableSize', size);
};
+ // Handle the import button click.
+ const handleImportBtnClick = () => {
+ history.push('/bills/import');
+ }
+
return (
@@ -130,6 +135,7 @@ function BillActionsBar({
className={Classes.MINIMAL}
icon={}
text={}
+ onClick={handleImportBtnClick}
/>